diff --git a/captcha.key b/captcha.key new file mode 100644 index 0000000..4461e6e Binary files /dev/null and b/captcha.key differ diff --git a/requirements.txt b/requirements.txt index a0cbb23..85f6972 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ flask types-flask flask-sqlalchemy +flask-ishuman +crc4 diff --git a/src/aw/__init__.py b/src/aw/__init__.py index 5dd8209..374ecfa 100644 --- a/src/aw/__init__.py +++ b/src/aw/__init__.py @@ -3,6 +3,7 @@ """ari.lt""" import datetime +import hashlib import os import sys from typing import Any @@ -23,6 +24,8 @@ def create_app(name: str) -> flask.Flask: app.config["PREFERRED_URL_SCHEME"] = "http" if app.debug else "https" app.config["DOMAIN"] = "ari.lt" + app.config["SECRET_KEY"] = os.urandom(4096) + app.config["SESSION_COOKIE_SAMESITE"] = "strict" app.config["SESSION_COOKIE_SECURE"] = True app.config["SESSION_COOKIE_HTTPONLY"] = True @@ -31,12 +34,35 @@ def create_app(name: str) -> flask.Flask: app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"pool_pre_ping": True} app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + app.config["CAPTCHA_PEPPER_FILE"] = "captcha.key" + app.config["CAPTCHA_EXPIRY"] = 60 * 10 # 10 minutes + app.config["CAPTCHA_CHARSET"] = "abdefghmnqrtyABDEFGHLMNRTY2345689#@%?!" + app.config["CAPTCHA_RANGE"] = (4, 6) + app.config["USE_SESSION_FOR_NEXT"] = True + from .models import Admin, db + + with app.app_context(): + db.init_app(app) + db.create_all() + + # if db.session.query(Admin).count() < 1: + # print("Creating an admin account...") + + # full_name: str = input("Full name: ") + # email: str = input("Email: ") + # salt: bytes = os.urandom(64) + # pwhash: bytes = hashlib.sha3_512(salt + input("Password: ")).digest() + from .views import views app.register_blueprint(views, url_prefix="/") + from .c import c + + c.init_app(app) + @app.context_processor # type: ignore def _() -> Any: """Context processor""" diff --git a/src/aw/c.py b/src/aw/c.py new file mode 100644 index 0000000..a6aa029 --- /dev/null +++ b/src/aw/c.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Captcha""" + +import flask_ishuman + +c: flask_ishuman.IsHuman = flask_ishuman.IsHuman() diff --git a/src/aw/const.py b/src/aw/const.py index c07181f..61c90ca 100644 --- a/src/aw/const.py +++ b/src/aw/const.py @@ -5,3 +5,10 @@ from typing import Final HUGEINT_MAX: Final[int] = (10**65) - 1 + +USERNAME_SIZE: Final[int] = 64 + +NAME_SIZE: Final[int] = 256 +WEBSITE_SIZE: Final[int] = 256 +EMAIL_CT_SIZE: Final[int] = 256 +COMMENT_SIZE: Final[int] = 1024 diff --git a/src/aw/models.py b/src/aw/models.py index 6b662d3..7b61033 100644 --- a/src/aw/models.py +++ b/src/aw/models.py @@ -2,15 +2,20 @@ # -*- coding: utf-8 -*- """DB Models""" +import datetime +import string import typing as t from decimal import Decimal +from secrets import SystemRandom +import crc4 from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import DECIMAL, Dialect, TypeDecorator +from sqlalchemy import DECIMAL, DateTime, Dialect, TypeDecorator, Unicode from . import const db: SQLAlchemy = SQLAlchemy() +rand: SystemRandom = SystemRandom() class HugeUInt(TypeDecorator): # type: ignore @@ -54,7 +59,55 @@ class Counter(db.Model): id: int = db.Column( db.Integer, - primary_key=True, unique=True, + primary_key=True, ) count: int = db.Column(HugeUInt()) + + def __init__(self, count: int = 0) -> None: + assert count >= 0 and count <= const.HUGEINT_MAX, "count out of range" + self.count: int = count + + +class Comment(db.Model): + """Comment""" + + id: int = db.Column( + db.Integer, + unique=True, + primary_key=True, + ) + + name: str = db.Column(Unicode(const.NAME_SIZE)) + website: t.Optional[str] = db.Column(db.String(const.WEBSITE_SIZE), nullable=True) + + email_ct: bytes = db.Column(db.LargeBinary(length=const.EMAIL_CT_SIZE)) + key: bytes = db.Column(db.LargeBinary(length=32)) + + comment: str = db.Column(Unicode(const.COMMENT_SIZE)) + confirmed: bool = db.Column(db.Boolean, default=False) + posted: datetime.datetime = db.Column(DateTime, nullable=False) + + token: str = db.Column(db.String(32)) + + def __init__( + self, name: str, website: t.Optional[str], email: str, comment: str + ) -> None: + assert len(name) <= const.NAME_SIZE, "Name too long" + assert len(website or "") <= const.WEBSITE_SIZE, "Website too long" + assert len(email) <= const.EMAIL_CT_SIZE, "Email too long" + assert len(comment) <= const.COMMENT_SIZE, "Comment too long" + + self.name: str = name + 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.comment: str = comment + self.confirmed: bool = False + self.posted: datetime.datetime = datetime.datetime.now(datetime.timezone.utc) + + self.token: str = "".join( + rand.choices(string.ascii_letters + string.digits, k=32) + ) diff --git a/src/aw/views.py b/src/aw/views.py index b3ba3cb..215c11d 100644 --- a/src/aw/views.py +++ b/src/aw/views.py @@ -8,6 +8,7 @@ import flask from werkzeug.wrappers import Response from .routing import Bp +from .c import c views: Bp = Bp("views", __name__) @@ -55,6 +56,14 @@ 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={"_": ""}) @@ -77,3 +86,33 @@ def favicon() -> Response: mimetype="image/vnd.microsoft.icon", ) ) + +@views.get("/captcha.png") +def captcha() -> flask.Response: + """CAPTCHA""" + return flask.Response( + c.new().rawpng(), + mimetype="image/png" + ) + + +@views.get("/badge.png") +def badge() -> Response: + """Website badge""" + return flask.redirect( + flask.url_for( + "static", + filename="badges/badge.png", + ) + ) + + +@views.get("/badge-yellow.png") +def badge_yellow() -> Response: + """Website badge (yellow)""" + return flask.redirect( + flask.url_for( + "static", + filename="badges/badge-yellow.png", + ) + ) diff --git a/src/static/badges/badge-yellow.png b/src/static/badges/badge-yellow.png new file mode 100644 index 0000000..4f965c7 Binary files /dev/null and b/src/static/badges/badge-yellow.png differ diff --git a/src/static/badges/badge.png b/src/static/badges/badge.png new file mode 100644 index 0000000..9187534 Binary files /dev/null and b/src/static/badges/badge.png differ diff --git a/src/static/css/base.css b/src/static/css/base.css index 73c2eab..14a1d92 100644 --- a/src/static/css/base.css +++ b/src/static/css/base.css @@ -48,7 +48,7 @@ body { margin: auto; padding: 0; - max-width: 1400px; + max-width: 1600px; text-rendering: optimizeSpeed; line-height: 1.5; @@ -94,7 +94,7 @@ li { margin: 0.5em 0; } -p, table { +p, table, table * { text-align: justify; } @@ -112,6 +112,10 @@ a:hover { text-decoration: underline; } +.mob { + display: none; +} + @media (prefers-reduced-motion: reduce) { *, *::before, @@ -129,3 +133,9 @@ a:hover { scroll-behavior: auto !important; } } + +@media only screen and (max-width: 1250px) { + .mob { + display: initial; + } +} diff --git a/src/static/css/index.css b/src/static/css/index.css index acc2f41..83c7e29 100644 --- a/src/static/css/index.css +++ b/src/static/css/index.css @@ -4,14 +4,24 @@ font-family: Hack, hack, sans-serif; } -.split > * { - padding: 2em; +.split { + display: grid; + grid-template-columns: 3fr 1fr; + grid-gap: 0.5em; + align-items: stretch; + height: 100%; } -.split { - display: grid; - grid-gap: 0.5em; - grid-template-columns: 3fr 1fr; +.split > * { + display: flex; + flex-direction: column; + justify-content: center; + padding: 2em; + height: 100%; +} + +.esplit { + grid-template-columns: 1fr 1fr; } .split > :first-child { @@ -24,25 +34,13 @@ position: absolute; left: 0; right: 0; - top: 50%; - bottom: 50%; + top: 0; + bottom: 0; border-right: 2px solid var(--red); animation: grow 0.5s forwards; pointer-events: none; } -@keyframes grow { - from { - top: 50%; - bottom: 50%; - } - - to { - top: 0; - bottom: 0; - } -} - canvas#particles { position: fixed; top: 0; @@ -53,28 +51,78 @@ canvas#particles { pointer-events: none; } -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - -webkit-animation-duration: 0.01ms !important; - animation-duration: 0.01ms !important; +form { + background-color: #030303; + width: 100%; + padding: 1em; + margin: 1em; + border-bottom: 1px solid var(--red); +} - -webkit-animation-iteration-count: 1 !important; - animation-iteration-count: 1 !important; +form > button { + background-color: #111; + border: none; + padding: 0.5em; + width: 100%; + cursor: pointer; +} - -webkit-transition-duration: 0.01ms !important; - -o-transition-duration: 0.01ms !important; - transition-duration: 0.01ms !important; +.form-group { + display: grid; + grid-template-columns: 8em auto; + grid-gap: 1em; + margin: 0.4em; +} - scroll-behavior: auto !important; +.form-group * { + border: none; + padding: 0.3em; +} + +.form-group > :last-child { + background-color: #111; + resize: vertical; + min-height: 3em; +} + +.captcha { + display: grid; + place-items: center; + margin: 0.5em; +} + +.captcha > img { + display: block; + cursor: pointer; +} + +#comments > div { + background-color: #111; + padding: 0.5em; + margin: 0.5em; +} + +#comments > div button { + border: none; + background-color: #000; + cursor: pointer; + border-bottom: 1px solid var(--red); +} + +@keyframes grow { + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); } } -@media only screen and (max-width: 1000px) { +@media only screen and (max-width: 1250px) { .split { grid-template-columns: 1fr; grid-template-rows: auto auto; + align-items: unset; } .split > :first-child { @@ -90,11 +138,6 @@ canvas#particles { display: none; } - .split { - grid-template-columns: 1fr; - border-bottom: 2px dashed gray; - } - .split > :last-child { word-break: break-all; } diff --git a/src/static/js/index.js b/src/static/js/index.js index 0951e53..1576515 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -33,8 +33,9 @@ document.addEventListener("DOMContentLoaded", function () { canvas.height = window.innerHeight; let particles = []; + const particle_density = 1.5e-5; + let num_particles; - const num_particles = 512; const particle_size = 2; const avoidance_radius = 48; @@ -87,6 +88,10 @@ document.addEventListener("DOMContentLoaded", function () { class Particle { constructor() { + this.reset(); + } + + reset() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.velocity = { @@ -96,19 +101,21 @@ document.addEventListener("DOMContentLoaded", function () { } update(particles) { - if (this.x < 0 || this.x > canvas.width) { + if (this.x <= 0 || this.x >= canvas.width) { this.velocity.x *= -1; + this.x = Math.max(Math.min(this.x, canvas.width), 0); } - if (this.y < 0 || this.y > canvas.height) { + if (this.y <= 0 || this.y >= canvas.height) { this.velocity.y *= -1; + this.y = Math.max(Math.min(this.y, canvas.height), 0); } let dx = mouse.x - this.x; let dy = mouse.y - this.y; let distance = Math.sqrt(dx * dx + dy * dy); - if (distance < avoidance_radius) { + if (distance < avoidance_radius && distance > 0) { this.velocity.x += (dx / distance) * 0.5; this.velocity.y += (dy / distance) * 0.5; } @@ -149,13 +156,17 @@ document.addEventListener("DOMContentLoaded", function () { draw() { ctx.beginPath(); ctx.arc(this.x, this.y, particle_size, 0, Math.PI * 2); - ctx.fillStyle = "rgba(255, 81, 71, 0.2)"; + ctx.fillStyle = "rgba(255, 81, 71, 0.5)"; ctx.fill(); } } function init() { particles = []; + num_particles = Math.floor( + particle_density * canvas.width * canvas.height, + ); + num_particles = Math.min(num_particles, 1024); for (let i = 0; i < num_particles; i++) { particles.push(new Particle()); } diff --git a/src/templates/base.j2 b/src/templates/base.j2 index 23f0e69..ed048f3 100644 --- a/src/templates/base.j2 +++ b/src/templates/base.j2 @@ -38,7 +38,7 @@
{% block header %}{% endblock %}
{% block main %}{% endblock %}
diff --git a/src/templates/index.j2 b/src/templates/index.j2 index 8c76172..bd8b899 100644 --- a/src/templates/index.j2 +++ b/src/templates/index.j2 @@ -4,7 +4,7 @@ {% block description %}Ari-web: A personal website of a Lithuanian open source developer Ari Archer (Arija A.) who provides different free, private, and open source services for others.{% endblock %} -{% block keywords %}ari::web, services, foss services, homepage, portfolio{% endblock %} +{% block keywords %}ari::web, services, foss services, homepage, portfolio, resume{% endblock %} {% block head %} @@ -47,22 +47,44 @@ {% block header %}

Ari.lt: Free and open source world of Ari Archer.

+ + {% endblock %} {% block main %}
-

Welcome to my website, visitor {{ visitor }}!

+

Welcome to my website, visitor {{ visitor }}!

+ +
+ 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 ari@ari.lt email) + or see the list of staff. You may also leave a comment on the guestbook. +

My name is Ari Archer also known as Arija A., but most commonly referred to as - Ari. I am a {{ ari_age }} year old, neurodivergent, transgender, open source developer from Lithuania who does - fun stuff in primarily Python (~{{ python_exp }} years) - and C (89, 99) (~{{ c_exp }} years). + Ari. I am a {{ ari_age }} year old, neurodivergent, transgender, open source developer from Lithuania who does + fun stuff in primarily Python (~{{ python_exp }} years) + and C (89, 99) (~{{ c_exp }} years).

- I've picked up programming at first in Python, writing various programs on my phone, and later on discovering a communty + I've picked up programming at first in Python, writing various programs on my phone, and later on discovering a community where I could share my code. At the time I didn't have access to a computer, so I used to play around with basic JavaScript on my local library computers. At around 2019 I got access to a personal computer, which is where my main FOSS journey started - @@ -72,12 +94,12 @@

I hated the limiting feeling of being on Microsoft Windows 10 very limiting, so soon after getting a personal computer I installed a free and open source Linux - kernel distribution - and I've never went back to Windows ever since then. Democracy, freedom, shareability - and customization ("hackability") are huge values for me. + kernel distribution - and I've never went back to Windows ever since then. Democracy, freedom, shareability + and customization ("hackability") are huge values for me.

- Technology is a big part of my life, and I am way past the stage where I find just technology facinating - + Technology is a big part of my life, and I am way past the stage where I find just technology fascinating - I find various problems and their optimal solutions very interesting and I like to come up with my own - even if it's "reinventing the wheel" or "impractical" - I like to dig deep into it and understand how it works at its core, rather than relying on high-level features of an @@ -95,6 +117,18 @@

  • Cooking - I really enjoy putting together a healthy, vegan dish, which's recipe I can share with others.
  • And also blogging, as archiving things and thoughts has been a huge part of my life since forever.
  • + +

    + But generally, I am pretty much open to try everything at least once if I have the time and energy to do so. +

    + +

    + People often describe my personality as kind, accepting, open-minded, and nonjudgmental due to the way I interact + with the world and people - I am always accepting of people no matter what they've gone through or are + going through. They also see my logical side and describe me as intelligent, analytical, and creative + because of how I tend to approach various logical problems. Though, this side of me can sometimes be + overpowered by my emotional side if I lose control of my emotions. +

    @@ -106,10 +140,13 @@
  • Name: Arija A. (Ari Archer)
  • Pronouns: She/Her
  • Age: {{ ari_age }} years old.
  • +
  • Education: Primary school, middle school, high school (ongoing).
  • Country: Lithuania (Lietuva).
  • -
  • Languages: Lithuanian (native), English (B2), German (basic).
  • +
  • Languages: Lithuanian (native, {{ ari_age }}y), English (B2, {{ ari_age - 8 }}y), German (basic, {{ ari_age - 12 }}y).
  • Programming Experience: {{ programming_exp }} years ({{python_exp }}y in Python, {{ c_exp }}y in C).
  • Skills: Backend web development in Python using Flask, basic front-end development (HTML, JS, CSS, SCSS) and SEO (search engine optimization), software and library development in C and Python, Linux and intermediate Linux systems administration, technical documentation and specifications, application of different mathematical methods.
  • +
  • Learning: Better communication skills, self-improvement, cryptography (post-quantum cryptography), working on mental health and emotional grounding.
  • +
  • Personality: Recognised as kind, open-minded, intelligent, analytical, non-Judgmental, chill, anxious, generous, introverted, independent, caring, helpful, and supportive by people in my life.
  • Contacts @@ -125,14 +162,65 @@
    "Talk is cheap. Show me the code."
    -
    -        - Linus Torvalds, creator of Linux
    -        
    +
           - Linus Torvalds, creator of Linux
  • - That was a short introduction of who I am and what I do. + This is a brief description of me as a person, I probably cannot fit all of it in a single page. I do hope that + 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
    +
    + + + +

    + Many of my links, social media, etc. can be found at: +

    + +
    +

    Contacts

    + +
    + +

    Activity

    + + + +

    Projects

    + + + +

    + Note that these are not the only projects I've ever worked on. These are a few + highlights that I find neat personally for differnt reasons. There don't showcase all + of my abilities, although these are practical examples of things I work on + sometimes.

    # Staff

    @@ -142,7 +230,8 @@ Feel free to visit their websites and show appriciation :).

    -
    +
    +
    @@ -170,6 +259,16 @@
    Namesulian.eu
    +
    + +
    +

    + Ari.lt is mainly a personal site, although it is not limited to being just a personal website. + I, together with a couple of staff members, host some services on + a shared server. Some of these services are a bit locked down or have aggressive policies to prevent spam + as our team is small. +

    +

    # Services

    @@ -236,7 +335,7 @@ pb.ari.lt - db.cubiq.dev + Private PocketBase instance Private database storage for Github: TheCubiq db.cubiq.dev @@ -246,10 +345,74 @@ t1nklas.lt - git.kappach.at + Forgejo instance for kappach.at Git forge instance of KappaChat - An extensible Matrix client written in C. git.kappach.at
    + +

    # 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. +

    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + An image CAPTCHA + Click the image above to reload and get a new CAPTCHA +
    + +
    + + +
    + + +
    + +
    + +
    + Report any impersonation to the owner of this website: Ari Archer. +
    + +
    +
    +

    #1: Cool Person (https://example.com/) <> at 2024-06-05 11:11:11 UTC says...

    +
    Cool website, but you should kill yourself!
    +Meow!
    +
    + +
    +

    #1: Cool Person (https://example.com/) <> at 2024-06-05 11:11:11 UTC says...

    +
    Cool website, but you should kill yourself!
    +Meow!
    +
    +
    + {% endblock %}