Initial commit: Added current work!
This commit is contained in:
commit
10b7960dd9
9 changed files with 250 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.idea/
|
||||
__pycache__/
|
||||
database/data.sqlite
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true
|
||||
}
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# GayBook
|
||||
|
||||
This is a simple GuestBook software written in Python, using Flask!
|
||||
|
||||
It uses a sqlite database for storage, and avoids spam with a simple proof-of-work system
|
||||
|
||||
## Running
|
||||
|
||||
You'll need python and flask installed on your machine.
|
||||
|
||||
First, set up the database
|
||||
|
||||
```console
|
||||
$ cd database/
|
||||
$ python setup_db.py
|
||||
```
|
||||
|
||||
Then, run the Flask app
|
||||
|
||||
```console
|
||||
$ python app.py
|
||||
```
|
||||
|
||||
To run in debug mode, use
|
||||
|
||||
```console
|
||||
$ FLASK_DEBUG=1 python app.py
|
||||
```
|
75
app.py
Normal file
75
app.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import flask
|
||||
import dataclasses
|
||||
import sqlite3
|
||||
import datetime
|
||||
import hashlib
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
|
||||
def get_database_connection():
|
||||
database_connection = sqlite3.connect("database/data.sqlite")
|
||||
database_connection.row_factory = sqlite3.Row
|
||||
return database_connection
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Quote:
|
||||
author: str
|
||||
content: str
|
||||
timestamp: datetime.datetime = datetime.datetime.now()
|
||||
|
||||
def proof_of_work_is_valid(self, work: str):
|
||||
to_hash = f"{self.author}\0{self.content}\0{work}"
|
||||
hashed = hashlib.sha1(to_hash.encode("utf-8")).hexdigest()
|
||||
return hashed[:5] == "00000"
|
||||
|
||||
|
||||
def get_all_quotes() -> list[Quote]:
|
||||
db_connection = get_database_connection()
|
||||
db_quotes = db_connection.execute(
|
||||
"SELECT author, content, timestamp FROM quotes ORDER BY timestamp DESC"
|
||||
).fetchall()
|
||||
db_connection.close()
|
||||
|
||||
quotes: list[Quote] = []
|
||||
for db_quote in db_quotes:
|
||||
quotes.append(Quote(db_quote["author"], db_quote["content"], db_quote["timestamp"]))
|
||||
|
||||
return quotes
|
||||
|
||||
|
||||
def add_quote(new_quote: Quote):
|
||||
db_connexion = get_database_connection()
|
||||
db_connexion.execute(
|
||||
"INSERT INTO quotes (author, content) VALUES (?, ?)",
|
||||
(new_quote.author, new_quote.content)
|
||||
)
|
||||
db_connexion.commit()
|
||||
db_connexion.close()
|
||||
|
||||
|
||||
@app.route('/', methods=["POST", "GET"])
|
||||
def index():
|
||||
message: str = ""
|
||||
response_code = 200
|
||||
|
||||
if flask.request.method == "POST":
|
||||
form = flask.request.form
|
||||
if "username" not in form or "message" not in form or "proof-of-work" not in form:
|
||||
message = "Did not provide username, quote or proof of work."
|
||||
response_code = 400
|
||||
else:
|
||||
quote = Quote(form["username"], form["message"])
|
||||
if quote.proof_of_work_is_valid(form["proof-of-work"]):
|
||||
add_quote(Quote(form["username"], form["message"]))
|
||||
message = "Your quote was added successfully"
|
||||
else:
|
||||
message = "You didn't do the work, silly!"
|
||||
response_code = 400
|
||||
|
||||
return flask.render_template("index.html", quotes=get_all_quotes(), message=message), response_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
6
database/quotes.sql
Normal file
6
database/quotes.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS quotes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
author TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
9
database/setup_db.py
Normal file
9
database/setup_db.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
import sqlite3
|
||||
|
||||
connection = sqlite3.connect("data.sqlite")
|
||||
|
||||
with open("quotes.sql") as fp:
|
||||
connection.execute(fp.read())
|
||||
|
||||
connection.commit()
|
||||
connection.close()
|
33
static/proof-of-work.js
Normal file
33
static/proof-of-work.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
const generateButton = document.getElementById("generate-pow");
|
||||
generateButton.style.display = "block";
|
||||
|
||||
const usernameField = document.getElementById("username");
|
||||
const messageField = document.getElementById("message");
|
||||
const powField = document.getElementById("proof-of-work");
|
||||
|
||||
const hash = (message) =>
|
||||
crypto.subtle.digest("SHA-1",
|
||||
(new TextEncoder()).encode(message)
|
||||
);
|
||||
const isValidHash = (hashBuffer) =>
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
.map(byte => byte.toString(16).padStart(2, "0"))
|
||||
.join("").startsWith("00000");
|
||||
|
||||
const messageBase = () => usernameField.value + "\0" + messageField.value + "\0";
|
||||
|
||||
generateButton.onclick = async (event) => {
|
||||
event.preventDefault();
|
||||
const base = messageBase();
|
||||
let currentNonce = 0;
|
||||
|
||||
powField.disabled = true;
|
||||
powField.value = "Generating... Please wait";
|
||||
|
||||
console.log("Starting PoW generation, this may take a while....");
|
||||
while (!isValidHash(await hash(base + currentNonce)))
|
||||
++currentNonce;
|
||||
console.log("Done!");
|
||||
powField.value = currentNonce;
|
||||
powField.disabled = false;
|
||||
};
|
29
static/style.css
Normal file
29
static/style.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
body {
|
||||
font-family: monospace;
|
||||
background: #330080;
|
||||
color: #e5e5e5;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 40rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#website-message {
|
||||
background: purple;
|
||||
padding: 0.5rem;
|
||||
}
|
63
templates/index.html
Normal file
63
templates/index.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>gaybook</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/proof-of-work.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>The GayBook™</h1>
|
||||
<p>If you pass by, feel free to leave a message here!</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% if message != "" %}
|
||||
<section id="website-message">
|
||||
<h1>Message from the website</h1>
|
||||
<p>{{ message }}</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h1>Leave a quote!</h1>
|
||||
<form method="POST">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" name="username" id="username" required>
|
||||
<br>
|
||||
<label for="message">Message:</label>
|
||||
<textarea name="message" id="message" rows="5" required></textarea>
|
||||
<br>
|
||||
<label for="proof-of-work">Proof of work:</label>
|
||||
<input type="text" name="proof-of-work" id="proof-of-work" required>
|
||||
<button id="generate-pow" style="display: none">
|
||||
Generate proof of work
|
||||
</button>
|
||||
<noscript>
|
||||
<p>
|
||||
You have Javascript disabled. You'll need to generate the proof-of-work manually.
|
||||
<br>
|
||||
Compute a nonce such that the ascii hex sha1 digest of
|
||||
"$USERNAME\0$MESSAGE\0$NONCE" starts with 5 zeros. The nonce needs to be a base-10 integer
|
||||
number encoded in ASCII.
|
||||
Once you've verified the nonce works, enter it in base10 ascii in the "Proof of work" field.
|
||||
</p>
|
||||
</noscript>
|
||||
<br>
|
||||
<input type="submit" value="Publish">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>Past quotes</h1>
|
||||
{% for quote in quotes %}
|
||||
<p>From: {{ quote.author }}, at {{ quote.timestamp }}</p>
|
||||
<p>{{ quote.content }}</p>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue