Implemented ticket #192 for Python: Add callback to notify application about incoming SUBSCRIBE request, and add subscription state and termination reason in buddy info

git-svn-id: https://svn.pjsip.org/repos/pjproject/trunk@2156 74dad513-b988-da41-8d7b-12977e46ad98
diff --git a/pjsip-apps/src/python/_pjsua.c b/pjsip-apps/src/python/_pjsua.c
index ab55077..6590dd5 100644
--- a/pjsip-apps/src/python/_pjsua.c
+++ b/pjsip-apps/src/python/_pjsua.c
@@ -340,6 +340,58 @@
     }
 }
 
+/* 
+ * cb_on_incoming_subscribe
+ */
+static void cb_on_incoming_subscribe( pjsua_acc_id acc_id,
+				      pjsua_srv_pres *srv_pres,
+				      pjsua_buddy_id buddy_id,
+				      const pj_str_t *from,
+				      pjsip_rx_data *rdata,
+				      pjsip_status_code *code,
+				      pj_str_t *reason,
+				      pjsua_msg_data *msg_data)
+{
+    static char reason_buf[64];
+
+    PJ_UNUSED_ARG(rdata);
+    PJ_UNUSED_ARG(msg_data);
+
+    if (PyCallable_Check(g_obj_callback->on_incoming_subscribe))
+    {
+	PyObject *ret;
+
+	ENTER_PYTHON();
+
+        ret = PyObject_CallFunctionObjArgs(
+	    g_obj_callback->on_incoming_subscribe,
+	    Py_BuildValue("i", acc_id),
+	    Py_BuildValue("i", buddy_id),
+	    PyString_FromStringAndSize(from->ptr, from->slen),
+	    PyLong_FromLong((long)srv_pres),
+	    NULL
+	);
+
+	if (ret && PyTuple_Check(ret)) {
+	    if (PyTuple_Size(ret) >= 1)
+		*code = (int)PyInt_AsLong(PyTuple_GetItem(ret, 0));
+	    if (PyTuple_Size(ret) >= 2) {
+		if (PyTuple_GetItem(ret, 1) != Py_None) {
+		    pj_str_t tmp;
+		    tmp = PyString_to_pj_str(PyTuple_GetItem(ret, 1));
+		    reason->ptr = reason_buf;
+		    pj_strncpy(reason, &tmp, sizeof(reason_buf));
+		} else {
+		}
+	    }
+
+	} else if (ret) {
+	    Py_XDECREF(ret);
+	}
+
+	LEAVE_PYTHON();
+    }
+}
 
 /*
  * cb_on_buddy_state
@@ -371,6 +423,8 @@
                         const pj_str_t *mime_type, const pj_str_t *body,
 			pjsip_rx_data *rdata, pjsua_acc_id acc_id)
 {
+    PJ_UNUSED_ARG(rdata);
+
     if (PyCallable_Check(g_obj_callback->on_pager))
     {
 	ENTER_PYTHON();
@@ -911,6 +965,7 @@
     	cfg_ua.cb.on_call_replace_request = &cb_on_call_replace_request;
     	cfg_ua.cb.on_call_replaced = &cb_on_call_replaced;
     	cfg_ua.cb.on_reg_state = &cb_on_reg_state;
+	cfg_ua.cb.on_incoming_subscribe = &cb_on_incoming_subscribe;
     	cfg_ua.cb.on_buddy_state = &cb_on_buddy_state;
     	cfg_ua.cb.on_pager2 = &cb_on_pager;
     	cfg_ua.cb.on_pager_status2 = &cb_on_pager_status;
@@ -1879,6 +1934,66 @@
 }
 
 
+/*
+ * py_pjsua_acc_pres_notify
+ */
+static PyObject *py_pjsua_acc_pres_notify
+(PyObject *pSelf, PyObject *pArgs)
+{
+    static char reason_buf[64];
+    int acc_id, state;
+    PyObject *arg_pres, *arg_msg_data;
+    void *srv_pres;
+    pjsua_msg_data msg_data;
+    const char *arg_reason;
+    pj_str_t reason;
+    pj_bool_t with_body;
+    pj_pool_t *pool = NULL;
+    int status;	
+    
+    PJ_UNUSED_ARG(pSelf);
+
+    if (!PyArg_ParseTuple(pArgs, "iOisO", &acc_id, &arg_pres, 
+			  &state, &arg_reason, &arg_msg_data))
+    {
+        return NULL;
+    }	
+    
+    srv_pres = (void*) PyLong_AsLong(arg_pres);
+    pjsua_msg_data_init(&msg_data);
+    with_body = (state != PJSIP_EVSUB_STATE_TERMINATED);
+
+    if (arg_reason) {
+	strncpy(reason_buf, arg_reason, sizeof(reason_buf));
+	reason.ptr = reason_buf;
+	reason.slen = strlen(arg_reason);
+    } else {
+	reason = pj_str("");
+    }
+
+    if (arg_msg_data && arg_msg_data != Py_None) {
+        PyObj_pjsua_msg_data *omd = (PyObj_pjsua_msg_data *)arg_msg_data;
+        msg_data.content_type.ptr = PyString_AsString(omd->content_type);
+        msg_data.content_type.slen = PyString_Size(omd->content_type);
+        msg_data.msg_body.ptr = PyString_AsString(omd->msg_body);
+        msg_data.msg_body.slen = PyString_Size(omd->msg_body);
+        pool = pjsua_pool_create("pytmp", POOL_SIZE, POOL_SIZE);
+        translate_hdr(pool, &msg_data.hdr_list, omd->hdr_list);
+    } else if (arg_msg_data) {
+	Py_XDECREF(arg_msg_data);    
+    }
+
+    status = pjsua_pres_notify(acc_id, (pjsua_srv_pres*)srv_pres,
+			       (pjsip_evsub_state)state, NULL,
+			       &reason, with_body, &msg_data);
+    
+    if (pool) {
+	pj_pool_release(pool);
+    }
+
+    return Py_BuildValue("i", status);
+}
+
 static char pjsua_acc_config_default_doc[] =
     "_pjsua.Acc_Config _pjsua.acc_config_default () "
     "Call this function to initialize account config with default values.";
