diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0715a1b
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/ari-lt.env.example b/ari-lt.env.example
index 318fb2b..2da3411 100644
--- a/ari-lt.env.example
+++ b/ari-lt.env.example
@@ -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='...'
diff --git a/requirements.txt b/requirements.txt
index 85f6972..d7a6bfb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,8 @@ types-flask
flask-sqlalchemy
flask-ishuman
crc4
+validators
+markdown
+jinja2
+MarkupSafe
+bleach
diff --git a/src/aw/__init__.py b/src/aw/__init__.py
index 374ecfa..a4219d4 100644
--- a/src/aw/__init__.py
+++ b/src/aw/__init__.py
@@ -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
diff --git a/src/aw/const.py b/src/aw/const.py
index 61c90ca..d9b4e4d 100644
--- a/src/aw/const.py
+++ b/src/aw/const.py
@@ -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",
+ )
+)
diff --git a/src/aw/email.py b/src/aw/email.py
new file mode 100644
index 0000000..c4d937e
--- /dev/null
+++ b/src/aw/email.py
@@ -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()
diff --git a/src/aw/models.py b/src/aw/models.py
index 7b61033..fbaf3aa 100644
--- a/src/aw/models.py
+++ b/src/aw/models.py
@@ -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
diff --git a/src/aw/util.py b/src/aw/util.py
new file mode 100644
index 0000000..758020e
--- /dev/null
+++ b/src/aw/util.py
@@ -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"},
+ )
+ )
diff --git a/src/aw/views.py b/src/aw/views.py
index 215c11d..bfebd1a 100644
--- a/src/aw/views.py
+++ b/src/aw/views.py
@@ -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": "No status",
+ "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///", alias=True)
+@views.get("/confirm//")
+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 ""}
+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/")
+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/")
+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/")
+def lh(_: str) -> Response:
+ """Main git organization account"""
+ return flask.redirect(
+ f"https://github.com/ari-lt/{flask.request.full_path[3:]}",
+ code=302,
+ )
diff --git a/src/static/css/base.css b/src/static/css/base.css
index 14a1d92..a2a8ede 100644
--- a/src/static/css/base.css
+++ b/src/static/css/base.css
@@ -94,7 +94,7 @@ li {
margin: 0.5em 0;
}
-p, table, table * {
+table, table * {
text-align: justify;
}
diff --git a/src/static/css/index.css b/src/static/css/index.css
index 83c7e29..c3017b3 100644
--- a/src/static/css/index.css
+++ b/src/static/css/index.css
@@ -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;
}
diff --git a/src/static/js/index.js b/src/static/js/particles.js
similarity index 99%
rename from src/static/js/index.js
rename to src/static/js/particles.js
index 1576515..7b82c5a 100644
--- a/src/static/js/index.js
+++ b/src/static/js/particles.js
@@ -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;
diff --git a/src/static/js/rc4.js b/src/static/js/rc4.js
new file mode 100644
index 0000000..1da0be1
--- /dev/null
+++ b/src/static/js/rc4.js
@@ -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;
+}
diff --git a/src/templates/base.j2 b/src/templates/base.j2
index ed048f3..53476ac 100644
--- a/src/templates/base.j2
+++ b/src/templates/base.j2
@@ -36,7 +36,19 @@
{% block body %}{% endblock %}
{% block header %}{% endblock %}
- {% block main %}{% endblock %}
+
+ {% with messages = get_flashed_messages(with_categories=True) %}
+ {% if messages %}
+
+ messages from the server
+ {% for category, message in messages %}
+ {{ message | escape }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+ {% block main %}{% endblock %}
+
diff --git a/src/templates/index.j2 b/src/templates/index.j2
index bd8b899..f6e8d3a 100644
--- a/src/templates/index.j2
+++ b/src/templates/index.j2
@@ -38,7 +38,8 @@
//-->
-
+
+
{% endblock %}
{% block body %}
@@ -54,6 +55,7 @@
Navigation
@@ -171,14 +173,42 @@
this is a good introduction to what I do, what skills I posses.
-
- Current status:
-
-
-
This is a status
-Hello world
-Last updated: 2024-06-06 00:17:11 UTC
+
*** Current status ***
+
+
{{ status["status"] }}
+
+
Last updated: {{ status["last_updated"] }}
+
+
+# Pages and redirects
+
+
+
+
+
+
+ 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 full list of all pages see the sitemap.xml
+ which has all possible pages on this site in an XML format - mainly used by indexers.
+
+
+
+ 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.
+
+
# Links
@@ -194,10 +224,17 @@ Last updated: 2024-06-06 00:17:11 UTC
GitHub organization: ari-lt (org@ari.lt)
Matrix: @ari:ari.lt
Fediverse: @ari@ak.ari.lt
- E-Mail: ari@ari.lt (GPG: 4FAD63E936B305906A6C4894A50D5B4B599AF8A2)
+ E-Mail: ari@ari.lt (GPG: 4FAD63E936B305906A6C4894A50D5B4B599AF8A2)
+Badges
+
+
+ - Normal:
+ - Yellow:
+
+
Activity
@@ -223,6 +260,13 @@ Last updated: 2024-06-06 00:17:11 UTC
sometimes.
+Support
+
+
+
# Staff
@@ -306,7 +350,7 @@ Last updated: 2024-06-06 00:17:11 UTC
Email server hosting Mailcow |
- Contact ari@ari.lt for custom domains (aggressive policy) |
+ Contact ari@ari.lt for custom domains (aggressive policy) |
mail.ari.lt (register here) |
@@ -355,10 +399,11 @@ Last updated: 2024-06-06 00:17:11 UTC
# Guestbook
- 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 RC4 (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
+ you 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.