summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorilotterytea <iltsu@alright.party>2025-12-08 22:17:05 +0500
committerilotterytea <iltsu@alright.party>2025-12-08 22:17:05 +0500
commit95800ffe216a83bc0eba994ecc53ed22860fe90e (patch)
tree69f1bcb85e63a5fc0fcbc6d70eb56e22940fd6fd /lib
parent57472eab3c7b035392c6a5aa240593ecaa7d1ccf (diff)
upd: include paths
Diffstat (limited to 'lib')
-rw-r--r--lib/accounts.php101
-rw-r--r--lib/alert.php40
-rw-r--r--lib/captcha.php151
-rw-r--r--lib/config.sample.php74
-rw-r--r--lib/emote.php332
-rw-r--r--lib/images.php78
-rw-r--r--lib/partials.php179
-rw-r--r--lib/user.php102
-rw-r--r--lib/utils.php68
-rw-r--r--lib/version.php11
10 files changed, 1136 insertions, 0 deletions
diff --git a/lib/accounts.php b/lib/accounts.php
new file mode 100644
index 0000000..51cb3f6
--- /dev/null
+++ b/lib/accounts.php
@@ -0,0 +1,101 @@
+<?php
+include_once "config.php";
+
+function authorize_user(bool $required = false): bool
+{
+ session_start();
+
+ if (!isset($_COOKIE["secret_key"]) && !isset($_SERVER["HTTP_AUTHORIZATION"])) {
+ if (isset($_SESSION["user_id"])) {
+ session_unset();
+ }
+
+ if ($required) {
+ if (isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json") {
+ http_response_code(401);
+ echo json_encode([
+ "status_code" => 401,
+ "message" => "Unauthorized",
+ "data" => null
+ ]);
+ } else {
+ header("Location: /account");
+ }
+ }
+
+ return false;
+ }
+
+ include_once "config.php";
+
+ $db = new PDO(DB_URL, DB_USER, DB_PASS);
+
+ $key = $_SERVER["HTTP_AUTHORIZATION"] ?? $_COOKIE["secret_key"];
+
+ $stmt = $db->prepare("SELECT id, username FROM users WHERE secret_key = ?");
+ $stmt->execute([$key]);
+
+ if ($row = $stmt->fetch()) {
+ $_SESSION["user_id"] = $row["id"];
+ $_SESSION["user_name"] = $row["username"];
+
+ $stmt = $db->prepare("UPDATE users SET last_active_at = UTC_TIMESTAMP WHERE id = ?");
+ $stmt->execute([$row["id"]]);
+
+ // fetching role
+ $stmt = $db->prepare("SELECT * FROM roles r
+ INNER JOIN role_assigns ra ON ra.user_id = ?
+ WHERE r.id = ra.role_id
+ ");
+ $stmt->execute([$row["id"]]);
+
+ $_SESSION["user_role"] = null;
+
+ if ($role_row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $_SESSION["user_role"] = $role_row;
+ }
+
+ $stmt = $db->prepare("SELECT es.*, aes.is_default FROM emote_sets es
+ INNER JOIN acquired_emote_sets aes ON aes.emote_set_id = es.id
+ WHERE aes.user_id = ?
+ ORDER BY
+ CASE WHEN es.id = ? THEN 0 ELSE 1 END,
+ es.id
+ ");
+ $stmt->execute([$row["id"], $_SESSION["user_active_emote_set_id"] ?? ""]);
+
+ $emote_sets = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if (!isset($_SESSION["user_active_emote_set_id"])) {
+ foreach ($emote_sets as $es) {
+ if ($es["is_default"]) {
+ $_SESSION["user_active_emote_set"] = $es;
+ $_SESSION["user_active_emote_set_id"] = $es["id"];
+ }
+ }
+ }
+
+ $_SESSION["user_emote_sets"] = $emote_sets;
+ } else {
+ session_regenerate_id();
+ session_unset();
+ setcookie("secret_key", "", time() - 1000);
+
+ if ($required) {
+ if (isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json") {
+ http_response_code(401);
+ echo json_encode([
+ "status_code" => 401,
+ "message" => "Unauthorized",
+ "data" => null
+ ]);
+ } else {
+ header("Location: /account");
+ }
+ }
+ }
+
+ $db = null;
+ $stmt = null;
+ return isset($_SESSION["user_name"]);
+} \ No newline at end of file
diff --git a/lib/alert.php b/lib/alert.php
new file mode 100644
index 0000000..823a97a
--- /dev/null
+++ b/lib/alert.php
@@ -0,0 +1,40 @@
+<?php
+function generate_alert(string $path, string $error, int $status = 400)
+{
+ http_response_code($status);
+
+ if (isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json") {
+ header("Content-Type: application/json");
+ echo json_encode([
+ "status_code" => $status,
+ "message" => $error,
+ "data" => null
+ ]);
+ } else {
+ header("Location: $path" . (str_contains($path, "?") ? "&" : "?") . "error_status=$status&error_reason=$error");
+ }
+}
+
+function display_alert()
+{
+ if (!isset($_GET["error_status"], $_GET["error_reason"])) {
+ return;
+ }
+
+ $status = $_GET["error_status"];
+ $reason = str_safe($_GET["error_reason"], 100);
+ $ok = substr($status, 0, 1) == '2';
+
+ echo '' ?>
+ <div class="box row alert <?php echo $ok ? '' : 'red' ?>" style="gap:8px;" id="alert-box">
+ <img src="/static/img/icons/<?php echo $ok ? 'yes' : 'no' ?>.png" alt="">
+ <p><b><?php echo $reason ?></b></p>
+ </div>
+ <script>
+ setTimeout(() => {
+ const alertBox = document.getElementById("alert-box");
+ alertBox.remove();
+ }, 5000);
+ </script>
+ <?php
+} \ No newline at end of file
diff --git a/lib/captcha.php b/lib/captcha.php
new file mode 100644
index 0000000..d6d2547
--- /dev/null
+++ b/lib/captcha.php
@@ -0,0 +1,151 @@
+<?php
+function generate_image_captcha(int $width, int $height, int $difficulty, string $file_name, string $file_folder): string
+{
+ $image = imagecreatetruecolor($width, $height);
+
+ $background = imagecolorallocate($image, 0xDD, 0xDD, 0xDD);
+ imagefilledrectangle($image, 0, 0, $width, $height, $background);
+
+ $files = scandir($file_folder);
+ array_splice($files, 0, 2);
+
+ for ($i = 0; $i < 50 * $difficulty; $i++) {
+ $unprocessed = imagecreatefrompng("$file_folder/" . $files[random_int(0, count($files) - 1)]);
+
+ $oldw = imagesx($unprocessed);
+ $oldh = imagesy($unprocessed);
+
+ $w = random_int(round($oldw / 4), round($oldw / 2));
+ $h = random_int(round($oldh / 4), round($oldh / 2));
+
+ $file = imagecreatetruecolor($w, $h);
+ imagealphablending($file, false);
+ $transparent = imagecolorallocatealpha($file, 0, 0, 0, 127);
+ imagefill($file, 0, 0, $transparent);
+ imagesavealpha($file, true);
+
+ imagecopyresampled($file, $unprocessed, 0, 0, 0, 0, $w, $h, $oldw, $oldh);
+
+ $angle = random_int(0, 360);
+
+ $file = imagerotate($file, $angle, $transparent);
+
+ for ($j = 0; $j < random_int(2, 5 * $difficulty); $j++) {
+ imagefilter($file, IMG_FILTER_GAUSSIAN_BLUR);
+ }
+
+ if (random_int(0, 15) % 3 == 0) {
+ imagefilter($file, IMG_FILTER_NEGATE);
+ }
+
+ if (random_int(0, 20) % 4 == 0) {
+ imagefilter($file, IMG_FILTER_PIXELATE, 4);
+ }
+
+ $w = imagesx($file);
+ $h = imagesy($file);
+
+ imagecopy(
+ $image,
+ $file,
+ random_int(0, $width - $w),
+ random_int(0, $height - $h),
+ 0,
+ 0,
+ $w,
+ $h
+ );
+ }
+
+ $foreground = imagecreatefrompng("$file_folder/$file_name.png");
+ $transparent = imagecolorallocatealpha($foreground, 0, 0, 0, 127);
+ $angle = random_int(0, max: 180);
+ $foreground = imagerotate($foreground, $angle, $transparent);
+ $w = imagesx($foreground);
+ $h = imagesy($foreground);
+ imagecopy(
+ $image,
+ $foreground,
+ random_int(0, $width - $w),
+ random_int(0, $height - $h),
+ 0,
+ 0,
+ $w,
+ $h
+ );
+
+ ob_start();
+ imagepng($image);
+ $source = ob_get_contents();
+ ob_clean();
+
+ return "data:image/png;base64," . base64_encode($source);
+}
+
+function html_captcha_form()
+{
+ echo '' ?>
+ <div class="box" id="form-captcha-wrapper" style="display: none;">
+ <div class="box navtab">
+ Solve captcha
+ </div>
+ <div class="box content">
+ <noscript>JavaScript is required for captcha!</noscript>
+ <form id="form-captcha">
+ <img src="" alt="Generating captcha..." id="form-captcha-img" width="256">
+ <div class="column small-gap">
+ <div class="row small-gap">
+ <input type="text" name="answer" placeholder="Enter emote name..." class="grow"
+ id="form-captcha-answer">
+ <button type="submit" class="green" id="form-captcha-solve">Solve</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ <script>
+ const formElement = document.getElementById("form-captcha");
+ const formWrapper = document.getElementById("form-captcha-wrapper");
+
+ function get_captcha() {
+ fetch("/captcha.php")
+ .then((response) => response.json())
+ .then((json) => {
+ if (json.data == null) {
+ formWrapper.style.display = "none";
+ return;
+ }
+
+ document.getElementById("form-captcha-answer").value = null;
+
+ formWrapper.style.display = "flex";
+
+ document.getElementById("form-captcha-img").setAttribute("src", json.data);
+ });
+ }
+
+ get_captcha();
+
+ formElement.addEventListener("submit", (e) => {
+ e.preventDefault();
+
+ const answer = document.getElementById("form-captcha-answer");
+ const body = new FormData(formElement);
+
+ fetch("/captcha.php", {
+ "method": "POST",
+ "body": body
+ })
+ .then((response) => response.json())
+ .then((json) => {
+ if (json.status_code == 200 && json.data == null) {
+ formWrapper.style.display = "none";
+ return;
+ }
+
+ get_captcha();
+ });
+ });
+ </script>
+ <?php ;
+} \ No newline at end of file
diff --git a/lib/config.sample.php b/lib/config.sample.php
new file mode 100644
index 0000000..3d30044
--- /dev/null
+++ b/lib/config.sample.php
@@ -0,0 +1,74 @@
+<?php
+// INSTANCE
+define("INSTANCE_NAME", "TinyEmotes");
+define("INSTANCE_STATIC_FOLDER", "static"); // Static folder. Used only in /404.php.
+
+// DATABASE
+define("DB_USER", "ENTER_DATABASE_USER"); // Database user. MANDATORY!
+define("DB_PASS", "ENTER_DATABASE_PASSWORD"); // Database password. MANDATORY!
+define("DB_HOST", "ENTER_DATABASE_HOST"); // Database host. Can be 'localhost' if it's on the same machine as Tinyemotes.
+define("DB_NAME", "ENTER_DATABASE_NAME"); // Database name.
+define("DB_URL", 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';port=3306'); // Database URL. Change it if you don't use MySQL/MariaDB.
+
+// RATINGS
+define("RATING_ENABLE", true); // Enable ratings for emotes.
+define("RATING_NAMES", [
+ "-1" => "COAL",
+ "1" => "GEM",
+]); // Rating names. The schema is [ "id/rating_point" => "name" ].
+define("RATING_EMOTE_MIN_VOTES", 10); // Minimal amount of votes to display emote rating.
+
+// UPLOADS
+define("ANONYMOUS_UPLOAD", false); // Allow anonymous upload for emotes.
+define("ANONYMOUS_DEFAULT_NAME", "Anonymous"); // Default uploader name for anonymous emotes. It's also used when original uploader has been deleted.
+
+// EMOTES
+define("EMOTE_UPLOAD", true); // Enable emote upload.
+define("EMOTE_NAME_MAX_LENGTH", 100); // Max length for emote name.
+define("EMOTE_COMMENT_MAX_LENGTH", 100); // Max length for emote comment.
+define("EMOTE_VISIBILITY_DEFAULT", 2); // Default visibility for emotes. 0 - unlisted, 1 - public, 2 - pending approval (same as unlisted).
+define("EMOTE_MAX_SIZE", [128, 128]); // Max size of emote.
+define("EMOTE_NAME_REGEX", "/^[A-Za-z0-9_]+$/"); // RegEx filter for emote names.
+define("EMOTE_STORE_ORIGINAL", true); // Store original uploads of emotes.
+
+// TAGS
+define("TAGS_ENABLE", true); // Allow emote tagging.
+define("TAGS_CODE_REGEX", "/^[A-Za-z0-9_]+$/");
+define("TAGS_MAX_COUNT", 10); // Maximum tags per emote. Set -1 for unlimited amount.
+
+// EMOTESETS
+define("EMOTESET_PUBLIC_LIST", true); // Show emotesets public.
+
+// MODERATION
+define("MOD_SYSTEM_DASHBOARD", true); // Enable system dashboard for moderators (/system).
+define("MOD_EMOTES_APPROVE", true); // Enable manual emote approval (/system/emotes).
+
+// REPORTS
+define("REPORTS_ENABLE", true); // Enable emote, user reports.
+
+// ACCOUNTS
+define("ACCOUNT_REGISTRATION_ENABLE", true); // Enable account registration.
+define("ACCOUNT_COOKIE_MAX_LIFETIME", 86400 * 30); // Remember user for a month.
+define("ACCOUNT_USERNAME_REGEX", "/^[A-Za-z0-9_]+$/"); // RegEx filter for account usernames.
+define("ACCOUNT_USERNAME_LENGTH", [2, 20]); // [Min, Max] length for account usernames.
+define("ACCOUNT_PASSWORD_MIN_LENGTH", 10); // Minimal length for passwords.
+define("ACCOUNT_SECRET_KEY_LENGTH", 32); // The length for secret keys.
+define("ACCOUNT_PFP_MAX_SIZE", [128, 128]); // Max dimensions for account pictures.
+define("ACCOUNT_BANNER_MAX_SIZE", [1920, 1080]); // Max dimensions for account banners.
+define("ACCOUNT_BADGE_MAX_SIZE", [72, 72]); // Max dimensions for account badges.
+define("ACCOUNT_PUBLIC_LIST", true); // The public list of accounts.
+define("ACCOUNT_LOG_ACTIONS", true); // Log user's actions (emote addition, etc.).
+
+// TWITCH
+define("TWITCH_REGISTRATION_ENABLE", false); // Enable account registration via Twitch.
+define("TWITCH_CLIENT_ID", "AAAAAAAAA"); // Client ID of your Twitch application.
+define("TWITCH_SECRET_KEY", "BBBBBBBBB"); // Secret key of your Twitch application.
+define("TWITCH_REDIRECT_URI", ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http") . "://$_SERVER[HTTP_HOST]/account/login/twitch.php"); // Redirect URI of your Twitch application.
+
+// CAPTCHA
+define("CAPTCHA_ENABLE", true); // Enable built-in captcha.
+define("CAPTCHA_SIZE", [580, 220]); // Captcha size.
+define("CAPTCHA_FORCE_USERS", false); // Force authorized users to solve captcha.
+
+// FOR DEVELOPERS
+define("CLIENT_REQUIRES_JSON", isset($_SERVER["HTTP_ACCEPT"]) && $_SERVER["HTTP_ACCEPT"] == "application/json"); \ No newline at end of file
diff --git a/lib/emote.php b/lib/emote.php
new file mode 100644
index 0000000..ce39c09
--- /dev/null
+++ b/lib/emote.php
@@ -0,0 +1,332 @@
+<?php
+include_once "user.php";
+
+class Emote
+{
+ public string $id;
+ public string $code;
+ public string $ext;
+ public mixed $uploaded_by;
+ public int $created_at;
+ public mixed $rating;
+ public bool $is_in_user_set;
+ public int $visibility;
+
+ public string|null $source;
+
+ public array $tags;
+
+ public static function from_array(array $arr): Emote
+ {
+ $e = new Emote();
+
+ $e->id = $arr["id"];
+ $e->code = $arr["code"];
+ $e->ext = $arr["ext"] ?? "webp";
+ $e->uploaded_by = $arr["uploaded_by"];
+ $e->created_at = strtotime($arr["created_at"] ?? 0);
+ $e->is_in_user_set = $arr["is_in_user_set"] ?? false;
+ $e->visibility = $arr["visibility"];
+ $e->source = $arr["source"] ?? null;
+ $e->tags = $arr["tags"] ?? [];
+
+ if (isset($arr["total_rating"], $arr["average_rating"])) {
+ $e->rating = [
+ "total" => $arr["total_rating"],
+ "average" => $arr["average_rating"]
+ ];
+ } else {
+ $e->rating = $arr["rating"] ?? null;
+ }
+
+ return $e;
+ }
+
+ public static function from_array_with_user(array $arr, PDO &$db): Emote
+ {
+ if ($arr["uploaded_by"]) {
+ $arr["uploaded_by"] = User::get_user_by_id($db, $arr["uploaded_by"]);
+ }
+
+ return Emote::from_array($arr);
+ }
+
+ function get_id()
+ {
+ return $this->id;
+ }
+
+ function get_code()
+ {
+ return $this->code;
+ }
+
+ function get_ext()
+ {
+ return $this->ext;
+ }
+
+ function get_created_at()
+ {
+ return $this->created_at;
+ }
+
+ function get_uploaded_by()
+ {
+ return $this->uploaded_by;
+ }
+
+ function is_added_by_user()
+ {
+ return $this->is_in_user_set;
+ }
+
+ function get_rating()
+ {
+ return $this->rating;
+ }
+
+ function get_visibility()
+ {
+ return $this->visibility;
+ }
+
+ function get_source()
+ {
+ return $this->source;
+ }
+
+ function get_tags(): array
+ {
+ return $this->tags;
+ }
+}
+
+class Emoteset
+{
+ public string $id;
+ public string $name;
+ public User|null $owner;
+ public array $emotes;
+
+ public bool $is_default;
+
+ public static function from_array(array $arr): Emoteset
+ {
+ $s = new Emoteset();
+
+ $s->id = $arr["id"];
+ $s->name = $arr["name"];
+ $s->owner = $arr["owner_id"];
+ $s->emotes = $arr["emotes"] ?? [];
+ $s->is_default = $arr["is_default"] ?? false;
+
+ return $s;
+ }
+
+ public static function from_array_extended(array $arr, string $user_id, PDO &$db): Emoteset
+ {
+ if ($arr["owner_id"]) {
+ $arr["owner_id"] = User::get_user_by_id($db, $arr["owner_id"]);
+ }
+
+ $arr["emotes"] = fetch_all_emotes_from_emoteset($db, $arr["id"], $user_id);
+
+ return Emoteset::from_array($arr);
+ }
+
+ public static function get_all_user_emotesets(PDO &$db, string $user_id): array
+ {
+ $stmt = $db->prepare("SELECT es.*, aes.is_default FROM emote_sets es
+ INNER JOIN acquired_emote_sets aes ON aes.emote_set_id = es.id
+ WHERE aes.user_id = ?
+ ");
+ $stmt->execute([$user_id]);
+
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ $emote_sets = [];
+
+ foreach ($rows as $row) {
+ array_push($emote_sets, Emoteset::from_array_extended($row, $user_id, $db));
+ }
+
+ return $emote_sets;
+ }
+}
+
+function fetch_all_emotes_from_emoteset(PDO &$db, string $emote_set_id, string $user_id, int|null $limit = null): array
+{
+ // fetching emotes
+ $sql = "SELECT
+ e.id, e.created_at, e.visibility,
+ CASE
+ WHEN esc.code IS NOT NULL THEN esc.code
+ ELSE e.code
+ END AS code,
+ CASE
+ WHEN esc.code IS NOT NULL THEN e.code
+ ELSE NULL
+ END AS original_code,
+ CASE WHEN up.private_profile = FALSE OR up.id = ? THEN e.uploaded_by ELSE NULL END AS uploaded_by
+ FROM emotes e
+ LEFT JOIN user_preferences up ON up.id = e.uploaded_by
+ INNER JOIN emote_set_contents AS esc
+ ON esc.emote_set_id = ?
+ WHERE esc.emote_id = e.id";
+
+ if ($limit) {
+ $sql .= " LIMIT $limit";
+ }
+
+ $stmt = $db->prepare($sql);
+ $stmt->execute([$user_id, $emote_set_id]);
+
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ $emotes = [];
+
+ // fetching uploaders
+ foreach ($rows as $row) {
+ if ($row["uploaded_by"]) {
+ $row["uploaded_by"] = User::get_user_by_id($db, $row["uploaded_by"]);
+ }
+
+ array_push($emotes, Emote::from_array($row));
+ }
+
+ return $emotes;
+}
+
+function html_random_emote(PDO &$db)
+{
+ $stmt = $db->prepare("SELECT id, code FROM emotes WHERE visibility = 1 ORDER BY RAND() LIMIT 1");
+ $stmt->execute();
+
+ if ($row = $stmt->fetch()) {
+ echo ''
+ ?>
+ <section class="box" id="box-random-emote">
+ <div class="box navtab">
+ <p>Random emote</p>
+ </div>
+ <div class="box content center">
+ <a href="/emotes?id=<?php echo $row["id"] ?>">
+ <img src="/static/userdata/emotes/<?php echo $row["id"] ?>/3x.webp" alt="<?php echo $row["code"] ?>"
+ width="192">
+ </a>
+ </div>
+ </section>
+ <?php
+ ;
+ }
+}
+
+function html_featured_emote(PDO &$db)
+{
+ $stmt = $db->prepare("SELECT e.id, e.code FROM emotes e
+ INNER JOIN emote_sets es ON es.is_featured = TRUE
+ INNER JOIN emote_set_contents esc ON es.id = esc.emote_set_id
+ WHERE e.visibility = 1 AND e.id = esc.emote_id ORDER BY esc.added_at DESC LIMIT 1");
+ $stmt->execute();
+
+ if ($row = $stmt->fetch()) {
+ echo ''
+ ?>
+ <section class="box" id="box-featured-emote">
+ <div class="box navtab">
+ <p>Featured emote</p>
+ </div>
+ <div class="box content center">
+ <a href="/emotes?id=<?php echo $row["id"] ?>">
+ <img src="/static/userdata/emotes/<?php echo $row["id"] ?>/3x.webp" alt="<?php echo $row["code"] ?>"
+ width="192">
+ </a>
+ </div>
+ </section>
+ <?php
+ ;
+ }
+}
+
+function html_display_emotes(array $emotes, int $scale = 3)
+{
+ $emote_wall = intval($_COOKIE["emotelist_wall"] ?? "0") == 1;
+
+ foreach ($emotes as $e) {
+ echo "<a class='box emote column justify-center items-center' href='/emotes?id={$e->id}'>";
+
+ if ($e->is_added_by_user()) {
+ echo '<img src="/static/img/icons/yes.png" class="emote-check" />';
+ }
+
+ // icon
+ echo '<div class="flex justify-center items-center grow emote-icon">';
+ $scale = $emote_wall ? "3" : ((string) $scale);
+ echo "<img src='/static/userdata/emotes/{$e->id}/{$scale}x.webp' alt='{$e->code}' />";
+ echo '</div>';
+
+ // info
+ echo '<div class="flex column justify-bottom items-center emote-desc';
+ if ($emote_wall) {
+ echo ' none';
+ }
+ echo '">';
+
+ echo "<h1 title='{$e->code}'>{$e->code}</h1>";
+ if ($e->get_uploaded_by()) {
+ echo "<p>{$e->uploaded_by->username}</p>";
+ }
+
+ echo '</div></a>';
+ }
+}
+
+function html_display_emoteset(array $emotesets)
+{
+ foreach ($emotesets as $es) {
+ echo "<a href='/emotesets.php?id={$es->id}' class='box column small-gap'>";
+
+ echo '<div>';
+ echo "<p>$es->name</p>";
+ echo '</div>';
+
+ echo '<div class="small-gap row">';
+
+ foreach ($es->emotes as $e) {
+ echo "<img src='/static/userdata/emotes/{$e->id}/1x.webp' alt='{$e->code}' title='{$e->code}' height='16' />";
+ }
+
+ echo '</div></a>';
+
+ }
+}
+
+function html_emotelist_mode()
+{
+ echo '' ?>
+ <div class="row small-gap" id="control-emotelist" style="display: none;">
+ <a href="#" onclick="set_emotelist_mode('wall')">
+ <img src="/static/img/icons/emotes/emote.png" alt="Emote Wall" title="Emote Wall">
+ </a>
+ <a href="#" onclick="set_emotelist_mode('table')">
+ <img src="/static/img/icons/table.png" alt="Emote Descriptions" title="Emote Descriptions">
+ </a>
+ </div>
+ <script>
+ document.getElementById("control-emotelist").style.display = "flex";
+
+ function set_emotelist_mode(mode) {
+ const elements = document.querySelectorAll(".emote .emote-desc");
+
+ for (const element of elements) {
+ if (mode == "wall") {
+ element.classList.add("none");
+ document.cookie = "emotelist_wall = 1; expires=Tue, 19 Jan 2038 00:00:00 UTC; path=/";
+ } else {
+ element.classList.remove("none");
+ document.cookie = "emotelist_wall = 0; expires=Tue, 19 Jan 2038 00:00:00 UTC; path=/";
+ }
+ }
+ }
+ </script>
+ <?php ;
+} \ No newline at end of file
diff --git a/lib/images.php b/lib/images.php
new file mode 100644
index 0000000..9ec0301
--- /dev/null
+++ b/lib/images.php
@@ -0,0 +1,78 @@
+<?php
+function resize_image(string $src_path, string $dst_path, int $max_width, int $max_height, bool $set_format = true, bool $stretch = false): int|null
+{
+ if ($src_path == "") {
+ return -2;
+ }
+
+ $image = getimagesize($src_path);
+
+ if ($image == false) {
+ return -1;
+ }
+
+ $format = $set_format ? ".webp" : "";
+
+ $width = $image[0];
+ $height = $image[1];
+ $ratio = min($max_width / $width, $max_height / $height);
+ $new_width = $stretch ? $max_width : (int) ($width * $ratio);
+ $new_height = $stretch ? $max_height : (int) ($height * $ratio);
+
+ $input_path = escapeshellarg($src_path);
+ $output_path = escapeshellarg("$dst_path$format");
+
+ $result_code = null;
+
+ exec(command: "magick convert $input_path -coalesce -resize {$new_width}x{$new_height}! -loop 0 $output_path", result_code: $result_code);
+
+ return $result_code;
+}
+
+function create_image_bundle(string $src_path, string $dst_path, int $max_width, int $max_height, bool $set_format = true, bool $stretch = false): int|null
+{
+ if (!is_dir($dst_path)) {
+ if (!mkdir($dst_path, 0777, true)) {
+ return -3;
+ }
+ }
+
+ if ($err = resize_image($src_path, "$dst_path/3x", $max_width, $max_height, $set_format, $stretch)) {
+ array_map("unlink", glob("$dst_path/*.*"));
+ rmdir($dst_path);
+ return $err;
+ }
+
+ if ($err = resize_image($src_path, "$dst_path/2x", round($max_width / 2), round($max_height / 2), $set_format, $stretch)) {
+ array_map("unlink", glob("$dst_path/*.*"));
+ rmdir($dst_path);
+ return $err;
+ }
+
+ if ($err = resize_image($src_path, "$dst_path/1x", round($max_width / 4), round($max_height / 4), $set_format, $stretch)) {
+ array_map("unlink", glob("$dst_path/*.*"));
+ rmdir($dst_path);
+ return $err;
+ }
+
+ return null;
+}
+
+function get_file_extension(string $path): string|null
+{
+ if ($file = getimagesize($path)) {
+ return image_type_to_extension(intval($file[2]), false);
+ }
+
+ return null;
+}
+
+function does_file_meet_requirements(string $path, int $max_width, int $max_height): array
+{
+ $file = getimagesize($path);
+ if (!$file) {
+ return [false, null];
+ }
+
+ return [$file[0] <= $max_width && $file[1] <= $max_height, image_type_to_extension(intval($file[2]), false)];
+} \ No newline at end of file
diff --git a/lib/partials.php b/lib/partials.php
new file mode 100644
index 0000000..760923a
--- /dev/null
+++ b/lib/partials.php
@@ -0,0 +1,179 @@
+<?php
+function html_navigation_bar()
+{
+ include_once "config.php";
+
+ echo '' ?>
+ <section class="navbar">
+ <a href="/" class="brand" style="color:black;text-decoration:none;">
+ <img src="/static/img/brand/mini.webp" alt="">
+ <h2 style="margin-left:8px;font-size:24px;"><b><?php echo INSTANCE_NAME ?></b></h2>
+ </a>
+ <div class="links">
+ <a href="/emotes" class="button">Emotes</a>
+
+ <?php if (EMOTESET_PUBLIC_LIST): ?>
+ <a href="/emotesets.php" class="button">Emotesets</a>
+ <?php endif; ?>
+
+ <?php if (ACCOUNT_PUBLIC_LIST): ?>
+ <a href="/users.php" class="button">Users</a>
+ <?php endif; ?>
+
+ <?php if (EMOTE_UPLOAD && (ANONYMOUS_UPLOAD || (isset($_SESSION["user_role"]) && $_SESSION["user_role"]["permission_upload"]))) {
+ echo '<a href="/emotes/upload.php" class="button">Upload</a>';
+ } ?>
+ <a href="/account" class="button">Account</a>
+ <?php
+ if (isset($_SESSION["user_id"])) {
+ $db = new PDO(DB_URL, DB_USER, DB_PASS);
+
+ // getting inbox
+ $stmt = $db->prepare("SELECT COUNT(*) FROM inbox_messages WHERE recipient_id = ? AND has_read = false");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $unread_count = intval($stmt->fetch()[0]);
+ echo '' ?>
+ <a href="/inbox.php" class="button">
+ Inbox <?php echo $unread_count > 0 ? "($unread_count)" : "" ?>
+ </a>
+ <?php ;
+ $stmt = null;
+
+ if (isset($_SESSION["user_role"])) {
+ if (REPORTS_ENABLE && $_SESSION["user_role"]["permission_report"]) {
+ // getting reports
+ $stmt = $db->prepare("SELECT COUNT(*) FROM reports WHERE sender_id = ? AND resolved_by IS NULL");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $unread_count = intval($stmt->fetch()[0]);
+
+ echo '' ?>
+ <a href="/report/list.php" class="button">
+ Reports <?php echo $unread_count > 0 ? "($unread_count)" : "" ?>
+ </a>
+ <?php ;
+ }
+
+ if (MOD_SYSTEM_DASHBOARD && ($_SESSION["user_role"]["permission_approve_emotes"] || $_SESSION["user_role"]["permission_report_review"])) {
+ $system_count = 0;
+
+ if ($_SESSION["user_role"]["permission_approve_emotes"] && MOD_EMOTES_APPROVE) {
+ $system_count += intval($db->query("SELECT COUNT(*) FROM emotes WHERE visibility = 2")->fetch()[0]);
+ }
+
+ if ($_SESSION["user_role"]["permission_report_review"]) {
+ $system_count += intval($db->query("SELECT COUNT(*) FROM reports WHERE resolved_by IS NULL")->fetch()[0]);
+ }
+
+ echo '<a href="/system" class="button">System';
+ if ($system_count > 0) {
+ echo " ($system_count)";
+ }
+ echo '</a>';
+ }
+ }
+
+ $stmt = null;
+ $db = null;
+ }
+ ?>
+ </div>
+ <?php if (isset($_SESSION["user_id"])): ?>
+ <div class="flex items-bottom small-gap" style="margin-left: auto;">
+ <?php if (isset($_SESSION["user_emote_sets"])): ?>
+ <form action="/account/change_emoteset.php" method="POST" id="form-change-emoteset">
+ <input type="text" name="redirect" value="<?php echo $_SERVER["REQUEST_URI"] ?>" style="display: none;">
+ <div class="row small-gap">
+ <label for="id">Current emote set: </label>
+ <select name="id" onchange="send_change_emoteset(event)">
+ <?php
+ foreach ($_SESSION["user_emote_sets"] as $es) {
+ echo '<option value="' . $es["id"] . '">' . $es["name"] . '</option>';
+ }
+ ?>
+ </select>
+ </div>
+ </form>
+ <script>
+ function send_change_emoteset(e) {
+ document.getElementById("form-change-emoteset").submit();
+ }
+ </script>
+ <?php endif; ?>
+
+ <a href="/users.php?id=<?php echo $_SESSION["user_id"] ?>" class="flex items-bottom small-gap">
+ Signed in as <?php echo $_SESSION["user_name"] ?>
+ <?php
+ echo '<img src="/static/';
+ if (is_dir($_SERVER['DOCUMENT_ROOT'] . "/static/userdata/avatars/" . $_SESSION["user_id"])) {
+ echo 'userdata/avatars/' . $_SESSION["user_id"] . "/1x.webp";
+ } else {
+ echo 'img/defaults/profile_picture.png';
+ }
+ echo '" width="24" height="24" />';
+ ?>
+ </a>
+ <a href="/account/signout.php?local">
+ <img src="/static/img/icons/door_out.png" alt="[ Log out ]" title="Log out">
+ </a>
+ </div>
+ <?php endif; ?>
+ </section>
+ <?php ;
+}
+
+function html_navigation_search()
+{
+ echo '' ?>
+ <section class="box">
+ <div class="box navtab">
+ Search...
+ </div>
+ <div class="box content">
+ <form action="<?php echo $_SERVER["REQUEST_URI"] ?>" method="GET">
+ <input type="text" name="q" style="padding:4px;" value="<?php echo $_GET["q"] ?? "" ?>"><br>
+ <?php
+ if (str_starts_with($_SERVER["REQUEST_URI"], "/emotes")) {
+ ?>
+ <label for="sort_by">Sort by</label>
+ <select name="sort_by">
+ <option value="high_ratings" <?php echo ($_GET["sort_by"] ?? "") == "high_ratings" ? "selected" : "" ?>>
+ High ratings</option>
+ <option value="low_ratings" <?php echo ($_GET["sort_by"] ?? "") == "low_ratings" ? "selected" : "" ?>>Low
+ ratings</option>
+ <option value="recent" <?php echo ($_GET["sort_by"] ?? "") == "recent" ? "selected" : "" ?>>Recent
+ </option>
+ <option value="oldest" <?php echo ($_GET["sort_by"] ?? "") == "oldest" ? "selected" : "" ?>>Oldest
+ </option>
+ </select>
+ <?php
+ }
+ ?>
+ <button type="submit" style="width:100%;margin-top:6px;">Find</button>
+ </form>
+ </div>
+ </section>
+ <?php ;
+}
+
+function html_pagination(int $total_pages, int $current_page, string $redirect)
+{
+ if (str_contains($redirect, "?")) {
+ $redirect .= "&p=";
+ } else {
+ $redirect .= "?p=";
+ }
+
+ if ($total_pages > 1) {
+ echo '' ?>
+ <div class="pagination">
+ <?php if ($current_page > 1): ?>
+ <a href="<?php echo $redirect . ($current_page - 1) ?>">[ prev ]</a>
+ <?php endif; ?>
+ <?php if ($current_page < $total_pages): ?>
+ <a href="<?php echo $redirect . ($current_page + 1) ?>">[ next ]</a>
+ <?php endif; ?>
+
+ </div>
+ <?php ;
+ }
+} \ No newline at end of file
diff --git a/lib/user.php b/lib/user.php
new file mode 100644
index 0000000..d22eeb4
--- /dev/null
+++ b/lib/user.php
@@ -0,0 +1,102 @@
+<?php
+class Badge
+{
+ public string $id;
+
+ public static function from_array(array $arr, string $prefix = ""): Badge|null
+ {
+ if (!empty($prefix)) {
+ $prefix .= "_";
+ }
+ if (!isset($arr["{$prefix}badge_id"])) {
+ return null;
+ }
+
+ $b = new Badge();
+ $b->id = $arr["{$prefix}badge_id"];
+
+ return $b;
+ }
+}
+
+class Role
+{
+ public string $name;
+ public Badge|null $badge;
+
+ public static function from_array(array $arr): Role|null
+ {
+ if (!isset($arr["role_name"])) {
+ return null;
+ }
+
+ $r = new Role();
+
+ $r->name = $arr["role_name"];
+ $r->badge = Badge::from_array($arr, "role");
+
+ return $r;
+ }
+}
+
+class User
+{
+ public string $id;
+ public string $username;
+ public int $joined_at;
+ public int $last_active_at;
+
+ public Badge|null $custom_badge;
+
+ public Role|null $role;
+
+ public bool $private_profile;
+
+ public static function from_array(array $arr): User
+ {
+ $u = new User();
+
+ $u->id = $arr["id"];
+ $u->username = $arr["username"];
+ $u->joined_at = strtotime($arr["joined_at"] ?? "0");
+ $u->last_active_at = strtotime($arr["last_active_at"] ?? "0");
+
+ $u->private_profile = $row["private_profile"] ?? false;
+
+ $u->custom_badge = Badge::from_array($arr, "custom");
+
+ $u->role = Role::from_array($arr);
+
+ return $u;
+ }
+
+ public static function get_user_by_id(PDO &$db, string $user_id): User|null
+ {
+ $stmt = $db->prepare("SELECT
+ u.id,
+ u.username,
+ u.joined_at,
+ u.last_active_at,
+
+ up.private_profile,
+ r.name AS role_name,
+ r.badge_id AS role_badge_id,
+ ub.badge_id AS custom_badge_id
+ FROM users u
+ INNER JOIN user_preferences up ON up.id = u.id
+ LEFT JOIN role_assigns ra ON ra.user_id = u.id
+ LEFT JOIN roles r ON r.id = ra.role_id
+ LEFT JOIN user_badges ub ON ub.user_id = u.id
+ WHERE u.id = ?
+ ");
+ $stmt->execute([$user_id]);
+
+ $u = null;
+
+ if ($uploader_row = $stmt->fetch()) {
+ $u = User::from_array($uploader_row);
+ }
+
+ return $u;
+ }
+} \ No newline at end of file
diff --git a/lib/utils.php b/lib/utils.php
new file mode 100644
index 0000000..87d96c6
--- /dev/null
+++ b/lib/utils.php
@@ -0,0 +1,68 @@
+<?php
+function json_response(mixed $response, int $status = 200)
+{
+ http_response_code($status);
+ header("Content-Type: application/json");
+ echo json_encode($response);
+}
+
+function generate_random_string(int $length): string
+{
+ $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ $output = "";
+
+ for ($i = 0; $i < $length; $i++) {
+ $charindex = random_int(0, strlen($chars) - 1);
+ $output .= $chars[$charindex];
+ }
+
+ return $output;
+}
+
+function str_safe(string $s, int|null $max_length, bool $remove_new_lines = true): string
+{
+ $output = $s;
+
+ if ($remove_new_lines) {
+ $output = str_replace(PHP_EOL, "", $output);
+ }
+
+ $output = htmlspecialchars($output);
+ $output = strip_tags($output);
+
+ if ($max_length) {
+ $output = substr($output, 0, $max_length);
+ }
+
+ $output = trim($output);
+
+ return $output;
+}
+
+function format_timestamp(int $timestamp_secs)
+{
+ $days = (int) floor($timestamp_secs / (60.0 * 60.0 * 24.0));
+ $hours = (int) floor(round($timestamp_secs / (60 * 60)) % 24);
+ $minutes = (int) floor(round($timestamp_secs % (60 * 60)) / 60);
+ $seconds = (int) floor($timestamp_secs % 60);
+
+ if ($days == 0 && $hours == 0 && $minutes == 0) {
+ return "$seconds second" . ($seconds > 1 ? "s" : "");
+ } else if ($days == 0 && $hours == 0) {
+ return "$minutes minute" . ($minutes > 1 ? "s" : "");
+ } else if ($days == 0) {
+ return "$hours hour" . ($hours > 1 ? "s" : "");
+ } else {
+ return "$days day" . ($days > 1 ? "s" : "");
+ }
+}
+
+function clamp(int $current, int $min, int $max): int
+{
+ return max($min, min($max, $current));
+}
+
+function in_range(float $value, float $min, float $max): bool
+{
+ return $min <= $value && $value <= $max;
+} \ No newline at end of file
diff --git a/lib/version.php b/lib/version.php
new file mode 100644
index 0000000..d666783
--- /dev/null
+++ b/lib/version.php
@@ -0,0 +1,11 @@
+<?php
+// please leave it as it is ;)
+define("TINYEMOTES_NAME", "TinyEmotes");
+define("TINYEMOTES_VERSION", "0.3.1");
+define("TINYEMOTES_LINK", "https://git.ilt.su/tiny/emotes.git");
+
+if ($s = file_get_contents("{$_SERVER['DOCUMENT_ROOT']}/.git/refs/heads/master")) {
+ define("TINYEMOTES_COMMIT", $s);
+} else {
+ define("TINYEMOTES_COMMIT", null);
+} \ No newline at end of file