@@ -5542,6 +5657,10 @@
         pjsua_acc_get_info_doc
     },
     {
+	"acc_pres_notify", py_pjsua_acc_pres_notify, METH_VARARGS,
+	"Accept or reject subscription request"
+    },
+    {
         "enum_accs", py_pjsua_enum_accs, METH_VARARGS,
         pjsua_enum_accs_doc
     },
diff --git a/pjsip-apps/src/python/_pjsua.h b/pjsip-apps/src/python/_pjsua.h
index 269c30d..8c16c0f 100644
--- a/pjsip-apps/src/python/_pjsua.h
+++ b/pjsip-apps/src/python/_pjsua.h
@@ -507,6 +507,7 @@
     PyObject * on_call_replace_request;
     PyObject * on_call_replaced;
     PyObject * on_reg_state;
+    PyObject * on_incoming_subscribe;
     PyObject * on_buddy_state;
     PyObject * on_pager;
     PyObject * on_pager_status;
@@ -529,6 +530,7 @@
     Py_XDECREF(self->on_call_replace_request);
     Py_XDECREF(self->on_call_replaced);
     Py_XDECREF(self->on_reg_state);
+    Py_XDECREF(self->on_incoming_subscribe);
     Py_XDECREF(self->on_buddy_state);
     Py_XDECREF(self->on_pager);
     Py_XDECREF(self->on_pager_status);
@@ -616,6 +618,8 @@
             Py_DECREF(Py_None);
             return NULL;
         }
+	Py_INCREF(Py_None);
+        self->on_incoming_subscribe = Py_None;
         Py_INCREF(Py_None);
         self->on_buddy_state = Py_None;
         if (self->on_buddy_state == NULL)
@@ -719,6 +723,11 @@
         "may then query the account info to get the registration details."
     },
     {
+        "on_incoming_subscribe", T_OBJECT_EX,
+        offsetof(PyObj_pjsua_callback, on_incoming_subscribe), 0,
+        "Notification when incoming SUBSCRIBE request is received."
+    },
+    {
         "on_buddy_state", T_OBJECT_EX,
         offsetof(PyObj_pjsua_callback, on_buddy_state), 0,
         "Notify application when the buddy state has changed. Application may "
@@ -2768,6 +2777,8 @@
     PyObject	*status_text;
     int		 monitor_pres;
     int		 activity;
+    int		 sub_state;
+    PyObject	*sub_term_reason;
 } PyObj_pjsua_buddy_info;
 
 
@@ -2781,6 +2792,7 @@
     Py_XDECREF(self->uri);
     Py_XDECREF(self->contact);
     Py_XDECREF(self->status_text);
+    Py_XDECREF(self->sub_term_reason);
     
     self->ob_type->tp_free((PyObject*)self);
 }
@@ -2800,6 +2812,10 @@
 						  info->status_text.slen);
     obj->monitor_pres = info->monitor_pres;
     obj->activity = info->rpid.activity;
+    obj->sub_state = info->sub_state;
+    Py_XDECREF(obj->sub_term_reason);
+    obj->sub_term_reason = PyString_FromStringAndSize(info->sub_term_reason.ptr, 
+						      info->sub_term_reason.slen);
 }
 
 
@@ -2823,7 +2839,7 @@
         if (self->uri == NULL) {
             Py_DECREF(self);
             return NULL;
-        }        
+        }
 	self->contact = PyString_FromString("");
         if (self->contact == NULL) {
             Py_DECREF(self);
@@ -2834,7 +2850,7 @@
             Py_DECREF(self);
             return NULL;
         }
-	
+	self->sub_term_reason = PyString_FromString("");
     }
     return (PyObject *)self;
 }
@@ -2882,6 +2898,16 @@
         offsetof(PyObj_pjsua_buddy_info, activity), 0,
         "Activity type. "
     },
+    {
+        "sub_state", T_INT, 
+        offsetof(PyObj_pjsua_buddy_info, sub_state), 0,
+        "Subscription state."
+    },
+    {
+        "sub_term_reason", T_INT, 
+        offsetof(PyObj_pjsua_buddy_info, sub_term_reason), 0,
+        "Subscription termination reason."
+    },
     
     
     {NULL}  /* Sentinel */
diff --git a/pjsip-apps/src/python/pjsua.py b/pjsip-apps/src/python/pjsua.py
index df7d735..6506944 100644
--- a/pjsip-apps/src/python/pjsua.py
+++ b/pjsip-apps/src/python/pjsua.py
@@ -147,13 +147,13 @@
     
     Member documentation:
 
-    NONE        -- media is not available.
+    NULL        -- media is not available.
     ACTIVE      -- media is active.
     LOCAL_HOLD  -- media is put on-hold by local party.
     REMOTE_HOLD -- media is put on-hold by remote party.
     ERROR       -- media error (e.g. ICE negotiation failure).
     """
-    NONE = 0
+    NULL = 0
     ACTIVE = 1
     LOCAL_HOLD = 2
     REMOTE_HOLD = 3
@@ -165,12 +165,12 @@
     
     Member documentation:
 
-    NONE              -- media is not active
+    NULL              -- media is not active
     ENCODING          -- media is active in transmit/encoding direction only.
     DECODING          -- media is active in receive/decoding direction only
     ENCODING_DECODING -- media is active in both directions.
     """
-    NONE = 0
+    NULL = 0
     ENCODING = 1
     DECODING = 2
     ENCODING_DECODING = 3
@@ -189,6 +189,20 @@
     AWAY = 1
     BUSY = 2
 
+
+class SubscriptionState:
+    """Presence subscription state constants.
+
+    """
+    NULL = 0
+    SENT = 1
+    ACCEPTED = 2
+    PENDING = 3
+    ACTIVE = 4
+    TERMINATED = 5
+    UNKNOWN = 6
+
+
 class TURNConnType:
     """These constants specifies the connection type to TURN server.
     
@@ -861,11 +875,38 @@
         Unless this callback is implemented, the default behavior is to
         reject the call with default status code.
 
-    Keyword arguments:
-    call    -- the new incoming call
+        Keyword arguments:
+        call    -- the new incoming call
         """
         call.hangup()
 
+    def on_incoming_subscribe(self, buddy, from_uri, pres_obj):
+        """Notification when incoming SUBSCRIBE request is received. 
+        
+        Application may use this callback to authorize the incoming 
+        subscribe request (e.g. ask user permission if the request 
+        should be granted)
+
+        Keyword arguments:
+        buddy       -- The buddy object, if buddy is found. Otherwise
+                       the value is None.
+        from_uri    -- The URI string of the sender.
+        pres_obj    -- Opaque presence subscription object, which is
+                       needed by Account.pres_notify()
+
+        Return:
+            Tuple (code, reason), where:
+             code:      The status code. If code is >= 300, the
+                        request is rejected. If code is 200, the
+                        request is accepted and NOTIFY will be sent
+                        automatically. If code is 202, application
+                        must accept or reject the request later with
+                        Account.press_notify().
+             reason:    Optional reason phrase, or None to use the
+                        default reasoh phrase for the status code.
+        """
+        return (200, None)
+
     def on_pager(self, from_uri, contact, mime_type, body):
         """
         Notification that incoming instant message is received on
@@ -951,7 +992,7 @@
         lib -- the Lib instance.
         id  -- the pjsua account ID.
         """
-        _cb = AccountCallback(self)
+        self._cb = AccountCallback(self)
         self._id = id
         self._lib = lib
         self._lib._associate_account(self._id, self)
@@ -1101,6 +1142,19 @@
         self._lib._err_check("add_buddy()", self, err)
         return Buddy(self._lib, buddy_id, self)
 
+    def pres_notify(self, pres_obj, state, reason="", hdr_list=None):
+        """Send NOTIFY to inform account presence status or to terminate
+        server side presence subscription.
+        
+        Keyword arguments:
+        pres_obj    -- The subscription object from on_incoming_subscribe()
+                       callback
+        state       -- Subscription state, from SubscriptionState
+        reason      -- Optional reason phrase.
+        hdr_list    -- Optional header list.
+        """
+        _pjsua.acc_pres_notify(self._id, pres_obj, state, reason, 
+                               Lib._create_msg_data(hdr_list))
 
 class CallCallback:
     """Class to receive event notification from Call objects. 
@@ -1275,8 +1329,8 @@
     state_text = ""
     last_code = 0
     last_reason = ""
-    media_state = MediaState.NONE
-    media_dir = MediaDir.NONE
+    media_state = MediaState.NULL
+    media_dir = MediaDir.NULL
     conf_slot = -1
     call_time = 0
     total_time = 0
@@ -1529,6 +1583,9 @@
     activity        -- the PresenceActivity
     subscribed      -- specify whether buddy's presence status is currently
                        being subscribed.
+    sub_state       -- SubscriptionState
+    sub_term_reason -- The termination reason string of the last presence
+                       subscription to this buddy, if any.
     """
     uri = ""
     contact = ""
@@ -1536,6 +1593,8 @@
     online_text = ""
     activity = PresenceActivity.UNKNOWN
     subscribed = False
+    sub_state = SubscriptionState.NULL
+    sub_term_reason = ""
 
     def __init__(self, pjsua_bi=None):
         if pjsua_bi:
@@ -1548,6 +1607,8 @@
         self.online_text = inf.status_text
         self.activity = inf.activity
         self.subscribed = inf.monitor_pres
+        self.sub_state = inf.sub_state
+        self.sub_term_reason = inf.sub_term_reason
 
 
 class BuddyCallback:
@@ -1866,6 +1927,7 @@
         py_ua_cfg.cb.on_call_replace_request = _cb_on_call_replace_request
         py_ua_cfg.cb.on_call_replaced = _cb_on_call_replaced
         py_ua_cfg.cb.on_reg_state = _cb_on_reg_state
+        py_ua_cfg.cb.on_incoming_subscribe = _cb_on_incoming_subscribe
         py_ua_cfg.cb.on_buddy_state = _cb_on_buddy_state
         py_ua_cfg.cb.on_pager = _cb_on_pager
         py_ua_cfg.cb.on_pager_status = _cb_on_pager_status
@@ -2266,11 +2328,9 @@
         self.buddy_by_uri[(uri.user, uri.host)] = buddy
 
     def _lookup_buddy(self, buddy_id, uri=None):
-        print "lookup_buddy, id=", buddy_id
         buddy = self.buddy.has_key(buddy_id) and self.buddy[buddy_id] or None
         if uri and not buddy:
             sip_uri = SIPUri(uri)
