<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>