Speeddial teams.xml

speeddial_teams.xml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
<templates>
    <template name="@string/speed_dial_title" prio="30" icon="@drawable/invite">
        <keyConfiguration>
        <lua>
            <code><![CDATA[local gUserpath = "/identities/identity["..pIdentity.."]/"
    local gSubscriptionIsTrying = false
    local gSubscriptionIsWorking  = false
    local gUser = "" -- username of associated identity
    local gDomain = "" -- domain (registrar) of associated identity
    local gPickupCode = nil -- pickup-code of associated identity
    local gDoPickupWithReplaces = false -- whether or not to use a replaces header (instead of pickup-code) to pick a call
    local gRemoteUri = nil -- sipUri-object of remote
    local gRemoteUser = nil -- user-part of remote uri
    local gOngoingSubscription = nil
    local gWithSubscription = pSubscriptionEnabled=="true"
    local gWithPickup = pDoPickup=="true"
    local gWithMissed = pBlinkOnMissed=="true"
    local gWithAutoAnswer = pRequestAutoAnswer=="true"
    local gHaveMissedCall = false
    -- remember what we're signaling to the user, this will influence what is expected when key is pressed
    -- one of:
    --   - errors-state: error, trying
    --   - subscription-event: idle, pickable, busy
    --   - local call: calling, active, holding, ringing
    --   - missed call: missed
    --   - nothing going on: idle
    local gLastReportedState = nil
    local gLocalCallId = nil -- wenn we have local call with gRemoteUser -> store its call-id here (e.g. TC@4)
    local gLocalCallState = nil -- wenn we have local call with gRemoteUser -> store its state here (calling, active, holding or ringing)
    local gFctName = "Blf" -- name for this key in logs (uri and identity will be added during initialization)
    -- keep infos of all monitored calls on our remote, only used when gWithSubscription==true
    --  contains these entries:
    --  - "entity" : attribute from dialog-info-xml
    --  - "version" : attribute from dialog-info-xml
    --  - "metaState" : one of free/pickable/busy -> combined meta-state accumulated from all dialogs (monitored calls)
    --  - "activeDialogs" : array of all dialog-ids of the all dialogs responsible for signaled meta-state.
    --  - "dialogs" : map by dialog-id of all dialogs (monitored calls)
    local gDialogInfo = nil
    local gTeamsFailedIdAquire = false
    local gTeamsReceivedServerResponse = false
    local gTeamsServerResponseWasGood = false
    local gTeamsServerResponse = 42
    local gTeamsState = "Unknown" -- one of: Available, Away, BeRightBack, Busy, DoNotDisturb, Offline, Unknown
    local gTeamsRefreshInterval = 17 -- poll for new state every xy seconds
    local gTeamsId = nil
    local gTeamsPresenceUrl = "https://graph.microsoft.com/beta/communications/presences/"
    local gTeamsFetchIdUrl = "https://graph.microsoft.com/beta/users/"

    local function isValid() -- reports if key has all it's params set to valid values. Set to nil to keep this default
        if pIdentity == "" or pIdentity == nil then
            return false
        end
        if pRemote == "" or pRemote == nil then
            return false
        end
        return true
    end

    -- Create value for replaces-header for first pickable dialog
    local function getFirstReplacesInfo()
        if #gDialogInfo.activeDialogs == 0 or not gDialogInfo.metaState == "pickable" then
            debug.log(gFctName..": cannot find pickable call for replaces", "e")
            return nil
        end
        local dlgXml = gDialogInfo.dialogs[gDialogInfo.activeDialogs[1]]
        local callId = dlgXml["call-id"]
        if not callId or string.len(callId) == 0 then
            debug.log(gFctName..": cannot find call-id for replaces-header", "e")
            return nil
        end
        return sipTools.mkReplaces{callId=callId, to=dlgXml["remote-tag"], from=dlgXml["local-tag"]}
    end

    -- analyze existing dialogs and find a text that describes who our monitored remote is talking to
    local function getBlfRemoteText()
        if #gDialogInfo.activeDialogs == 0 then
            return nil
        elseif #gDialogInfo.activeDialogs > 1 and not gDoPickupWithReplaces then
            -- wouldn't know which one would be picked
            return "multiple calls"
        end
        -- find something to name the pick-party:
        local remote = gDialogInfo.dialogs[gDialogInfo.activeDialogs[1]]:find("remote")
        if not remote then
            debug.log(gFctName..": missing <remote> within <dialog-info>, cannot name remote party", "w")
            return "@string/call_unknown"
        end
        local dispName = remote:find("identity", "display")
        if dispName and string.len(dispName.display) > 0 then return dispName.display end
        local remote_usr = remote:find("identity")
        if not remote_usr then remote_usr = remote:find("target") end
        if not remote_usr or not remote_usr[1] then
            debug.log(gFctName..": missing remote-uri within <dialog-info>, cannot name remote party", "w")
            return "@string/call_unknown"
        end
        if string.find(string.lower(remote_usr[1]), "anonymous") then
            return "@string/call_anonyme"
        end
        local remote_uri = sipTools.parseUri{sipUri=remote_usr[1]}
        if not remote_uri or not remote_uri:getUiNumber() then
            return remote_usr[1]
        end
        return remote_uri:getUiNumber()
    end

    local function updateKey()
        local desiredInfo = " "
        local desiredIcon = nil

        if not isValid() then
            gLastReportedState = "error"
            key:setLed("orange", false)
            desiredInfo = "setup failed"
            desiredIcon = "@drawable/block"
        else
            gLastReportedState = "idle"
            if gLocalCallState == "calling" then
                gLastReportedState = "calling"
                key:setLed("red", true)
                desiredInfo = "calling"
            elseif gLocalCallState == "ringing" then
                gLastReportedState = "ringing"
                key:setLed("red", true)
                desiredInfo = "ringing"
            elseif gLocalCallState == "holding" then
                gLastReportedState = "holding"
                key:setLed("red", true)
                desiredInfo = "holding"
            elseif gLocalCallState == "active" then
                gLastReportedState = "active"
                key:setLed("red", false)
                desiredInfo = "active"
            elseif gWithSubscription then
                if gSubscriptionIsWorking  then
                    if gDialogInfo.metaState == "busy" then
                        gLastReportedState = "busy"
                        key:setLed("red", false)
                        desiredInfo = getBlfRemoteText()
                    elseif gDialogInfo.metaState == "pickable" then
                        gLastReportedState = "pickable"
                        key:setLed("red", true)
                        desiredIcon = "@drawable/pick_up"
                        desiredInfo = getBlfRemoteText()
                    else
                        gLastReportedState = "idle"
                        key:setLed("off")
                    end
                elseif gSubscriptionIsTrying then
                    gLastReportedState = "trying"
                    key:setLed("orange", true)
                    desiredInfo = "trying to subscribe"
                else
                    gLastReportedState = "error"
                    key:setLed("orange", false)
                    desiredInfo = "failed to subscribe"
                end
            else
                key:setLed("off")
            end
            if gLastReportedState == "idle" and gTeamsState ~= "Unknown" then
                desiredInfo = gTeamsState
                if gTeamsState == "DoNotDisturb" or gTeamsState == "Busy" then
                    gLastReportedState = "busy"
                    key:setLed("red")
                elseif gTeamsState == "Available" then
                    gLastReportedState = "idle"
                    key:setLed("green")
                else -- Away, BeRightBack, Offline
                    gLastReportedState = "idle"
                    key:setLed("off")
                end
            end
            if gLastReportedState == "idle" and gHaveMissedCall then
                gLastReportedState = "missed"
                key:setLed("green", true)
                desiredInfo = "missed call"
            end
        end
        key:setInfo(desiredInfo)
        key:setIcon(desiredIcon)
    end

    function onDbResult(entries)
        gHaveMissedCall = false
        for i, queryParam in ipairs(entries) do
            local entryUser = sipTools.parseUri{sipUri=queryParam.number}:getUser()
            if entryUser == gRemoteUser then
                debug.log(gFctName..": found missed call from "..tostring(queryParam.number), "d")
                gHaveMissedCall = true
                break
            end
        end
        updateKey()
    end

    function onCallEvent(event, call1, call2)
        if event == nil or call1 == nil then
            debug.log(gFctName..": invalid parameters at callListChanged("..tostring(event).." ,"..tostring(call1)..")", "e")
            return
        end
        local callId = call1:getId()
        local state = call1:getState()
        local number, name = call1:getRemote()
        local regId = tostring(call1:getIdentityIndex())
        if regId ~= pIdentity then
            return
        end
        debug.log("received "..tostring(event).." for call to "..tostring(number)..", id="..tostring(regId)..", state="..tostring(state)..", callId="..tostring(callId), "v")
        local remoteUri = sipTools.parseUri{sipUri=number}
        local remoteUser = remoteUri:getUser()
        if remoteUser ~= gRemoteUser then
            if callId == gLocalCallId then
                debug.log("call-partner of local "..callId.." changed, call no longer belongs to "..gFctName, "d")
                gLocalCallId = nil
                gLocalCallState = nil
                updateKey()
            end
            return
        end
        if state == "calling" or state == "ringing" or state == "active" or state == "holding" then
            if gLocalCallId ~= callId then
                debug.log(gFctName.." found new local call "..callId.." in state "..state, "d")
            elseif gLocalCallState ~= state then
                debug.log(gFctName.." "..callId.." changed state to "..state, "d")
            end
            gLocalCallId = callId
            gLocalCallState = state
        else
            debug.log(gFctName.." "..callId.." ended", "d")
            gLocalCallId = nil
            gLocalCallState = nil
        end
        updateKey()
    end

    local function createDialogInfoStruct(dlgVers, dlgEntity)
        local result = {}
        result.entity = dlgEntity
        result.version = dlgVers
        result.metaState = "free"
        result.activeDialogs = {}
        result.dialogs = {}
        return result
    end

    local function handleNotify(subscr, data, headers)
        gSubscriptionIsWorking  = true
        gSubscriptionIsTrying = false
        local dlgInfo = xml.eval(data)
        if dlgInfo ~= nil then
            debug.log(gFctName.." received Notify", "d")
            if dlgInfo:tag() ~= "dialog-info" then
                debug.log(gFctName..": did not receive a dialog-info, ignoring "..tostring(dlgInfo:tag()) , "e")
                return
            end
            local dlgVers = dlgInfo.version
            local dlgEntity = dlgInfo.entity
            local dlgState = dlgInfo.state
            if not dlgEntity or not dlgVers or not dlgState then
                debug.log(gFctName..": received dialog-info is missing essencial attributes, ignoring dialog-info with "
                        ..tostring(dlgVers).." / "..tostring(dlgEntity).." / "..tostring(dlgState) , "e")
                return
            end
            if not gDialogInfo then
                -- assuming this is the first dialog-info we've received -> initialize gDialogInfo
                gDialogInfo = createDialogInfoStruct(dlgVers, dlgEntity)
            else
                if gDialogInfo.entity ~= dlgEntity then -- strange -> begin again
                    debug.log(gFctName..": entity in dialog-info changed, re-initializing gDialogInfo", "w")
                    gDialogInfo = createDialogInfoStruct(dlgVers, dlgEntity)
                elseif gDialogInfo.version >= dlgVers then
                    debug.log(gFctName..": received outdated dialog-info -> ignoring version "..tostring(dlgVers), "i")
                end
            end
            if dlgState == "full" then
                gDialogInfo.dialogs = {}
            end
            for _,dlg in ipairs(dlgInfo) do
                local dlgId = dlg.id
                if dlgId and string.len(dlgId) > 0 then
                gDialogInfo.dialogs[dlgId] = dlg
                end
            end
            -- evaluate metaState:
            local busyDlgs = {}
            local pickableDlgs = {}
            for dlgId, dlg in pairs(gDialogInfo.dialogs) do
                local stateXml = dlg:find("state")
                local state = nil
                if stateXml and #stateXml >= 1 then state = stateXml[1] end
                if state == "confirmed" then
                    busyDlgs[#busyDlgs+1] = dlgId
                elseif state == "terminated" then
                    -- ignore
                else
                    -- assuming proceeding, early oder trying
                    local direction = dlg.direction
                    if direction == "initiator" then
                        busyDlgs[#busyDlgs+1] = dlgId
                    else
                        pickableDlgs[#pickableDlgs+1] = dlgId
                    end
                end
            end
            if #pickableDlgs > 0 then
                gDialogInfo.activeDialogs = pickableDlgs
                gDialogInfo.metaState = "pickable"
            elseif #busyDlgs > 0 then
                gDialogInfo.activeDialogs = busyDlgs
                gDialogInfo.metaState = "busy"
            else
                gDialogInfo.activeDialogs = {}
                gDialogInfo.metaState = "free"
            end
            debug.log(gFctName..": state from Notify: "..gDialogInfo.metaState, "d")
        else
            debug.log(gFctName..": received empty notify from "..subscr:getUri() , "e")
        end
        updateKey()
    end

    local function unsubscribe()
        gSubscriptionIsWorking  = false
        gSubscriptionIsTrying = false
        if (gOngoingSubscription) then
            gOngoingSubscription:unsubscribe()
            gOngoingSubscription = nil
        end
    end

    local function subscriptionTerminated()
        gSubscriptionIsWorking  = false
        gSubscriptionIsTrying = false
        updateKey()
    end

    local function onSubscrIsTrying()
        gSubscriptionIsWorking  = false
        gSubscriptionIsTrying = true
        updateKey()
    end

    local function subscribe()
        unsubscribe()
        gOngoingSubscription = sip.subscribe{uri=pRemote, onNotify=handleNotify, line=pIdentity,
                                            onTerminated=subscriptionTerminated, onTrying=onSubscrIsTrying}
    end

    local function identityChanged(path)
        gPickupCode = config.get(gUserpath .. "pickupCode")
        gDoPickupWithReplaces = not gPickupCode or string.len(gPickupCode) == 0
        local new_user = config.get(gUserpath .. "username")
        local new_domain = sip.identities.getDomain(pIdentity);
        if new_user ~= gUser or new_domain ~= domain then
            unsubscribe()
            gUser = new_user
            gDomain = new_domain
            subscribe()
            updateKey()
        end
    end

    local function setupRegChange()
        config.register(gUserpath, identityChanged)
        identityChanged()
    end

    function onKeyUp()
        local headers = {}
        local doPickup = false

        debug.log(gFctName.."-key pressed", "d")
        if not isValid() then
            debug.log("missing parameter(s), "..gFctName.."-key won't work ever", "e")
            return
        elseif gLastReportedState == "error" then
            debug.log(gFctName..": subscription had failed before, attempting restart", "d")
            subscribe()
            updateKey()
            return
        elseif gLastReportedState == "trying" then
            debug.log(gFctName..": subscription is being tried: stopping and restarting", "d")
            unsubscribe()
            subscribe()
            updateKey()
            return
        elseif gLastReportedState == "calling" then
            debug.log(gFctName..": outgoing call thats not connected yet -> ignore key-press", "d")
            return
        elseif gLastReportedState == "active" then
            debug.log(gFctName..": connected call -> hold", "d")
            sip.calls.hold(gLocalCallId)
            return
        elseif gLastReportedState == "holding" then
            debug.log(gFctName..": held call -> retrieve", "d")
            sip.calls.resume(gLocalCallId)
            return
        elseif gLastReportedState == "ringing" then
            debug.log(gFctName..": ringing call -> accept", "d")
            sip.calls.accept(gLocalCallId)
            return
        elseif gLastReportedState == "pickable" then
            debug.log(gFctName..": picking the call", "d")
            local replHdr, replVal = getFirstReplacesInfo()
            if replHdr then -- always add replace, even with pickup-code .. cant hurt
                headers[replHdr] = replVal
            end
            if not gDoPickupWithReplaces then
                debug.log(gFctName..": picking with pickup-code '"..gPickupCode.."'", "d")
                doPickup = true
            else
                if replHdr then
                    debug.log(gFctName..": picking with Replaces-Header: "..replVal, "d")
                else
                    debug.log(gFctName..": cannot pick, replaces-info unavailable", "e")
                    return
                end
            end
        else
            debug.log(gFctName..": simply calling the remote (state: "..gLastReportedState..", autoAnswer: "..tostring(gWithAutoAnswer)..")", "d")
            if gWithAutoAnswer then
                headers['Alert-Info'] = '<http://192.168.182.156>;info=alert-autoanswer'
            end
        end
        sip.invite{uri=pRemote, line=pIdentity, hidden=false, headers=headers, pickup=doPickup}
    end

    --Analyzes the response of Microsoft and check if the user in question is available
    local function onTeamsPresenceStatus(responseCode, responseBody, responseHeaders)
        gTeamsReceivedServerResponse = true
        gTeamsServerResponseWasGood = responseCode == 200 and responseBody
        gTeamsServerResponse = responseCode
        if gTeamsServerResponseWasGood then
            values = json.eval(responseBody)
            if values["availability"] == "AvailableIdle" then
                gTeamsState = "Available"
            elseif values["availability"] == "PresenceUnknown" then
                gTeamsState = "Unknown"
            elseif values["availability"] == "BusyIdle" then
                gTeamsState = "Busy"
            else
                gTeamsState = values["availability"]
            end
        else
            gTeamsState = "Unknown"
            if responseCode == 401 then
                shared["msTeamsReAuthNeeded"] = "true"
            end
        end
        updateKey()
    end

    --Requests the teams status of someone
    local function requestTeamsPresenceStatus()
        gTeamsReceivedServerResponse = false
        local header = {}
        header["Authorization"] = "Bearer "..persisted["msTeamsToken"]
        http.get(gTeamsPresenceUrl..gTeamsId, onTeamsPresenceStatus, header)
    end

    -- read persons ID from response
    local function onTeamsIdResponse(responseCode, responseBody, responseHeaders)
        gTeamsServerResponse = responseCode
        if responseCode== 200 and responseBody then
            values = json.eval(responseBody)
            gTeamsId = values["id"]
            debug.log(gFctName..", Teams: got id"..gTeamsId, "v")
            requestTeamsPresenceStatus()
        else
            debug.log(gFctName..", Teams: failed to inquire id", "d")
            gTeamsFailedIdAquire = true
            if responseCode == 401 then
                shared["msTeamsReAuthNeeded"] = "true"
            end
        end
        updateKey()
    end

    -- if pTeamsId is an e-mail -> requests basic data from Microsoft to get the ID
    local function fetchTeamsId()
        x,y = string.find(pTeamsId, "@", 1, true)
        if x ~= nil then
            local header = {}
            header["Authorization"] = "Bearer "..persisted["msTeamsToken"]
            http.get(gTeamsFetchIdUrl..pTeamsId, onTeamsIdResponse, header)
        else
            gTeamsId = pTeamsId
            debug.log(gFctName..", Teams: got id"..gTeamsId, "v")
            requestTeamsPresenceStatus()
        end
    end

    local function teamsRefreshLoop()
        if not persisted["msTeamsToken"] then
            debug.log(gFctName..", Teams: not ready yet", "d")
        elseif not pTeamsId then
            debug.log(gFctName..", Teams: disabled", "d")
            return
        elseif not gTeamsId then
            debug.log(gFctName..", Teams: fetch id", "v")
            fetchTeamsId()
        else
            requestTeamsPresenceStatus()
        end
        time.callbackIn(teamsRefreshLoop, gTeamsRefreshInterval)
    end

    local function initialize()
        if isValid() then
            setupRegChange()
        else
            debug.log("missing parameters, "..gFctName.."-key won't work", "e")
            updateKey()
            return
        end
        gRemoteUri = sipTools.parseUri{sipUri=pRemote, fromUserInput=true, idIdx=pIdentity}
        gRemoteUser = gRemoteUri:getUser()
        local dbQuery = "(is_read=0 OR is_read is NULL ) AND new=1 AND subscription_id="..pIdentity.." AND number LIKE '%"..gRemoteUser.."%' AND (type=3 OR type=5)"
        database.subscribe{dbUri="content://call_log/calls", onChanged=onDbResult, query=dbQuery, columns={"number"}}
        sip.calls.listen(onCallEvent)
        gFctName = "BLF["..gRemoteUri:getUiNumber().." on id "..pIdentity.."]"
        teamsRefreshLoop()
        updateKey()
    end

    initialize()]]></code>
            <params>
            <param name="pRemote"/>
            <param name="pIdentity"/>
            <param name="pSubscriptionEnabled"><value>true</value></param>
            <param name="pDoPickup"><value>true</value></param>
            <param name="pRequestAutoAnswer"><value>false</value></param>
            <param name="pBlinkOnMissed"><value>true</value></param>
            <param name="pTeamsId"/>
            </params>
        </lua>
        </keyConfiguration>
        <parameters>
        <parameter name="@string/uri" type="text">
            <path>//param[@name="pRemote"]/value</path>
        </parameter>
        <parameter optional="true" name="@string/identity" type="identity">
            <path>//param[@name="pIdentity"]/value</path>
        </parameter>
        <parameter optional="true" name="@string/subscription_enabled" type="boolean">
            <path>//param[@name="pSubscriptionEnabled"]/value</path>
        </parameter>
        <parameter optional="true" name="@string/do_pickup" type="boolean">
            <path>//param[@name="pDoPickup"]/value</path>
        </parameter>
        <parameter optional="true" name="@string/auto_answer" type="boolean">
            <path>//param[@name="pRequestAutoAnswer"]/value</path>
        </parameter>
        <parameter optional="true" name="@string/with_calllog" type="boolean">
            <path>//param[@name="pBlinkOnMissed"]/value</path>
        </parameter>
        <parameter optional="true" name="E-Mail" type="text">
            <path>//param[@name="pTeamsId"]/value</path>
        </parameter>
        </parameters>
    </template>
</templates>