final beta

Signed-off-by: Ari Archer <ari@ari.lt>
This commit is contained in:
Arija A. 2024-06-06 17:23:49 +03:00
parent 3c218cd957
commit dd15783241
15 changed files with 544 additions and 69 deletions

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# New era of ari.lt
Ari.lt has become quite messy over the years, so this is meant to replace the website.
It is still a work in progress, but it's better than a mess.

View file

@ -1,2 +1,6 @@
# export DB='mysql+pymysql://user:password@127.0.0.1/arilt?charset=utf8mb4'
export DB='sqlite:///arilt.db'
export EMAIL_USER='someone@ari.lt'
export EMAIL_SERVER='mail.ari.lt'
export EMAIL_PASSWORD='...'
export ADMIN_KEY='...'

View file

@ -3,3 +3,8 @@ types-flask
flask-sqlalchemy
flask-ishuman
crc4
validators
markdown
jinja2
MarkupSafe
bleach

View file

@ -3,18 +3,20 @@
"""ari.lt"""
import datetime
import hashlib
import os
import sys
from base64 import b64encode
from typing import Any
import flask
from . import util
def create_app(name: str) -> flask.Flask:
"""create ari.lt app"""
for var in ("DB",):
for var in ("DB", "EMAIL_USER", "EMAIL_SERVER", "EMAIL_PASSWORD"):
if var not in os.environ:
print(f"Environment variable {var} is unset.", file=sys.stderr)
sys.exit(1)
@ -41,19 +43,17 @@ def create_app(name: str) -> flask.Flask:
app.config["USE_SESSION_FOR_NEXT"] = True
from .models import Admin, db
from .models import Counter, db
with app.app_context():
db.init_app(app)
db.create_all()
# if db.session.query(Admin).count() < 1:
# print("Creating an admin account...")
if db.session.query(Counter).count() < 1:
print("Creating a website counter...")
db.session.add(Counter(int(input("Count: "))))
# full_name: str = input("Full name: ")
# email: str = input("Email: ")
# salt: bytes = os.urandom(64)
# pwhash: bytes = hashlib.sha3_512(salt + input("Password: ")).digest()
db.session.commit()
from .views import views
@ -63,6 +63,8 @@ def create_app(name: str) -> flask.Flask:
c.init_app(app)
app.jinja_env.filters["markdown"] = util.markdown_to_html
@app.context_processor # type: ignore
def _() -> Any:
"""Context processor"""
@ -75,6 +77,7 @@ def create_app(name: str) -> flask.Flask:
"programming_exp": y - 2016,
"python_exp": y - 2016,
"c_exp": y - 2020,
"b64encode": b64encode,
}
return app

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
"""Constants"""
from typing import Final
from typing import Dict, Final, FrozenSet
HUGEINT_MAX: Final[int] = (10**65) - 1
@ -12,3 +12,19 @@ NAME_SIZE: Final[int] = 256
WEBSITE_SIZE: Final[int] = 256
EMAIL_CT_SIZE: Final[int] = 256
COMMENT_SIZE: Final[int] = 1024
ALLOWED_TAGS: Final[FrozenSet[str]] = frozenset(
(
"em",
"strong",
"a",
"code",
"pre",
"blockquote",
"p",
"ul",
"ol",
"li",
"br",
)
)

27
src/aw/email.py Normal file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""email"""
import os
import smtplib
from email.mime.text import MIMEText
def sendmail(to: str, subject: str, content: str) -> None:
"""send mail to an address"""
msg: MIMEText = MIMEText(content)
msg["Subject"] = f"[Ari-web] {subject}"
msg["From"] = os.environ["EMAIL_USER"]
msg["To"] = to
server: smtplib.SMTP = smtplib.SMTP(os.environ["EMAIL_SERVER"], 587)
server.ehlo()
server.starttls()
server.login(os.environ["EMAIL_USER"], os.environ["EMAIL_PASSWORD"])
server.send_message(msg)
server.quit()

View file

@ -68,6 +68,17 @@ class Counter(db.Model):
assert count >= 0 and count <= const.HUGEINT_MAX, "count out of range"
self.count: int = count
def inc(self) -> "Counter":
"""increment and return self"""
self.count += 1
db.session.commit()
return self
@staticmethod
def first() -> "Counter":
"""get counter"""
return db.session.query(Counter).first() # type: ignore
class Comment(db.Model):
"""Comment"""
@ -102,7 +113,7 @@ class Comment(db.Model):
self.website: t.Optional[str] = website
self.key: bytes = rand.randbytes(32)
self.email_ct: bytes = crc4.rc4(self.email.encode(), self.key) # type: ignore
self.email_ct: bytes = crc4.rc4(email.encode(), self.key) # type: ignore
self.comment: str = comment
self.confirmed: bool = False

25
src/aw/util.py Normal file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""utilities"""
import bleach
from markdown import markdown
from markupsafe import Markup
from . import const
def markdown_to_html(text: str) -> Markup:
"""Convert Markdown text to safe HTML"""
return Markup(
bleach.clean(
markdown(text, extensions=("extra", "smarty")),
tags=const.ALLOWED_TAGS,
attributes={
"*": ["href", "title"],
"a": ["href"],
},
protocols={"http", "https"},
)
)