-            print "lookup_buddy, uri=", sip_uri.user, sip_uri.host
             buddy = self.buddy_by_uri.has_key( (sip_uri.user, sip_uri.host) ) \
                     and self.buddy_by_uri[(sip_uri.user, sip_uri.host)] or \
                     None
@@ -2289,6 +2349,14 @@
         if acc:
             acc._cb.on_reg_state()
 
+    def _cb_on_incoming_subscribe(self, acc_id, buddy_id, from_uri, pres_obj):
+        acc = self._lookup_account(acc_id)
+        if acc:
+            buddy = self._lookup_buddy(buddy_id)
+            return acc._cb.on_incoming_subscribe(buddy, from_uri, pres_obj)
+        else:
+            return (404, None)
+
     def _cb_on_incoming_call(self, acc_id, call_id, rdata):
         acc = self._lookup_account(acc_id)
         if acc:
@@ -2424,6 +2492,9 @@
 def _cb_on_reg_state(acc_id):
     _lib._cb_on_reg_state(acc_id)
 
+def _cb_on_incoming_subscribe(acc_id, buddy_id, from_uri, pres):
+    return _lib._cb_on_incoming_subscribe(acc_id, buddy_id, from_uri, pres)
+
 def _cb_on_buddy_state(buddy_id):
     _lib._cb_on_buddy_state(buddy_id)
 
diff --git a/pjsip-apps/src/python/samples/subscribe.py b/pjsip-apps/src/python/samples/subscribe.py
new file mode 100644
index 0000000..e48aeee
--- /dev/null
+++ b/pjsip-apps/src/python/samples/subscribe.py
@@ -0,0 +1,94 @@
+# $Id$
+#
+# Authorization of incoming subscribe request
+#
+# Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
+#
+import sys
+import pjsua as pj
+
+LOG_LEVEL = 3
+
+pending_pres = None
+
+def log_cb(level, str, len):
+    print str,
+
+class MyAccountCallback(pj.AccountCallback):
+    def __init__(self, account):
+        pj.AccountCallback.__init__(self, account)
+
+    def on_incoming_subscribe(self, buddy, from_uri, pres):
+        # Allow buddy to subscribe to our presence
+        global pending_pres
+
+        if buddy:
+            return (200, None)
+        print 'Incoming SUBSCRIBE request from', from_uri
+        print 'Press "A" to accept and add, "R" to reject the request'
+        pending_pres = pres
+        return (202, None)
+
+
+lib = pj.Lib()
+
+try:
+    # Init library with default config and some customized
+    # logging config.
+    lib.init(log_cfg = pj.LogConfig(level=LOG_LEVEL, callback=log_cb))
+
+    # Create UDP transport which listens to any available port
+    transport = lib.create_transport(pj.TransportType.UDP, 
+                                     pj.TransportConfig(0))
+    print "\nListening on", transport.info().host, 
+    print "port", transport.info().port, "\n"
+    
+    # Start the library
+    lib.start()
+
+    # Create local account
+    acc = lib.create_account_for_transport(transport)
+    acc.set_callback(MyAccountCallback(acc))
+
+    my_sip_uri = "sip:" + transport.info().host + \
+                 ":" + str(transport.info().port)
+
+    buddy = None
+
+    # Menu loop
+    while True:
+        print "My SIP URI is", my_sip_uri
+        print "Menu:  t=toggle online status, q=quit"
+
+        input = sys.stdin.readline().rstrip("\r\n")
+
+        if input == "t":
+            acc.set_basic_status(not acc.info().online_status)
+
+        elif input == "A":
+            if pending_pres:
+                acc.pres_notify(pending_pres, pj.SubscriptionState.ACTIVE)
+                pending_pres = None
+            else:
+                print "No pending request"
+
+        elif input == "R":
+            if pending_pres:
+                acc.pres_notify(pending_pres, pj.SubscriptionState.TERMINATED,
+                                "rejected")
+                pending_pres = None
+            else:
+                print "No pending request"
+        
+        elif input == "q":
+            break
+
+    # Shutdown the library
+    lib.destroy()
+    lib = None
+
+except pj.Error, e:
+    print "Exception: " + str(e)
+    lib.destroy()
+    lib = None
+