Skip to content

Commit 851e5ce

Browse files
committed
ID based info requester
1 parent 55de3cc commit 851e5ce

10 files changed

+310
-233
lines changed

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# config file
2+
config.yaml
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

Diff for: config.yaml

-35
This file was deleted.

Diff for: config_example.yaml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
idn:
2+
max_attemps: 3
3+
time_out: 2500
4+
regexp: "[1-9][0-9]{5,10}"
5+
field: "document"
6+
7+
sounds:
8+
enter_id:
9+
# Por favor ingrese el documento seguido de la tecla numeral
10+
prompt: "custom/id-please-type"
11+
# No he recibido su respuesta
12+
empty: "custom/id-you-have-not-entered"
13+
# El número de documento ingresado no es válido
14+
invalid: "custom/id-invalid"
15+
# El número de documento ingresado es...
16+
number-is: "custom/id-number-is"
17+
# Si es correcto marque 1, de lo contrario marque 0
18+
confirm: "custom/id-number-confirm"
19+
# Vamos a intentarlo de nuevo
20+
repeat: "custom/id-repeat"
21+
# Por favor intente de nuevo
22+
try-again: "custom/try-again"
23+
choose-option:
24+
# No ha digitado ninguna opción
25+
empty: "custom/option-empty"
26+
# La opción ingresada es incorrecta
27+
invalid: "custom/option-invalid"
28+
# Por favor intente de nuevo
29+
try-again: "custom/try-again"
30+
31+
request:
32+
url: "http://localhost:8080/api/get-info"
33+
auth:
34+
login: null
35+
password: null
36+
timeout: 5
37+
38+
variables:
39+
ID_NUMBER: "{document}"
40+
EXTRA1: "{name}"
41+
42+
status:
43+
variable: "ID_STATUS"

Diff for: docker-compose.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: "3.7"
2+
services:
3+
fastagi:
4+
build: fastagi
5+
restart: always
6+
ports:
7+
- 4573:8080
8+
environment:
9+
GETIDDIR: /etc/getid
10+
volumes:
11+
- ./fastagi/src/:/app
12+
- ./config.yaml:/etc/config.yaml
13+
- /etc/localtime:/etc/localtime
14+
entrypoint: watchmedo auto-restart --recursive --pattern="*.py" --directory="." python server.py

Diff for: Dockerfile renamed to fastagi/Dockerfile

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Building stage
2-
FROM python:3.5
2+
FROM python:3.7
33

44
# copy the dependencies file to the working directory
55
COPY requirements.txt .
@@ -8,7 +8,6 @@ COPY requirements.txt .
88
RUN pip install --no-cache-dir -r requirements.txt
99

1010
RUN mkdir -p /etc/getid/
11-
COPY ./config.yaml /etc/getid/config.yaml
1211

1312
# set the working directory in the container
1413
WORKDIR /app
File renamed without changes.
File renamed without changes.

Diff for: src/logger.py renamed to fastagi/src/logger.py

File renamed without changes.