View file

@ -2,22 +2,171 @@
# -*- coding: utf-8 -*-
"""views"""
import datetime
import os
import typing as t
import flask
import validators
from werkzeug.wrappers import Response
from .routing import Bp
from . import email, models
from .c import c
from .routing import Bp
views: Bp = Bp("views", __name__)
status: t.Dict[str, t.Any] = {
"status": "<i>No status</i>",
"last_updated": datetime.datetime.now(datetime.timezone.utc),
}
@views.get("/status")
def get_status() -> t.Any:
"""Get status"""
return flask.jsonify( # type: ignore
{
"status": status["status"],
"last_updated": status["last_updated"].timestamp(),
}
)
@views.post("/status")
def set_status() -> Response:
"""Set status"""
if (
"status" in flask.request.form
and flask.request.headers.get("X-Admin-Key", None) == os.environ["ADMIN_KEY"]
):
status["status"] = str(flask.request.form["status"]) # type: ignore
status["last_updated"] = datetime.datetime.now(datetime.timezone.utc)
return flask.jsonify( # type: ignore
{
"status": status["status"],
"last_updated": status["last_updated"].timestamp(),
}
)
else:
flask.abort(401)
@views.get("/index.html", alias=True)
@views.get("/")
def index() -> str:
"""Home page"""
return flask.render_template("index.j2")
return flask.render_template(
"index.j2",
visitor=models.Counter.first().inc().count,
comments=models.Comment.query.filter_by(confirmed=True).order_by(
models.Comment.posted.desc() # type: ignore
),
status=status,
)
@views.get("/confirm/<int:comment_id>/<string:token>/", alias=True)
@views.get("/confirm/<int:comment_id>/<string:token>")
def confirm(comment_id: int, token: str):
"""confirm publishing of a comment"""
comment: models.Comment = models.Comment.query.filter_by(
id=comment_id, token=token, confirmed=False
).first_or_404()
comment.confirmed = True
models.db.session.commit()
email.sendmail(
"ari@ari.lt",
f"Comment #{comment.id} on the guestbook",
f"""New comment on the guestbook for you to check out.
URL: {flask.url_for("views.index")}#{comment.id}
Name: {comment.name}
Website: {comment.website}
Comment:
```
{comment.comment}
```""",
)
flask.flash(f"Comment #{comment_id} confirmed.")
return flask.redirect(flask.url_for("views.index"))
@views.post("/")
def comment():
"""publish a comment"""
for field in "name", "email", "comment", "code":
if field not in flask.request.form:
flask.abort(400)
if not c.verify(flask.request.form["code"]): # type: ignore
flask.abort(403)
if not validators.email(flask.request.form["email"]):
flask.abort(400)
if (
"website" in flask.request.form
and flask.request.form["website"]
and not validators.url(flask.request.form["website"])
):
flask.abort(400)
try:
comment: models.Comment = models.Comment(
flask.request.form["name"], # type: ignore
flask.request.form.get("website", None), # type: ignore
flask.request.form["email"], # type: ignore
flask.request.form["comment"], # type: ignore
)
except Exception:
flask.abort(400)
models.db.session.add(comment)
models.db.session.commit()
try:
email.sendmail(
flask.request.form["email"], # type: ignore
f"Email confirmation for guestbook comment #{comment.id}",
f"""Hello!
Someone (or you) have commented on the https://ari.lt/ guestbook. If it was you, please confirm your email address below. Else - you may ignore it.
The comment is:
Name: {comment.name}
Website: {comment.website or "<none>"}
Comment:
```
{comment.comment}
```
Visit the following URL to *confirm* your email:
{flask.request.url.rstrip("/")}{flask.url_for("views.confirm", comment_id=comment.id, token=comment.token)}
...Or paste it into your browser.""",
)
except Exception:
models.db.session.delete(comment)
models.db.session.commit()
flask.abort(400)
flask.flash("Check your mailbox.")
return flask.redirect(flask.url_for("views.index"))
@views.get("/manifest.json")
@ -56,14 +205,6 @@ def license() -> flask.Response:
with open("LICENSE", "r") as fp:
return flask.Response(fp.read(), mimetype="text/plain")
@views.post("/")
def comment() -> flask.Response:
"""publish a comment"""
return flask.Response(
c.new().rawpng(),
mimetype="image/png"
)
@views.get("/git", defaults={"_": ""})
@views.get("/git/", defaults={"_": ""})
@ -71,7 +212,7 @@ def comment() -> flask.Response:
def git(_: str) -> Response:
"""Git source code"""
return flask.redirect(
f"https://ari.lt/lh/us.ari.lt/{flask.request.full_path[4:]}",
f"/lh/ari.lt/{flask.request.full_path[4:]}",
code=302,
)
@ -87,32 +228,118 @@ def favicon() -> Response:
)
)
@views.get("/captcha.png")
def captcha() -> flask.Response:
"""CAPTCHA"""
return flask.Response(
c.new().rawpng(),
mimetype="image/png"
)
return flask.Response(c.new().rawpng(), mimetype="image/png")
@views.get("/badge.png")
def badge() -> Response:
"""Website badge"""
return flask.redirect(
r: Response = flask.redirect(
flask.url_for(
"static",
filename="badges/badge.png",
)
)
r.headers["Access-Control-Allow-Origin"] = "*"
r.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, HEAD"
return r
@views.get("/badge-yellow.png")
def badge_yellow() -> Response:
"""Website badge (yellow)"""
return flask.redirect(
"""Website badge"""
r: Response = flask.redirect(
flask.url_for(
"static",
filename="badges/badge-yellow.png",
)
)
r.headers["Access-Control-Allow-Origin"] = "*"
r.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, HEAD"
return r
@views.get("/btc")
def btc() -> Response:
"""Bitcoin address"""
return flask.redirect(
"https://www.blockchain.com/explorer/addresses/btc/bc1qn3k75kmyvpw9sc58t63hk4ej4pc0d0w52tvj7w"
)
@views.get("/xmr")
def xmr() -> Response:
"""Monero address"""
return flask.redirect(
"https://moneroexplorer.org/search?value=451VZy8FPDXCVvKWkq5cby3V24ApLnjaTdwDgKG11uqbUJYjxQWZVKiiefi4HvFd7haeUtGFRBaxgKNTr3vR78pkMzgJaAZ"
)
@views.get("/page/canary", alias=True)
@views.get("/canary")
def canary():
"""Warrant Canary"""
return "Unavailable due to migration reasons."
@views.get("/page/casey", alias=True)
@views.get("/casey")
def casey():
"""Open letter to my best friend"""
return "Unavailable due to migration reasons."
@views.get("/page/matrix", alias=True)
@views.get("/matrix")
def matrix():
"""Matrix homeserver guidelines and Registration"""
return "Unavailable due to migration reasons."
@views.get("/mp")
def mp():
"""Music playlist"""
return flask.redirect(
"https://www.youtube.com/playlist?list=PL7UuKajElTaChff3BkcJE6620lSuSUaDC"
)
@views.get("/dotfiles", defaults={"_": ""})
@views.get("/dotfiles/", defaults={"_": ""})
@views.get("/dotfiles/<path:_>")
def dotfiles(_: str) -> Response:
"""Dotfiles"""
return flask.redirect(
f"https://github.com/TruncatedDinoSour/dotfiles-cleaned/{flask.request.full_path[9:]}",
code=302,
)
@views.get("/gh", defaults={"_": ""})
@views.get("/gh/", defaults={"_": ""})
@views.get("/gh/<path:_>")
def gh(_: str) -> Response:
"""Main git account"""
return flask.redirect(
f"https://github.com/TruncatedDinoSour/{flask.request.full_path[3:]}",
code=302,
)
@views.get("/lh", defaults={"_": ""})
@views.get("/lh/", defaults={"_": ""})
@views.get("/lh/<path:_>")
def lh(_: str) -> Response:
"""Main git organization account"""
return flask.redirect(
f"https://github.com/ari-lt/{flask.request.full_path[3:]}",
code=302,
)

View file

@ -94,7 +94,7 @@ li {
margin: 0.5em 0;
}
p, table, table * {
table, table * {
text-align: justify;
}

View file

@ -5,28 +5,41 @@
}
.split {
display: -ms-grid;
display: grid;
-ms-grid-columns: 3fr 0.5em 1fr;
grid-template-columns: 3fr 1fr;
grid-gap: 0.5em;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
height: 100%;
}
.split > * {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding: 2em;
height: 100%;
}
.esplit {
-ms-grid-columns: 1fr 1fr;
grid-template-columns: 1fr 1fr;
}
.split > :first-child {
position: relative;
overflow: hidden;
text-align: justify;
}
.split > :first-child::before {
@ -37,6 +50,7 @@
top: 0;
bottom: 0;
border-right: 2px solid var(--red);
-webkit-animation: grow 0.5s forwards;
animation: grow 0.5s forwards;
pointer-events: none;
}
@ -68,9 +82,11 @@ form > button {
}
.form-group {
display: -ms-grid;
display: grid;
-ms-grid-columns: 8rem 1rem auto;
grid-template-columns: 8em auto;
grid-gap: 1em;
grid-gap: 1rem;
margin: 0.4em;
}
@ -86,6 +102,7 @@ form > button {
}
.captcha {
display: -ms-grid;
display: grid;
place-items: center;
margin: 0.5em;
@ -100,36 +117,59 @@ form > button {
background-color: #111;
padding: 0.5em;
margin: 0.5em;
width: 100%;
}
#comments > div button {
border: none;
background-color: #000;
cursor: pointer;
border-bottom: 1px solid var(--red);
@-webkit-keyframes grow {
from {
-webkit-transform: scaleY(0);
transform: scaleY(0);
}
to {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
@keyframes grow {
from {
-webkit-transform: scaleY(0);
transform: scaleY(0);
}
to {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
@media only screen and (max-width: 1250px) {
.split {
-ms-grid-columns: 1fr;
grid-template-columns: 1fr;
-ms-grid-rows: auto 0.5em auto;
grid-template-rows: auto auto;
-webkit-box-align: unset;
-ms-flex-align: unset;
align-items: unset;
}
.split > *:nth-child(1) {
-ms-grid-row: 1;
-ms-grid-column: 1;
}
.split > *:nth-child(2) {
-ms-grid-row: 3;
-ms-grid-column: 1;
}
.split > :first-child {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
}
.split > :last-child {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}

View file

@ -33,7 +33,7 @@ document.addEventListener("DOMContentLoaded", function () {
canvas.height = window.innerHeight;
let particles = [];
const particle_density = 1.5e-5;
const particle_density = 2e-5;
let num_particles;
const particle_size = 2;

62
src/static/js/rc4.js Normal file
View file

@ -0,0 +1,62 @@
/*
* @licstart The following is the entire license notice for the JavaScript code in this file.
*
* Copyright (C) 2024 Ari Archer
*
* This file is part of ari.lt.
*
* The JavaScript code in this file is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (AGPL) as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. The code is distributed WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
*
* As additional permission under AGPL version 3 section 7, you may distribute non-source
* (e.g., minimized or compacted) forms of that code without the copy of the AGPL normally
* required by section 4, provided you include this license notice and a URL through which
* recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice for the JavaScript code in this file.
*/
"use strict";
function rc4(b64_ct, b64_key) {
let ciphertext = base64_to_array_buffer(b64_ct);
let key = base64_to_array_buffer(b64_key);
let S = Array(256);
let j = 0,
temp;
for (let i = 0; i < 256; ++i) S[i] = i;
for (let i = 0; i < 256; ++i) {
j = (j + S[i] + key[i % key.length]) % 256;
temp = S[i];
S[i] = S[j];
S[j] = temp;
}
let i = 0;
j = 0;
let decrypted = new Uint8Array(ciphertext.length);
for (let y = 0; y < ciphertext.length; y++) {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
temp = S[i];
S[i] = S[j];
S[j] = temp;
decrypted[y] = ciphertext[y] ^ S[(S[i] + S[j]) % 256];
}
return new TextDecoder().decode(decrypted);
}
function base64_to_array_buffer(base64) {
let bin = window.atob(base64);
let bytes = new Uint8Array(bin.length);
for (let idx = 0; idx < bin.length; ++idx) bytes[idx] = bin.charCodeAt(idx);
return bytes;
}

View file

@ -36,7 +36,19 @@
{% block body %}{% endblock %}
<article>
<header>{% block header %}{% endblock %}</header>
<main>{% block main %}{% endblock %}</main>
<main>
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
<details open>
<summary>messages from the server</summary>
{% for category, message in messages %}
<div data-category="{{ category }}">{{ message | escape }}</div>
{% endfor %}
</details>
{% endif %}
{% endwith %}
{% block main %}{% endblock %}
</main>
<footer>
<i>The <a target="_blank" href="{{ url_for('views.git') }}">source code</a> and all content, except the Nerd Hack font (see <a target="_blank" href="{{ url_for("static", filename="fonts/LICENSE") }}">Nerd Hack font license</a>), are licensed under the <a target="_blank" href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL-3.0-or-later</a> by Ari Archer &lt;<a target="_blank" href="mailto:ari@ari.lt">ari@ari.lt</a>&gt; as a part of the <a href="{{ url_for("views.badge") }}" target="_blank">ari-web</a> project. Copyright 2019-{{ current_year }}.</i>
</footer>

View file

@ -38,7 +38,8 @@
//--><!]]>
</script>
<script src="{{ url_for("static", filename="js/index.js") }}" referrerpolicy="no-referrer"></script>
<script src="{{ url_for("static", filename="js/particles.js") }}" referrerpolicy="no-referrer"></script>
<script src="{{ url_for("static", filename="js/rc4.js") }}" referrerpolicy="no-referrer"></script>
{% endblock %}
{% block body %}
@ -54,6 +55,7 @@
<summary>Navigation</summary>
<ul>
<li>Pages and redirects: <a href="#pages">#pages</a></li>
<li>Links: <a href="#links">#links</a></li>
<li>Staff list: <a href="#staff">#staff</a></li>
<li>Self-hosted services list: <a href="#services">#services</a></li>
@ -72,7 +74,7 @@
<blockquote>
Before I start explaining who I am, if you're here on this page because of one of the users
using ari-web services and you want to report abuse, please contact me using one of the
contacts listed on this page (such as the <a href="mailto:ari@ari.lt" target="_blank">ari@ari.lt</a> email)
contacts listed on this page (such as the <a href="mailto:ari@ari.lt">ari@ari.lt</a> email)
or see <a href="#staff">the list of staff</a>. You may also leave a comment on <a href="#gb">the guestbook</a>.
</blockquote>
@ -155,7 +157,7 @@
<li>GitHub organization: <a target="_blank" href="https://github.com/ari-lt">ari-lt</a> (<a href="mailto:org@ari.lt">org@ari.lt</a>)</li>
<li>Matrix: <a target="_blank" href="https://matrix.to/#/@ari:ari.lt">@ari:ari.lt</a></li>
<li>Fediverse: <a target="_blank" href="https://ak.ari.lt/ari">@ari@ak.ari.lt</a></li>
<li>E-Mail: <a target="_blank" href="mailto:ari@ari.lt">ari@ari.lt</a> (GPG: <a target="_blank" href="https://keys.openpgp.org/vks/v1/by-fingerprint/4FAD63E936B305906A6C4894A50D5B4B599AF8A2">4FAD63E936B305906A6C4894A50D5B4B599AF8A2</a>)</li>
<li>E-Mail: <a href="mailto:ari@ari.lt">ari@ari.lt</a> (GPG: <a target="_blank" href="https://keys.openpgp.org/vks/v1/by-fingerprint/4FAD63E936B305906A6C4894A50D5B4B599AF8A2">4FAD63E936B305906A6C4894A50D5B4B599AF8A2</a>)</li>
</ul>
</li>
</ul>
@ -171,14 +173,42 @@
this is a good introduction to what I do, what skills I posses.
</p>
<p>
Current status:
</p>
<div align="center">
<blockquote><pre>This is a status
Hello world</pre>
Last updated: 2024-06-06 00:17:11 UTC</blockquote>
<div><b>*** Current status ***</b></div>
<br />
<div>{{ status["status"] }}</div>
<br />
<div>Last updated: <b>{{ status["last_updated"] }}</b></div>
</div>
<h2 id="pages"><a href="#pages">#</a> Pages and redirects</h2>
<div class="split esplit">
<div>
<ul>
<li><a href="/">The home page of Ari.lt</a></li>
<li><a href="{{ url_for("views.canary") }}">Warrant canary of Ari.lt</a></li>
<li><a href="{{ url_for("views.casey") }}">Open letter to my best friend, Casey</a></li>
<li><a href="{{ url_for("views.matrix") }}">Matrix homeserver guidelines and registration</a></li>
<li><a target="_blank" href="{{ url_for("views.mp") }}">Music playlist (YouTube)</a></li>
<li><a target="_blank" href="{{ url_for("views.dotfiles") }}">My dotfiles (redirect)</a></li>
<li><a target="_blank" href="{{ url_for("views.gh") }}">My main Git account (redirect)</a></li>
<li><a target="_blank" href="{{ url_for("views.lh") }}">Main Ari.lt organization (redirect)</a></li>
</ul>
</div>
<div>
<p>
Ari.lt includes multiple pages and redirects which include various information. This is a manually collected list
of some important pages. If you want a <i>full</i> list of all pages see the <a href="/sitemap.xml">sitemap.xml</a>
which has all possible pages on this site in an XML format - mainly used by indexers.
</p>
<p>
Ari.lt is still in the middle of migration to full-featured self-hosting, so some stuff might break
or change with time, although I try to keep backwards compatibility possible.
</p>
</div>
</div>
<h2 id="links"><a href="#links">#</a> Links</h2>
@ -194,10 +224,17 @@ Last updated: 2024-06-06 00:17:11 UTC</blockquote>
<li>GitHub organization: <a target="_blank" href="https://github.com/ari-lt">ari-lt</a> (<a href="mailto:org@ari.lt">org@ari.lt</a>)</li>
<li>Matrix: <a target="_blank" href="https://matrix.to/#/@ari:ari.lt">@ari:ari.lt</a></li>
<li>Fediverse: <a target="_blank" href="https://ak.ari.lt/ari">@ari@ak.ari.lt</a></li>
<li>E-Mail: <a target="_blank" href="mailto:ari@ari.lt">ari@ari.lt</a> (GPG: <a target="_blank" href="https://keys.openpgp.org/vks/v1/by-fingerprint/4FAD63E936B305906A6C4894A50D5B4B599AF8A2">4FAD63E936B305906A6C4894A50D5B4B599AF8A2</a>)</li>
<li>E-Mail: <a href="mailto:ari@ari.lt">ari@ari.lt</a> (GPG: <a target="_blank" href="https://keys.openpgp.org/vks/v1/by-fingerprint/4FAD63E936B305906A6C4894A50D5B4B599AF8A2">4FAD63E936B305906A6C4894A50D5B4B599AF8A2</a>)</li>
</ul>
</div>
<h3>Badges</h3>
<ul>
<li>Normal: <a href="https://ari.lt/"> <img src="{{ url_for("views.badge") }}" loading="lazy" alt="ari-web badge" height="31px" width="88px" /> </a></li>
<li>Yellow: <a href="https://ari.lt/"> <img src="{{ url_for("views.badge_yellow") }}" loading="lazy" alt="ari-web badge" height="31px" width="88px" /> </a></li>
</ul>
<h3>Activity</h3>
<ul>
@ -223,6 +260,13 @@ Last updated: 2024-06-06 00:17:11 UTC</blockquote>
sometimes.
</p>
<h3>Support</h3>
<ul>
<li>Monero wallet address: <a target="_blank" href="{{ url_for("views.xmr") }}">{{ url_for("views.xmr") | escape }}</a></li>
<li>Bitcoin wallet address: <a target="_blank" href="{{ url_for("views.btc") }}">{{ url_for("views.btc") | escape }}</a></li>
</ul>
<h2 id="staff"><a href="#staff">#</a> Staff</h2>
<p>
@ -306,7 +350,7 @@ Last updated: 2024-06-06 00:17:11 UTC</blockquote>
</tr>
<tr>
<td>Email server hosting <a href="https://mailcow.email/" target="_blank">Mailcow</a></td>
<td>Contact <a href="mailto:ari@ari.lt" target="_blank">ari@ari.lt</a> for custom domains (aggressive policy)</td>
<td>Contact <a href="mailto:ari@ari.lt">ari@ari.lt</a> for custom domains (aggressive policy)</td>
<td><a href="https://mail.ari.lt/" target="_blank">mail.ari.lt</a> (<a href="https://mail.ari.lt/signup" target="_blank">register here</a>)</td>
</tr>
<tr>
@ -355,10 +399,11 @@ Last updated: 2024-06-06 00:17:11 UTC</blockquote>
<h2 id="gb"><a href="#gb">#</a> Guestbook</h2>
<p>
If you want to interact with the community feel free to leave your comments here! You will require an
email address to prevent spam and impersonation, it will be listed, although not as text as it will
be encrypted server-side using <a target="_blank" href="https://en.wikipedia.org/wiki/RC4">RC4</a> (a fast, but insecure cipher)
with a 32-byte (256 bit) key. Check your mailbox when you comment as you will need to verify the comment.
If you want to interact with the community feel free to leave your comments here!
You will require an email address so to prevent spam and scams, although, to protect
<i>you</i> from spam and scam emails, I have implemented measures to not store your
email address as plain text. Press 'show email' to show the email address on any given
comment.
</p>
<form method="post">
@ -378,8 +423,8 @@ Last updated: 2024-06-06 00:17:11 UTC</blockquote>
</div>
<div class="form-group">
<label for="comment">Comment:</label>
<textarea required type="text" id="comment" name="comment" placeholder="Hello!"></textarea>
<label for="comment">Comment (<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">markdown</a>):</label>
<textarea required type="text" id="comment" name="comment" placeholder="Hello! I like your website - &lt;https://ari.lt/&gt;."></textarea>
</div>
<div class="captcha">
@ -389,30 +434,23 @@ Last updated: 2024-06-06 00:17:11 UTC</blockquote>
<div class="form-group">
<label for="captcha">CAPTCHA:</label>
<input required type="text" id="comment" name="comment" placeholder="Enter the CAPTCHA code above." />
<input required type="text" id="code" name="code" placeholder="Enter the CAPTCHA code above." />
</div>
<button type="submit">Comment</button>
</form>
<hr />
<div align="center">
<i>Report any impersonation to the owner of this website: Ari Archer.</i>
</div>
<div id="comments">
<div id="guestbook-1">
<p><a href="#guestbook-1">#1:</a> Cool Person (<a target="_blank" href="#!">https://example.com/</a>) &lt;<i><button onclick="rc4('data','key')">show email</button></i>&gt; at 2024-06-05 11:11:11 UTC says...</p>
<pre>Cool website, but you should kill yourself!
Meow!</pre>
</div>
<div id="guestbook-1">
<p><a href="#guestbook-1">#1:</a> Cool Person (<a target="_blank" href="#!">https://example.com/</a>) &lt;<i><button onclick="rc4('data','key')">show email</button></i>&gt; at 2024-06-05 11:11:11 UTC says...</p>
<pre>Cool website, but you should kill yourself!
Meow!</pre>
{% for comment in comments %}
<div id="gb-{{ comment.id }}">
<p><a href="#gb-{{ comment.id }}">#{{ comment.id }}:</a> <span>{{ comment.name | escape }}</span> {% if comment.website %} (<a target="_blank" href="{{ comment.website }}" rel="noopener noreferrer" target="_blank">{{ comment.website | escape }}</a>) {% endif %} &lt;<i><a href="mailto:" style="color:#aaa" onclick="this.innerText=rc4('{{ b64encode(comment.email_ct).decode() }}','{{ b64encode(comment.key).decode() }}');this.href+=this.innerText;this.onclick=null;this.style='';return false">show email</a></i>&gt; at <time>{{ comment.posted }} UTC</time> says...</p>
<div>{{ comment.comment | markdown }}</div>
</div>
{% endfor %}
</div>
{% endblock %}