bumper/bumper/confserver.py
Brian Martin ac7dd3d302 add /upload/ api
- used for menu icons in ecovacs home
2020-01-05 09:43:35 -05:00

899 lines
No EOL
37 KiB
Python

#!/usr/bin/env python3
import json
import logging
import ssl
import string
import random
import bumper
import os
from bumper.models import *
from bumper import plugins
from datetime import datetime, timedelta
import asyncio
from aiohttp import web
import aiohttp_jinja2
import jinja2
import uuid
import xml.etree.ElementTree as ET
class aiohttp_filter(logging.Filter):
def filter(self, record):
if (
record.name == "aiohttp.access" and record.levelno == 20
): # Filters aiohttp.access log to switch it from INFO to DEBUG
record.levelno = 10
record.levelname = "DEBUG"
if (
record.levelno == 10
and logging.getLogger("confserver").getEffectiveLevel() == 10
):
return True
else:
return False
confserverlog = logging.getLogger("confserver")
logging.getLogger("aiohttp.access").addFilter(
aiohttp_filter()
) # Add logging filter above to aiohttp.access
class ConfServer:
def __init__(self, address, usessl=False):
self.usessl = usessl
self.address = address
self.app = None
self.site = None
self.runner = None
self.runners = []
self.excludelogging = ["base", "remove-bot", "remove-client", "restart-service"]
def get_milli_time(self, timetoconvert):
return int(round(timetoconvert * 1000))
def confserver_app(self):
self.app = web.Application(loop=asyncio.get_event_loop(), middlewares=[
self.log_all_requests,
])
aiohttp_jinja2.setup(self.app, loader=jinja2.FileSystemLoader(os.path.join(bumper.data_dir, "web","templates")))
self.app.add_routes(
[
web.get("", self.handle_base, name="base"),
web.get("/bot/remove/{did}", self.handle_RemoveBot, name='remove-bot'),
web.get("/client/remove/{resource}", self.handle_RemoveClient, name='remove-client'),
web.get("/restart_{service}", self.handle_RestartService, name='restart-service'),
web.post("/lookup.do", self.handle_lookup),
]
)
# common api paths
api_v1 = {"prefix": "/v1/", "app": web.Application()} # for /v1/
api_v2 = {"prefix": "/v2/", "app": web.Application()} # for /v2/
portal_api = {"prefix": "/api/", "app": web.Application()} # for /api/
upload_api = {"prefix": "/upload/", "app": web.Application()} # for /upload/
apis = {
"api_v1": api_v1,
"api_v2": api_v2,
"portal_api": portal_api,
"upload_api": upload_api,
}
# Load plugins
for plug in bumper.discovered_plugins:
if isinstance(bumper.discovered_plugins[plug].plugin, bumper.plugins.ConfServerApp):
plugin = bumper.discovered_plugins[plug].plugin
if plugin.plugin_type == "sub_api": # app or sub_api
if plugin.sub_api in apis:
if plugin.routes:
logging.debug(f"Adding confserver sub_api ({plugin.name})")
apis[plugin.sub_api]["app"].add_routes(plugin.routes)
elif plugin.plugin_type == "app":
if plugin.path_prefix and plugin.app:
logging.debug(f"Adding confserver plugin ({plugin.name})")
self.app.add_subapp(plugin.path_prefix, plugin.app)
for api in apis:
self.app.add_subapp(apis[api]["prefix"], apis[api]["app"])
#for resource in self.app.router.resources():
# print(resource)
async def start_site(self, app, address='localhost', port=8080, usessl=False):
runner = web.AppRunner(app)
self.runners.append(runner)
await runner.setup()
if usessl:
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(bumper.server_cert, bumper.server_key)
site = web.TCPSite(
runner,
host=address,
port=port,
ssl_context=ssl_ctx,
)
else:
site = web.TCPSite(
runner, host=address, port=port
)
await site.start()
async def start_server(self):
try:
confserverlog.info(
"Starting ConfServer at {}:{}".format(self.address[0], self.address[1])
)
self.runner = web.AppRunner(self.app)
await self.runner.setup()
if self.usessl:
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(bumper.server_cert, bumper.server_key)
self.site = web.TCPSite(
self.runner,
host=self.address[0],
port=self.address[1],
ssl_context=ssl_ctx,
)
else:
self.site = web.TCPSite(
self.runner, host=self.address[0], port=self.address[1]
)
await self.site.start()
except PermissionError as e:
confserverlog.error(e.strerror)
asyncio.create_task(bumper.shutdown())
except asyncio.CancelledError:
pass
except Exception as e:
confserverlog.exception("{}".format(e))
asyncio.create_task(bumper.shutdown())
async def stop_server(self):
try:
await self.runner.shutdown()
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_base(self, request):
try:
bots = bumper.db_get().table("bots").all()
clients = bumper.db_get().table("clients").all()
helperbot = bumper.mqtt_helperbot.Client.session.transitions.state
mqttserver = bumper.mqtt_server.broker
xmppserver = bumper.xmpp_server
mq_sessions = []
for sess in mqttserver._sessions:
tmpsess = []
tmpsess.append({
"username": mqttserver._sessions[sess][0].username,
"client_id": mqttserver._sessions[sess][0].client_id,
"state": mqttserver._sessions[sess][0].transitions.state,
})
mq_sessions.append(tmpsess)
all = {
"bots": bots,
"clients": clients,
"helperbot": [{"state": helperbot}],
"mqtt_server": [
{"state": mqttserver.transitions.state},
{
"sessions": [
{"count": len(mqttserver._sessions)},
{"clients": mq_sessions},
]
},
],
"xmpp_server": xmppserver
}
resp = aiohttp_jinja2.render_template('home.jinja2', request, context=all)
#return web.json_response(all)
return resp
except Exception as e:
confserverlog.exception("{}".format(e))
@web.middleware
async def log_all_requests(self, request, handler):
if request._match_info.route.name not in self.excludelogging:
try:
if request.content_length:
if request.content_type == "application/x-www-form-urlencoded":
postbody = await request.post()
elif request.content_type == "application/json":
try:
postbody = json.loads(await request.text())
except Exception as e:
confserverlog.error("Request body not json: {} - {}".format(e, e.doc))
postbody = e.doc
else:
postbody = await request.post()
else:
postbody = None
response = await handler(request)
if not "application/octet-stream" in response.content_type:
logall = {
"request": {
"route_name": f"{request.match_info.route.name}",
"method": f"{request.method}",
"path": f"{request.path}",
"query_string": f"{request.query_string}",
"raw_path": f"{request.raw_path}",
"raw_headers": f'{",".join(map("{}".format, request.raw_headers))}',
"body": f"{postbody}",
},
"response": {
"response_body": f"{json.loads(response.body)}",
"status": f"{response.status}",
}
}
else:
logall = {
"request": {
"route_name": f"{request.match_info.route.name}",
"method": f"{request.method}",
"path": f"{request.path}",
"query_string": f"{request.query_string}",
"raw_path": f"{request.raw_path}",
"raw_headers": f'{",".join(map("{}".format, request.raw_headers))}',
"body": f"{postbody}",
},
"response": {
"status": f"{response.status}",
}
}
confserverlog.debug(json.dumps(logall))
return response
except web.HTTPNotFound as notfound:
confserverlog.debug("Request path {} not found".format(request.raw_path))
requestlog = {
"request": {
"route_name": f"{request.match_info.route.name}",
"method": f"{request.method}",
"path": f"{request.path}",
"query_string": f"{request.query_string}",
"raw_path": f"{request.raw_path}",
"raw_headers": f'{",".join(map("{}".format, request.raw_headers))}',
"body": f"{postbody}",
}
}
confserverlog.debug(json.dumps(requestlog))
return notfound
except Exception as e:
confserverlog.exception("{}".format(e))
requestlog = {
"request": {
"route_name": f"{request.match_info.route.name}",
"method": f"{request.method}",
"path": f"{request.path}",
"query_string": f"{request.query_string}",
"raw_path": f"{request.raw_path}",
"raw_headers": f'{",".join(map("{}".format, request.raw_headers))}',
"body": f"{postbody}",
}
}
confserverlog.debug(json.dumps(requestlog))
return e
else:
return await handler(request)
async def restart_Helper(self):
await bumper.mqtt_helperbot.Client.disconnect()
asyncio.create_task(bumper.mqtt_helperbot.start_helper_bot())
async def restart_MQTT(self):
if not (bumper.mqtt_server.broker.transitions.state == "stopped" or bumper.mqtt_server.broker.transitions.state == "not_started"):
# close session writers - this was required so bots would reconnect properly after restarting
for sess in list(bumper.mqtt_server.broker._sessions):
sessobj = bumper.mqtt_server.broker._sessions[sess][1]
if sessobj.session.transitions.state == "connected":
await sessobj.writer.close()
#await bumper.mqtt_server.broker.shutdown()
aloop = asyncio.get_event_loop()
aloop.call_later(
0.1, lambda: asyncio.create_task(bumper.mqtt_server.broker.shutdown())
) # In .1 seconds shutdown broker
aloop = asyncio.get_event_loop()
aloop.call_later(
1.5, lambda: asyncio.create_task(bumper.mqtt_server.broker_coro())
) # In 1.5 seconds start broker
async def restart_XMPP(self):
bumper.xmpp_server.disconnect()
await bumper.xmpp_server.start_async_server()
async def handle_RestartService(self, request):
try:
service = request.match_info.get("service", "")
if service == "Helperbot":
await self.restart_Helper()
return web.json_response({"status": "complete"})
elif service == "MQTTServer":
asyncio.create_task(self.restart_MQTT())
aloop = asyncio.get_event_loop()
aloop.call_later(
5, lambda: asyncio.create_task(self.restart_Helper())
) # In 5 seconds restart Helperbot
return web.json_response({"status": "complete"})
elif service == "XMPPServer":
await self.restart_XMPP()
return web.json_response({"status": "complete"})
else:
return web.json_response({"status": "invalid service"})
except Exception as e:
confserverlog.exception("{}".format(e))
pass
async def handle_RemoveBot(self, request):
try:
did = request.match_info.get("did", "")
bumper.bot_remove(did)
if bumper.bot_get(did):
return web.json_response({"status": "failed to remove bot"})
else:
return web.json_response({"status": "successfully removed bot"})
except Exception as e:
confserverlog.exception("{}".format(e))
pass
async def handle_RemoveClient(self, request):
try:
resource = request.match_info.get("resource", "")
bumper.client_remove(resource)
if bumper.client_get(resource):
return web.json_response({"status": "failed to remove client"})
else:
return web.json_response({"status": "successfully removed client"})
except Exception as e:
confserverlog.exception("{}".format(e))
pass
async def handle_login(self, request):
try:
user_devid = request.match_info.get("devid", "")
countrycode = request.match_info.get("country", "us")
apptype = request.match_info.get("apptype", "")
confserverlog.info(
"client with devid {} attempting login".format(user_devid)
)
if bumper.use_auth:
if (
not user_devid == ""
): # Performing basic "auth" using devid, super insecure
user = bumper.user_by_deviceid(user_devid)
if "checkLogin" in request.path:
self.check_token(
apptype, countrycode, user, request.query["accessToken"]
)
else:
if "global_" in apptype: # EcoVacs Home
login_details = EcoVacsHome_Login()
login_details.ucUid = "fuid_{}".format(user["userid"])
login_details.loginName = "fusername_{}".format(
user["userid"]
)
login_details.mobile = None
else:
login_details = EcoVacs_Login()
# Deactivate old tokens and authcodes
bumper.user_revoke_expired_tokens(user["userid"])
login_details.accessToken = self.generate_token(user)
login_details.uid = "fuid_{}".format(user["userid"])
login_details.username = "fusername_{}".format(user["userid"])
login_details.country = countrycode
login_details.email = "null@null.com"
body = {
"code": API_ERRORS[RETURN_API_SUCCESS],
"data": json.loads(login_details.toJSON()),
# {
# "accessToken": self.generate_token(tmpuser), # Generate a token
# "country": countrycode,
# "email": "null@null.com",
# "uid": "fuid_{}".format(tmpuser["userid"]),
# "username": "fusername_{}".format(tmpuser["userid"]),
# },
"msg": "操作成功",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
body = {
"code": bumper.ERR_USER_NOT_ACTIVATED,
"data": None,
"msg": "当前密码错误",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
else:
return web.json_response(
self._auth_any(user_devid, apptype, countrycode, request)
)
except Exception as e:
confserverlog.exception("{}".format(e))
async def handle_lookup(self, request):
try:
body = {}
postbody = {}
if request.content_type == "application/x-www-form-urlencoded":
postbody = await request.post()
else:
postbody = json.loads(await request.text())
confserverlog.debug(postbody)
todo = postbody["todo"]
if todo == "FindBest":
service = postbody["service"]
if service == "EcoMsgNew":
srvip = bumper.bumper_announce_ip
srvport = 5223
confserverlog.info(
"Announcing EcoMsgNew Server to bot as: {}:{}".format(
srvip, srvport
)
)
msgserver = {"ip": srvip, "port": srvport, "result": "ok"}
msgserver = json.dumps(msgserver)
msgserver = msgserver.replace(
" ", ""
) # bot seems to be very picky about having no spaces, only way was with text
return web.json_response(text=msgserver)
elif service == "EcoUpdate":
srvip = "47.88.66.164" # EcoVacs Server
srvport = 8005
confserverlog.info(
"Announcing EcoUpdate Server to bot as: {}:{}".format(
srvip, srvport
)
)
body = {"result": "ok", "ip": srvip, "port": srvport}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def disconnect(self):
try:
confserverlog.info("shutting down")
await self.app.shutdown()
except Exception as e:
confserverlog.exception("{}".format(e))
class ConfServer_GeneralFunctions:
def __init__(self):
pass
def get_milli_time(self, timetoconvert):
return int(round(timetoconvert * 1000))
class ConfServer_AuthHandler:
def __init__(self):
self.get_milli_time = bumper.ConfServer.ConfServer_GeneralFunctions().get_milli_time
pass
def generate_token(self, user):
try:
tmpaccesstoken = uuid.uuid4().hex
bumper.user_add_token(user["userid"], tmpaccesstoken)
return tmpaccesstoken
except Exception as e:
confserverlog.exception("{}".format(e))
def generate_authcode(self, user, countrycode, token):
try:
tmpauthcode = "{}_{}".format(countrycode, uuid.uuid4().hex)
bumper.user_add_authcode(user["userid"], token, tmpauthcode)
return tmpauthcode
except Exception as e:
confserverlog.exception("{}".format(e))
async def login(self, request):
try:
user_devid = request.match_info.get("devid", "")
countrycode = request.match_info.get("country", "us")
apptype = request.match_info.get("apptype", "")
confserverlog.info(
"client with devid {} attempting login".format(user_devid)
)
if bumper.use_auth:
if (
not user_devid == ""
): # Performing basic "auth" using devid, super insecure
user = bumper.user_by_deviceid(user_devid)
if "checkLogin" in request.path:
self.check_token(
apptype, countrycode, user, request.query["accessToken"]
)
else:
if "global_" in apptype: # EcoVacs Home
login_details = EcoVacsHome_Login()
login_details.ucUid = "fuid_{}".format(user["userid"])
login_details.loginName = "fusername_{}".format(
user["userid"]
)
login_details.mobile = None
else:
login_details = EcoVacs_Login()
# Deactivate old tokens and authcodes
bumper.user_revoke_expired_tokens(user["userid"])
login_details.accessToken = self.generate_token(user)
login_details.uid = "fuid_{}".format(user["userid"])
login_details.username = "fusername_{}".format(user["userid"])
login_details.country = countrycode
login_details.email = "null@null.com"
body = {
"code": API_ERRORS[RETURN_API_SUCCESS],
"data": json.loads(login_details.toJSON()),
# {
# "accessToken": self.generate_token(tmpuser), # Generate a token
# "country": countrycode,
# "email": "null@null.com",
# "uid": "fuid_{}".format(tmpuser["userid"]),
# "username": "fusername_{}".format(tmpuser["userid"]),
# },
"msg": "操作成功",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
body = {
"code": bumper.ERR_USER_NOT_ACTIVATED,
"data": None,
"msg": "当前密码错误",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
else:
return web.json_response(
self._auth_any(user_devid, apptype, countrycode, request)
)
except Exception as e:
confserverlog.exception("{}".format(e))
async def get_AuthCode(self, request):
try:
apptype = request.match_info.get("apptype", "")
user_devid = request.match_info.get("devid", "") # Ecovacs
if user_devid == "":
user_devid = request.query["deviceId"] # Ecovacs Home
if not user_devid == "":
user = bumper.user_by_deviceid(user_devid)
token = ""
if user:
if "accessToken" in request.query:
token = bumper.user_get_token(
user["userid"], request.query["accessToken"]
)
if token:
authcode = ""
if not "authcode" in token:
authcode = self.generate_authcode(
user,
request.match_info.get("country", "us"),
request.query["accessToken"],
)
else:
authcode = token["authcode"]
if "global" in apptype:
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"authCode": authcode,
"ecovacsUid": request.query["uid"],
},
"msg": "操作成功",
"success": True,
"time": self.get_milli_time(
datetime.utcnow().timestamp()
),
}
else:
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"authCode": authcode,
"ecovacsUid": request.query["uid"],
},
"msg": "操作成功",
"time": self.get_milli_time(
datetime.utcnow().timestamp()
),
}
return web.json_response(body)
body = {
"code": bumper.ERR_TOKEN_INVALID,
"data": None,
"msg": "当前密码错误",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
def check_token(self, apptype, countrycode, user, token):
try:
if bumper.check_token(user["userid"], token):
if "global_" in apptype: # EcoVacs Home
login_details = EcoVacsHome_Login()
login_details.ucUid = "fuid_{}".format(user["userid"])
login_details.loginName = "fusername_{}".format(user["userid"])
login_details.mobile = None
else:
login_details = EcoVacs_Login()
login_details.accessToken = token
login_details.uid = "fuid_{}".format(user["userid"])
login_details.username = "fusername_{}".format(user["userid"])
login_details.country = countrycode
login_details.email = "null@null.com"
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": json.loads(login_details.toJSON()),
# {
# "accessToken": self.generate_token(tmpuser), # Generate a token
# "country": countrycode,
# "email": "null@null.com",
# "uid": "fuid_{}".format(tmpuser["userid"]),
# "username": "fusername_{}".format(tmpuser["userid"]),
# },
"msg": "操作成功",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
else:
body = {
"code": bumper.ERR_TOKEN_INVALID,
"data": None,
"msg": "当前密码错误",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
def _auth_any(self, devid, apptype, country, request):
try:
user_devid = devid
countrycode = country
user = bumper.user_by_deviceid(user_devid)
bots = bumper.db_get().table("bots").all()
if user: # Default to user 0
tmpuser = user
if "global_" in apptype: # EcoVacs Home
login_details = EcoVacsHome_Login()
login_details.ucUid = "fuid_{}".format(tmpuser["userid"])
login_details.loginName = "fusername_{}".format(tmpuser["userid"])
login_details.mobile = None
else:
login_details = EcoVacs_Login()
login_details.accessToken = self.generate_token(tmpuser)
login_details.uid = "fuid_{}".format(tmpuser["userid"])
login_details.username = "fusername_{}".format(tmpuser["userid"])
login_details.country = countrycode
login_details.email = "null@null.com"
bumper.user_add_device(tmpuser["userid"], user_devid)
else:
bumper.user_add("tmpuser") # Add a new user
tmpuser = bumper.user_get("tmpuser")
if "global_" in apptype: # EcoVacs Home
login_details = EcoVacsHome_Login()
login_details.ucUid = "fuid_{}".format(tmpuser["userid"])
login_details.loginName = "fusername_{}".format(tmpuser["userid"])
login_details.mobile = None
else:
login_details = EcoVacs_Login()
login_details.accessToken = self.generate_token(tmpuser)
login_details.uid = "fuid_{}".format(tmpuser["userid"])
login_details.username = "fusername_{}".format(tmpuser["userid"])
login_details.country = countrycode
login_details.email = "null@null.com"
bumper.user_add_device(tmpuser["userid"], user_devid)
for bot in bots: # Add all bots to the user
if "did" in bot:
bumper.user_add_bot(tmpuser["userid"], bot["did"])
else:
confserverlog.error("No DID for bot: {}".format(bot))
if "checkLogin" in request.path: # If request was to check a token do so
checkToken = self.check_token(
apptype, countrycode, tmpuser, request.query["accessToken"]
)
isGood = json.loads(checkToken.text)
if isGood["code"] == "0000":
return isGood
# Deactivate old tokens and authcodes
bumper.user_revoke_expired_tokens(tmpuser["userid"])
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": json.loads(login_details.toJSON()),
# {
# "accessToken": self.generate_token(tmpuser), # Generate a token
# "country": countrycode,
# "email": "null@null.com",
# "uid": "fuid_{}".format(tmpuser["userid"]),
# "username": "fusername_{}".format(tmpuser["userid"]),
# },
"msg": "操作成功",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return body
except Exception as e:
confserverlog.exception("{}".format(e))
def getUserAccountInfo(self, request):
try:
user_devid = request.match_info.get("devid", "")
countrycode = request.match_info.get("country", "us")
apptype = request.match_info.get("apptype", "")
user = bumper.user_by_deviceid(user_devid)
if "global_" in apptype: # EcoVacs Home
login_details = EcoVacsHome_Login()
login_details.ucUid = "fuid_{}".format(user["userid"])
login_details.loginName = "fusername_{}".format(user["userid"])
login_details.mobile = None
else:
login_details = EcoVacs_Login()
login_details.uid = "fuid_{}".format(user["userid"])
login_details.username = "fusername_{}".format(user["userid"])
login_details.country = countrycode
login_details.email = "null@null.com"
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": {
"email": login_details.email,
"hasMobile": "N",
"hasPassword": "Y",
"uid": login_details.uid,
"userName": login_details.username,
"obfuscatedMobile": None,
"mobile": None,
"loginName": login_details.loginName,
},
"msg": "操作成功",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
# Example body
# {
# "code": "0000",
# "data": {
# "email": "user@gmail.com",
# "hasMobile": "N",
# "hasPassword": "Y",
# "headIco": "",
# "loginName": "user@gmail.com",
# "mobile": null,
# "mobileAreaNo": null,
# "nickname": "",
# "obfuscatedMobile": null,
# "thirdLoginInfoList": [
# {
# "accountType": "WeChat",
# "hasBind": "N"
# }
# ],
# "uid": "20180719212155_*****",
# "userName": "EAY*****"
# },
# "msg": "操作成功",
# "success": true,
# "time": 1578203898343
# }
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
async def logout(self, request):
try:
user_devid = request.match_info.get("devid", "")
if not user_devid == "":
user = bumper.user_by_deviceid(user_devid)
if user:
if bumper.check_token(user["userid"], request.query["accessToken"]):
# Deactivate old tokens and authcodes
bumper.user_revoke_token(
user["userid"], request.query["accessToken"]
)
body = {
"code": bumper.RETURN_API_SUCCESS,
"data": None,
"msg": "操作成功",
"time": self.get_milli_time(datetime.utcnow().timestamp()),
}
return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))