06987dcee8a33d9e805b56748c9980728454549b
[asterisk/asterisk.git] / res / parking / parking_bridge_features.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2013, Digium, Inc.
5  *
6  * Jonathan Rose <jrose@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Parking Bridge DTMF and Interval features
22  *
23  * \author Jonathan Rose <jrose@digium.com>
24  */
25
26 #include "asterisk.h"
27
28 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
29
30 #include "res_parking.h"
31 #include "asterisk/utils.h"
32 #include "asterisk/astobj2.h"
33 #include "asterisk/logger.h"
34 #include "asterisk/pbx.h"
35 #include "asterisk/bridge.h"
36 #include "asterisk/bridge_internal.h"
37 #include "asterisk/bridge_channel.h"
38 #include "asterisk/bridge_features.h"
39 #include "asterisk/features.h"
40 #include "asterisk/say.h"
41 #include "asterisk/datastore.h"
42 #include "asterisk/stasis.h"
43
44 struct parked_subscription_datastore {
45         struct stasis_subscription *parked_subscription;
46 };
47
48 struct parked_subscription_data {
49         char *parkee_uuid;
50         char parker_uuid[0];
51 };
52
53 static void parked_subscription_datastore_destroy(void *data)
54 {
55         struct parked_subscription_datastore *subscription_datastore = data;
56
57         stasis_unsubscribe(subscription_datastore->parked_subscription);
58         subscription_datastore->parked_subscription = NULL;
59
60         ast_free(subscription_datastore);
61 }
62
63 static const struct ast_datastore_info parked_subscription_info = {
64         .type = "park subscription",
65         .destroy = parked_subscription_datastore_destroy,
66 };
67
68 static void wipe_subscription_datastore(struct ast_channel *chan)
69 {
70         struct ast_datastore *datastore;
71
72         ast_channel_lock(chan);
73
74         datastore = ast_channel_datastore_find(chan, &parked_subscription_info, NULL);
75         if (datastore) {
76                 ast_channel_datastore_remove(chan, datastore);
77                 ast_datastore_free(datastore);
78         }
79         ast_channel_unlock(chan);
80 }
81
82 static void parker_parked_call_message_response(struct ast_parked_call_payload *message, struct parked_subscription_data *data,
83         struct stasis_subscription *sub)
84 {
85         const char *parkee_to_act_on = data->parkee_uuid;
86         char saynum_buf[16];
87         struct ast_channel_snapshot *parkee_snapshot = message->parkee;
88         RAII_VAR(struct ast_channel *, parker, NULL, ao2_cleanup);
89         RAII_VAR(struct ast_bridge_channel *, bridge_channel, NULL, ao2_cleanup);
90
91         if (strcmp(parkee_to_act_on, parkee_snapshot->uniqueid)) {
92                 return;
93         }
94
95         if (message->event_type != PARKED_CALL && message->event_type != PARKED_CALL_FAILED) {
96                 /* We only care about these two event types */
97                 return;
98         }
99
100         parker = ast_channel_get_by_name(data->parker_uuid);
101         if (!parker) {
102                 return;
103         }
104
105         ast_channel_lock(parker);
106         bridge_channel = ast_channel_get_bridge_channel(parker);
107         ast_channel_unlock(parker);
108         if (!bridge_channel) {
109                 return;
110         }
111
112         if (message->event_type == PARKED_CALL) {
113                 /* queue the saynum on the bridge channel and hangup */
114                 snprintf(saynum_buf, sizeof(saynum_buf), "%u %u", 1, message->parkingspace);
115                 ast_bridge_channel_queue_playfile(bridge_channel, say_parking_space, saynum_buf, NULL);
116                 wipe_subscription_datastore(bridge_channel->chan);
117         }
118
119         if (message->event_type == PARKED_CALL_FAILED) {
120                 ast_bridge_channel_queue_playfile(bridge_channel, NULL, "pbx-parkingfailed", NULL);
121                 wipe_subscription_datastore(bridge_channel->chan);
122         }
123 }
124
125 static void parker_update_cb(void *data, struct stasis_subscription *sub, struct stasis_topic *topic, struct stasis_message *message)
126 {
127         if (stasis_subscription_final_message(sub, message)) {
128                 ast_free(data);
129                 return;
130         }
131
132         if (stasis_message_type(message) == ast_parked_call_type()) {
133                 struct ast_parked_call_payload *parked_call_message = stasis_message_data(message);
134                 parker_parked_call_message_response(parked_call_message, data, sub);
135         }
136 }
137
138 static int create_parked_subscription(struct ast_channel *chan, const char *parkee_uuid)
139 {
140         struct ast_datastore *datastore;
141         struct parked_subscription_datastore *parked_datastore;
142         struct parked_subscription_data *subscription_data;
143
144         char *parker_uuid = ast_strdupa(ast_channel_uniqueid(chan));
145         size_t parker_uuid_size = strlen(parker_uuid) + 1;
146
147         /* If there is already a subscription, get rid of it. */
148         wipe_subscription_datastore(chan);
149
150         if (!(datastore = ast_datastore_alloc(&parked_subscription_info, NULL))) {
151                 return -1;
152         }
153
154         if (!(parked_datastore = ast_calloc(1, sizeof(*parked_datastore)))) {
155                 ast_datastore_free(datastore);
156                 return -1;
157         }
158
159         if (!(subscription_data = ast_calloc(1, sizeof(*subscription_data) + parker_uuid_size +
160                         strlen(parkee_uuid) + 1))) {
161                 ast_datastore_free(datastore);
162                 ast_free(parked_datastore);
163                 return -1;
164         }
165
166         subscription_data->parkee_uuid = subscription_data->parker_uuid + parker_uuid_size;
167         strcpy(subscription_data->parkee_uuid, parkee_uuid);
168         strcpy(subscription_data->parker_uuid, parker_uuid);
169
170         if (!(parked_datastore->parked_subscription = stasis_subscribe(ast_parking_topic(), parker_update_cb, subscription_data))) {
171                 return -1;
172         }
173
174         datastore->data = parked_datastore;
175
176         ast_channel_lock(chan);
177         ast_channel_datastore_add(chan, datastore);
178         ast_channel_unlock(chan);
179
180         return 0;
181 }
182
183 /*!
184  * \internal
185  * \brief Helper function that creates an outgoing channel and returns it immediately. This function is nearly
186  *        identical to the dial_transfer function in bridge_basic.c, however it doesn't swap the
187  *        local channel and the channel that instigated the park.
188  */
189 static struct ast_channel *park_local_transfer(struct ast_channel *parker, const char *context, const char *exten)
190 {
191         RAII_VAR(struct ast_channel *, parkee_side_2, NULL, ao2_cleanup);
192         char destination[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 1];
193         struct ast_channel *parkee;
194         int cause;
195
196         /* Used for side_2 hack */
197         char *parkee_name;
198         char *semi_pos;
199
200         /* Fill the variable with the extension and context we want to call */
201         snprintf(destination, sizeof(destination), "%s@%s", exten, context);
202
203         /* Now we request that chan_local prepare to call the destination */
204         parkee = ast_request("Local", ast_channel_nativeformats(parker), parker, destination,
205                 &cause);
206         if (!parkee) {
207                 return NULL;
208         }
209
210         /* Before we actually dial out let's inherit appropriate information. */
211         ast_channel_lock_both(parker, parkee);
212         ast_connected_line_copy_from_caller(ast_channel_connected(parkee), ast_channel_caller(parker));
213         ast_channel_inherit_variables(parker, parkee);
214         ast_channel_datastore_inherit(parker, parkee);
215         ast_channel_unlock(parkee);
216         ast_channel_unlock(parker);
217
218         /* BUGBUG Use Richard's unreal channel stuff here instead of this hack */
219         parkee_name = ast_strdupa(ast_channel_name(parkee));
220
221         semi_pos = strrchr(parkee_name, ';');
222         if (!semi_pos) {
223                 /* There should always be a semicolon present in the string if is used since it's a local channel. */
224                 ast_assert(0);
225                 return NULL;
226         }
227
228         parkee_name[(semi_pos - parkee_name) + 1] = '2';
229         parkee_side_2 = ast_channel_get_by_name(parkee_name);
230
231         /* We need to have the parker subscribe to the new local channel before hand. */
232         create_parked_subscription(parker, ast_channel_uniqueid(parkee_side_2));
233
234         pbx_builtin_setvar_helper(parkee_side_2, "BLINDTRANSFER", ast_channel_name(parker));
235
236         /* Since the above worked fine now we actually call it and return the channel */
237         if (ast_call(parkee, destination, 0)) {
238                 ast_hangup(parkee);
239                 return NULL;
240         }
241
242         return parkee;
243 }
244
245 /*! \internal \brief Determine if an extension is a parking extension */
246 static int parking_is_exten_park(const char *context, const char *exten)
247 {
248         struct ast_exten *exten_obj;
249         struct pbx_find_info info = { .stacklen = 0 }; /* the rest is reset in pbx_find_extension */
250         const char *app_at_exten;
251
252         ast_debug(4, "Checking if %s@%s is a parking exten\n", exten, context);
253         exten_obj = pbx_find_extension(NULL, NULL, &info, context, exten, 1, NULL, NULL, E_MATCH);
254         if (!exten_obj) {
255                 return 0;
256         }
257
258         app_at_exten = ast_get_extension_app(exten_obj);
259         if (!app_at_exten || strcasecmp(PARK_APPLICATION, app_at_exten)) {
260                 return 0;
261         }
262
263         return 1;
264 }
265
266 /*!
267  * \internal
268  * \since 12.0.0
269  * \brief Perform a blind transfer to a parking lot
270  *
271  * In general, most parking features should work to call this function. This will safely
272  * park either a channel in the bridge with \ref bridge_channel or will park the entire
273  * bridge if more than one channel is in the bridge. It will create the correct data to
274  * pass to the \ref AstBridging Bridging API to safely park the channel.
275  *
276  * \param bridge_channel The bridge_channel representing the channel performing the park
277  * \param context The context to blind transfer to
278  * \param exten The extension to blind transfer to
279  *
280  * \retval 0 on success
281  * \retval non-zero on error
282  */
283 static int parking_blind_transfer_park(struct ast_bridge_channel *bridge_channel,
284                 const char *context, const char *exten)
285 {
286         RAII_VAR(struct ast_bridge_channel *, other, NULL, ao2_cleanup);
287         int peer_count;
288
289         if (ast_strlen_zero(context) || ast_strlen_zero(exten)) {
290                 return -1;
291         }
292
293         if (!bridge_channel->in_bridge) {
294                 return -1;
295         }
296
297         if (!parking_is_exten_park(context, exten)) {
298                 return -1;
299         }
300
301         ast_bridge_channel_lock_bridge(bridge_channel);
302         peer_count = bridge_channel->bridge->num_channels;
303         if (peer_count == 2) {
304                 other = ast_bridge_channel_peer(bridge_channel);
305                 ao2_ref(other, +1);
306         }
307         ast_bridge_unlock(bridge_channel->bridge);
308
309         if (peer_count < 2) {
310                 /* There is nothing to do if there is no one to park. */
311                 return -1;
312         }
313
314         /* With a multiparty bridge, we need to do a regular blind transfer. We link the
315          * existing bridge to the parking lot with a Local channel rather than
316          * transferring others. */
317         if (peer_count > 2) {
318                 struct ast_channel *transfer_chan = NULL;
319
320                 transfer_chan = park_local_transfer(bridge_channel->chan, context, exten);
321                 if (!transfer_chan) {
322                         return -1;
323                 }
324
325                 if (ast_bridge_impart(bridge_channel->bridge, transfer_chan, NULL, NULL, 1)) {
326                         ast_hangup(transfer_chan);
327                         return -1;
328                 }
329                 return 0;
330         }
331
332         /* Subscribe to park messages with the other channel entering */
333         if (create_parked_subscription(bridge_channel->chan, ast_channel_uniqueid(other->chan))) {
334                 return -1;
335         }
336
337         /* Write the park frame with the intended recipient and other data out to the bridge. */
338         ast_bridge_channel_write_park(bridge_channel,
339                 ast_channel_uniqueid(other->chan),
340                 ast_channel_uniqueid(bridge_channel->chan),
341                 NULL);
342
343         return 0;
344 }
345
346
347 /*!
348  * \internal
349  * \since 12.0.0
350  * \brief Perform a direct park on a channel in a bridge
351  *
352  * \note This will be called from within the \ref AstBridging Bridging API
353  *
354  * \param bridge_channel The bridge_channel representing the channel to be parked
355  * \param uuid_parkee The UUID of the channel being parked
356  * \param uuid_parker The UUID of the channel performing the park
357  * \param app_data Application parseable data to pass to the parking application
358  */
359 static int parking_park_bridge_channel(struct ast_bridge_channel *bridge_channel, const char *uuid_parkee, const char *uuid_parker, const char *app_data)
360 {
361         RAII_VAR(struct ast_bridge *, parking_bridge, NULL, ao2_cleanup);
362         RAII_VAR(struct ast_bridge *, original_bridge, NULL, ao2_cleanup);
363         RAII_VAR(struct ast_channel *, parker, NULL, ao2_cleanup);
364
365         if (strcmp(ast_channel_uniqueid(bridge_channel->chan), uuid_parkee)) {
366                 /* We aren't the parkee, so ignore this action. */
367                 return -1;
368         }
369
370         parker = ast_channel_get_by_name(uuid_parker);
371
372         if (!parker) {
373                 ast_log(LOG_NOTICE, "Channel with uuid %s left before we could start parking the call. Parking canceled.\n", uuid_parker);
374                 publish_parked_call_failure(bridge_channel->chan);
375                 return -1;
376         }
377
378         if (!(parking_bridge = park_application_setup(bridge_channel->chan, parker, app_data, NULL))) {
379                 publish_parked_call_failure(bridge_channel->chan);
380                 return -1;
381         }
382
383         pbx_builtin_setvar_helper(bridge_channel->chan, "BLINDTRANSFER", ast_channel_name(parker));
384
385         /* bridge_channel must be locked so we can get a reference to the bridge it is currently on */
386         ao2_lock(bridge_channel);
387
388         original_bridge = bridge_channel->bridge;
389         if (!original_bridge) {
390                 ao2_unlock(bridge_channel);
391                 publish_parked_call_failure(bridge_channel->chan);
392                 return -1;
393         }
394
395         ao2_ref(original_bridge, +1); /* Cleaned by RAII_VAR */
396
397         ao2_unlock(bridge_channel);
398
399         if (ast_bridge_move(parking_bridge, original_bridge, bridge_channel->chan, NULL, 1)) {
400                 ast_log(LOG_ERROR, "Failed to move %s into the parking bridge.\n",
401                         ast_channel_name(bridge_channel->chan));
402                 return -1;
403         }
404
405         return 0;
406 }
407
408 /*!
409  * \internal
410  * \since 12.0.0
411  * \brief Park a call
412  *
413  * \param parker The bridge_channel parking the call
414  * \param exten Optional. The extension where the call was parked.
415  * \param length Optional. If \c exten is specified, the length of the buffer.
416  *
417  * \note This will determine the context and extension to park the channel based on
418  * the configuration of the \ref ast_channel associated with \ref parker. It will then
419  * park either the channel or the entire bridge.
420  *
421  * \retval 0 on success
422  * \retval -1 on error
423  */
424 static int parking_park_call(struct ast_bridge_channel *parker, char *exten, size_t length)
425 {
426         RAII_VAR(struct parking_lot *, lot, NULL, ao2_cleanup);
427         const char *lot_name = NULL;
428
429         ast_channel_lock(parker->chan);
430         lot_name = find_channel_parking_lot_name(parker->chan);
431         if (!ast_strlen_zero(lot_name)) {
432                 lot_name = ast_strdupa(lot_name);
433         }
434         ast_channel_unlock(parker->chan);
435
436         if (ast_strlen_zero(lot_name)) {
437                 return -1;
438         }
439
440         lot = parking_lot_find_by_name(lot_name);
441         if (!lot) {
442                 ast_log(AST_LOG_WARNING, "Cannot Park %s: lot %s unknown\n",
443                         ast_channel_name(parker->chan), lot_name);
444                 return -1;
445         }
446
447         if (exten) {
448                 ast_copy_string(exten, lot->cfg->parkext, length);
449         }
450         return parking_blind_transfer_park(parker, lot->cfg->parking_con, lot->cfg->parkext);
451 }
452
453 static int feature_park_call(struct ast_bridge_channel *bridge_channel, void *hook_pvt)
454 {
455         return parking_park_call(bridge_channel, NULL, 0);
456 }
457
458 /*! \internal
459  * \brief Interval hook. Pulls a parked call from the parking bridge after the timeout is passed and sets the resolution to timeout.
460  *
461  * \param bridge_channel bridge channel this interval hook is being executed on
462  * \param hook_pvt A pointer to the parked_user struct associated with the channel is stuffed in here
463  */
464 static int parking_duration_callback(struct ast_bridge_channel *bridge_channel, void *hook_pvt)
465 {
466         struct parked_user *user = hook_pvt;
467         struct ast_channel *chan = user->chan;
468         struct ast_context *park_dial_context;
469         const char *dial_string;
470         char *dial_string_flat;
471         char parking_space[AST_MAX_EXTENSION];
472
473         char returnexten[AST_MAX_EXTENSION];
474         char *duplicate_returnexten;
475         struct ast_exten *existing_exten;
476         struct pbx_find_info pbx_finder = { .stacklen = 0 }; /* The rest is reset in pbx_find_extension */
477
478
479         /* We are still in the bridge, so it's possible for other stuff to mess with the parked call before we leave the bridge
480            to deal with this, lock the parked user, check and set resolution. */
481         ao2_lock(user);
482         if (user->resolution != PARK_UNSET) {
483                 /* Abandon timeout since something else has resolved the parked user before we got to it. */
484                 ao2_unlock(user);
485                 return -1;
486         }
487
488         user->resolution = PARK_TIMEOUT;
489         ao2_unlock(user);
490
491         ast_bridge_channel_leave_bridge(bridge_channel, BRIDGE_CHANNEL_STATE_END_NO_DISSOLVE);
492
493         /* Set parking timeout channel variables */
494         snprintf(parking_space, sizeof(parking_space), "%d", user->parking_space);
495         pbx_builtin_setvar_helper(chan, "PARKING_SPACE", parking_space);
496         pbx_builtin_setvar_helper(chan, "PARKINGSLOT", parking_space); /* Deprecated version of PARKING_SPACE */
497         pbx_builtin_setvar_helper(chan, "PARKEDLOT", user->lot->name);
498
499         dial_string = user->parker_dial_string;
500         dial_string_flat = ast_strdupa(dial_string);
501         flatten_dial_string(dial_string_flat);
502
503         pbx_builtin_setvar_helper(chan, "PARKER", dial_string);
504         pbx_builtin_setvar_helper(chan, "PARKER_FLAT", dial_string_flat);
505
506         /* Dialplan generation for park-dial extensions */
507
508         if (ast_wrlock_contexts()) {
509                 ast_log(LOG_ERROR, "Failed to lock the contexts list. Can't add the park-dial extension.\n");
510                 return -1;
511         }
512
513         if (!(park_dial_context = ast_context_find_or_create(NULL, NULL, PARK_DIAL_CONTEXT, BASE_REGISTRAR))) {
514                 ast_log(LOG_ERROR, "Parking dial context '%s' does not exist and unable to create\n", PARK_DIAL_CONTEXT);
515                 if (ast_unlock_contexts()) {
516                         ast_assert(0);
517                 }
518                 goto abandon_extension_creation;
519         }
520
521         if (ast_wrlock_context(park_dial_context)) {
522                 ast_log(LOG_ERROR, "failed to obtain write lock on context '%s'\n", PARK_DIAL_CONTEXT);
523                 if (ast_unlock_contexts()) {
524                         ast_assert(0);
525                 }
526                 goto abandon_extension_creation;
527         }
528
529         if (ast_unlock_contexts()) {
530                 ast_assert(0);
531         }
532
533         snprintf(returnexten, sizeof(returnexten), "%s,%u", dial_string,
534                 user->lot->cfg->comebackdialtime);
535
536         duplicate_returnexten = ast_strdup(returnexten);
537
538         if (!duplicate_returnexten) {
539                 ast_log(LOG_ERROR, "Failed to create parking redial parker extension %s@%s - Dial(%s)\n",
540                         dial_string_flat, PARK_DIAL_CONTEXT, returnexten);
541         }
542
543         /* If an extension already exists here because we registered it for another parked call timing out, then we may overwrite it. */
544         if ((existing_exten = pbx_find_extension(NULL, NULL, &pbx_finder, PARK_DIAL_CONTEXT, dial_string_flat, 1, NULL, NULL, E_MATCH)) &&
545             (strcmp(ast_get_extension_registrar(existing_exten), BASE_REGISTRAR))) {
546                 ast_debug(3, "An extension for '%s@%s' was already registered by another registrar '%s'\n",
547                         dial_string_flat, PARK_DIAL_CONTEXT, ast_get_extension_registrar(existing_exten));
548         } else if (ast_add_extension2_nolock(park_dial_context, 1, dial_string_flat, 1, NULL, NULL,
549                         "Dial", duplicate_returnexten, ast_free_ptr, BASE_REGISTRAR)) {
550                         ast_free(duplicate_returnexten);
551                 ast_log(LOG_ERROR, "Failed to create parking redial parker extension %s@%s - Dial(%s)\n",
552                         dial_string_flat, PARK_DIAL_CONTEXT, returnexten);
553         }
554
555         if (ast_unlock_context(park_dial_context)) {
556                 ast_assert(0);
557         }
558
559 abandon_extension_creation:
560
561         /* async_goto the proper PBX destination - this should happen when we come out of the bridge */
562         if (!ast_strlen_zero(user->comeback)) {
563                 ast_async_parseable_goto(chan, user->comeback);
564         } else {
565                 comeback_goto(user, user->lot);
566         }
567
568         return -1;
569 }
570
571 void say_parking_space(struct ast_bridge_channel *bridge_channel, const char *payload)
572 {
573         int numeric_value;
574         int hangup_after;
575
576         if (sscanf(payload, "%u %u", &hangup_after, &numeric_value) != 2) {
577                 /* If say_parking_space is called with a non-numeric string, we have a problem. */
578                 ast_assert(0);
579                 ast_bridge_channel_leave_bridge(bridge_channel, BRIDGE_CHANNEL_STATE_END_NO_DISSOLVE);
580                 return;
581         }
582
583         ast_say_digits(bridge_channel->chan, numeric_value, "", ast_channel_language(bridge_channel->chan));
584
585         if (hangup_after) {
586                 ast_bridge_channel_leave_bridge(bridge_channel, BRIDGE_CHANNEL_STATE_END_NO_DISSOLVE);
587         }
588 }
589
590 void parking_set_duration(struct ast_bridge_features *features, struct parked_user *user)
591 {
592         unsigned int time_limit;
593
594         time_limit = user->time_limit * 1000;
595
596         if (!time_limit) {
597                 /* There is no duration limit that we need to apply. */
598                 return;
599         }
600
601         /* If the time limit has already been passed, set a really low time limit so we can kick them out immediately. */
602         time_limit = ast_remaining_ms(user->start, time_limit);
603         if (time_limit <= 0) {
604                 time_limit = 1;
605         }
606
607         /* The interval hook is going to need a reference to the parked_user */
608         ao2_ref(user, +1);
609
610         if (ast_bridge_interval_hook(features, time_limit,
611                 parking_duration_callback, user, __ao2_cleanup, AST_BRIDGE_HOOK_REMOVE_ON_PULL)) {
612                 ast_log(LOG_ERROR, "Failed to apply duration limits to the parking call.\n");
613                 ao2_ref(user, -1);
614         }
615 }
616
617 struct ast_parking_bridge_feature_fn_table parking_provider = {
618         .module_version = PARKING_MODULE_VERSION,
619         .module_name = __FILE__,
620         .parking_is_exten_park = parking_is_exten_park,
621         .parking_blind_transfer_park = parking_blind_transfer_park,
622         .parking_park_bridge_channel = parking_park_bridge_channel,
623         .parking_park_call = parking_park_call,
624 };
625
626 void unload_parking_bridge_features(void)
627 {
628         ast_bridge_features_unregister(AST_BRIDGE_BUILTIN_PARKCALL);
629         ast_parking_unregister_bridge_features(parking_provider.module_name);
630 }
631
632 int load_parking_bridge_features(void)
633 {
634         if (ast_parking_register_bridge_features(&parking_provider)) {
635                 return -1;
636         }
637
638         ast_bridge_features_register(AST_BRIDGE_BUILTIN_PARKCALL, feature_park_call, NULL);
639         return 0;
640 }