DIRECTORY Commander USING [ CommandProc, Register ], FinchSmarts, GList, GVBasics USING [ MakeKey, Password ], IO, LarkFeepRpcControl USING [ Feep, ImportNewInterface ], LupineRuntime USING [ BindingError ], MBQueue USING [ Create, QueueClientAction ], NameDB USING [ GetAttribute, SetAttribute ], NamesRPC USING [ StartConversation ], Nice, Process USING [ Detach, SecondsToTicks, SetTimeout, Ticks ], PupSocket USING [ GetUniqueID ], RefID USING [ ID ], Rope, RPC USING [ AuthenticateFailed, CallFailed, EncryptionKey, ImportFailed, VersionRange ], ThParty USING [ Advance, Alert, CheckIn, ConversationInfo, CreateConversation, Deregister, DescribeParty, Enable, GetConversationInfo, GetParty, GetNumbersForRName, GetPartyFromNumber, GetPartyInfo, LookupServiceInterface, Register, Unvisit, Visit ], ThPartyRpcControl, Thrush USING[ ActionReport, ConversationID, ConvEvent, Credentials, InterfaceSpec, NB, none, notReallyInConv, nullConvID, nullID, PartyID, Reason, ROPE, SHHH, SmartsID, StateInConv, unencrypted ], ThSmarts, ThSmartsRpcControl, ThVersions USING [ GetThrushVR, FinchVersion, FinchVR ], UserProfile USING [ Token], VoiceUtils USING [ CurrentPasskey, CurrentPassword, CurrentRName, InstanceFromNetAddress, MakeRName, NetAddress, NetAddressFromRope, OwnNetAddress, Problem, ProblemFR, Registrize, Report, ReportFR ] ; FinchSmartsImpl: CEDAR MONITOR IMPORTS Commander, GList, GVBasics, IO, LarkFeepRpcControl, LupineRuntime, MBQueue, NameDB, NamesRPC, Nice, Process, PupSocket, Rope, RPC, ThParty, ThPartyRpcControl, ThSmartsRpcControl, ThVersions, UserProfile, VoiceUtils EXPORTS FinchSmarts, ThSmarts = { OPEN IO; ConvDesc: TYPE = FinchSmarts.ConvDesc; ConversationID: TYPE = Thrush.ConversationID; nullConvID: ConversationID = Thrush.nullConvID; FinchInfo: TYPE = FinchSmarts.FinchInfo; NB: TYPE = Thrush.NB; none: SHHH = Thrush.none; nullID: RefID.ID = Thrush.nullID; PartyID: TYPE = Thrush.PartyID; ROPE: TYPE = Thrush.ROPE; SHHH: TYPE = Thrush.SHHH; SmartsID: TYPE = Thrush.SmartsID; StateInConv: TYPE = Thrush.StateInConv; nullInterfaceSpec: Thrush.InterfaceSpec = [serviceID: nullID]; finchPollerWait: CONDITION; PD: TYPE = RECORD [ doReports: BOOL_FALSE, doNice: BOOL_FALSE, timeoutPoller: INTEGER _ 15, timeoutNoAction: INTEGER _ 30, timeoutJayConnect: INTEGER _ 1, noJayTicks: Process.Ticks _ Process.SecondsToTicks[1], keyDistTicks: Process.Ticks _ Process.SecondsToTicks[10], encryptionRequested: BOOLEAN_TRUE, interfacesAreImported: BOOLEAN_FALSE, smartsIsExported: BOOLEAN_FALSE, waitsForConnect: NAT_6, queueIt: BOOL_FALSE, finchOn: BOOLEAN_FALSE, maxProseLength: INT_1000, systemStateSubscribers: LIST OF FinchSmarts.ReportSystemStateProc, convStateSubscribers: LIST OF FinchSmarts.ReportConversationStateProc, requestStateSubscribers: LIST OF FinchSmarts.ReportRequestStateProc ]; info: FinchInfo; pd: REF PD _ NEW[PD_[]]; Report: PROC[what: ROPE] = { IF NOT pd.doReports THEN RETURN; VoiceUtils.Report[what, $Finch]; }; BeNice: PROC[r: REF, d: INT] = { IF NOT pd.doNice THEN RETURN; Nice.BeNice[r, d, $Finch, NIL]; }; NConcConvs: PROC[l1, l2: LIST OF ConvDesc] RETURNS [LIST OF ConvDesc] = INLINE { RETURN[NARROW[GList.Nconc[l1, l2]]]; }; DequeueConv: PROC[l1: LIST OF ConvDesc, c: ConvDesc] RETURNS [LIST OF ConvDesc] = { IF l1=NIL THEN RETURN[l1]; -- No list IF l1.first=c THEN RETURN[l1.rest]; -- Head of list FOR convs: LIST OF ConvDesc _ l1, convs.rest WHILE l1.rest#NIL DO IF l1.rest.first=c THEN { l1.rest _ l1.rest.rest; RETURN[l1]; }; -- Internal to list ENDLOOP; RETURN[l1]; -- Not in list }; Progress: PUBLIC ENTRY PROC[ shh: SHHH, convEvent: Thrush.ConvEvent ] = { ENABLE UNWIND => NULL; -- RestoreInvariant; IF convEvent.self.partyID = convEvent.other.partyID THEN [] _ NoteNewState[convEvent]; }; Substitution: PUBLIC ENTRY PROC[ shh: SHHH, convEvent: Thrush.ConvEvent, oldPartyID: Thrush.PartyID, newPartyID: Thrush.PartyID ] = { ENABLE UNWIND => NULL; -- RestoreInvariant; cDesc: ConvDesc = GetConv[convEvent.self.convID, FALSE]; IF cDesc#NIL THEN cDesc.numParties _ 0; -- This will force the information update. [] _ NoteNewState[ convEvent ]; }; ReportAction: PUBLIC ENTRY PROC[ shh: SHHH, report: Thrush.ActionReport ] = { ENABLE UNWIND => NULL; ReportRequestState[GetConv[report.self.convID], report]; BROADCAST info.stateChange; -- Sometimes callers wait (Ugh!)} }; CallInfo: TYPE = REF CallInfoBody; -- needed because MBQueue requires a REF CallInfoBody: TYPE = RECORD [ convID: Thrush.ConversationID, calledPartyID: Thrush.PartyID_nullID, reason: Thrush.Reason_NIL, comment: ROPE_NIL ]; DisconnectCall: PUBLIC ENTRY PROC[ convID: Thrush.ConversationID, reason: Thrush.Reason _ $terminating, comment: ROPE_NIL, newState: StateInConv _ $idle] = { ENABLE UNWIND => NULL; -- RestoreInvariant; cDesc: ConvDesc; state: StateInConv; IF NOT(([,cDesc, state]_FinchOn[convID]).on) THEN RETURN; SELECT state FROM $idle, $neverWas => { ReportConversationState[$convNotActive, NIL, "No conversation to disconnect"]; }; ENDCASE => []_Request[cDesc, newState, reason, comment]; }; AnswerCall: PUBLIC ENTRY PROC[convID: Thrush.ConversationID] = { ENABLE UNWIND => NULL; -- RestoreInvariant; cDesc: ConvDesc; state: StateInConv; [,cDesc, state]_FinchOn[convID]; SELECT state FROM $ringing, $notified => []_Request[cDesc, $active]; ENDCASE; }; PlaceCall: PUBLIC ENTRY PROC [ convID: Thrush.ConversationID_nullConvID, rName: ROPE, -- rName or description number: ROPE, -- telephone number, if present useNumber: BOOL_FALSE ] = { ENABLE { UNWIND=>NULL; -- RestoreInvariant; RPC.CallFailed => { UninitFinchSmarts["Communication Failure"]; CONTINUE; }; }; calledPartyID: PartyID; nb: NB; reason: Thrush.Reason _ $notFound; comment: Rope.ROPE _ NIL; IF ~useNumber THEN { [nb, calledPartyID] _ThParty.GetParty[shh:info.shh, partyID:info.partyID, rName:rName]; SELECT nb FROM $noSuchParty2, $noIdentSupplied => useNumber _ TRUE; ENDCASE; }; IF useNumber AND number.Length[]>0 THEN [nb, calledPartyID] _ ThParty.GetPartyFromNumber[ shh: info.shh, partyID: info.partyID, phoneNumber: number, description: rName]; SELECT nb FROM $success => reason _ NIL; -- reason: NIL, comment: NIL $noSuchParty2 => NULL; -- reason: $notFound, comment: NIL $narcissism => comment _ "Attempt to call self rejected on philosophical grounds"; ENDCASE => { ReportConversationState[nb, NIL, NIL]; RETURN; }; [--nb, cDesc--]_Connect[calledPartyID, convID, reason, comment]; }; Feep: PUBLIC ENTRY PROC[convID: ConversationID, feepString: ROPE] = { cDesc: ConvDesc; nb: Thrush.NB_$success; state: StateInConv; IF NOT(([,cDesc, state]_FinchOn[convID]).on) OR state#$active THEN nb _ $convNotActive; IF nb=$success AND cDesc.feepInterface=NIL THEN { IF cDesc.numParties>1 THEN [nb, cDesc.feepInterfaceSpec] _ ThParty.LookupServiceInterface[ info.shh, cDesc.situation.self, cDesc.partyInfo[1].partyID, "LarkFeep"] ELSE nb_$noFeepingParty; IF nb=$success THEN cDesc.feepInterface _ LarkFeepRpcControl.ImportNewInterface[ interfaceName: cDesc.feepInterfaceSpec.interfaceName, hostHint: cDesc.feepInterfaceSpec.hostHint!RPC.ImportFailed => CONTINUE ]; }; IF nb=$success THEN nb _ LarkFeepRpcControl.Feep[ interface: cDesc.feepInterface, shhh: info.shh, serviceID: cDesc.feepInterfaceSpec.serviceID, convID: cDesc.situation.self.convID, requestingParty: cDesc.situation.self.partyID, number: feepString, noisy: TRUE, on: 100, off: 80 ]; IF nb # $success THEN VoiceUtils.Report["Sorry, was not able to `feep'.", $Finch]; }; GetNumbersForRName: PUBLIC PROC[rName: ROPE] RETURNS [fullRName: ROPE, number: ROPE, homeNumber: ROPE] = { [fullRName, number, homeNumber] _ ThParty.GetNumbersForRName[shh: info.shh, rName: rName]; }; IdentifyVisitor: PUBLIC ENTRY PROC [visitor, password: Rope.ROPE, complain: BOOL _ TRUE] RETURNS [nb: NB] ~ { visitingPartyID: PartyID; visitingPassKey: GVBasics.Password _ GVBasics.MakeKey[password]; visitor _ VoiceUtils.Registrize[visitor]; IF Rope.Equal[info.myRName, visitor, FALSE] THEN { ReleaseVisitorSelf[]; RETURN }; [nb, visitingPartyID] _ThParty.GetParty[shh:info.shh, partyID:info.partyID, rName:visitor]; IF nb#$success THEN RETURN; nb _ ThParty.Visit[shh: info.shh, visitedParty: info.partyID, visitingParty: visitingPartyID, visitingPassword: visitingPassKey]; SELECT nb FROM $success => { VoiceUtils.ReportFR["%s visiting %s -- %s calls will ring in both offices.", $Finch, NIL, IO.rope[visitor], IO.rope[info.myRName], IO.rope[visitor]]; }; $passwordNotValid => IF complain THEN VoiceUtils.ReportFR["Invalid password for %s.", $Finch, NIL, IO.rope[visitor]]; ENDCASE; }; ReleaseVisitor: PUBLIC ENTRY PROC [visitor, password: Rope.ROPE] ~ { nb: NB; visitingPartyID: PartyID; visitingPassKey: GVBasics.Password _ GVBasics.MakeKey[password]; IF visitor.Length[]=0 THEN { -- assume cancelling visiting at home via "Unvisit" ReleaseVisitorSelf[]; RETURN }; visitor _ VoiceUtils.Registrize[visitor]; IF Rope.Equal[info.myRName, visitor, FALSE] THEN { ReleaseVisitorSelf[]; RETURN }; [nb, visitingPartyID] _ThParty.GetParty[shh:info.shh, partyID:info.partyID, rName:visitor]; IF nb=$success THEN { visitee: PartyID; [visitee: visitee] _ ThParty.DescribeParty[partyID: visitingPartyID, nameReq: $none]; IF visitee=info.partyID THEN { nb _ ThParty.Unvisit[shh: info.shh, visitedParty: visitee, visitingParty: visitingPartyID, visitingPassword: visitingPassKey]; IF nb=$success THEN { VoiceUtils.ReportFR["Visiting cancelled for %s.", $Finch, NIL, IO.rope[visitor]]; RETURN; }; }; }; VoiceUtils.ReportFR["%s was not visiting here.", $Finch, NIL, IO.rope[visitor]]; }; -- Correct error management ReleaseVisitorSelf: INTERNAL PROC ~ { nb: NB; visitee: PartyID; [visitee: visitee] _ ThParty.DescribeParty[partyID: info.partyID, nameReq: $none]; nb _ ThParty.Unvisit[shh: info.shh, visitedParty: visitee, visitingParty: info.partyID, visitingPassword: VoiceUtils.CurrentPassword[]]; VoiceUtils.ReportFR[ IF nb=$success THEN "Visiting cancelled for %s. Welcome home." ELSE "%s was not visiting elsewhere.", $Finch, NIL, IO.rope[info.myRName] ]; }; VoiceConnect: PUBLIC ENTRY PROC[ serviceName: ROPE, convID: ConversationID_nullConvID ] RETURNS [ nb: NB_$success, cDesc: ConvDesc_NIL ] = { ENABLE UNWIND=>NULL; state: StateInConv; calledPartyID: PartyID; IF NOT (([,cDesc, state]_FinchOn[convID]).on) THEN { Problem["Your Finch is not running"]; RETURN[nb: $finchNotRunning]; }; IF cDesc=NIL THEN [convID, cDesc, state] _ GetActiveConversation[]; -- Hack SELECT state FROM idle, reserved => { [nb, calledPartyID] _ ThParty.GetParty[shh: info.shh, partyID: info.partyID, rName: serviceName, type: $service]; IF nb=$success THEN [nb, cDesc] _ Connect[calledPartyID, convID, NIL, NIL] }; ENDCASE; IF nb=$success THEN nb _ WaitForActive[cDesc]; }; LookupServiceInterface: PUBLIC PROC[ serviceName: ROPE, interfaceName: ROPE, cDesc: ConvDesc_NIL ] RETURNS [ nb: NB_$success, shhh: Thrush.SHHH, interfaceSpec: Thrush.InterfaceSpec ] = { partyID: Thrush.PartyID_Thrush.nullID; credentials: Thrush.Credentials; IF cDesc#NIL THEN { credentials _ cDesc.situation.self; IF cDesc.numParties>1 THEN partyID _ cDesc.partyInfo[1].partyID; } ELSE { -- want interface only credentials _ [info.partyID, info.smartsID]; [nb, partyID] _ThParty.GetParty[ shh: info.shh, partyID: info.partyID, rName: serviceName, type: $service]; IF nb#$success THEN RETURN; }; IF partyID = Thrush.nullID THEN RETURN[nb: $noSuchParty2, shhh: none, interfaceSpec: nullInterfaceSpec]; [nb, interfaceSpec] _ ThParty.LookupServiceInterface[info.shh, credentials, partyID, interfaceName]; shhh _ info.shh; }; GetConv: PUBLIC PROC[convID: ConversationID, createOK: BOOL_FALSE] RETURNS [ cDesc: ConvDesc_NIL ] = --INLINE-- { cL: LIST OF ConvDesc; IF convID#nullConvID THEN FOR convs: LIST OF ConvDesc _ info.conversations, convs.rest WHILE convs#NIL DO cDesc _ convs.first; IF cDesc.situation.self.convID = convID THEN RETURN; ENDLOOP; cDesc _ NIL; IF ~createOK THEN RETURN; cDesc _ NEW[FinchSmarts.ConvDescBody_[ situation: [ self: [ convID: convID, smartsID: info.smartsID, partyID: info.partyID ]]]]; cL _ LIST[cDesc]; info.conversations _ NConcConvs[info.conversations, cL]; }; FinchOn: INTERNAL PROC[convID: ConversationID_nullConvID] RETURNS [ on: BOOL_FALSE, cDesc: ConvDesc_NIL, state: StateInConv_$idle] = { IF ~pd.finchOn OR info=NIL THEN { VoiceUtils.Report["Your Finch is not running", $Finch]; RETURN; }; on_TRUE; IF convID=nullConvID THEN RETURN; cDesc _ GetConv[convID, FALSE]; IF cDesc#NIL THEN state_cDesc.situation.self.state; }; NoteNewState: INTERNAL PROC[convEvent: Thrush.ConvEvent] RETURNS[cDesc: ConvDesc]= { cInfo: ThParty.ConversationInfo; nb: NB; cDesc _ GetConv[convEvent.self.convID, TRUE]; cDesc.situation _ convEvent^; [nb, cInfo] _ ThParty.GetConversationInfo[shh: info.shh, convID: convEvent.self.convID]; SELECT TRUE FROM nb # $success => Problem["Can't obtain conversation information"]; cDesc.numParties#cInfo.numParties, cDesc.numActive#cInfo.numActive, cDesc.numIdle#cInfo.numIdle => { cDesc.subject _ cInfo.subject; cDesc.startTime _ cInfo.startTime; cDesc.numParties _ cInfo.numParties; cDesc.numActive _ cInfo.numActive; cDesc.numIdle _ cInfo.numIdle; [nb, cDesc.partyInfo] _ ThParty.GetPartyInfo[shh: info.shh, credentials: cDesc.situation.self, nameReq: $description, allParties: TRUE]; IF nb#$success THEN Problem["Can't obtain conversation information", nb] ELSE IF cDesc.numParties>=1 THEN cDesc.whoOriginated _ IF cDesc.partyInfo[0].partyID=cInfo.originator THEN $us ELSE $them; }; ENDCASE; SELECT cDesc.situation.self.state FROM $idle, $neverWas => info.conversations _ DequeueConv[info.conversations, cDesc]; $notified => SELECT filtering FROM -- Needs tests for calls already in progress.  1 => []_Request[cDesc, $idle, $notImportantEnough, "This is a test"]; 2 => []_Request[cDesc, $ringing]; 3 => []_Request[cDesc, $active, $whatever, "Hot line!"]; ENDCASE; ENDCASE => cDesc.ultimateState _ cDesc.situation.self.state; ReportConversationState[$success, cDesc, NIL]; BROADCAST info.stateChange; }; Request: INTERNAL PROC[ cDesc: ConvDesc, state: StateInConv, reason: Thrush.Reason_NIL, comment: ROPE_NIL ] RETURNS [nb: NB] = { DO convEvent: Thrush.ConvEvent; reportToAll: BOOL = SELECT state FROM $idle, $active, $inactive, $ringing => TRUE, ENDCASE=>FALSE; [nb, convEvent] _ ThParty.Advance[shhh: info.shh, credentials: cDesc.situation.self, state: state, reportToAll: reportToAll, reason: reason, comment: comment]; SELECT nb FROM $success => []_NoteNewState[convEvent]; $stateMismatch => { cDesc_NoteNewState[convEvent]; IF state=$idle AND cDesc.situation.self.state # $idle THEN LOOP; }; ENDCASE => -- Some cases are fatal, requiring re-registration! -- ReportConversationState[nb, cDesc, "Telephone server action failed"]; EXIT; ENDLOOP; }; Connect: INTERNAL PROC [calledPartyID: PartyID, convID: ConversationID_nullConvID, reason: Thrush.Reason _ NIL, comment: ROPE_NIL] RETURNS [ nb: NB_$success, cDesc: ConvDesc_NIL ] = { state: StateInConv; newState: StateInConv =IF reason=NIL THEN $initiating ELSE $failed; convEvent: Thrush.ConvEvent; IF ~(([,cDesc, state]_FinchOn[convID]).on) THEN { nb _ $finchInactive; RETURN; }; -- SELECT state FROM $idle => { [nb, convEvent] _ ThParty.CreateConversation[ shhh: info.shh, credentials: [partyID: info.partyID, smartsID: info.smartsID], state: newState, reason: reason, comment: comment ]; IF nb = $success THEN cDesc _ NoteNewState[convEvent]; }; $reserved, $parsing => nb _ Request[cDesc, newState, reason, comment]; ENDCASE => { ReportConversationState[$convStillActive, NIL, "Conversation already in progress"]; RETURN; }; IF nb # $success OR newState=$failed THEN RETURN; cDesc.originatorRecorded _ TRUE; nb_ThParty.Alert[shhh: info.shh, credentials: cDesc.situation.self, calledPartyID: calledPartyID]; SELECT nb FROM $success => NULL; $stateMismatch => { nb_Request[cDesc, $failed, $error, "Conversation already in progress"]; RETURN; }; ENDCASE => { -- Do something interesting  see other smarts for more confusion -- ERROR; }; }; WaitForActive: INTERNAL PROC[cDesc: ConvDesc] RETURNS [nb: NB_$success] = { FOR i: NAT IN [0..pd.waitsForConnect) DO SELECT cDesc.situation.self.state FROM $active => RETURN; $failed => EXIT; ENDCASE; TRUSTED { Process.SetTimeout[@info.stateChange, pd.noJayTicks]; }; WAIT info.stateChange; -- <> SELECT cDesc.situation.self.state FROM $idle, $neverWas => EXIT; -- something wrong. ENDCASE; ENDLOOP; nb_Request[cDesc, $failed, $error, "Finch failed to connect to voice service."]; IF nb=$success THEN nb_$timedOut; }; GetActiveConversation: PROC RETURNS[convID: ConversationID_Thrush.nullConvID, cDesc: ConvDesc_NIL, state: StateInConv_$idle] = { FOR convs: LIST OF ConvDesc _ info.conversations, convs.rest WHILE convs#NIL DO cDesc _ NARROW[convs.first]; IF cDesc.situation.self.state > Thrush.notReallyInConv THEN { convID _ cDesc.situation.self.convID; state _ cDesc.situation.self.state; RETURN; } ENDLOOP; }; Problem: PROC[remark: ROPE_NIL, nb: NB_NIL] = { IF nb=NIL THEN VoiceUtils.Problem[remark, $Finch] ELSE VoiceUtils.ProblemFR["%g, reason: %g", $Finch, NIL, rope[remark], atom[nb]]; }; NewID: PROC RETURNS[LONG CARDINAL] ={RETURN[LOOPHOLE[PupSocket.GetUniqueID[]]]}; InitFinchSmarts: PUBLIC PROC [thrushInstance: Thrush.ROPE_NIL] = { InitFinchSmartsDo[thrushInstance]; IF pd.finchOn THEN StartServerPoller[]; }; InitFinchSmartsDo: PROC [thrushInstance: Thrush.ROPE_NIL] = { problem: ROPE_NIL; nb: NB; { ENABLE RPC.CallFailed => { problem _ "Communication Failure"; GOTO InitFailed; }; credentials: Thrush.Credentials; thVR: RPC.VersionRange = ThVersions.GetThrushVR; problem: Thrush.ROPE _ NIL; namesGVInstance: Thrush.ROPE_NIL; hostHint: VoiceUtils.NetAddress; UninitFinchSmarts[NIL]; info _ NEW[FinchSmarts.FinchInfoBody]; -- Dump any old one! info.requests _ MBQueue.Create[]; info.conversations _ NIL; thrushInstance _ VoiceUtils.MakeRName[style: rName, name: IF thrushInstance#NIL THEN thrushInstance ELSE UserProfile.Token[key: "ThrushClientServerInstance", default: "Strowger.Lark"]]; info.prevThrushInstance _ thrushInstance; info.myName _ [ type: "ThSmarts.Lark", instance: VoiceUtils.InstanceFromNetAddress[netAddress: VoiceUtils.OwnNetAddress[], suffix: "0"], version: ThVersions.FinchVR]; info.myRName _ VoiceUtils.CurrentRName[]; info.myPassword _ VoiceUtils.CurrentPasskey[]; ThSmartsRpcControl.ExportInterface[ interfaceName: info.myName, user: info.myRName, password: info.myPassword]; pd.smartsIsExported_TRUE; info.shh _ IF NOT pd.encryptionRequested THEN Thrush.unencrypted ELSE NamesRPC.StartConversation [ caller: info.myRName, callee: thrushInstance, key: info.myPassword, level: --<>--CBCCheck ! RPC.AuthenticateFailed=> { problem_"Could not authenticate"; GOTO InitFailed}]; hostHint_VoiceUtils.NetAddressFromRope[NameDB.GetAttribute[thrushInstance, $connect]]; ThPartyRpcControl.ImportInterface[ interfaceName: [type: "ThParty.Lark", instance: thrushInstance, version: thVR], hostHint: hostHint! RPC.ImportFailed=> { IF why=wrongVersion THEN problem _ IO.PutFR["Finch version %d too old; import failed", card[ThVersions.FinchVersion]]; GOTO InitFailed; }]; pd.interfacesAreImported_TRUE; [nb, credentials]_ThParty.Register[ shh: info.shh, rName: info.myRName, type: $individual, interface: info.myName, properties: [$controller, VoiceUtils.OwnNetAddress[]] ]; IF nb=$success THEN nb _ ThParty.Enable[shh: info.shh, smartsID: credentials.smartsID]; IF nb#$success THEN { problem_"Can't register with server"; GOTO InitFailed; }; info.smartsID _ credentials.smartsID; info.partyID _ credentials.partyID; pd.finchOn_TRUE; ReportSystemState[TRUE]; EXITS InitFailed => UninitFinchSmarts[problem, nb]; };}; UninitFinchSmarts: PUBLIC PROC[problem: ROPE_NIL, nb: NB_NIL] = { ENABLE RPC.CallFailed => GOTO Failed; IF problem#NIL THEN Problem[problem, nb]; IF info#NIL THEN { IF pd.finchOn AND info.partyID#nullID AND info.smartsID#nullID THEN [-- nb --]_ThParty.Deregister[info.shh, info.smartsID!RPC.CallFailed=>CONTINUE]; info.shh_none; ReportSystemState[FALSE]; FOR convs: LIST OF ConvDesc _ info.conversations, convs.rest WHILE convs#NIL DO convs.first.clientData _ NIL; ENDLOOP; info.conversations _ NIL; }; pd.finchOn_FALSE; pd.interfacesAreImported_FALSE; IF pd.smartsIsExported THEN ThSmartsRpcControl.UnexportInterface[!LupineRuntime.BindingError=>CONTINUE]; pd.smartsIsExported_FALSE; EXITS Failed => pd.interfacesAreImported _ pd.smartsIsExported _ FALSE; }; StartServerPoller: PUBLIC PROC = TRUSTED { Process.Detach[FORK PollServer[]]; }; PollServer: ENTRY PROC = { ENABLE { UNWIND=>NULL; -- RestoreInvariant; RPC.CallFailed => GOTO PollFailed; }; nb: NB; credentials: Thrush.Credentials _ [partyID: info.partyID, smartsID: info.smartsID, convID: Thrush.nullConvID]; TRUSTED {Process.SetTimeout[@finchPollerWait, Process.SecondsToTicks[pd.timeoutPoller]];}; WAIT finchPollerWait; nb _ ThParty.CheckIn[shh: info.shh, credentials: credentials]; IF nb#$success THEN GOTO PollFailed; EXITS PollFailed => InitFinchSmartsDo[info.prevThrushInstance]; }; FinchIsRunning: PUBLIC PROC RETURNS [finchIsRunning: BOOL] = { RETURN[pd.finchOn]; }; RegisterForReports: PUBLIC ENTRY PROC [s: FinchSmarts.ReportSystemStateProc _ NIL, c: FinchSmarts.ReportConversationStateProc _ NIL, r: FinchSmarts.ReportRequestStateProc _ NIL, before: BOOL _ TRUE] = { UnRegisterForReportsInt[s, c, r]; -- Multiple registrations do not hurt anything. IF before THEN { IF s # NIL THEN pd.systemStateSubscribers _ CONS[s, pd.systemStateSubscribers]; IF c # NIL THEN pd.convStateSubscribers _ CONS[c, pd.convStateSubscribers]; IF r # NIL THEN pd.requestStateSubscribers _ CONS[r, pd.requestStateSubscribers]; } ELSE { IF s # NIL THEN IF pd.systemStateSubscribers=NIL THEN pd.systemStateSubscribers _ LIST[s] ELSE FOR l: LIST OF FinchSmarts.ReportSystemStateProc _ pd.systemStateSubscribers, l.rest DO IF l.rest=NIL THEN { l.rest _ LIST[s]; EXIT }; ENDLOOP; IF c # NIL THEN IF pd.convStateSubscribers=NIL THEN pd.convStateSubscribers _ LIST[c] ELSE FOR l: LIST OF FinchSmarts.ReportConversationStateProc _ pd.convStateSubscribers, l.rest DO IF l.rest=NIL THEN { l.rest _ LIST[c]; EXIT }; ENDLOOP; IF r # NIL THEN IF pd.requestStateSubscribers=NIL THEN pd.requestStateSubscribers _ LIST[r] ELSE FOR l: LIST OF FinchSmarts.ReportRequestStateProc _ pd.requestStateSubscribers, l.rest DO IF l.rest=NIL THEN { l.rest _ LIST[r]; EXIT }; ENDLOOP; }; }; UnRegisterForReports: PUBLIC ENTRY PROC [s: FinchSmarts.ReportSystemStateProc _ NIL, c: FinchSmarts.ReportConversationStateProc _ NIL, r: FinchSmarts.ReportRequestStateProc _ NIL] = { UnRegisterForReportsInt[s, c, r]; }; UnRegisterForReportsInt: INTERNAL PROC [s: FinchSmarts.ReportSystemStateProc _ NIL, c: FinchSmarts.ReportConversationStateProc _ NIL, r: FinchSmarts.ReportRequestStateProc _ NIL] = { IF s # NIL AND pd.systemStateSubscribers # NIL THEN { prev: LIST OF FinchSmarts.ReportSystemStateProc _ NIL; FOR list: LIST OF FinchSmarts.ReportSystemStateProc _ pd.systemStateSubscribers, list.rest UNTIL list=NIL DO IF list.first=s THEN { IF prev = NIL THEN pd.systemStateSubscribers _ list.rest ELSE prev.rest _ list.rest; EXIT }; prev _ list; ENDLOOP; }; IF c # NIL AND pd.convStateSubscribers # NIL THEN { prev: LIST OF FinchSmarts.ReportConversationStateProc _ NIL; FOR list: LIST OF FinchSmarts.ReportConversationStateProc _ pd.convStateSubscribers, list.rest UNTIL list=NIL DO IF list.first=c THEN { IF prev = NIL THEN pd.convStateSubscribers _ list.rest ELSE prev.rest _ list.rest; EXIT }; prev _ list; ENDLOOP; }; IF r # NIL AND pd.requestStateSubscribers # NIL THEN { prev: LIST OF FinchSmarts.ReportRequestStateProc _ NIL; FOR list: LIST OF FinchSmarts.ReportRequestStateProc _ pd.requestStateSubscribers, list.rest UNTIL list=NIL DO IF list.first=r THEN { IF prev = NIL THEN pd.requestStateSubscribers _ list.rest ELSE prev.rest _ list.rest; EXIT }; prev _ list; ENDLOOP; }; }; ReportSystemState: PROC [on: BOOL] ~ { FOR l: LIST OF FinchSmarts.ReportSystemStateProc _ pd.systemStateSubscribers, l.rest UNTIL l=NIL DO l.first[on]; ENDLOOP; }; ReportConversationState: PUBLIC PROC [nb: NB, cDesc: ConvDesc, remark: Rope.ROPE] ~ { info.requests.QueueClientAction[ReallyReportConversationState, NEW[CSBody _ [nb, cDesc, remark]]]; }; CS: TYPE = REF CSBody; CSBody: TYPE = RECORD[nb: NB, cDesc: ConvDesc, remark: Rope.ROPE]; ReallyReportConversationState: PROC[r: REF] = { cS: CS _ NARROW[r]; FOR l: LIST OF FinchSmarts.ReportConversationStateProc _ pd.convStateSubscribers, l.rest UNTIL l=NIL DO l.first[cS.nb, cS.cDesc, cS.remark]; ENDLOOP; }; ReportRequestState: PROC [cDesc: ConvDesc, actionReport: Thrush.ActionReport] ~ { actionRequest: REF_NIL; FOR l: LIST OF FinchSmarts.ReportRequestStateProc _ pd.requestStateSubscribers, l.rest UNTIL l=NIL DO actionRequest _ l.first[cDesc, actionReport, actionRequest]; ENDLOOP; }; ViewCmd: Commander.CommandProc = TRUSTED { Nice.View[pd, "Finch PD"]; }; SetFiltering: PROC[which: INT, deferAnswer: ROPE_ "true"] _ { filtering _ which; NameDB.SetAttribute[VoiceUtils.CurrentRName[], $deferanswer, deferAnswer]; }; filtering: INT_0; FilterResetCmd: Commander.CommandProc = TRUSTED { SetFiltering[0, "false"]; }; FilterZeroCmd: Commander.CommandProc = TRUSTED { SetFiltering[0]; }; FilterOneCmd: Commander.CommandProc = TRUSTED { SetFiltering[1]; }; FilterTwoCmd: Commander.CommandProc = TRUSTED { SetFiltering[2]; }; FilterThreeCmd: Commander.CommandProc = TRUSTED { SetFiltering[3]; }; Commander.Register["VuFinch", ViewCmd, "Program Management variables Finch"]; Commander.Register["FReset", FilterResetCmd, "Reset filtration method. This is a test."]; Commander.Register["F0", FilterZeroCmd, "Zeroth filtration method. This is a test."]; Commander.Register["F1", FilterOneCmd, "First filtration method. This is a test."]; Commander.Register["F2", FilterTwoCmd, "Second filtration method. This is a test."]; Commander.Register["F3", FilterThreeCmd, "Third filtration method. This is a test."]; }. ΐFinchSmartsImpl.mesa Copyright Σ 1984, 1986, 1987 by Xerox Corporation. All rights reserved. Last Edited by: Swinehart, April 6, 1987 2:27:41 pm PDT Doug Terry, September 5, 1986 4:17:06 pm PDT Polle Zellweger (PTZ) March 2, 1987 7:15:18 pm PST Declarations Call backs Supervision Some party has changed state in a conversation we know about (or should). Two cases: Our party has changed state in a conversation, whether or not we're going to deal with it (at present, LarkSmarts makes all such decisions) Another party changed state in a conversation we know about (at present, we don't need to know that; again, LarkSmarts does all the work.) That case is filtered out in Progress, above. It is our duty to obtain all relevant information about the call when the information becomes available, and to notify any Finch Clients. One party has replaced another (they were or are now in a poacher/poachee relationship) in this conversation, so we need to update our information about the conversation. Same as Progress, except that we clear any indication that we know anything about the present state before noting the new state. If this notice indicates that our own party is the oldPartyID, we should find a way to inform our ReportConversationState clients that we are no longer involved in this conversation. We should also pull the conv. from our active list. This happens when the same user logs in and runs Finch somewhere else. Client Functions Perhaps allow a NIL password as default param - GVBasics.MakeKey does the right thing with NIL -- also with ReleaseVisitor, below. cancelling visiting at home via "Visit self" Need to handle noSuchParty & noSuchParty2? cancelling visiting at home via "Unvisit self" If ThParty.Unvisit really cared about passwords (which it does not currently), we would have to keep passwords of relevant visitors, probably in the FinchInfo record. Utilities New conversation Now gather up additional information that we might not know yet: Who started this? Who the other party(s) is (are) State information guaranteed correct only as of last time one of these numbers changed. If we've been called due to a Substitution that's kicking us out (cf Subst.), we should find a way to inform our ReportConversationState clients that we are no longer involved in this conversation. We should also do a Dequeue as above.  Real confused about Problem (typeout only) reporting vs. client reporting. The responsibilities are unconvincingly distributed among Smarts and client. If there's a reason, the connect request's only purpose is to record an error condition, audibly as well as visually. This requires the creation of a conversation, then its immediate termination. Some smarts don't do this. They probably should. For use in Recording, and so on, where client needs to wait for setup. Hack. Won't work when lots of conversations are the norm Registration ThParty.ConversationsForParty[shh: info.shh, partyID: partyID]; Warning: This facility has the same risks as most registration mechanisms: running multiple versions of the client code can cause unintended multiple registrations. Queued so that reports do not deadlock with the callers of procedure that invoked the reports. ReportSystemState would cause this trouble, too, but it has been avoided in a different way. Perhaps this approach should be used there, too. Action reports are already decoupled from requests through the auspices of ThParty. Debugging nonsense Swinehart, May 22, 1985 1:08:51 pm PDT Changes to GetParty. changes to: JayConnection Polle Zellweger (PTZ) July 13, 1985 5:20:26 pm PDT adding Text-to-Speech server changes to: DIRECTORY, ProseSpec, ProseSpecs, stopIntervalSpec (was stopSpec), stopProseSpec, Progress, Supervise, DisconnectCall, PlaceCall, StopSpeech, TextToSpeech, InitFinchSmarts, GetConv, ReportProses, EnqueueProses, Complain, Connect, ProseConnection Swinehart, August 6, 1985 5:43:08 pm PDT Merge PTZ prose changes changes to: DIRECTORY, NB, PD, stopIntervalSpec, stopProseSpec, Report, Progress, Supervise, DisconnectCall, PlaceCall, StopTune, NoiseSpec, PlaybackTune, TextToSpeech, InitFinchSmarts, GetConv, ReportProses, CompareIntID, EnqueueProses, Complain, Connect, JayConnection, ProseConnection, RepRet, FinchSmarts Polle Zellweger (PTZ) August 19, 1985 4:16:40 pm PDT Meter text in TextToSpeech so as to avoid sending large ropes all at once. Allows speech to begin sooner and flush faster. Also flushing changes. changes to: FinchInfo, PD, Supervise, TextToSpeech, StopSpeech, ReportProses (comments only) Polle Zellweger (PTZ) September 3, 1985 6:28:29 pm PDT Allow registration of defaultTranslateProc. changes to: Supervise, defaultTranslateProc, RegisterTranslateProc, TextToSpeech, FinchSmarts Swinehart, September 16, 1985 10:00:59 am PDT If Finch has never been initialized, info is NIL -- don't let that bother you. changes to: GetRname, PlayNoise, FinchOn Polle Zellweger (PTZ) October 22, 1985 5:10:44 pm PDT changes to: Progress (add prose debugging reports), TextToSpeech (report connection failure), ReportProses (fix debugging reports), JayConnection (report connection failure), ProseConnection (report connection failure) Polle Zellweger (PTZ) October 24, 1985 4:53:21 pm PDT Move bluejayConnection and proseConnection from FinchInfo to ConvDesc. changes to: Supervise, DisconnectCall, InitFinchSmarts, Connect, JayConnection, ProseConnection Swinehart, October 28, 1985 12:31:20 pm PST Merge Above changes with other other minor changes (RefQ and the like) changes to: DIRECTORY, FinchSmartsImpl, Progress, Supervise, DisconnectCall, TextToSpeech, InitFinchSmarts, UninitFinchSmarts, IsConv, GetConv, IsConv (local of GetConvDesc), GetConvDesc, ReportProses, FinchOn, Connect, JayConnection, ProseConnection, PlaybackTune, ClearConvs (local of UninitFinchSmarts) Swinehart, December 13, 1985 2:52:25 pm PST Massive change to new Thrush, leaving out proses and intervals for the moment. changes to: ConversationHandle, FinchInfo, Progress, Supervise, DisconnectCall, AnswerCall, PlaceCall, Apprise, Complain, Connect, Problem, FinchSmarts, Progress, QdProgress Swinehart, December 17, 1985 10:05:46 am PST Major revision for new Thrush. Removed all Prose and Bluejay stuff temporarily changes to: QdProgress, NoteNewState, PutFTime, Connect, Problem, Progress, Other, Supervise Swinehart, May 19, 1986 12:22:17 pm PDT Cedar 6.1 changes to: DIRECTORY, SHHH Swinehart, June 1, 1986 7:48:10 pm PDT Add ReportAction, eliminate the MBQueueing of ThParty-provoked actions. Others remain, but should go. changes to: Progress, Substitution, ReportAction Doug Terry, August 28, 1986 4:59:33 pm PDT Changed report registration; removed key distribution; removed routines for recording/playing, tunes and related operations. changes to: FinchIsRunning, RegisterForReports, UnRegisterForReports, ReportAction, DisconnectCall, PlaceCall, PlaybackTune, NoteNewState, Request, Connect, InitFinchSmarts, UninitFinchSmarts, ReportSystemState, ReportConversationState, ReportRequestState, DIRECTORY, FinchSmartsImpl, GetConv Doug Terry, August 29, 1986 12:14:14 pm PDT New interface procedures for FinchSmarts clients to get at voice services: VoiceConnect and LookupServiceInterface. changes to: ReleaseVisitor, VoiceConnect, WaitForActive, LookupServiceInterface, Problem ΚΩ˜Jšœ™šœH™HJšœ7™7Icode™,Kšœ2™2—J˜šΟk ˜ Jšœ œ˜*Jšœ ˜ J˜Jšœ œ˜%Jšœ˜Jšœœ˜6Jšœœ˜%Jšœœ˜,Jšœœ ˜,Jšœ œ˜%J˜Jšœœ/˜œœ'˜Ά—J˜ Jšœ˜J˜8Jšœ œ ˜Jšœ œΆ˜ΖJ˜J˜—šœœ˜Jšœœ`œU˜ήJšœ˜!Jšœœ˜J˜—™ J˜Jšœ œ˜&šœœ˜-J˜/—Jšœ œ˜(Jšœœ œ˜Jšœœ˜J˜!Jšœ œ˜Jšœœ œ˜Jšœœ œ˜Jšœ œ˜!Jšœ œ˜'Jšœ>˜>J˜Jšœ œ˜J˜šœœœ˜Jšœ œœ˜Jšœœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœ6˜6J˜9Jšœœœ˜"Jšœœœ˜%Jšœœœ˜ Jšœœ˜Jšœ œœ˜Jšœ œ˜Jšœœ˜J˜J™ Jšœœœ#˜BJšœœœ)˜FJšœœœ$˜DJ˜—J˜Jšœ˜Jš œœœœœ˜J˜šΟnœœœ˜Jšœœœœ˜ J˜ J˜—J˜šžœœœœ˜ Jšœœ œœ˜Jšœœ˜J˜J˜—šž œœ œœ œœœ œœœ˜xJ˜—šž œœœœœœœ˜SJš œœœœΟc ˜%Jšœ œœ Ÿ˜3š œœœœ œ˜AJšœœœ Ÿ˜TJšœ˜—JšœŸ˜Jšœ˜J˜——™ J™šžœœœœ˜Jšœœ˜ Jšœ˜Jšœ˜KšœœœŸ˜-J™I™ J™‹J™ΉJ™‰—Jšœ2œ˜VJ˜J˜—šž œœœœ˜ Jšœœ˜ Jšœ˜J˜J˜Jšœ˜KšœœœŸ˜-J™¬J™΅Jšœ1œ˜8JšœœœŸ*˜RJšœ˜J˜J˜—šž œœœœ˜ Jšœœ˜ Jšœ˜Jšœ˜Kšœœœ˜Jšœ8˜8Jš œŸ œ˜=Jšœ˜J˜——™J™Jšœ œœŸ(˜Kšœœœ˜Jšœ˜J˜%Jšœœ˜Jšœ œ˜J˜J˜—šžœœœœ˜"Jšœ˜J˜%Jšœ œ˜Jšœ"˜"KšœœœŸ˜-J˜$Jšœœ'œœ˜9šœ˜šœ˜Jšœ(œ#˜NJ˜—Jš œ-˜8—J˜J˜—šž œœœœ#˜@KšœœœŸ˜-J˜$Jšœ ˜ Jšœœ2œ˜MJšœ˜J˜—šž œœœœ˜Jšœ)˜)JšœœŸ˜$JšœœŸ˜-Jšœ œ˜Jšœ˜šœ˜JšœœŸ˜$Jšœ?œ˜NJšœ˜—Jšœ˜Jšœœ˜Jšœ"˜"Jšœœœ˜šœ œ˜JšœW˜WJšœœ0œœ˜LJ˜—šœ œ˜'Jšœ1˜1JšœO˜O—šœ˜JšœœŸ˜6JšœœŸ"˜9JšœR˜RJšœ"œœœ˜>—JšœΟtœ  œ4˜BJšœ˜J˜—š žœœœœ%œ˜EJšœ˜Jšœ œ ˜J˜Jšœœ'œœ˜Wšœ œœœ˜1šœœ@˜ZJšœG˜G—Jšœ˜šœ ˜šœ<˜Jšœ˜J˜J˜J˜—Jšœœ!˜6J˜—JšœF˜Fšœ˜ Jšœ*œ&˜SJšœ˜Jšœ˜——Jšœœœœ˜1Jšœœ˜ ˜ J˜A—šœ˜Jšœ œ˜šœ˜šœG˜GJ™3—Jšœ˜Jšœ˜—šœ˜ JšŸFœœ˜P——J˜J˜—š ž œœœœœ˜KJ™Fšœœœ˜(šœ˜&Jšœ œ˜Jšœ œ˜Jšœ˜—Jšœ;˜BJšœŸ4˜Kšœ˜&JšœœŸ˜-Jšœ˜—Jšœ˜—J˜PJšœ œ˜!Jšœ˜J˜—šžœ˜Jšœ;œ˜dJ™9š œœœ+œœ˜OJšœœ˜šœ5œ˜=JšœJœ˜S—Jšœ˜—J˜J˜—š žœœ œœœœ˜/šœœœ#˜1Jšœ0œ˜Q—Jšœ˜J˜—šžœœœœœœœ˜PJ˜——™ J˜š žœœœœœ˜BJšœ"˜"Kšœ œ˜'J˜—J˜šžœœœœ˜=Jšœ œœœ˜Jšœœ4œ˜QJ˜ J˜0Jšœœœ˜Jšœœœ˜!J˜ J˜Jšœœ˜J˜JšœœŸ˜;J˜!Jšœœ˜šœ9˜9Jšœœœ˜)JšœQ˜U—Jšœ)˜)šœ˜J˜J˜—Jšœ)˜)Jšœ.˜.˜#J˜Jšœ˜Jšœ˜—šœœ˜J˜—Jšœ œœœ˜@šœ˜!Jšœ˜Jšœ˜Jšœ˜JšœŸ œ ˜Jšœ:œ˜OJ˜—JšœV˜VJ˜˜"JšœO˜OšœŸœ˜(šœ˜šœ=˜=Jšœ˜——Jšœ ˜Jšœ˜——Jšœœ˜J˜šœ#˜#Jšœ˜Jšœ˜Jšœ˜Jšœ˜Jšœ5˜5Jšœ˜—Jšœ œD˜Wšœ œ˜Jšœ&œ˜9—J˜%Jšœ#˜#J™AJšœ œ˜Jšœœ˜š˜J˜-—J˜—J˜šžœœœ œœœœ˜AJšœœœ˜%Jšœ œœ˜)šœœœ˜šœ œœ˜CJšœ8œ œ˜R—J˜Jšœœ˜š œœœ+œœ˜OJšœœ˜Jšœ˜—Jšœœ˜J˜—Jšœ œ˜Jšœœ˜šœ˜JšœBœ˜L—Jšœœ˜š˜Jšœ;œ˜A—˜J˜——šžœ œœ˜*Kšœœ˜"Jšœ˜J˜—šž œœœ˜šœ˜JšœœŸ˜$Jšœœ ˜"J˜—Jšœœ˜Jšœn˜nJšœS˜ZJšœ˜Jšœ>˜>Jšœ  œ ˜$š˜Jšœ9˜9—Jšœ˜—J˜Jš žœœ œœœ˜UJ˜šžœœ œΟr œœ’ œœ’ œœ œœ˜Κšœ"Ÿ/˜QJš Οbœ ™¦—šœœ˜šœœ˜Jšœœ˜?—šœœ˜Jšœœ˜;—šœœ˜Jšœœ ˜A—J˜—šœ˜šœœ˜Kšœœœœ˜Iš œœœœ’ œ;˜\Kš œœœ œœ˜.Kšœ˜——šœœ˜Kšœœœœ˜Eš œœœœ’ œ?˜`Kš œœœ œœ˜.Kšœ˜——šœœ˜Kšœœœœ˜Kš œœœœ’ œ=˜^Kš œœœ œœ˜.Kšœ˜——K˜—J˜J˜—šžœœ’ œœ’ œœ’ œœ*˜άJ˜—šžœ œ’ œœ’ œœ’ œœ˜Άš œœœœœ˜5Kš œœœ’ œœ˜6š œœœ’ œ>œœ˜lšœœ˜Kšœœœ&˜8Kšœ˜Kšœ˜—K˜ Kšœ˜—Kšœ˜—š œœœœœ˜3Kš œœœ’ œœ˜<š œœœ’ œBœœ˜pšœœ˜Kšœœœ$˜6Kšœ˜Kšœ˜—K˜ Kšœ˜—Kšœ˜—š œœœœœ˜6Kš œœœ’ œœ˜7š œœœ’ œ@œœ˜nšœœ˜Kšœœœ'˜9Kšœ˜Kšœ˜—K˜ Kšœ˜—Kšœ˜—J˜J˜—šžœœœ˜&š œœœ’ œ;œœ˜cKšœ ˜ Kšœ˜—K˜K˜—š žœœœœ œ˜UK™Γšœ>˜>Kšœ ˜#—K˜—K˜Kšœœœ˜š œœœœ œ˜BK˜—šžœœœ˜/Kšœœœ˜š œœœ’ œ?œœ˜gKšœ$˜$Kšœ˜—K˜K˜K˜—šžœœ9˜QKšœœœ˜š œœœ’ œ=œœ˜eKšœ<˜