[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\nPipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\npoetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n.idea/\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n.pdm-python\n*.db\n*.sqlite"
  },
  {
    "path": "README.md",
    "content": "## Chatfairy\n\nThis is a minimal chat application with no dependancy but Flask! I call it Chatfairy.\n\nWith only 115 locs in a singel file, Chatfairy support:\n\n- user authentication\n- multiple users chat simultaneously\n- online/offline notification\n- route protection\n\nhttps://github.com/yuxiaoy1/chatfairy/assets/24974936/13186e61-1082-41c3-aa16-250b58572975\n\nYou can clone this repo and run `pip install flask && flask run`, then open `http://127.0.0.1:5000` to play with Chatfairy.\n"
  },
  {
    "path": "app.py",
    "content": "from functools import wraps\n\nfrom flask import (\n    Flask,\n    json,\n    redirect,\n    render_template,\n    request,\n    session,\n    url_for,\n)\n\napp = Flask(__name__)\napp.secret_key = \"secret\"\n\nmessages = []\n\n\ndef send_message(message):\n    messages.append(message)\n\n\ndef login_required(func):\n    @wraps(func)\n    def inner(*args, **kwargs):\n        if \"username\" not in session:\n            return redirect(url_for(\"login\"))\n        return func(*args, **kwargs)\n\n    return inner\n\n\n@app.get(\"/\")\n@login_required\ndef index():\n    return render_template(\"index.html\", username=session[\"username\"])\n\n\n@app.route(\"/login\", methods=[\"GET\", \"POST\"])\ndef login():\n    if \"username\" in session:\n        return redirect(url_for(\"index\"))\n    if request.method == \"POST\":\n        username = request.form[\"username\"]\n        session[\"username\"] = username\n        send_message(\n            {\n                \"username\": username,\n                \"message\": f\"{username} has joined.\",\n                \"type\": \"auth\",\n            },\n        )\n        return redirect(url_for(\"index\"))\n    return render_template(\"login.html\")\n\n\n@app.get(\"/logout\")\ndef logout():\n    if \"username\" in session:\n        send_message(\n            {\n                \"username\": session[\"username\"],\n                \"message\": f\"{session['username']} has left.\",\n                \"type\": \"auth\",\n            }\n        )\n        session.pop(\"username\")\n    return redirect(url_for(\"index\"))\n\n\n@app.get(\"/events\")\n@login_required\ndef events():\n    def generate_response():\n        while True:\n            for message in messages:\n                yield f\"data: {json.dumps(message)}\\n\\n\"\n            messages.clear()\n\n    return app.response_class(generate_response(), mimetype=\"text/event-stream\")\n\n\n@app.post(\"/message\")\n@login_required\ndef message():\n    username = session[\"username\"]\n    message = request.json[\"message\"]\n    send_message({\"username\": username, \"message\": message})\n    return \"OK\"\n"
  },
  {
    "path": "templates/base.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\" />\n    <style>\n      :root {\n        --bg: #fff;\n        --accent-bg: #f5f7ff;\n        --text: #212121;\n        --text-light: #585858;\n        --border: #898ea4;\n        --accent: #0d47a1;\n        --accent-hover: #1266e2;\n        --accent-text: var(--bg);\n        --code: #d81b60;\n        --preformatted: #444;\n        --marked: #ffdd33;\n        --disabled: #efefef;\n      }\n    </style>\n    <title>Chatfairy</title>\n  </head>\n  <body>\n    <h3>\n      <a href=\"/\" style=\"text-decoration: none\"> Welcom to Chatfairy!</a>\n    </h3>\n    {% block content %} {% endblock %}\n  </body>\n</html>\n"
  },
  {
    "path": "templates/index.html",
    "content": "{% extends 'base.html' %} {% block content %}\n<div>Hello {{username}} <a href=\"{{ url_for('logout') }}\">Logout</a></div>\n<div\n  id=\"chat-box\"\n  style=\"\n    height: 300px;\n    width: 100%;\n    border: 1px solid var(--border);\n    border-radius: 4px;\n    margin: 10px 0;\n    padding: 8px;\n    overflow: auto;\n  \"\n></div>\n<form id=\"chat-form\">\n  <input type=\"text\" id=\"chat-message\" placeholder=\"Type your message\" />\n  <button>Send</button>\n</form>\n<script>\n  let chatBox = document.getElementById('chat-box')\n  let chatForm = document.getElementById('chat-form')\n  let chatMessage = document.getElementById('chat-message')\n  let sse = new EventSource('/events')\n\n  sse.onmessage = ({ data }) => {\n    let { username, message, type } = JSON.parse(data)\n    if (username === '{{username}}' && type === 'auth') return\n    chatBox.innerHTML +=\n      type === 'auth'\n        ? `<div style='color: gray'><em>${message}</em></div>`\n        : `<div>${username}: <strong>${message}</strong></div>`\n  }\n\n  chatForm.addEventListener('submit', async event => {\n    event.preventDefault()\n\n    let message = chatMessage.value.trim()\n    if (!message) return\n\n    await fetch('/message', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ message }),\n    })\n    chatMessage.value = ''\n  })\n</script>\n{% endblock %}\n"
  },
  {
    "path": "templates/login.html",
    "content": "{% extends 'base.html' %} {% block content %}\n<form action=\"{{ url_for('login') }}\" method=\"POST\">\n  <input\n    required\n    type=\"text\"\n    name=\"username\"\n    placeholder=\"Input your username\"\n  />\n  <button>Login</button>\n</form>\n{% endblock %}\n"
  }
]