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 @@
-
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