Compare commits

..

No commits in common. "73f2e54a749158a0054ee1a91ac11efc14ad2635" and "29c38c55d96205ab3bdb3648506a0ad59bc2a793" have entirely different histories.

10 changed files with 68 additions and 123 deletions

View File

@ -1,2 +0,0 @@
[flake8]
max-line-length = 120

2
.gitignore vendored
View File

@ -2,5 +2,3 @@ __pycache__
*.egg-info *.egg-info
*.sqlite *.sqlite
stickynotes.cfg stickynotes.cfg
build
dist

View File

@ -1,62 +0,0 @@
[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"

View File

@ -5,8 +5,7 @@ StickyNotes, yet another paste bin
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
import quart_flask_patch # noqa: F401 from flask import Flask
from quart import Quart
from stickynotes.db import db from stickynotes.db import db
from stickynotes.views import views from stickynotes.views import views
@ -20,8 +19,6 @@ def read_config(config_path=None):
config_file = config_path / 'stickynotes.cfg' config_file = config_path / 'stickynotes.cfg'
else: else:
config_file = Path(__file__).parent / '..' / 'stickynotes.cfg' config_file = Path(__file__).parent / '..' / 'stickynotes.cfg'
if not config_file.exists():
return {}
config_parser = ConfigParser() config_parser = ConfigParser()
config_parser.read(config_file) config_parser.read(config_file)
config = {} config = {}
@ -34,16 +31,13 @@ def make_app(config_path=None):
""" """
Create the application object Create the application object
""" """
app = Quart(__name__) app = Flask(__name__)
# Load the config file # Load the config file
config = read_config(config_path) config = read_config(config_path)
app.config.update(config) app.config.update(config)
app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False}) app.config.update({'SQLALCHEMY_TRACK_MODIFICATIONS': False})
db.init_app(app) db.init_app(app)
app.register_blueprint(views) with app.app_context():
@app.before_first_request
async def setup_db():
db.create_all() db.create_all()
app.register_blueprint(views)
return app return app

View File

@ -1,3 +0,0 @@
from stickynotes.app import application
application.run()

View File

@ -65,9 +65,3 @@ td.linenos pre {
.form-control:focus { .form-control:focus {
color: var(--white); color: var(--white);
} }
.computer {
display: none;
left: -9000000000px;
position: absolute;
}

View File

@ -3,7 +3,7 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12"> <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h2>About StickyNotes</h2> <h2>About StickyNotes</h2>
<p>StickyNotes is a quick code paste application written in Python with Quartz, SQLAlchemy, Mako, Pygments and a few other Python libraries.</p> <p>StickyNotes is a quick code paste application written in Python with Flask, SQLAlchemy, Mako, Pygments and a few other Python libraries.</p>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -29,10 +29,17 @@
<option value="1m">1 Month</option> <option value="1m">1 Month</option>
</select> </select>
</div> </div>
<div class="form-group computer"> <div class="form-group">
<label for="computer">Computer? (just type "no")</label> <div class="custom-control custom-checkbox">
<input type="text" name="computer" class="form-control" id="computer" autocomplete="off" placeholder="Computers type yes"> <input type="checkbox" name="private" class="custom-control-input" id="private">
<label for="private" class="custom-control-label">Unlisted (doesn't appear in the list on the notes page)</label>
</div> </div>
</div>
{% if recaptcha_site_key %}
<div class="form-group">
<div class="g-recaptcha" data-sitekey="{{recaptcha_site_key}}"></div>
</div>
{% endif %}
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
</form> </form>
</div> </div>

View File

@ -3,11 +3,11 @@
The views The views
""" """
import logging import logging
import secrets
import string
from datetime import timedelta, datetime from datetime import timedelta, datetime
from quart import Blueprint, redirect, request, flash, make_response, current_app, render_template import requests
import short_url
from flask import Blueprint, redirect, request, flash, make_response, current_app, render_template
from pygments import highlight from pygments import highlight
from pygments.formatters.html import HtmlFormatter from pygments.formatters.html import HtmlFormatter
from pygments.lexers import get_lexer_by_name, get_all_lexers from pygments.lexers import get_lexer_by_name, get_all_lexers
@ -30,28 +30,34 @@ EXPIRY_DELTAS = {
} }
def _generate_short_url(): def _is_recaptcha_valid(secret, response, remote_ip=None):
""" """
Encode the URL POST to the recaptcha service to check if the recaptcha is valid
""" """
alphabet = string.ascii_lowercase + string.digits data = {'secret': secret, 'response': response}
short_url = ''.join(secrets.choice(alphabet) for _ in range(8)) if remote_ip:
return short_url 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
@views.route('/', methods=['GET']) @views.route('/', methods=['GET'])
async def index(): def index():
""" """
Add a new sticky note 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 = [(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()) all_lexers.sort(key=lambda x: x[1].lower())
recaptcha_site_key = current_app.config.get('RECAPTCHA_SITE_KEY') recaptcha_site_key = current_app.config.get('RECAPTCHA_SITE_KEY')
return await render_template('index.html', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key) return render_template('index.html', lexers=all_lexers, recaptcha_site_key=recaptcha_site_key)
@views.route('/notes', methods=['GET']) @views.route('/notes', methods=['GET'])
async def notes(): def notes():
""" """
Show a list of recent notes Show a list of recent notes
""" """
@ -60,41 +66,54 @@ async def notes():
.filter(~StickyNote.private)\ .filter(~StickyNote.private)\
.order_by(StickyNote.created.desc())\ .order_by(StickyNote.created.desc())\
.limit(10) # noqa .limit(10) # noqa
return await render_template('notes.html', notes=notes) return render_template('notes.html', notes=notes)
@views.route('/about', methods=['GET']) @views.route('/about', methods=['GET'])
async def about(): def about():
""" """
Show the about page Show the about page
""" """
return await render_template('about.html') return render_template('about.html')
@views.route('/', methods=['POST']) @views.route('/', methods=['POST'])
async def save(): def save():
""" """
Save a sticky note Save a sticky note
""" """
form = await request.form # Check if the recaptcha is valid
if form.get('computer') and form['computer'].lower() != 'no': 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:
return redirect('/') return redirect('/')
# Save the note # Save the note
try: try:
created = datetime.utcnow() created = datetime.utcnow()
expiry = created + EXPIRY_DELTAS.get(form['expiry'], EXPIRY_DELTAS['1d']) expiry = EXPIRY_DELTAS.get(request.form['expiry'])
# Generate a short url, and check if it exists in the db if expiry:
url = _generate_short_url() expiry = created + expiry
while StickyNote.query.filter(StickyNote.url == url).first(): source = request.form['source']
url = _generate_short_url() lexer = request.form['language']
# Create a new note 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)
note = StickyNote( note = StickyNote(
title=form.get('title', ''), title=title,
source=form['source'], source=source,
lexer=form['language'], lexer=lexer,
created=created, created=created,
expiry=expiry, expiry=expiry,
private=True, private=private,
url=url url=url
) )
session.add(note) session.add(note)
@ -107,7 +126,7 @@ async def save():
@views.route('/<string:note_url>', methods=['GET']) @views.route('/<string:note_url>', methods=['GET'])
async def view(note_url): def view(note_url):
""" """
Show a sticky note Show a sticky note
@ -121,11 +140,11 @@ async def view(note_url):
lexer = get_lexer_by_name(note.lexer) lexer = get_lexer_by_name(note.lexer)
formatter = HtmlFormatter(linenos=True, cssclass='source') formatter = HtmlFormatter(linenos=True, cssclass='source')
result = highlight(note.source, lexer, formatter) result = highlight(note.source, lexer, formatter)
return await render_template('view.html', note=note, source=result) return render_template('view.html', note=note, source=result)
@views.route('/raw/<string:note_url>', methods=['GET']) @views.route('/raw/<string:note_url>', methods=['GET'])
async def raw(note_url): def raw(note_url):
""" """
Show the raw version of a sticky note Show the raw version of a sticky note
@ -135,14 +154,14 @@ async def raw(note_url):
if not note: if not note:
flash('That note does not exist', 'danger') flash('That note does not exist', 'danger')
return redirect('/') return redirect('/')
return await render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'} return render_template('raw.html', source=note.source), 200, {'Content-Type': 'text/plain; charset=utf-8'}
@views.route('/pygments.css', methods=['GET']) @views.route('/pygments.css', methods=['GET'])
async def pygments_css(): def pygments_css():
""" """
Return the Pygments CSS to the browser Return the Pygments CSS to the browser
""" """
response = await make_response(HtmlFormatter(style='nord').get_style_defs()) response = make_response(HtmlFormatter(style='nord').get_style_defs())
response.headers['Content-Type'] = 'text/css' response.headers['Content-Type'] = 'text/css'
return response return response

View File

@ -5,7 +5,7 @@ This is the entry point for the WSGI server
from pathlib import Path from pathlib import Path
from stickynotes import make_app from stickynotes import make_app
application = make_app(Path(__file__).parent.parent) application = make_app(Path(__file__).parent)
if __name__ == '__main__': if __name__ == '__main__':
application.run(debug=True) application.run(debug=True)