Diff for: fastagi/src/server.py

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import re
2+
import json
3+
import yaml
4+
import string
5+
import aiohttp
6+
import logging.config
7+
8+
from aioagi import runner
9+
from aioagi.app import AGIApplication
10+
from aioagi.log import agi_server_logger as logger
11+
from aioagi.urldispathcer import AGIView
12+
13+
from logger import LOGGING
14+
15+
16+
class PartialFormatter(string.Formatter):
17+
def __init__(self, missing='~~~', bad_fmt='!!!'):
18+
self.missing, self.bad_fmt = missing, bad_fmt
19+
20+
def get_field(self, field_name, args, kwargs):
21+
# Handle a key not found
22+
try:
23+
val = super().get_field(field_name, args, kwargs)
24+
except (KeyError, AttributeError):
25+
val = None, field_name
26+
return val
27+
28+
def format_field(self, value, spec):
29+
if value is None:
30+
return self.missing
31+
try:
32+
return super(PartialFormatter, self).format_field(value, spec)
33+
except ValueError:
34+
if self.bad_fmt is not None:
35+
return self.bad_fmt
36+
else:
37+
raise
38+
39+
40+
class GetIDView(AGIView):
41+
http_client = None
42+
43+
with open("/etc/config.yaml") as stream:
44+
config = yaml.safe_load(stream)
45+
46+
def __init__(self, *args, **kwargs):
47+
self.user = {}
48+
self.status = None
49+
50+
logger.info(f"{self.config}")
51+
52+
if not self.http_client:
53+
self.http_client = aiohttp.ClientSession()
54+
55+
super().__init__(*args, **kwargs)
56+
57+
async def sip(self):
58+
await self.start()
59+
60+
async def local(self):
61+
await self.start()
62+
63+
async def dahdi(self):
64+
await self.start()
65+
66+
async def start(self):
67+
await self.agi.answer()
68+
try:
69+
# await self.get_id()
70+
id_number = await self.get_information(self.config["idn"]["regexp"],
71+
self.config["sounds"]["enter_id"],
72+
func_slide=True,
73+
tries=2,
74+
optionList=["1", "0"]
75+
)
76+
id_field = self.config["idn"]["field"]
77+
self.user[id_field] = id_number
78+
logger.info(f"ID Number: {id_number}")
79+
logger.info(f'Request: {self.config["request"]}')
80+
81+
http_request = self.config["request"]["url"]
82+
if http_request and id_number:
83+
user_info = await self.http_request(http_request,
84+
params={id_field: id_number},
85+
auth_data=self.config["request"]["auth"],
86+
timeout=self.config["request"]["timeout"]
87+
)
88+
else:
89+
user_info = {}
90+
91+
logger.info(f"User: {self.user}")
92+
logger.info(f"ID Number: {user_info}")
93+
if user_info:
94+
self.user.update(user_info)
95+
96+
except Exception as error:
97+
logger.info(error)
98+
self.status = 'ERROR'
99+
# await self.set_vars()
100+
await self.set_custom_vars()
101+
102+
async def choose_option(self, audioPrompt, optionList=['1', '2', '*'], tries=3):
103+
logger.info(f"Starting choose_option with {audioPrompt}")
104+
sounds = self.config["sounds"]["choose-option"]
105+
attemps = 0
106+
while not tries or attemps < tries:
107+
attemps = attemps + 1
108+
response = await self.agi.get_data(audioPrompt, timeout=2500, max_digits=1)
109+
result, _ = response.result, response.info == "(timeout)"
110+
if not result and 'T' in optionList:
111+
result = 'T'
112+
if result not in optionList:
113+
if not result:
114+
await self.agi.stream_file(sounds["empty"])
115+
else:
116+
await self.agi.stream_file(sounds["invalid"])
117+
if not tries or attemps < tries:
118+
await self.agi.stream_file(sounds["try-again"])
119+
else:
120+
event, info = ("TIMEOUT", "NONE") if result == 'T' else ("OPTION", result)
121+
# await self.log_ivr_try(event, info)
122+
return result
123+
return None
124+
125+
async def get_information(self, regExp, sounds, func_slide=None, tries=3,
126+
confirm=3, timeout=2500, max_digits=255, cancel=None,
127+
optionList=["1", "0", "*"]):
128+
attemps = 0
129+
try_again = False
130+
while tries is None or attemps < tries:
131+
if attemps > 0 and try_again and sounds.get("try-again"):
132+
await self.agi.stream_file(sounds.get("try-again"))
133+
attemps += 1
134+
response = await self.agi.get_data(sounds.get("prompt"),
135+
timeout=timeout, max_digits=max_digits)
136+
result, timeout = response.result, response.info == "(timeout)"
137+
if not result:
138+
if sounds.get("empty", sounds.get("invalid")):
139+
await self.agi.stream_file(sounds.get("empty", sounds.get("invalid")))
140+
try_again = True
141+
elif result == cancel:
142+
return None
143+
elif not re.search(regExp, result):
144+
if sounds.get("invalid"):
145+
await self.agi.stream_file(sounds.get("invalid"))
146+
try_again = True
147+
else:
148+
# await self.log_ivr_try("EXPRESSION", result)
149+
if func_slide:
150+
await self.agi.stream_file(sounds.get("number-is"))
151+
await self.agi.say_digits(result)
152+
# for slide in func_slide(result):
153+
# await self.agi.stream_file(slide)
154+
choice = await self.choose_option(sounds.get("confirm"),
155+
optionList, tries=confirm)
156+
if not choice and (tries is None or attemps < tries):
157+
await self.agi.stream_file(sounds.get("repeat"))
158+
try_again = False
159+
elif choice == '1':
160+
return result
161+
else:
162+
return result
163+
return None
164+
165+
async def get_id(self):
166+
167+
for attemp in range(self.config["idn"]["max_attemps"]):
168+
if attemp != 0:
169+
await self.agi.stream_file(self.config["sounds"]["try_again"])
170+
171+
data_result = await self.agi.get_data(self.config["sounds"]["enter_id"], self.config["idn"]["time_out"])
172+
id_number, timeout = data_result.result, data_result.info == "(timeout)"
173+
174+
if not id_number:
175+
logger.info("No id_number has been entered")
176+
self.status = 'EMPTY' if not timeout else 'TIMEOUT'
177+
await self.agi.stream_file(self.config["sounds"]["no_id"])
178+
179+
elif not re.search(self.config["idn"]["regexp"], id_number):
180+
logger.info("The input doesn't match the regular expression")
181+
self.status = 'WRONG'
182+
await self.agi.stream_file(self.config["sounds"]["id_invalid"])
183+
184+
else:
185+
logger.info("The id_number value is correct")
186+
self.user[self.config["idn"]["field"]] = id_number
187+
self.status = 'OK'
188+
break
189+
190+
async def http_request(self, url, method="GET", params=None, auth_data=None,
191+
json_data=None, headers=None, json_result=True, timeout=5):
192+
193+
if auth_data and auth_data.get("login"):
194+
auth = aiohttp.BasicAuth(**auth_data)
195+
else:
196+
auth = None
197+
198+
if timeout:
199+
timeout = aiohttp.ClientTimeout(total=timeout)
200+
201+
logger.debug(f"{url}, {params}")
202+
async with self.http_client.request(method, url,
203+
auth=auth,
204+
params=params,
205+
json=json_data,
206+
headers=headers,
207+
timeout=timeout
208+
) as resp:
209+
logger.debug(f"Response: {resp}")
210+
if json_result:
211+
try:
212+
response = await resp.json(content_type=None)
213+
except Exception as error:
214+
logger.error(f"Something failed trying to convert result into json: {error}")
215+
response = await resp.text()
216+
else:
217+
response = await resp.text()
218+
219+
logger.info(f"Response: {response}")
220+
221+
return response
222+
223+
async def set_vars(self):
224+
await self.agi.set_variable(self.config["status"]["variable"], self.status)
225+
226+
vars_dict = self.config["variables"]
227+
for var, value in vars_dict.items():
228+
try:
229+
if type(value) is str:
230+
value = value.format(**self.user)
231+
await self.agi.set_variable(var, value)
232+
except Exception as error:
233+
logger.warning("It's not posible to set ({var}, {value}): {error}".format(**locals()))
234+
235+
async def set_custom_vars(self):
236+
fmt = PartialFormatter()
237+
vars_dict = self.config["variables"]
238+
for var, value in vars_dict.items():
239+
if type(value) is str:
240+
value = fmt.format(value, **self.user)
241+
logger.info(f"Setting '{var}' to '{value}'")
242+
await self.agi.set_variable(var, value)
243+
244+
245+
if __name__ == '__main__':
246+
logging.config.dictConfig(LOGGING)
247+
app = AGIApplication()
248+
app.router.add_route('*', '/hello-view/', GetIDView)
249+
runner.run_app(app)

0 commit comments

Comments
 (0)