Compare commits

...

3 commits

Author SHA1 Message Date
Penelope Gwen
762377f325 update main.rs 2026-03-10 21:43:58 -07:00
Penelope Gwen
c657df92d9 update cargo files 2026-03-10 21:43:37 -07:00
Penelope Gwen
38588715d8 initial release and packaging 2026-03-10 21:43:14 -07:00
85 changed files with 460 additions and 1689 deletions

1216
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,21 @@
[package]
name = "server"
version = "0.1.0"
name = "mdws"
version = "0.1.3"
edition = "2024"
authors = ["Penelope Gwen <support@pogmom.me>"]
license-file = "LICENSE.md"
description = "a markdown-based webserver built with rust and estrogen"
[[bin]]
name = "mdws"
path = "src/main.rs"
[dependencies]
chrono = "0.4.43"
markdown = "1.0.0"
tokio = { version = "1", features = ["full"] }
warp = { version = "0.4", features = ["server"] }
termimad = "0.34.1"
text-template = "0.1.0"
lipgloss = "0.1.1"
mdriver = "0.14.0"
mq-view = "0.1.9"
mq-markdown = { version = "0.5.14", features = ["color"] }
toml = "1.0.1"
@ -21,6 +25,15 @@ toml-frontmatter = "0.1.0"
serde = "1.0.228"
rand = "0.10.0"
strip-ansi-escapes = "0.2.1"
dir_walker = "0.1.9"
walkdir = "2.5.0"
ptree = "0.5.2"
dirs = "6.0.0"
[package.metadata.deb]
changelog = "debian/changelog"
maintainer-scripts = "debian/"
assets = [
["target/release/mdws", "usr/bin/", "755"],
["config.toml", "/etc/mdws/config.toml", "644" ]
]
systemd-units = { enable = true }

60
LICENSE.md Normal file
View file

@ -0,0 +1,60 @@
# 🏳️‍🌈 Opinionated Queer License v1.3
© Copyright {Licensor}
## Permissions
The creators of this Work (“The Licensor”) grant permission
to any person, group or legal entity that doesn't violate the prohibitions below (“The User”),
to do everything with this Work that would otherwise infringe their copyright or any patent claims,
subject to the following conditions:
## Obligations
The User must give appropriate credit to the Licensor,
provide a copy of this license or a (clickable, if the medium allows) link to
[oql.avris.it/license/v1.3](https://oql.avris.it/license/v1.3),
and indicate whether and what kind of changes were made.
The User may do so in any reasonable manner,
but not in any way that suggests the Licensor endorses the User or their use.
## Prohibitions
No one may use this Work for prejudiced or bigoted purposes, including but not limited to:
racism, xenophobia, queerphobia, queer exclusionism, homophobia, transphobia, enbyphobia, misogyny.
No one may use this Work to inflict or facilitate violence or abuse of human rights,
as defined in either of the following documents:
[Universal Declaration of Human Rights](https://www.un.org/en/about-us/universal-declaration-of-human-rights),
[European Convention on Human Rights](https://prd-echr.coe.int/web/echr/european-convention-on-human-rights)
along with the rulings of the [European Court of Human Rights](https://www.echr.coe.int/).
No entity that commits such abuses or materially supports entities that do
may use the Work for any reason.
No law enforcement, carceral institutions, immigration enforcement entities, military entities or military contractors
may use the Work for any reason. This also applies to any individuals employed by those entities.
No business entity where the ratio of pay (salaried, freelance, stocks, or other benefits)
between the highest and lowest individual in the entity is greater than 50 : 1
may use the Work for any reason.
No private business run for profit with more than a thousand employees
may use the Work for any reason.
Unless the User has made substantial changes to the Work,
or uses it only as a part of a new work (eg. as a library, as a part of an anthology, etc.),
they are prohibited from selling the Work.
That prohibition includes processing the Work with machine learning models.
## Sanctions
If the Licensor notifies the User that they have not complied with the rules of the license,
they can keep their license by complying within 30 days after the notice.
If they do not do so, their license ends immediately.
## Warranty
This Work is provided “as is”, without warranty of any kind, express or implied.
The Licensor will not be liable to anyone for any damages related to the Work or this license,
under any kind of legal claim as far as the law allows.

4
config.toml Normal file
View file

@ -0,0 +1,4 @@
server_root = "/srv/mdws/root"
listen_port = 3030
bind_address = [127, 0, 0, 1]
server_domain = "127.0.0.1:3030"

23
debian/changelog vendored Normal file
View file

@ -0,0 +1,23 @@
mdws 0.1.3-1 semistable; urgency=medium
* i should test before pushing updates
-- Penelope Gwen <support@pogmom.me> Tue, 10 Mar 2026 21:09:42 -0700
mdws 0.1.2-1 semistable; urgency=medium
* fix another good
-- Penelope Gwen <support@pogmom.me> Tue, 10 Mar 2026 21:06:23 -0700
mdws 0.1.1-1 semistable; urgency=medium
* fix ua and page visit stuff
-- Penelope Gwen <support@pogmom.me> Tue, 10 Mar 2026 20:58:43 -0700
mdws 0.1.0-1 semistable; urgency=medium
* Initial release
-- Penelope Gwen <support@pogmom.me> Tue, 10 Mar 2026 20:09:05 -0700

8
debian/service vendored Normal file
View file

@ -0,0 +1,8 @@
[Unit]
Description=Markdown Webserver
[Service]
ExecStart=/usr/bin/mdws
[Install]
WantedBy=multi-user.target

View file

@ -1,111 +0,0 @@
@font-face {
font-family: OverpassNerd;
src: url("/assets/fonts/overpass/OverpassNerdFont-Regular.otf") format("opentype");
}
:root {
--main-bg-color: #111317;
--main-fg-color: #d7afaf;
--theme-white: #e4e4e4;
--secondary-bg-color: color-mix(in srgb, var(--main-bg-color) 35%, var(--main-fg-color) 65%);
}
body {
background-color: rgba(from var(--main-bg-color) r g b / 1);
}
h1,
h2,
h3 {
text-align: center;
}
h1,
h2,
h3,
p,
a {
font-family: 'OverpassNerd', sans-serif;
color: rgba(from var(--main-fg-color) r g b / 1);
}
.body {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap-reverse;
/* max-width: 80vmax;
margin: auto;*/
gap: 2vw;
}
.sidebar {
min-width: 30%;
max-width: 400px;
flex-grow: 1;
/* background-color: rgba(255,0,0,0.3);*/
}
.content {
/* background-color: rgba(0,255,0,0.3);*/
max-width: 60vmax;
}
#buttons {
img {
display: block;
margin: auto;
/* width: 60%;*/
width: 12em;
aspect-ratio: 88 / 31;
}
h1 {
padding-top: 1vh;
}
h2 {
border-top: 1px dotted;
padding-top: 1vh;
}
p {
text-align: center;
font-size: 1.1em;
}
}
#sitemap {
p {
line-height: 0.25;
}
}
#rats {
img {
display: block;
margin: auto;
width: 90%;
}
p {
text-align: center;
}
}
img {
width: 10vmax;
}
.markdown-module {
padding: 1vmax 2vmax;
margin: 1vmax 0;
background-color: rgba(from var(--secondary-bg-color) r g b / 0.2);
border-radius: 8px;
.timestamp {
opacity: 0.5;
font-style: italic;
line-height: 0.5;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,000 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,012 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

View file

@ -1,36 +0,0 @@
---toml
title = "buttons"
date_created = "2026-02-14"
index = 2
---
![pogmommy](http://pogmom.me/assets/img/webring/pogmommy/1056x372.webp)
![${file_name}](${file_path})
# Check out my friends!
## [TerminalLesbian](https://hotn.gay)
[![TerminalLesbian](http://pogmom.me/assets/img/webring/terminallesbian/1080x380.webp)][2]
## [Cassie Candles](https://cassiecandles.net)
[![Cassie Candles](http://pogmom.me/assets/img/webring/cassiecandles/1056x372.webp)][3]
# Webrings
## [No AI webring](https://baccyflap.com/noai)
[<](https://baccyflap.com/noai/?prv&s=mom) [random](https://baccyflap.com/noai/?rnd) [>](https://baccyflap.com/noai/?nxt&s=mom)
## [Fediring](https://fediring.net)
[<](https://fediring.net/previous?host=pogmom.me) [random](https://fediring.net/rand) [>](https://fediring.net/next?host=pogmom.me)
## [Geekring](http://geekring.net)
[<](http://geekring.net/site/473/previous) [random](http://geekring.net/site/473/random) [>](http://geekring.net/site/473/next)
[2]:https://hotn.gay
[3]:https://cassiecandles.net

View file

@ -1,9 +0,0 @@
---toml
title = "rats"
date_created = "2026-02-15"
index = 1
---
![${file_name}](${file_path})
Please enjoy a picture of my sons :)

View file

@ -1,8 +0,0 @@
---toml
title = "sitemap"
index = 0
---
# Sitemap
${sitemap}

View file

@ -1,24 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>${title}</title>
<link rel="stylesheet" type="text/css" href="/assets/css/style.css">
</head>
<body>
<!-- ${time}-->
<div class="header" id="header">
${header}
</div>
<div class="body" id="body">
<div class="sidebar" id="sidebar">
${sidebar}
</div>
<div class="content" id="content">
${content}
</div>
</div>
<div class="footer" id="footer">
${footer}
</div>
</body>
</html>

View file

@ -1,23 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>${title}</title>
<link rel="stylesheet" type="text/css" href="/assets/css/style.css">
</head>
<body>
<div class="header" id="header">
${header}
</div>
<div class="body" id="body">
<div class="sidebar" id="sidebar">
${sidebar}
</div>
<div class="content" id="content">
${content}
</div>
</div>
<div class="footer" id="footer">
${footer}
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

View file

@ -1,7 +0,0 @@
---toml
title = "Home Page"
date_created = "2026-01-15"
---
# Penelope Gwen
## it/its, she/her
### Technologist & Sociologist

View file

@ -1,6 +0,0 @@
---toml
title = "home"
index = 0
---
I'm Penelope/pogmommy, a queer rat mom with a background in sociology, software development, and network engineering.

View file

@ -1,12 +0,0 @@
---toml
title = "MusicBrainz Artist Monitoring"
date_created = "2026-02-15"
index = 3
---
# MusicBrainz Artist Monitoring
I do my best to honor the importance of the art and artists in my life by documenting their work on MusicBrainz, an open music encyclopedia.
To see the statuses of my actively-monitored artists, please see the public spreadsheet [here](https://cloud.pogmom.me/s/b8CSpAPNs8GwtWn).
If you would like me to consider adding an artist to the above spreadsheet, please fill out the form [here](https://cloud.pogmom.me/apps/forms/s/qzFg5kGbtqzXxHjMwS6xpPH9).

View file

@ -1,13 +0,0 @@
---toml
title = "Archives, Feudalism, and Digital Literacy"
date_created = "2023-09-12"
date_updated = "2024-05-03"
---
# Archives, Feudalism, and Digital Literacy
My undergraduate thesis synthesized my sociological studies with my aptitude for technology. Inspired by my work with my mentor, I spent the final year of my studies articulating the failure our our academic and employment infrastucture to foster digital literacy in the public.
While I consider my work on the topic far from finished, the version published here represents the spirit of my work and captures the motivation behind much of my other work.
[Click to download](https://pogmom.me/assets/pdf/archive-feudalism-and-digital-literacy_penelope-gomez.pdf)

View file

@ -1,8 +0,0 @@
---toml
title = "header"
date_created = "2026-02-22"
---
# Writings
## This is a space for thoughts I have and things I write, that I might eventually return to

View file

@ -1,8 +0,0 @@
---toml
title = "Queer Religion"
date_created = "2026-01-07"
---
# Something about queerness and religion
This topic is just a concept right now! It's inspired by and dedicated to my friend Euthie, who introduced me to [The Reverent Marigold](https://musicbrainz.org/artist/4c6d5428-73f4-454b-bc93-6a8f016ee490) and their incredible music.

View file

@ -1,8 +0,0 @@
---toml
title = "Ratgender"
date_created = "2026-01-08"
---
# Ratgender
This topic is just a concept right now! This one was inspired by my son, after he passed away in January 2024. It's dedicated to him, his brothers past and future, and his other parent. All of whom I miss dearly.

View file

@ -1,21 +0,0 @@
---toml
title = "ESP32-S3 USB Authentication Key"
date_created = "2026-02-14"
index = 2
---
# ESP32-S3 USB Authentication Key
A DIY FIDO2 key using an ESP32-S3, a [top-to-top](/vocab) type-C coupler, and the [Pico-Fido firmware](https://github.com/polhenarejos/pico-fido).
This was kind of my introduction to 3D modeling, as I had gotten access to a (frustrating and poorly-maintained) 3D printer when I started my (at time of writing) current job several months prior.
I'd also been wanting a USB authentication key for use with my computers, but didn't want to shell out the cash for a Yubikey. I realized for the same price, I could make my own and have parts left over.
It works well, honestly I've no complaints with the firmware, and am happy with how the casing I printed turned out. I did wind up having to dremel some of the aligment walls for the button and shave it down.
![A photograph of an ESP32 and USB-C coupler sitting inside of a 3D printed housing, with a metal keyring attached to the back](https://pogmom.me/assets/img/plog/pico-fido/2025-07-17.jpg)
![A photgraph of a 3D printed flash drive-sized enclosure with a button on the side facing the camera, and metal keyring protruding from the back](https://pogmom.me/assets/img/plog/pico-fido/2025-07-18.jpg)
### Resources
- [Housing STL Files](https://pogmom.me/assets/stl/pico-fido.zip)

View file

@ -1,3 +0,0 @@
# project blog
## This will ideally create more problems than it could ever possibly solve

View file

@ -1,11 +0,0 @@
---toml
title = "Debian on an iMac G3"
date_created = "2026-01-08"
date_updated = "2026-02-03"
index = 1
---
# Debian Sid on an iMac G3
![A photograph of an orange iMac G3 running Debian Buster logged into a TTY](https://pogmom.me/assets/img/plog/imac_g3/2025-07-25.jpg)
[Read More >](/plog/imac-g3-debian/)

View file

@ -1,18 +0,0 @@
## 2026-02-03
In November 2017 while thrifting with my ex, I found a tangerine iMac G3/333 with its original keyboard.
I had no idea if it worked, nor did I know all that much about it, architecturally speaking. However, it was $70, and this was during the time in my life that I was a rabid Apple fanatic- don't apologize for the judgements, I understand.
I bought it, and was thrilled to find that after bringing it back to my parents' house, it worked. It had no operating system installed, but it otherwise booted without any issues. One ebay purchase of some OS X Panther install disks later, and I had it up and fully running. I frankly don't remember too much of what I managed to do with it- I think I managed to get opera or some other contemporary browser installed on it? Though I'm not even sure if I had ever managed to get it online.
At some point, it turned into a very large paperweight that moved between desks, closets, and corners of rooms. But when I leapt out of the Apple frying pan in 2022/2023 and into the Linux fire, somewhere along the way I had the idea to put Debian on it.
The natural starting place for this was obviously Debian Jessie, the last version of the distro that officially supported the 32-bit PowerPC architecture. And wouldn't you know it? It just worked. Well, after wasting a few CDs because I'd apparently written then too quickly, if I recall correctly. I also at this point upgraded the base memory (from 256MiB, I think?) to 512MiB using an "[A0383205 512MB PC133 Memory Dell Inspiron 3700 4100](https://www.ebay.com/itm/192791214383)" Memory Stick.
This made for an awesome novelty project- I even got some cool terminal programs like cmatrix and hyfetch running on it, which were fun to show off to my friends. But then I found in [a MacRumors forum thread](https://web.archive.org/web/20250908055651/https://forums.macrumors.com/threads/debian-sid-installation-guide-powerpc.2146795/) that [debian-ports](https://deb.debian.org/debian-ports/) had some level of support for PowerPC, and proceeded to completely fuck up my debian install, because I couldn't be bothered to buy more blank CDs to put experimental disc images on, and tried to directly jump 4 releases at once. I hadn't gotten the opportunity to return to this project before I eventually moved to another state, into an apartment that didn't really provide me room for projects like that, so I wound up leaving it in my home town. Upon moving out of that apartment and into my (at time of writing) current residence, I reacquired it and am hoping to see just how far I can push this thing. Below is an image of the first time I booted it back up again after my move, apparently displaying the state I left this thing in.
![A photograph of an orange iMac G3 running Debian Buster logged into a TTY](https://pogmom.me/assets/img/plog/imac_g3/2025-07-25.jpg)
I have no idea how viable this really is right now ([apparently the kernel might be troublesome?](https://web.archive.org/web/20260204063354/https://lists.debian.org/debian-powerpc/2020/04/msg00087.html)) but I have much more experience at this point with manual Debian installs, including on unconventional hardware. That said, I expect PowerPC support in Debian's unstable version has not gotten any better than when I first attempted this, especially seeing as Trixie (current Stable release) marked the end of support for another 32-bit architecture, i386.
I think my next approach will focus on [booting via USB](https://idevicecollector.home.blog/2019/04/02/how-to-boot-your-powerpc-g3-g4-or-g5-from-usb-using-open-firmware-mode/), which I couldn't seem to get working in the past. But if that fails, I've got enough blank CDs now that I should be set- and I plan to document commands & post disk images that I confirm work.

View file

@ -1,14 +0,0 @@
---toml
title = "OnePlus 6T Cyberdeck"
date_created = "2026-02-14"
index = 3
---
# OnePlus 6T "CyberDeck"
Linux phone
Linux phone
It feels like cheating to call this a cyberdeck since this is just kinda a linux phone glued to a bluetooth keyboard (after I 3D print some glue).
![A photograph of a smartphone running a tiling window manager with hyfetch indicating a debian operating system](https://pogmom.me/assets/img/plog/oneplus-6t/2025-10-10.jpg)

View file

@ -1,12 +0,0 @@
---toml
title = "Personal Website"
date_created = "2026-03-09"
index = 0
---
# Personal Website
I'm rewriting my website!
I was inspired to do so by my friend Val who wrote her own web server in Rust using [warp](https://lib.rs/crates/warp) for her personal website (linked in the sidebar under "TerminalLesbian").

View file

@ -1,10 +0,0 @@
---toml
title = "To-Do List"
date_created = "2026-02-14"
index = 99
---
# to-do list
- Wearable Variable Power Supply
- Arc Lighter
- Long-range WiFi Antenna with POE

View file

@ -1,15 +0,0 @@
---toml
title = "Pogmom Suite"
date_created = "2026-02-14"
index = 1
---
# Pogmom Suite
In response to predatory data collection, hostile platform siezure, and [enshittification](/vocab#enshittification) of the centralized internet, I host a collection of online services called the 'Pogmom Suite'.
These services utilize federated social networking standards and rely on free and open source software as exclusively as possible.
While security and practicality require that registration not be open to the general public, I am more than happy to offer advice and guidance to those interested in pursuing similar projects.
[Learn More](https://suite.pogmom.me)

View file

@ -1,13 +0,0 @@
---toml
title = "Project Blog"
date_created = "2026-02-15"
index = 5
---
# Project Blog
I'd like to start documenting the various projects I'm working on, especially those involving niche/old hardware!
If you have any interest in forcing linux onto ancient computers or abysmal soldering jobs, this'll probably be your thing.
[Click to Visit](/plog)

View file

@ -1,14 +0,0 @@
---toml
title = "Software"
date_created = "2026-02-15"
index = 2
---
# Software
My current software development projects are primarily in Rust and Bash, targeting my own Linux server, desktop, and mobile usage.
In the past, I have developed software for Pebble smartwatches and Apple devices, both jailbroken and stock.
Most of my software can be found at my [Forgejo](https://git.pogmom.me/pogmommy/), or at its mirror on [Codeberg](https://codeberg.org/pogmommy). My presence on [Github](https://github.com/pogmommy) is limited to pull requests, discussion, and other contributions toward software hosted on the platform.
I also package software for the Debian operating system, which I distribute on [my apt repository](https://apt.pogmom.me/).

View file

View file

View file

View file

@ -1,5 +0,0 @@
## top and bottom cables
Saying "male" and "female" connectors doesn't make sense when i have a female penis
Besides, plugging in a coupler/adapter is more analagous to a strap-on than it is to surgery

View file

@ -1,14 +0,0 @@
---toml
title = "Writing"
date_created = "2026-02-15"
date_updated = "2026-03-09"
index = 4
---
# Writings
I've been writing more! I'd like to start collecting those thoughts and sharing them somewhere again.
These might be less technical and more personal, veering into topics such as gender and spirituality.
[Click to Visit](/musings)

48
src/lib/config.rs Normal file
View file

@ -0,0 +1,48 @@
use dirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerConfig {
pub server_root: PathBuf,
pub listen_port: u16,
pub bind_address: [u8; 4],
pub server_domain: String,
}
impl ServerConfig {
fn read_config() -> Option<String> {
let _test = dirs::config_dir();
let config_paths: Vec<Option<PathBuf>> =
vec![dirs::config_dir(), Some(PathBuf::from("/etc"))];
let valid_conf = config_paths
.iter()
.find_map(|p| {
p.as_ref().and_then(|path| {
let config_path = path.join("mdws/config.toml");
if config_path.exists() {
Some(config_path)
} else {
None
}
})
})
.and_then(|p| if p.is_file() { Some(p) } else { None });
match valid_conf {
Some(config_file) => std::fs::read_to_string(config_file).ok(),
None => None,
}
}
pub fn get_config() -> ServerConfig {
match Self::read_config().and_then(|t| toml::from_str(t.as_str()).ok()) {
Some(toml) => toml,
None => ServerConfig {
server_root: PathBuf::from("/srv/mdws/root"),
listen_port: 3030,
bind_address: [127, 0, 0, 1],
server_domain: "127.0.0.1:3030".to_string(),
},
}
}
}

View file

@ -102,18 +102,26 @@ pub fn curl_response(
page_contents: Vec<MarkdownModule>,
sidebar_contents: Vec<MarkdownModule>,
width: Option<i32>,
server_domain: String,
path: &str,
) -> Box<dyn Reply> {
let w = width.unwrap_or(100);
let shell_header = if width.is_none() {
"curl -s beta.pogmom.me/?width=$(tput cols);exit 0\n".to_string()
//"clear;curl -s beta.pogmom.me/?width=$(tput cols);exit 0\n".to_string()
format!(
"curl -s {}{}?width=$(tput cols);exit 0\n",
server_domain, path
)
} else {
"".to_string()
};
let shell_footer = if width.is_none() {
box_content(
&"Did you know‽\nIf you (dangerously) pipe this output to your shell, it autosizes! more interactivity is planned in the future\nex: `curl -s beta.pogmom.me | bash`".to_string(),
w - 4)
&format!(
"Did you know‽\nIf you (dangerously) pipe this output to your shell, it autosizes! more interactivity is planned in the future\nex: `curl -s {}{} | bash`",
server_domain, path
),
w - 4,
)
} else {
"".to_string()
};

View file

@ -1,6 +1,13 @@
use crate::markdowner::MarkdownModule;
use std::path::PathBuf;
use warp::Reply;
fn make_ascii_titlecase(s: &mut str) {
if let Some(r) = s.get_mut(0..1) {
r.make_ascii_uppercase();
}
}
fn get_header_html(page_contents: &Vec<MarkdownModule>) -> String {
let header = page_contents
.iter()
@ -12,6 +19,13 @@ fn get_header_html(page_contents: &Vec<MarkdownModule>) -> String {
}
}
fn get_page_title(page_contents: &Vec<MarkdownModule>) -> Option<String> {
page_contents
.iter()
.find(|m| m.path.file_name().unwrap_or_default().eq("header.md"))
.map(|h| h.metadata.title.clone())
}
fn get_content_html(page_contents: &Vec<MarkdownModule>) -> String {
let content: Vec<&MarkdownModule> = page_contents
.iter()
@ -22,7 +36,7 @@ fn get_content_html(page_contents: &Vec<MarkdownModule>) -> String {
.filter_map(|m| {
markdown::to_html_with_options(m.content.as_str(), &markdown::Options::gfm())
.ok()
.and_then(|c| {
.map(|c| {
let mut paragraph_module = format!(
"<div id='{}' class='markdown-module'>{}",
m.path
@ -47,7 +61,7 @@ fn get_content_html(page_contents: &Vec<MarkdownModule>) -> String {
};
paragraph_module = format!("{}</div>", paragraph_module);
Some(paragraph_module)
paragraph_module
})
})
.collect();
@ -59,8 +73,9 @@ fn fill_template(
content_html: String,
sidebar_html: String,
page_title: String,
server_root: PathBuf,
) -> String {
let template_html = std::fs::read_to_string("./serve/assets/templates/page.html")
let template_html = std::fs::read_to_string(server_root.join("assets/templates/page.html"))
.expect("could not read page.html template");
let template = text_template::Template::from(template_html.as_str());
let mut values = std::collections::HashMap::new();
@ -74,9 +89,15 @@ fn fill_template(
pub fn html_response(
page_contents: Vec<MarkdownModule>,
sidebar_contents: Vec<MarkdownModule>,
page_title: String,
route_title: String,
server_root: PathBuf,
) -> Box<dyn Reply> {
let header_html = get_header_html(&page_contents);
let mut page_title = match get_page_title(&page_contents) {
Some(parsed) => parsed,
None => route_title,
};
make_ascii_titlecase(&mut page_title);
let content_html = get_content_html(&page_contents);
let sidebar_html = get_content_html(&sidebar_contents);
@ -86,5 +107,6 @@ pub fn html_response(
content_html,
sidebar_html,
page_title,
server_root,
)))
}

View file

@ -42,47 +42,23 @@ fn build_markdown_module(markdown_path: &PathBuf) -> Option<MarkdownModule> {
}
pub fn get_markdown_modules(target_path: &PathBuf) -> Vec<MarkdownModule> {
//if target_path.exists() {}
let mut mds: Vec<MarkdownModule> = std::fs::read_dir(target_path.clone())
.expect("could not read target directory")
.filter_map(|d| {
println!("found file! {:?}", d);
d.ok().and_then(|f| build_markdown_module(&f.path()))
d.ok().and_then(|f| {
if !f
.file_name()
.into_string()
.unwrap_or_default()
.starts_with(".")
{
build_markdown_module(&f.path())
} else {
None
}
})
})
.collect();
mds.sort_by_key(|e| e.metadata.index);
mds
//println!("{:#?}", mds);
/*for md in mds {
println!("title: {:?}", md.metadata.title);
println!("path: {:?}", md.path);
println!("content: {:?}", md.content);
}*/
/*std::fs::read_dir(target_path.clone())
.unwrap()
.filter(|x| {
x.as_ref()
.unwrap()
.path()
.extension()
.unwrap_or_default()
.eq("md")
&& !x.as_ref().unwrap().file_name().eq("header.md")
})
.map(|x| x.unwrap().path())
.collect();*/
/*let test = MarkdownModule {
path: "".to_owned().into(),
content: "".to_string(),
metadata: Some(FrontMatter {
title: "".to_string(),
date_created: Some("".to_string()),
date_updated: Some("".to_string()),
index: Some(0),
}),
};
vec![test]*/
}

View file

@ -1,4 +1,4 @@
use crate::{MarkdownModule, markdowner};
use crate::{MarkdownModule, config, markdowner};
use ptree::{Style, TreeItem};
use rand::seq::IteratorRandom;
use std::borrow::Cow;
@ -12,15 +12,17 @@ impl TreeItem for PathItem {
type Child = Self;
fn write_self<W: io::Write>(&self, f: &mut W, style: &Style) -> io::Result<()> {
let cwd = std::env::current_dir()
.expect("unable to parse working directory")
.join("serve");
let cwd = config::ServerConfig::get_config().server_root;
if let Some(n) = self.0.file_name() {
write!(
f,
"[{}](/{})\n",
style.paint(n.to_string_lossy()),
style.paint(
n.to_str()
.expect("could not convert file name to str")
.replace("-", " ")
),
self.0
.clone()
.to_path_buf()
@ -34,7 +36,7 @@ impl TreeItem for PathItem {
}
}
fn children(&self) -> Cow<[Self::Child]> {
fn children(&self) -> Cow<'_, [Self::Child]> {
let v = if let Ok(list) = fs::read_dir(&self.0) {
list.filter_map(|item| item.ok())
.map(|entry| entry.path())
@ -43,6 +45,10 @@ impl TreeItem for PathItem {
e.read_dir()
.unwrap()
.any(|x| x.unwrap().path().extension().unwrap_or_default().eq("md"))
&& !e
.read_dir()
.unwrap()
.any(|x| x.unwrap().path().file_name().unwrap().eq(".secret"))
} else {
false
}
@ -57,28 +63,32 @@ impl TreeItem for PathItem {
}
}
fn random_image_module(module: &MarkdownModule, directory: &str) -> MarkdownModule {
let image = random_image(directory);
fn random_image_module(
module: &MarkdownModule,
directory: &str,
server_root: PathBuf,
) -> MarkdownModule {
let image = random_image(directory, server_root.clone());
let template_content = module.content.clone();
let template = text_template::Template::from(template_content.as_str());
let mut values = std::collections::HashMap::new();
values.insert(
"file_path",
let image_path = format!(
"/{}",
image
.strip_prefix("./serve/")
.strip_prefix(server_root)
.unwrap()
.to_str()
.unwrap_or_default(),
);
values.insert(
"file_name",
image
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
);
values.insert("file_path", image_path.as_str());
let image_name = image
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.replace("_", " ");
values.insert("file_name", image_name.as_str());
let module = MarkdownModule {
path: module.path.clone(),
content: template.fill_in(&values).to_string(),
@ -87,14 +97,19 @@ fn random_image_module(module: &MarkdownModule, directory: &str) -> MarkdownModu
module.clone()
}
pub fn sidebar_content(target_path: &PathBuf, root_path: PathBuf) -> Vec<MarkdownModule> {
pub fn sidebar_content(
target_path: &PathBuf,
root_path: PathBuf,
counter: i32,
path: &str,
) -> Vec<MarkdownModule> {
let sidebar_modules = markdowner::get_markdown_modules(target_path);
let sidebar_modules: Vec<MarkdownModule> = sidebar_modules
.iter()
.map(|f| match f.metadata.title.as_str() {
"rats" => random_image_module(f, "rats"),
"buttons" => random_image_module(f, "buttons"),
"sitemap" => sitemap(f, &root_path),
"rats" => random_image_module(f, "rats", root_path.clone()),
"buttons" => random_image_module(f, "buttons", root_path.clone()),
"sitemap" => sitemap(f, &root_path, counter, path),
_ => MarkdownModule {
path: f.path.clone(),
content: f.content.clone(),
@ -105,11 +120,32 @@ pub fn sidebar_content(target_path: &PathBuf, root_path: PathBuf) -> Vec<Markdow
sidebar_modules
}
fn sitemap(module: &MarkdownModule, root_path: &PathBuf) -> MarkdownModule {
fn sitemap(
module: &MarkdownModule,
root_path: &PathBuf,
counter: i32,
path: &str,
) -> MarkdownModule {
let mut writer: Vec<u8> = Vec::new();
let dir = PathItem(root_path.clone());
ptree::write_tree(&dir, &mut writer).expect("could not write tree");
let ptree_chars = ptree::print_config::IndentChars {
down_and_right: "".to_string(),
down: "".to_string(),
turn_right: "".to_string(),
right: "".to_string(),
empty: "".to_string(),
};
let ptree_conf = ptree::PrintConfig {
depth: 20,
indent: 3,
padding: 1,
styled: ptree::print_config::StyleWhen::Never,
characters: ptree_chars,
branch: ptree::Style::default(),
leaf: ptree::Style::default(),
};
ptree::write_tree_with(&dir, &mut writer, &ptree_conf).expect("could not write tree");
let template_content = module.content.clone();
let template = text_template::Template::from(template_content.as_str());
@ -117,7 +153,11 @@ fn sitemap(module: &MarkdownModule, root_path: &PathBuf) -> MarkdownModule {
let tree_text = String::from_utf8(writer).expect("could not parse string from utf8");
let visit_count = format!("{}", counter);
values.insert("sitemap", tree_text.as_str());
values.insert("location", path);
values.insert("visit_count", &visit_count.as_str());
let modu = MarkdownModule {
path: module.path.clone(),
@ -127,8 +167,8 @@ fn sitemap(module: &MarkdownModule, root_path: &PathBuf) -> MarkdownModule {
modu.clone()
}
fn random_image(directory: &str) -> PathBuf {
let rat_image = std::fs::read_dir(PathBuf::from("./serve/assets/img/random/").join(directory))
fn random_image(directory: &str, server_root: PathBuf) -> PathBuf {
let rat_image = std::fs::read_dir(server_root.join("assets/img/random/").join(directory))
.expect("where the fuck are your rat pictures?")
.map(|f| f.expect("umm what is this?").path())
.choose(&mut rand::rng())

View file

@ -1,10 +1,13 @@
#![warn(unused_extern_crates)]
#![allow(clippy::style)]
use http::{Uri, uri::Authority};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use warp::{Filter, filters::path::FullPath};
#[path = "lib/config.rs"]
mod config;
#[path = "lib/curl.rs"]
mod curl;
#[path = "lib/html.rs"]
@ -14,7 +17,8 @@ mod markdowner;
#[path = "lib/sidebar.rs"]
mod sidebar;
use crate::{
curl::curl_response, html::html_response, markdowner::MarkdownModule, sidebar::sidebar_content,
config::ServerConfig, curl::curl_response, html::html_response, markdowner::MarkdownModule,
sidebar::sidebar_content,
};
#[derive(Serialize, Deserialize)]
@ -22,40 +26,99 @@ struct WebQuery {
width: Option<i32>,
}
fn router(request_path: PathBuf) -> PathBuf {
std::env::current_dir()
.expect("unable to determine current directory")
.join("serve")
.join(request_path)
}
fn renderer(path: FullPath, user_agent: String, query: WebQuery) -> Box<dyn warp::Reply> {
fn renderer(
path: FullPath,
user_agent: String,
query: WebQuery,
host: Option<Authority>,
config: ServerConfig, //server_root: PathBuf,
) -> Box<dyn warp::Reply> {
println!("{:?} requested by {}", path, user_agent);
let request_path: PathBuf = path.as_str().strip_prefix("/").unwrap_or_default().into();
let target_path = router(request_path);
if !target_path.exists() || target_path.is_file() {
return Box::new(warp::redirect(
warp::http::uri::Builder::new()
.authority(".")
.build()
.unwrap(),
));
let target_path = config.server_root.join(request_path.clone());
if !target_path.exists()
|| target_path.is_file()
|| (request_path.starts_with("assets") && target_path.is_dir())
{
return Box::new(warp::redirect(Uri::from_static("/")));
}
// this list will grow
let ai_user_agent_list = vec![
"GPTBot",
"openai",
"ChatGPT",
"Claude",
"CCBot",
"anthropic",
"PerplexityBot",
"Amazonbot",
"Googlebot",
];
if ai_user_agent_list.iter().any(|ua| user_agent.contains(ua)) {
return Box::new(warp::reply::with_status(
"llms breaks the internet and our world, go fuck yourself",
warp::http::StatusCode::OK,
));
};
let target_page_visits = target_path.join(".visits");
let mut counter = if target_page_visits.exists() {
std::fs::read_to_string(target_page_visits.clone())
.unwrap_or_default()
.parse::<i32>()
.unwrap()
} else {
0
};
let ua_filter = [
"Safari", "curl", "Windows", "Mac OS", "Linux", "iPhone", "iPad", "Android",
];
if host.is_some_and(|x| x.as_str().eq("pogmom.me"))
&& ua_filter.iter().any(|ua| user_agent.contains(ua))
{
if target_page_visits.exists() {
if target_page_visits
.metadata()
.unwrap()
.modified()
.unwrap()
.elapsed()
.unwrap()
.as_secs()
.gt(&1)
{
counter = counter + 1;
let _ = std::fs::write(target_page_visits, format!("{}", counter));
}
} else {
counter = counter + 1;
let _ = std::fs::write(target_page_visits, format!("{}", counter));
}
};
println!("serving path: {:?}", target_path);
let page_contents = markdowner::get_markdown_modules(&target_path);
let sidebar_dir = PathBuf::from("assets/sidebar/");
let sidebar_contents = sidebar_content(
&router(sidebar_dir),
std::env::current_dir()
.expect("unable to determine current directory")
.join("serve"),
&config.server_root.join(sidebar_dir),
config.server_root.clone(),
counter,
path.as_str(),
);
let response = if user_agent.starts_with("curl/") {
curl_response(page_contents, sidebar_contents, query.width)
curl_response(
page_contents,
sidebar_contents,
query.width,
config.server_domain,
path.as_str(),
)
} else {
html_response(
page_contents,
@ -66,6 +129,7 @@ fn renderer(path: FullPath, user_agent: String, query: WebQuery) -> Box<dyn warp
.to_str()
.unwrap_or_default()
.to_owned(),
config.server_root,
)
};
response
@ -73,17 +137,47 @@ fn renderer(path: FullPath, user_agent: String, query: WebQuery) -> Box<dyn warp
#[tokio::main]
async fn main() {
println!("Hello, world!");
let assets = warp::path("assets").and(warp::fs::dir("./serve/assets/"));
let favicon = warp::path("favicon.ico").and(warp::fs::file("./serve/favicon.ico"));
let config = ServerConfig::get_config();
println!(
"Serving content at {:?} on {}.{}.{}.{}:{}",
config.server_root,
config.bind_address[0],
config.bind_address[1],
config.bind_address[2],
config.bind_address[3],
config.listen_port
);
let assets = warp::path("assets").and(warp::fs::dir(config.server_root.join("assets")));
let favicon = warp::path("favicon.ico").and(warp::fs::file(
config.server_root.join("assets/img/favicon.ico"),
));
let robots =
warp::path("robots.txt").and(warp::fs::file(config.server_root.join("assets/robots.txt")));
//let robots = include_str!("../robots.txt").to_string().clone();
let markdowns = warp::any() //path::end()
// let robots_reply = warp::path("robots.txt").map(||warp::reply::with_status(robots, warp:http::StatusCode::OK));
/* .and(warp::reply::with_status(
format!("{}", robots.clone()),
warp::http::StatusCode::OK,
)); //.and(warp::reply::with_status(robots, warp::http::StatusCode::OK)); */
let config_clone = config.clone();
let markdowns = warp::any()
.and(warp::path::full())
.and(warp::header("user-agent"))
.and(warp::query::<WebQuery>())
.map(|path: FullPath, agent: String, query: WebQuery| renderer(path, agent, query));
.and(warp::host::optional())
.map(
move |path: FullPath, agent: String, query: WebQuery, host| {
renderer(path, agent, query, host, config_clone.clone())
},
);
let routes = favicon.or(assets).or(markdowns);
let routes = robots.or(favicon).or(assets).or(markdowns);
warp::serve(routes).run(([0, 0, 0, 0], 3030)).await;
warp::serve(routes)
.run((config.bind_address, config.listen_port))
.await;
}

View file