diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/.gitignore b/.gitignore index e844511..d497ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__ *.egg-info *.sqlite stickynotes.cfg +build +dist diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c98b5f5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "StickyNotes" +dynamic = ["version"] +description = "A simple pastebin" +license = "GPL-3.0-or-later" +requires-python = ">=3.11" +authors = [ + { name = "Raoul Snyman", email = "raoul@snyman.info" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", +# "Framework :: Quart", + "Intended Audience :: Other Audience", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] +dependencies = [ + "Quart", + "Quart-Flask-Patch", + "Flask-SQLAlchemy", + "nord-pygments", + "psycopg2_binary", + "Pygments", + "requests", + "short_url", +] + +[project.optional-dependencies] +dev = [ + "pytest-cov", + "pytest", +] + +[project.urls] +Homepage = "https://bin.snyman.info" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.targets.sdist] +include = [ + "/stickynotes", +] + +[tool.hatch.envs.default.scripts] +server = "quart -A stickynotes.app run" diff --git a/stickynotes/__init__.py b/stickynotes/__init__.py index 2c3124e..b932ff0 100644 --- a/stickynotes/__init__.py +++ b/stickynotes/__init__.py @@ -5,7 +5,8 @@ StickyNotes, yet another paste bin from configparser import ConfigParser from pathlib import Path -from flask import Flask +import quart_flask_patch # noqa: F401 +from quart import Quart from stickynotes.db import db from stickynotes.views import views @@ -19,6 +20,8 @@ def read_config(config_path=None): config_file = config_path / 'stickynotes.cfg' else: config_file = Path(__file__).parent / '..' / 'stickynotes.cfg' + if not config_file.exists(): + return {} config_parser = ConfigParser() config_parser.read(config_file) config = {} @@ -31,13 +34,16 @@ def make_app(config_path=None): """ Create the application object """ - app = Flask(__name__) + app = Quart(__name__) # Load the config file config = read_config(config_path) app.config.update(config) app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False}) db.init_app(app) - with app.app_context(): - db.create_all() app.register_blueprint(views) + + @app.before_first_request + async def setup_db(): + db.create_all() + return app diff --git a/stickynotes/__main__.py b/stickynotes/__main__.py new file mode 100644 index 0000000..2369d32 --- /dev/null +++ b/stickynotes/__main__.py @@ -0,0 +1,3 @@ +from stickynotes.app import application + +application.run() diff --git a/wsgi.py b/stickynotes/app.py similarity index 78% rename from wsgi.py rename to stickynotes/app.py index 0e3bac0..68079aa 100644 --- a/wsgi.py +++ b/stickynotes/app.py @@ -5,7 +5,7 @@ This is the entry point for the WSGI server from pathlib import Path from stickynotes import make_app -application = make_app(Path(__file__).parent) +application = make_app(Path(__file__).parent.parent) if __name__ == '__main__': application.run(debug=True) diff --git a/stickynotes/static/custom.css b/stickynotes/static/custom.css index 406e79e..bda9b2c 100644 --- a/stickynotes/static/custom.css +++ b/stickynotes/static/custom.css @@ -65,3 +65,9 @@ td.linenos pre { .form-control:focus { color: var(--white); } + +.computer { + display: none; + left: -9000000000px; + position: absolute; +} diff --git a/stickynotes/templates/about.html b/stickynotes/templates/about.html index badad55..46c170c 100644 --- a/stickynotes/templates/about.html +++ b/stickynotes/templates/about.html @@ -3,7 +3,7 @@

About StickyNotes

-

StickyNotes is a quick code paste application written in Python with Flask, SQLAlchemy, Mako, Pygments and a few other Python libraries.

+

StickyNotes is a quick code paste application written in Python with Quartz, SQLAlchemy, Mako, Pygments and a few other Python libraries.

{% endblock %} diff --git a/stickynotes/templates/index.html b/stickynotes/templates/index.html index 1a1a9ac..be7fac3 100644 --- a/stickynotes/templates/index.html +++ b/stickynotes/templates/index.html @@ -29,17 +29,10 @@ -
-
- - -
+
+ +
- {% if recaptcha_site_key %} -
-
-
- {% endif %}
diff --git a/stickynotes/views.py b/stickynotes/views.py index 37f9dae..14a36a1 100644 --- a/stickynotes/views.py +++ b/stickynotes/views.py @@ -3,11 +3,11 @@ The views """ import logging +import secrets +import string from datetime import timedelta, datetime -import requests -import short_url -from flask import Blueprint, redirect, request, flash, make_response, current_app, render_template +from quart import Blueprint, redirect, request, flash, make_response, current_app, render_template from pygments import highlight from pygments.formatters.html import HtmlFormatter from pygments.lexers import get_lexer_by_name, get_all_lexers @@ -30,34 +30,28 @@ EXPIRY_DELTAS = { } -def _is_recaptcha_valid(secret, response, remote_ip=None): +def _generate_short_url(): """ - POST to the recaptcha service to check if the recaptcha is valid + Encode the URL """ - data = {'secret': secret, 'response': response} - if remote_ip: - data['remoteip'] = remote_ip - response = requests.post('https://www.google.com/recaptcha/api/siteverify', data=data) - try: - json_response = response.json() - return json_response['success'] - except ValueError: - return False + alphabet = string.ascii_lowercase + string.digits + short_url = ''.join(secrets.choice(alphabet) for _ in range(8)) + return short_url @views.route('/', methods=['GET']) -def index(): +async def index(): """ Add a new sticky note """ all_lexers = [(lexer[1][0], lexer[0]) for lexer in get_all_lexers() if len(lexer) > 1 and len(lexer[1]) > 0] all_lexers.sort(key=lambda x: x[1].lower()) recaptcha_site_key = current_app.config.get('RECAPTCHA_SITE_KEY') - return render_template('index.html', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key) + return await render_template('index.html', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key) @views.route('/notes', methods=['GET']) -def notes(): +async def notes(): """ Show a list of recent notes """ @@ -66,54 +60,41 @@ def notes(): .filter(~StickyNote.private)\ .order_by(StickyNote.created.desc())\ .limit(10) # noqa - return render_template('notes.html', notes=notes) + return await render_template('notes.html', notes=notes) @views.route('/about', methods=['GET']) -def about(): +async def about(): """ Show the about page """ - return render_template('about.html') + return await render_template('about.html') @views.route('/', methods=['POST']) -def save(): +async def save(): """ Save a sticky note """ - # Check if the recaptcha is valid - recaptcha_secret_key = current_app.config.get('RECAPTCHA_SECRET_KEY') - if recaptcha_secret_key: - is_recaptcha_valid = False - try: - is_recaptcha_valid = _is_recaptcha_valid(recaptcha_secret_key, request.form['g-recaptcha-response']) - except KeyError: - flash('Unable to verify you, don\'t forget to complete the captcha.', 'danger') - print('No form variable') - else: - is_recaptcha_valid = True - if not is_recaptcha_valid: + form = await request.form + if form.get('computer') and form['computer'].lower() != 'no': return redirect('/') # Save the note try: created = datetime.utcnow() - expiry = EXPIRY_DELTAS.get(request.form['expiry']) - if expiry: - expiry = created + expiry - source = request.form['source'] - lexer = request.form['language'] - title = request.form.get('title', '') - private = True if request.form.get('private') else False - string_id = ''.join([source, lexer, title, created.isoformat()]) - url = short_url.encode_url(sum([ord(char) for char in string_id]), min_length=8) + expiry = created + EXPIRY_DELTAS.get(form['expiry'], EXPIRY_DELTAS['1d']) + # Generate a short url, and check if it exists in the db + url = _generate_short_url() + while StickyNote.query.filter(StickyNote.url == url).first(): + url = _generate_short_url() + # Create a new note note = StickyNote( - title=title, - source=source, - lexer=lexer, + title=form.get('title', ''), + source=form['source'], + lexer=form['language'], created=created, expiry=expiry, - private=private, + private=True, url=url ) session.add(note) @@ -126,7 +107,7 @@ def save(): @views.route('/', methods=['GET']) -def view(note_url): +async def view(note_url): """ Show a sticky note @@ -140,11 +121,11 @@ def view(note_url): lexer = get_lexer_by_name(note.lexer) formatter = HtmlFormatter(linenos=True, cssclass='source') result = highlight(note.source, lexer, formatter) - return render_template('view.html', note=note, source=result) + return await render_template('view.html', note=note, source=result) @views.route('/raw/', methods=['GET']) -def raw(note_url): +async def raw(note_url): """ Show the raw version of a sticky note @@ -154,14 +135,14 @@ def raw(note_url): if not note: flash('That note does not exist', 'danger') return redirect('/') - return render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'} + return await render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'} @views.route('/pygments.css', methods=['GET']) -def pygments_css(): +async def pygments_css(): """ Return the Pygments CSS to the browser """ - response = make_response(HtmlFormatter(style='nord').get_style_defs()) + response = await make_response(HtmlFormatter(style='nord').get_style_defs()) response.headers['Content-Type'] = 'text/css' return response