Commit d32da007 authored by Simon Morlat's avatar Simon Morlat

many bugfixes and improvements for multicall

parent 9d37f359
This diff is collapsed.
......@@ -118,7 +118,7 @@ static void linphonec_display_refer (LinphoneCore * lc, const char *refer_to);
static void linphonec_display_something (LinphoneCore * lc, const char *something);
static void linphonec_display_url (LinphoneCore * lc, const char *something, const char *url);
static void linphonec_display_warning (LinphoneCore * lc, const char *something);
static void linphonec_notify_received(LinphoneCore *lc,const char *from,const char *msg);
static void linphonec_notify_received(LinphoneCore *lc, LinphoneCall *call, const char *from,const char *event);
static void linphonec_notify_presence_received(LinphoneCore *lc,LinphoneFriend *fid);
static void linphonec_new_unknown_subscriber(LinphoneCore *lc,
......@@ -169,6 +169,24 @@ static ortp_pipe_t server_sock;
#endif /*_WIN32_WCE*/
void linphonec_call_identify(LinphoneCall* call){
static long callid=1;
linphone_call_set_user_pointer (call,(void*)callid);
callid++;
}
LinphoneCall *linphonec_get_call(long id){
const MSList *elem=linphone_core_get_calls(linphonec);
for (;elem!=NULL;elem=elem->next){
LinphoneCall *call=(LinphoneCall*)elem->data;
if (linphone_call_get_user_pointer (call)==(void*)id){
return call;
}
}
linphonec_out("Sorry, no call with id %i exists at this time.",id);
return NULL;
}
/***************************************************************************
*
* Linphone core callbacks
......@@ -252,13 +270,12 @@ linphonec_prompt_for_auth(LinphoneCore *lc, const char *realm, const char *usern
* Linphone core callback
*/
static void
linphonec_notify_received(LinphoneCore *lc,const char *from,const char *msg)
linphonec_notify_received(LinphoneCore *lc, LinphoneCall *call, const char *from,const char *event)
{
printf("Notify type %s from %s\n", msg, from);
if(!strcmp(msg,"refer"))
if(!strcmp(event,"refer"))
{
printf("The distant SIP end point get the refer we can close the call\n");
linphonec_parse_command_line(linphonec, "terminate");
linphonec_out("The distand endpoint %s of call %li has been transfered, you can safely close the call.\n",
from,(long)linphone_call_get_user_pointer (call));
}
}
......@@ -291,27 +308,34 @@ linphonec_new_unknown_subscriber(LinphoneCore *lc, LinphoneFriend *lf,
static void linphonec_call_state_changed(LinphoneCore *lc, LinphoneCall *call, LinphoneCallState st, const char *msg){
char *from=linphone_call_get_remote_address_as_string(call);
long id=(long)linphone_call_get_user_pointer (call);
switch(st){
case LinphoneCallEnd:
printf("Call with %s ended.\n", from);
linphonec_out("Call %i with %s ended.\n", id, from);
break;
case LinphoneCallResuming:
printf("Resuming call with %s.\n", from);
linphonec_out("Resuming call %i with %s.\n", id, from);
break;
case LinphoneCallStreamsRunning:
printf("Media streams established with %s.\n", from);
linphonec_out("Media streams established with %s for call %i.\n", from,id);
break;
case LinphoneCallPausing:
printf("Pausing call with %s.\n", from);
linphonec_out("Pausing call %i with %s.\n", id, from);
break;
case LinphoneCallPaused:
printf("Call with %s is now paused.\n", from);
linphonec_out("Call %i with %s is now paused.\n", id, from);
break;
case LinphoneCallIncomingReceived:
linphonec_call_identify(call);
id=(long)linphone_call_get_user_pointer (call);
linphonec_set_caller(from);
if ( auto_answer) {
answer_call=TRUE;
}
linphonec_out("Receiving new incoming call from %s, assigned id %i", from,id);
break;
case LinphoneCallOutgoingInit:
linphonec_call_identify(call);
break;
default:
break;
......@@ -682,11 +706,10 @@ void linphonec_main_loop_exit(void){
void
linphonec_finish(int exit_status)
{
printf("Terminating...\n");
linphonec_out("Terminating...\n");
/* Terminate any pending call */
linphonec_parse_command_line(linphonec, "terminate");
linphonec_command_finished();
linphone_core_terminate_all_calls(linphonec);
#ifdef HAVE_READLINE
linphonec_finish_readline();
#endif
......
......@@ -112,6 +112,8 @@ void linphonec_set_autoanswer(bool_t enabled);
bool_t linphonec_get_autoanswer();
void linphonec_command_finished(void);
void linphonec_set_caller(const char *caller);
LinphoneCall *linphonec_get_call(long id);
void linphonec_call_identify(LinphoneCall* call);
#endif /* def LINPHONEC_H */
......
......@@ -95,8 +95,8 @@ static void call_received(SalOp *h){
if (lc->vtable.display_status)
lc->vtable.display_status(lc,barmesg);
/* play the ring */
if (lc->sound_conf.ring_sndcard!=NULL && !linphone_core_in_call(lc)){
/* play the ring if this is the only call*/
if (lc->sound_conf.ring_sndcard!=NULL && ms_list_size(lc->calls)==1){
if(lc->ringstream==NULL){
MSSndCard *ringcard=lc->sound_conf.lsd_card ?lc->sound_conf.lsd_card : lc->sound_conf.ring_sndcard;
ms_message("Starting local ring...");
......@@ -106,6 +106,8 @@ static void call_received(SalOp *h){
{
ms_message("the local ring is already started");
}
}else{
/*TODO : play a tone within the context of the current call */
}
sal_call_notify_ringing(h);
#if !(__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000)
......@@ -198,6 +200,16 @@ static void call_accepted(SalOp *op){
ms_free(msg);
}
linphone_call_set_state(call,LinphoneCallPaused,"Call paused");
}else if (sal_media_description_has_dir(call->resultdesc,SalStreamRecvOnly)){
/*we are put on hold when the call is initially accepted */
if (lc->vtable.display_status){
char *tmp=linphone_call_get_remote_address_as_string (call);
char *msg=ms_strdup_printf(_("Call answered by %s - on hold."),tmp);
lc->vtable.display_status(lc,msg);
ms_free(tmp);
ms_free(msg);
}
linphone_call_set_state(call,LinphoneCallPaused,"Call paused");
}else{
linphone_call_set_state(call,LinphoneCallStreamsRunning,"Connected (streams running)");
}
......@@ -241,7 +253,7 @@ static void call_ack(SalOp *op){
}
/* this callback is called when an incoming re-INVITE modifies the session*/
static void call_updated(SalOp *op){
static void call_updating(SalOp *op){
LinphoneCore *lc=(LinphoneCore *)sal_get_user_pointer(sal_op_get_sal(op));
LinphoneCall *call=(LinphoneCall*)sal_op_get_user_pointer(op);
if (call->resultdesc)
......@@ -254,6 +266,13 @@ static void call_updated(SalOp *op){
{
if (call->state==LinphoneCallPaused &&
sal_media_description_has_dir(call->resultdesc,SalStreamSendRecv) && strcmp(call->resultdesc->addr,"0.0.0.0")!=0){
/*make sure we can be resumed */
if (lc->current_call!=NULL && lc->current_call!=call){
ms_warning("Attempt to be resumed but already in call with somebody else!");
/*we are actively running another call, reject with a busy*/
sal_call_decline (op,SalReasonBusy,NULL);
return;
}
if(lc->vtable.display_status)
lc->vtable.display_status(lc,_("We have been resumed..."));
linphone_call_set_state (call,LinphoneCallStreamsRunning,"Connected (streams running)");
......@@ -263,12 +282,18 @@ static void call_updated(SalOp *op){
if(lc->vtable.display_status)
lc->vtable.display_status(lc,_("We are being paused..."));
linphone_call_set_state (call,LinphoneCallPaused,"Call paused");
if (lc->current_call!=call){
ms_error("Inconsitency detected: current call is %p but call %p is being paused !",lc->current_call,call);
}
lc->current_call=NULL;
}
/*accept the modification (sends a 200Ok)*/
sal_call_accept(op);
linphone_call_stop_media_streams (call);
linphone_call_init_media_streams (call);
linphone_call_start_media_streams (call);
}
if (lc->current_call==NULL) linphone_core_start_pending_refered_calls (lc);
}
static void call_terminated(SalOp *op, const char *from){
......@@ -459,12 +484,18 @@ static void refer_received(Sal *sal, SalOp *op, const char *referto){
LinphoneCore *lc=(LinphoneCore *)sal_get_user_pointer(sal);
LinphoneCall *call=(LinphoneCall*)sal_op_get_user_pointer(op);
if (call){
if (call->refer_to!=NULL){
ms_free(call->refer_to);
}
call->refer_to=ms_strdup(referto);
call->refer_pending=TRUE;
linphone_call_set_state(call,LinphoneCallRefered,"Refered");
if (lc->vtable.display_status){
char *msg=ms_strdup_printf(_("We are transferred to %s"),referto);
lc->vtable.display_status(lc,msg);
ms_free(msg);
}
if (lc->current_call==NULL) linphone_core_start_pending_refered_calls (lc);
sal_refer_accept(op);
}else if (lc->vtable.refer_received){
lc->vtable.refer_received(lc,referto);
......@@ -479,10 +510,10 @@ static void text_received(Sal *sal, const char *from, const char *msg){
static void notify(SalOp *op, const char *from, const char *msg){
LinphoneCore *lc=(LinphoneCore *)sal_get_user_pointer(sal_op_get_sal(op));
LinphoneCall *call=(LinphoneCall*)sal_op_get_user_pointer (op);
ms_message("get a %s notify from %s",msg,from);
if(lc->vtable.notify_recv)
lc->vtable.notify_recv(lc,from,msg);
lc->vtable.notify_recv(lc,call,from,msg);
}
static void notify_presence(SalOp *op, SalSubscribeState ss, SalPresenceStatus status, const char *msg){
......@@ -525,7 +556,7 @@ SalCallbacks linphone_sal_callbacks={
call_ringing,
call_accepted,
call_ack,
call_updated,
call_updating,
call_terminated,
call_failure,
auth_requested,
......
......@@ -87,7 +87,7 @@ static int find_port_offset(LinphoneCore *lc){
MSList *elem;
int audio_port;
bool_t already_used=FALSE;
for(offset=0;offset<100;++offset){
for(offset=0;offset<100;offset+=2){
audio_port=linphone_core_get_audio_port (lc)+offset;
already_used=FALSE;
for(elem=lc->calls;elem!=NULL;elem=elem->next){
......@@ -204,13 +204,17 @@ static void linphone_call_set_terminated(LinphoneCall *call){
}
linphone_call_log_completed(call->log,call, status);
if (linphone_core_del_call(lc,call) != 0){
ms_error("Could not remove the call from the list !!!");
}
if (call == lc->current_call){
ms_message("Resetting the current call");
lc->current_call=NULL;
linphone_core_start_pending_refered_calls(lc);
}
if (linphone_core_del_call(lc,call) != 0){
ms_error("Could not remove the call from the list !!!");
}
if (ms_list_size(lc->calls)==0)
linphone_core_notify_all_friends(lc,lc->presence_mode);
......@@ -226,7 +230,11 @@ static void linphone_call_set_terminated(LinphoneCall *call){
void linphone_call_set_state(LinphoneCall *call, LinphoneCallState cstate, const char *message){
LinphoneCore *lc=call->core;
if (call->state!=cstate){
call->state=cstate;
if (cstate!=LinphoneCallRefered){
/*LinphoneCallRefered is rather an event, not a state.
Indeed it does not change the state of the call (still paused or running)*/
call->state=cstate;
}
if (lc->vtable.call_state_changed)
lc->vtable.call_state_changed(lc,call,cstate,message);
}
......@@ -251,6 +259,9 @@ static void linphone_call_destroy(LinphoneCall *obj)
if (obj->ping_op) {
sal_op_release(obj->ping_op);
}
if (obj->refer_to){
ms_free(obj->refer_to);
}
ms_free(obj);
}
......@@ -336,7 +347,7 @@ LinphoneCallLog *linphone_call_get_call_log(const LinphoneCall *call){
}
/**
* Returns the refer-to uri (if the call received was transfered).
* Returns the refer-to uri (if the call was transfered).
**/
const char *linphone_call_get_refer_to(const LinphoneCall *call){
return call->refer_to;
......@@ -346,6 +357,18 @@ LinphoneCallDir linphone_call_get_dir(const LinphoneCall *call){
return call->log->dir;
}
/**
* Returns true if this calls has received a transfer that has not been
* executed yet.
* Pending transfers are executed when this call is being paused or closed,
* locally or by remote endpoint.
* If the call is already paused while receiving the transfer request, the
* transfer immediately occurs.
**/
bool_t linphone_call_has_transfer_pending(const LinphoneCall *call){
return call->refer_pending;
}
/**
* @}
**/
......
......@@ -1671,25 +1671,22 @@ void linphone_core_iterate(LinphoneCore *lc){
call = linphone_core_get_current_call(lc);
if(call)
{
if (call->state==LinphoneCallConnected)
if (one_second_elapsed)
{
if (one_second_elapsed)
{
RtpSession *as=NULL,*vs=NULL;
lc->prevtime=curtime;
if (call->audiostream!=NULL)
as=call->audiostream->session;
if (call->videostream!=NULL)
vs=call->videostream->session;
display_bandwidth(as,vs);
}
#ifdef VIDEO_ENABLED
RtpSession *as=NULL,*vs=NULL;
lc->prevtime=curtime;
if (call->audiostream!=NULL)
as=call->audiostream->session;
if (call->videostream!=NULL)
video_stream_iterate(call->videostream);
#endif
if (call->audiostream!=NULL && disconnect_timeout>0)
disconnected=!audio_stream_alive(call->audiostream,disconnect_timeout);
vs=call->videostream->session;
display_bandwidth(as,vs);
}
#ifdef VIDEO_ENABLED
if (call->videostream!=NULL)
video_stream_iterate(call->videostream);
#endif
if (call->audiostream!=NULL && disconnect_timeout>0)
disconnected=!audio_stream_alive(call->audiostream,disconnect_timeout);
}
if (linphone_core_video_preview_enabled(lc)){
if (lc->previewstream==NULL && lc->calls==NULL)
......@@ -1835,6 +1832,19 @@ bool_t linphone_core_is_in_communication_with(LinphoneCore *lc, const char *to)
return returned;
}
void linphone_core_start_pending_refered_calls(LinphoneCore *lc){
MSList *elem;
for(elem=lc->calls;elem!=NULL;elem=elem->next){
LinphoneCall *call=(LinphoneCall*)elem->data;
if (call->refer_pending){
ms_message("Starting new call to refered address %s",call->refer_to);
call->refer_pending=FALSE;
linphone_core_invite(lc,call->refer_to);
break;
}
}
}
LinphoneProxyConfig * linphone_core_lookup_known_proxy(LinphoneCore *lc, const LinphoneAddress *uri){
const MSList *elem;
LinphoneProxyConfig *found_cfg=NULL;
......@@ -2036,7 +2046,13 @@ LinphoneCall * linphone_core_invite_address(LinphoneCore *lc, const LinphoneAddr
return call;
}
int linphone_core_refer(LinphoneCore *lc, LinphoneCall *call, const char *url)
/**
* Performs a simple call transfer to the specified destination.
*
* The remote endpoint is expected to issue a new call to the specified destination.
* The current call remains active and thus can be later paused or terminated.
**/
int linphone_core_transfer_call(LinphoneCore *lc, LinphoneCall *call, const char *url)
{
char *real_url=NULL;
LinphoneAddress *real_parsed_url=linphone_core_interpret_url(lc,url);
......@@ -2053,6 +2069,7 @@ int linphone_core_refer(LinphoneCore *lc, LinphoneCall *call, const char *url)
real_url=linphone_address_as_string (real_parsed_url);
sal_refer(call->op,real_url);
ms_free(real_url);
linphone_address_destroy(real_parsed_url);
return 0;
}
......@@ -2114,7 +2131,7 @@ int linphone_core_accept_call(LinphoneCore *lc, LinphoneCall *call)
MSList *elem;
for(elem=lc->calls;elem!=NULL;elem=elem->next){
LinphoneCall *c=(LinphoneCall*)elem->data;
if (c!=call && (c->state!=LinphoneCallPaused || c->state!=LinphoneCallPausing)){
if (c!=call && (c->state!=LinphoneCallPaused)){
ms_warning("Cannot accept this call as another one is running, pause it before.");
return -1;
}
......@@ -2181,8 +2198,10 @@ int linphone_core_terminate_call(LinphoneCore *lc, LinphoneCall *the_call)
LinphoneCall *call;
if (the_call == NULL){
call = linphone_core_get_current_call(lc);
if(call == NULL){
ms_warning("No currently active call to terminate !");
if (ms_list_size(lc->calls)==1){
call=(LinphoneCall*)lc->calls->data;
}else{
ms_warning("No unique call to terminate !");
return -1;
}
}
......@@ -2276,6 +2295,7 @@ int linphone_core_pause_call(LinphoneCore *lc, LinphoneCall *the_call)
if (lc->vtable.display_status)
lc->vtable.display_status(lc,_("Pausing the current call..."));
lc->current_call=NULL;
linphone_core_start_pending_refered_calls(lc);
return 0;
}
......
......@@ -172,6 +172,7 @@ void linphone_call_ref(LinphoneCall *call);
void linphone_call_unref(LinphoneCall *call);
LinphoneCallLog *linphone_call_get_call_log(const LinphoneCall *call);
const char *linphone_call_get_refer_to(const LinphoneCall *call);
bool_t linphone_call_has_transfer_pending(const LinphoneCall *call);
void *linphone_call_get_user_pointer(LinphoneCall *call);
void linphone_call_set_user_pointer(LinphoneCall *call, void *user_pointer);
......@@ -419,7 +420,7 @@ typedef void (*DisplayUrlCb)(struct _LinphoneCore *lc, const char *message, cons
/** Callback prototype */
typedef void (*LinphoneCoreCbFunc)(struct _LinphoneCore *lc,void * user_data);
/** Callback prototype */
typedef void (*NotifyReceivedCb)(struct _LinphoneCore *lc, const char *from, const char *msg);
typedef void (*NotifyReceivedCb)(struct _LinphoneCore *lc, LinphoneCall *call, const char *from, const char *event);
/** Callback prototype */
typedef void (*NotifyPresenceReceivedCb)(struct _LinphoneCore *lc, LinphoneFriend * fid);
/** Callback prototype */
......@@ -509,7 +510,7 @@ LinphoneCall * linphone_core_invite(LinphoneCore *lc, const char *url);
LinphoneCall * linphone_core_invite_address(LinphoneCore *lc, const LinphoneAddress *addr);
int linphone_core_refer(LinphoneCore *lc, LinphoneCall *call, const char *url);
int linphone_core_transfer_call(LinphoneCore *lc, LinphoneCall *call, const char *refer_to);
bool_t linphone_core_inc_invite_pending(LinphoneCore*lc);
......
......@@ -76,6 +76,7 @@ struct _LinphoneCall
struct _AudioStream *audiostream; /**/
struct _VideoStream *videostream;
char *refer_to;
bool_t refer_pending;
bool_t media_pending;
bool_t audio_muted;
};
......@@ -184,7 +185,7 @@ void linphone_core_update_progress(LinphoneCore *lc, const char *purpose, float
void linphone_core_stop_waiting(LinphoneCore *lc);
int linphone_core_start_invite(LinphoneCore *lc, LinphoneCall *call, LinphoneProxyConfig *dest_proxy);
void linphone_core_start_pending_refered_calls(LinphoneCore *lc);
extern SalCallbacks linphone_sal_callbacks;
......
......@@ -187,7 +187,7 @@ typedef void (*SalOnCallReceived)(SalOp *op);
typedef void (*SalOnCallRinging)(SalOp *op);
typedef void (*SalOnCallAccepted)(SalOp *op);
typedef void (*SalOnCallAck)(SalOp *op);
typedef void (*SalOnCallUpdated)(SalOp *op);
typedef void (*SalOnCallUpdating)(SalOp *op);/*< Called when a reINVITE is received*/
typedef void (*SalOnCallTerminated)(SalOp *op, const char *from);
typedef void (*SalOnCallFailure)(SalOp *op, SalError error, SalReason reason, const char *details, int code);
typedef void (*SalOnAuthRequested)(SalOp *op, const char *realm, const char *username);
......@@ -210,7 +210,7 @@ typedef struct SalCallbacks{
SalOnCallRinging call_ringing;
SalOnCallAccepted call_accepted;
SalOnCallAck call_ack;
SalOnCallUpdated call_updated;
SalOnCallUpdating call_updating;
SalOnCallTerminated call_terminated;
SalOnCallFailure call_failure;
SalOnAuthRequested auth_requested;
......@@ -273,6 +273,7 @@ void *sal_op_get_user_pointer(const SalOp *op);
int sal_call_set_local_media_description(SalOp *h, SalMediaDescription *desc);
int sal_call(SalOp *h, const char *from, const char *to);
int sal_call_notify_ringing(SalOp *h);
/*accept an incoming call or, during a call accept a reINVITE*/
int sal_call_accept(SalOp*h);
int sal_call_decline(SalOp *h, SalReason reason, const char *redirection /*optional*/);
int sal_call_hold(SalOp *h, bool_t holdon);
......
......@@ -284,8 +284,8 @@ void sal_set_callbacks(Sal *ctx, const SalCallbacks *cbs){
ctx->callbacks.call_failure=(SalOnCallFailure)unimplemented_stub;
if (ctx->callbacks.call_terminated==NULL)
ctx->callbacks.call_terminated=(SalOnCallTerminated)unimplemented_stub;
if (ctx->callbacks.call_updated==NULL)
ctx->callbacks.call_updated=(SalOnCallUpdated)unimplemented_stub;
if (ctx->callbacks.call_updating==NULL)
ctx->callbacks.call_updating=(SalOnCallUpdating)unimplemented_stub;
if (ctx->callbacks.auth_requested==NULL)
ctx->callbacks.auth_requested=(SalOnAuthRequested)unimplemented_stub;
if (ctx->callbacks.auth_success==NULL)
......@@ -773,35 +773,27 @@ static void handle_reinvite(Sal *sal, eXosip_event_t *ev){
sal_media_description_unref(op->base.remote_media);
op->base.remote_media=NULL;
}
eXosip_lock();
eXosip_call_build_answer(ev->tid,200,&msg);
eXosip_unlock();
if (msg==NULL) return;
if (op->base.root->session_expires!=0){
if (op->supports_session_timers) osip_message_set_supported(msg, "timer");
}
if (op->base.contact){
_osip_list_set_empty(&msg->contacts,(void (*)(void*))osip_contact_free);
osip_message_set_contact(msg,op->base.contact);
if (op->result){
sal_media_description_unref(op->result);
op->result=NULL;
}
if (sdp){
op->sdp_offering=FALSE;
op->base.remote_media=sal_media_description_new();
sdp_to_media_description(sdp,op->base.remote_media);
sdp_message_free(sdp);
sdp_process(op);
if (op->sdp_answer!=NULL){
set_sdp(msg,op->sdp_answer);
sdp_message_free(op->sdp_answer);
op->sdp_answer=NULL;
}
sal->callbacks.call_updating(op);
}else {
op->sdp_offering=TRUE;
set_sdp_from_desc(msg,op->base.local_media);
eXosip_lock();
eXosip_call_build_answer(ev->tid,200,&msg);
if (msg!=NULL){
set_sdp_from_desc(msg,op->base.local_media);
eXosip_call_send_answer(ev->tid,200,msg);
}
eXosip_unlock();
}
eXosip_lock();
eXosip_call_send_answer(ev->tid,200,msg);
eXosip_unlock();
}
static void handle_ack(Sal *sal, eXosip_event_t *ev){
......@@ -820,7 +812,7 @@ static void handle_ack(Sal *sal, eXosip_event_t *ev){
sdp_message_free(sdp);
}
if (op->reinvite){
sal->callbacks.call_updated(op);
if (sdp) sal->callbacks.call_updating(op);
op->reinvite=FALSE;
}else{
sal->callbacks.call_ack(op);
......
mediastreamer2 @ 6cc9076b
Subproject commit 236222b3f08baf502742b6c75633f50e3a14917f
Subproject commit 6cc9076b9cc4d6b88e7a0b93e6abdd1ad881e832
oRTP @ d2a8cb08
Subproject commit a084620745b1b1c81ec93501ffbb3de373f7c8c9
Subproject commit d2a8cb0890c7547703a092c839c55db10449ef92
......@@ -72,3 +72,5 @@ mediastreamer2/src/audiomixer.c
mediastreamer2/src/chanadapt.c
mediastreamer2/src/itc.c
mediastreamer2/src/extdisplay.c
mediastreamer2/src/msiounit.c
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment