wip: 2025-07-30T17:29:51+0200 (1753889391)

This commit is contained in:
Victor Westerlund 2025-07-30 17:29:51 +02:00
parent f398d17f81
commit eb2c7b7d82
Signed by: vlw
GPG key ID: D0AD730E1057DFC6
16 changed files with 297 additions and 85 deletions

View file

@ -11,5 +11,5 @@ reply_average_hours = 0;
available_from_hour = 0;
[service_forgejo]
base_url = ""
scan_profiles = ""
url = ""
profiles = ""

30
api/coffee/DELETE.php Normal file
View file

@ -0,0 +1,30 @@
<?php
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API;
use VLW\Helpers\UUID;
use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
require_once Path::root("src/UUID.php");
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
final class POST_Coffee extends API {
public function __construct() {
parent::__construct(new Ruleset()->GET([
new Rules(CoffeeTable::ID->value)
->required()
->type(Type::STRING)
->min(UUID::LENGTH)
->max(UUID::LENGTH)
]));
}
public function main(): Response {
return new Response(new Coffee($_GET[CoffeeTable::ID->value])->delete());
}
}

19
api/coffee/GET.php Normal file
View file

@ -0,0 +1,19 @@
<?php
use Reflect\{Response, Path};
use VLW\API\API;
use VLW\Database\Models\Coffee\Coffee;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
final class GET_Coffee extends API {
public function __construct() {
parent::__construct();
}
public function main(): Response {
return new Response(Coffee::all());
}
}

37
api/coffee/POST.php Normal file
View file

@ -0,0 +1,37 @@
<?php
use Reflect\{Response, Path};
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API;
use VLW\Database\Models\Coffee\Coffee;
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Coffee/Coffee.php");
require_once Path::root("src/Database/Tables/Coffee/Coffee.php");
final class POST_Coffee extends API {
public function __construct() {
parent::__construct(new Ruleset()->POST([
new Rules(CoffeeTable::DATE_CREATED->value)
->type(Type::STRING)
->default(null)
]));
}
public function main(): Response {
$datetime = new DateTimeImmutable();
// Parse DateTime from POST string
if ($_POST[CoffeeTable::DATE_CREATED->value]) {
try {
$datetime = new DateTimeImmutable($_POST[CoffeeTable::DATE_CREATED->value]);
} catch (DateMalformedStringException $error) {
return new Response($error->getMessage(), 400);
}
}
return new Response(Coffee::new($datetime));
}
}

19
api/languages/GET.php Normal file
View file

@ -0,0 +1,19 @@
<?php
use Reflect\{Response, Path};
use VLW\API\API;
use VLW\Database\Models\Languages\Language;
require_once Path::root("src/API/API.php");
require_once Path::root("src/Database/Models/Languages/Language.php");
final class GET_Languages extends API {
public function __construct() {
parent::__construct();
}
public function main(): Response {
return new Response(Language::all());
}
}

View file

@ -5,15 +5,17 @@
use Reflect\Rules\{Ruleset, Rules, Type};
use VLW\API\API;
use VLW\Helpers\GenerateTimeline;
use VLW\Helpers\{GenerateTimeline, Forgejo};
require_once Path::root("src/API/API.php");
require_once Path::root("src/Helpers/Forgejo.php");
require_once Path::root("src/Helpers/GenerateTimeline.php");
enum ServiceEnum: string {
use xEnum;
case ALL = "all";
case FORGEJO = "forgejo";
case TIMELINE = "timeline";
}
@ -28,18 +30,28 @@
]));
}
public function update_timeline(): bool {
return new GenerateTimeline()->generate();
}
public function main(): Response {
switch ($_GET[self::KEY_SERVICE]) {
case ServiceEnum::FORGEJO->value:
return new Response($this->update_forgejo());
case ServiceEnum::TIMELINE->value:
return new Response("OK");
return new Response($this->update_timeline());
case ServiceEnum::ALL->value:
default:
return new Response($this->update_timeline());
return new Response(
$this->update_timeline() &&
$this->update_forgejo()
);
}
}
private function update_timeline(): bool {
return new GenerateTimeline()->generate();
}
private function update_forgejo(): bool {
return new Forgejo()->update();
}
}

View file

@ -1,18 +1,15 @@
<?php
use Vegvisir\Path;
use VLW\API\{Client, Endpoints};
use VLW\Database\Tables\Messages\MessagesTable;
require_once VV::root("src/API/API.php");
require_once VV::root("src/API/Endpoints.php");
use VLW\Database\Models\Messages\Message;
use VLW\Database\Tables\Messages\Messages;
require_once VV::root("src/Database/Models/Messages/Message.php");
require_once VV::root("src/Database/Tables/Messages/Messages.php");
$date = new class extends DateTimeImmutable {
public function __construct() {
// Set DateTime for configured timezone
parent::__construct("now", new DateTimeZone($_ENV["client_time_available"]["time_zone"]));
parent::__construct("now", new DateTimeZone($_ENV["config_time_available"]["time_zone"]));
}
// Return current hour in 24-hour format
@ -22,20 +19,20 @@
// Returns true if current time is between available from and to hours
public function is_available(): bool {
return $this->hour() >= $_ENV["client_time_available"]["available_from_hour"] && $this->hour() < $_ENV["client_time_available"]["available_to_hour"];
return $this->hour() >= $_ENV["config_time_available"]["available_from_hour"] && $this->hour() < $_ENV["config_time_available"]["available_to_hour"];
}
public function get_estimated_reply_hours(): int {
// I'm available! Return the estimated reply time for that
if ($this->is_available()) {
return $_ENV["client_time_available"]["reply_average_hours"];
return $_ENV["config_time_available"]["reply_average_hours"];
}
return $this->hour() < $_ENV["client_time_available"]["available_from_hour"]
return $this->hour() < $_ENV["config_time_available"]["available_from_hour"]
// Return hours past midnight until I become available (clamped to estimated reply hours)
? max($_ENV["client_time_available"]["available_from_hour"] - $this->hour(), $_ENV["client_time_available"]["reply_average_hours"])
? max($_ENV["config_time_available"]["available_from_hour"] - $this->hour(), $_ENV["config_time_available"]["reply_average_hours"])
// Return hours before midnight until I become available (clamped to estimated reply hours)
: max($_ENV["client_time_available"]["available_from_hour"] + (24 - $this->hour()), $_ENV["client_time_available"]["reply_average_hours"]);
: max($_ENV["config_time_available"]["available_from_hour"] + (24 - $this->hour()), $_ENV["config_time_available"]["reply_average_hours"]);
}
}
@ -84,18 +81,9 @@
<?php // Send message on POST request ?>
<?php if ($_SERVER["REQUEST_METHOD"] === "POST"): ?>
<?php $message = Message::new($_POST[Messages::MESSAGE->name] ?? "", $_POST[Messages::EMAIL->name] ?? null); ?>
<?php
// Send message via API
$send = (new Client())->call(Endpoints::MESSAGES->value)->post([
MessagesTable::EMAIL->value => $_POST[MessagesTable::EMAIL->value],
MessagesTable::MESSAGE->value => $_POST[MessagesTable::MESSAGE->value]
]);
?>
<?php if ($send->ok): ?>
<?php if ($message->date_created): ?>
<section class="form-message sent">
<h3>🙏 Message sent!</h3>
</section>
@ -103,8 +91,6 @@
<?php // Show response body from endpoint as error if request failed ?>
<section class="form-message error">
<h3>😟 Oh no, something went wrong</h3>
<p>Response from API:</p>
<pre><?= $send->output() ?></pre>
</section>
<?php endif; ?>
<?php endif; ?>
@ -113,11 +99,11 @@
<form method="POST">
<input-group>
<label>your email (optional)</label>
<input type="email" name="<?= MessagesTable::EMAIL->value ?>" placeholder="nissehult@example.com" autocomplete="off"></input>
<input type="email" name="<?= Messages::EMAIL->name ?>" placeholder="nissehult@example.com" autocomplete="off"></input>
</input-group>
<input-group>
<label title="this field is required">your message (required)</label>
<textarea name="<?= MessagesTable::MESSAGE->value ?>" required placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed molestie dignissim mauris vel dignissim. Sed et aliquet odio, id egestas libero. Vestibulum ut dui a turpis aliquam hendrerit id et dui. Morbi eu tristique quam, sit amet dictum felis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ac nibh a ex accumsan ullamcorper non quis eros. Nam at suscipit lacus. Nullam placerat semper sapien, vitae aliquet nisl elementum a. Duis viverra quam eros, eu vestibulum quam egestas sit amet. Duis lobortis varius malesuada. Mauris in fringilla mi. "></textarea>
<textarea name="<?= Messages::MESSAGE->name ?>" required placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed molestie dignissim mauris vel dignissim. Sed et aliquet odio, id egestas libero. Vestibulum ut dui a turpis aliquam hendrerit id et dui. Morbi eu tristique quam, sit amet dictum felis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ac nibh a ex accumsan ullamcorper non quis eros. Nam at suscipit lacus. Nullam placerat semper sapien, vitae aliquet nisl elementum a. Duis viverra quam eros, eu vestibulum quam egestas sit amet. Duis lobortis varius malesuada. Mauris in fringilla mi. "></textarea>
</input-group>
<button class="inline solid">
<?= VV::embed("public/assets/media/icons/email.svg") ?>

View file

@ -27,7 +27,4 @@
* # Forgejo
* Constants related to the fetching and caching of real-time prog. language use on Forgejo
*/
const FORGEJO_HREF = "https://git.vlw.se/explore/repos?q=&sort=recentupdate&language=";
const FORGEJO_ENDPOINT_USER = "/api/v1/users/%s";
const FORGEJO_ENDPOINT_SEARCH = "/api/v1/repos/search?uid=%s";
const FORGEJO_SI_BYTE_MULTIPLE = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

View file

@ -3,33 +3,52 @@
namespace VLW\Database\Models\Coffee;
use \VV;
use \vlw\MySQL\Order;
use \DateTimeImmutable;
use VLW\API\Endpoints;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Tables\Coffee\CoffeeTable;
use VLW\Database\Tables\Coffee\Coffee as CoffeeTable;
require_once VV::root("src/API/Endpoints.php");
require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Tables/Coffee/Coffee.php");
require_once VV::root("src/Database/Models/Coffee/Coffee.php");
class Coffee extends Model {
final public static function new(?DateTimeImmutable $datetime = null): self {
$id = UUID::v4();
if (!parent::create(CoffeeTable::TABLE, [
CoffeeTable::ID->value => $id,
CoffeeTable::DATE_CREATED->value => $datetime ? $datetime->format(parent::DATE_FORMAT) : date(parent::DATE_FORMAT)
])) { throw new Exception("Failed to create Work entity"); }
return new Coffee($id);
}
final public static function all(): array {
return array_map(fn(array $work): Coffee => new Coffee($work[CoffeeTable::ID->value]), new Database()
->from(CoffeeTable::TABLE)
->order([CoffeeTable::DATE_CREATED->value => Order::DESC])
->select(CoffeeTable::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public function __construct(public readonly string $id) {
parent::__construct(Endpoints::COFFEE, [
parent::__construct(CoffeeTable::TABLE, CoffeeTable::values(), [
CoffeeTable::ID->value => $this->id
]);
}
public static function all(array $params = []): array {
return array_map(fn(array $item): Coffee => new Coffee($item[CoffeeTable::ID->value]), parent::list(Endpoints::COFFEE, $params));
public function delete(): bool {
return $this->db->delete([CoffeeTable::ID->value => $this->id]);
}
public function timestamp(): int {
return $this->get(CoffeeTable::ID->value);
}
public function datetime(): DateTimeImmutable {
return DateTimeImmutable::createFromFormat("U", $this->timestamp());
final public DateTimeImmutable $date_created {
get => new DateTimeImmutable($this->get(CoffeeTable::DATE_CREATED->value));
set (DateTimeImmutable $date_created) => $this->set(CoffeeTable::DATE_CREATED->value, $date_created->format(parent::DATE_FORMAT));
}
}

View file

@ -8,7 +8,7 @@
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Tables\Work\Languages;
use VLW\Database\Tables\Languages\Languages;
require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/Database/Database.php");
@ -28,6 +28,15 @@
return new Language($id);
}
final public static function all(): array {
return array_map(fn(array $language): Language => new Language($language[Languages::ID->value]), new Database()
->from(Languages::TABLE)
->order([Languages::BYTES->value => Order::DESC])
->select(Languages::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
final public static function from(string $name): ?self {
return array_map(fn(array $language): Language => new Language($language[Languages::ID->value]), new Database()
->from(Languages::TABLE)
@ -44,9 +53,9 @@
]);
}
final public int $name {
get => $this->get(Languages::NAME->value);
set (int $name) => $this->set(Languages::NAME->value, $name);
final public string $name {
get => $this->get(Languages::NAME->value);
set (string $name) => $this->set(Languages::NAME->value, $name);
}
final public int $bytes {

View file

@ -0,0 +1,62 @@
<?php
namespace VLW\Database\Models\Messages;
use \VV;
use \vlw\MySQL\Order;
use \DateTimeImmutable;
use VLW\Helpers\UUID;
use VLW\Database\Database;
use VLW\Database\Models\Model;
use VLW\Database\Tables\Messages\Messages;
require_once VV::root("src/Helpers/UUID.php");
require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Model.php");
require_once VV::root("src/Database/Tables/Messages/Messages.php");
class Message extends Model {
final public static function new(string $message, ?string $email = null): self {
$id = UUID::v4();
if (!parent::create(Messages::TABLE, [
Messages::ID->value => $id,
Messages::EMAIL->value => $email,
Messages::MESSAGE->value => $message,
Messages::DATE_CREATED->value => date(parent::DATE_FORMAT)
])) { throw new Exception("Failed to create Message entity"); }
return new Message($id);
}
final public static function all(): array {
return array_map(fn(array $Message): Message => new Message($Message[Messages::ID->value]), new Database()
->from(Messages::TABLE)
->order([Messages::DATE_CREATED->value => Order::DESC])
->select(Messages::ID->value)
->fetch_all(MYSQLI_ASSOC)
);
}
public function __construct(public readonly string $id) {
parent::__construct(Messages::TABLE, Messages::values(), [
Messages::ID->value => $this->id
]);
}
final public ?string $email {
get => $this->get(Messages::EMAIL->value);
set (?string $email) => $this->set(Messages::EMAIL->value, $email);
}
final public string $message {
get => $this->get(Messages::MESSAGE->value);
set (string $message) => $this->set(Messages::MESSAGE->value, $message);
}
final public DateTimeImmutable $date_created {
get => new DateTimeImmutable($this->get(Messages::DATE_CREATED->value));
set (DateTimeImmutable $date_created) => $this->set(Messages::DATE_CREATED->value, $date_created->format(parent::DATE_FORMAT));
}
}

View file

@ -9,5 +9,6 @@
const TABLE = "coffee";
case ID = "id";
case ID = "id";
case DATE_CREATED = "date_created";
}

View file

@ -1,6 +1,6 @@
<?php
namespace VLW\Database\Tables\About;
namespace VLW\Database\Tables\Languages;
use vlw\xEnum;

View file

@ -2,10 +2,15 @@
namespace VLW\Database\Tables\Messages;
use vlw\xEnum;
enum Messages: string {
use xEnum;
const TABLE = "messages";
case EMAIL = "email";
case MESSAGE = "message";
case TIMESTAMP_CREATED = "timestamp_created";
case ID = "id";
case EMAIL = "email";
case MESSAGE = "message";
case DATE_CREATED = "date_created";
}

View file

@ -2,12 +2,23 @@
namespace VLW\Helpers;
use VLW\Database\Models\Languages\Language;
use \VV;
use VLW\Database\Database;
use VLW\Database\Models\Languages\Language;
use VLW\Database\Tables\Languages\Languages;
require_once VV::root("src/Database/Database.php");
require_once VV::root("src/Database/Models/Languages/Language.php");
require_once VV::root("src/Database/Tables/Languages/Languages.php");
class Forgejo {
private readonly array $languages;
private const FORGEJO_HREF = "https://git.vlw.se/explore/repos?q=&sort=recentupdate&language=";
private const FORGEJO_ENDPOINT_USER = "/api/v1/users/%s";
private const FORGEJO_ENDPOINT_SEARCH = "/api/v1/repos/search?uid=%s";
private array $languages;
private readonly Database $db;
// Fetch JSON from URL
private static function fetch_json(string $url): array {
@ -16,26 +27,39 @@
// Fetch JSON from a Forgejo endpoint
private static function fetch_endpoint(string $endpoint): array {
$url = $_ENV["server_forgejo"]["base_url"] . $endpoint;
$url = $_ENV["service_forgejo"]["url"] . $endpoint;
return self::fetch_json($url);
}
public function __construct() {
$this->db = new Database();
$this->languages = [];
}
// Write $this->languages to a JSON file
private function cache_languages(): void {
// Delete existing cache
(new Call(Endpoints::ABOUT_LANGUAGES->value))->delete();
// Add languages from all public repositories for profiles in config
public function update(): bool {
foreach(explode(",", $_ENV["service_forgejo"]["profiles"]) as $profile) {
// Resolve user data from username
$user = self::fetch_endpoint(sprintf(self::FORGEJO_ENDPOINT_USER, $profile));
if (!$this->add_public_repositores($user["id"])) {
return false;
}
}
$this->db->for(LanguagesTable::NAME);
$this->save_languages();
return true;
}
private function truncate(): bool {
return $this->db->from(Languages::TABLE)->delete();
}
// Save languages from $this->languages to the database
private function save_languages(): void {
$this->truncate();
foreach ($this->languages as $language => $bytes) {
$this->db->insert([
LanguagesTable::ID->value => $language,
LanguagesTable::BYTES->value => $bytes
]);
Language::new($language, $bytes);
}
}
@ -54,7 +78,7 @@
// Tally languages from public repositories for user id
private function add_public_repositores(int $uid): bool {
$resp = self::fetch_endpoint(sprintf(FORGEJO_ENDPOINT_SEARCH, $uid));
$resp = self::fetch_endpoint(sprintf(self::FORGEJO_ENDPOINT_SEARCH, $uid));
// Bail out if request failed or if response indicated a problem
if (!$resp or $resp["ok"] === false) {
@ -68,14 +92,4 @@
return true;
}
// Add languages from all public repositories for profiles in config
private function add_repositories_from_config_profiles(): void {
foreach(explode(",", $_ENV["server_forgejo"]["scan_profiles"]) as $profile) {
// Resolve user data from username
$user = self::fetch_endpoint(sprintf(FORGEJO_ENDPOINT_USER, $profile));
$this->add_public_repositores($user["id"]);
}
}
}

View file

@ -3,6 +3,8 @@
namespace VLW\Helpers;
class UUID {
public const LENGTH = 36;
public static function nil(): string {
return "00000000-0000-0000-0000-000000000000";
}