rest_api.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import logging
  2. from base64 import b64encode
  3. from email.message import EmailMessage
  4. from email.generator import Generator
  5. from json import dumps
  6. from os import environ, urandom
  7. from smtplib import SMTP
  8. from tempfile import NamedTemporaryFile
  9. from flask import Flask, jsonify, render_template, request
  10. from flask_cors import CORS
  11. from scriptshifter import (
  12. EMAIL_FROM, EMAIL_TO,
  13. GIT_COMMIT, GIT_TAG,
  14. SMTP_HOST, SMTP_PORT,
  15. FEEDBACK_PATH)
  16. from scriptshifter.exceptions import ApiError
  17. from scriptshifter.tables import list_tables, get_language
  18. from scriptshifter.trans import transliterate
  19. logger = logging.getLogger(__name__)
  20. logging.basicConfig(level=environ.get("TXL_LOGLEVEL", logging.INFO))
  21. def create_app():
  22. flask_env = environ.get("TXL_APP_MODE", "production")
  23. app = Flask(__name__)
  24. app.config.update({
  25. "ENV": flask_env,
  26. "SECRET_KEY": environ.get("TXL_FLASK_SECRET", b64encode(urandom(64))),
  27. "JSON_AS_ASCII": False,
  28. "JSONIFY_PRETTYPRINT_REGULAR": True,
  29. })
  30. CORS(app)
  31. return app
  32. app = create_app()
  33. @app.errorhandler(ApiError)
  34. def handle_exception(e: ApiError):
  35. if e.status_code >= 500:
  36. warnings = [
  37. "An internal error occurred.",
  38. "If the error persists, contact the technical support team."
  39. ]
  40. else:
  41. warnings = [
  42. "ScriptShifter API replied with status code "
  43. f"{e.status_code}: {e.msg}"
  44. ]
  45. if e.status_code >= 400:
  46. warnings.append(
  47. "Please review your input before repeating this request.")
  48. body = {
  49. "warnings": warnings,
  50. "output": "",
  51. }
  52. if logging.DEBUG >= logging.root.level:
  53. body["debug"] = {
  54. "form_data": request.json or request.form,
  55. }
  56. return (body, e.status_code)
  57. @app.route("/", methods=["GET"])
  58. def index():
  59. return render_template(
  60. "index.html",
  61. languages=list_tables(),
  62. version_info=(GIT_TAG, GIT_COMMIT),
  63. feedback_form=SMTP_HOST is not None or FEEDBACK_PATH is not None)
  64. @app.route("/health", methods=["GET"])
  65. def health_check():
  66. return "I'm alive!\n"
  67. @app.route("/languages", methods=["GET"])
  68. def list_languages():
  69. return jsonify(list_tables())
  70. @app.route("/table/<lang>")
  71. def dump_table(lang):
  72. """
  73. Dump a language configuration from the DB.
  74. """
  75. return get_language(lang)
  76. @app.route("/options/<lang>", methods=["GET"])
  77. def get_options(lang):
  78. """
  79. Get extra options for a table.
  80. """
  81. tbl = get_language(lang)
  82. return jsonify(tbl.get("options", []))
  83. @app.route("/trans", methods=["POST"])
  84. def transliterate_req():
  85. lang = request.json["lang"]
  86. in_txt = request.json["text"]
  87. capitalize = request.json.get("capitalize", False)
  88. t_dir = request.json.get("t_dir", "s2r")
  89. if t_dir not in ("s2r", "r2s"):
  90. return f"Invalid direction: {t_dir}", 400
  91. if not len(in_txt):
  92. return ("No input text provided! ", 400)
  93. options = request.json.get("options", {})
  94. logger.debug(f"Extra options: {options}")
  95. try:
  96. out, warnings = transliterate(in_txt, lang, t_dir, capitalize, options)
  97. except (NotImplementedError, ValueError) as e:
  98. raise ApiError(str(e), 400)
  99. except Exception as e:
  100. raise ApiError(str(e), 500)
  101. return {"output": out, "warnings": warnings}
  102. @app.route("/feedback", methods=["POST"])
  103. def feedback():
  104. """
  105. Allows users to provide feedback to improve a specific result.
  106. """
  107. if not SMTP_HOST and not FEEDBACK_PATH:
  108. return {"message": "Feedback form is not configured."}, 501
  109. t_dir = request.json.get("t_dir", "s2r")
  110. options = request.json.get("options", {})
  111. contact = request.json.get("contact")
  112. msg = EmailMessage()
  113. msg["subject"] = "Scriptshifter feedback report"
  114. msg["from"] = EMAIL_FROM
  115. msg["to"] = EMAIL_TO
  116. if contact:
  117. msg["cc"] = contact
  118. msg.set_content(f"""
  119. *Scriptshifter feedback report from {contact or 'anonymous'}*\n\n
  120. *Language:* {request.json['lang']}\n
  121. *Direction:* {
  122. 'Roman to Script' if t_dir == 'r2s'
  123. else 'Script to Roman'}\n
  124. *Source:* {request.json['src']}\n
  125. *Result:* {request.json['result']}\n
  126. *Expected result:* {request.json['expected']}\n
  127. *Applied options:* {dumps(options)}\n
  128. *Notes:*\n
  129. {request.json['notes']}""")
  130. if SMTP_HOST:
  131. # TODO This uses a test SMTP server:
  132. # python -m smtpd -n -c DebuggingServer localhost:1025
  133. smtp = SMTP(SMTP_HOST, SMTP_PORT)
  134. smtp.send_message(msg)
  135. smtp.quit()
  136. else:
  137. with NamedTemporaryFile(
  138. suffix=".txt", dir=FEEDBACK_PATH, delete=False) as fh:
  139. gen = Generator(fh)
  140. gen.write(msg.as_bytes())
  141. logger.info(f"Feedback message generated at {fh.name}.")
  142. return {"message": "Feedback message sent."}