Initial commit
This commit is contained in:
		
						commit
						c6a49dc6e8
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| *.sqlite | ||||
| *.txt | ||||
							
								
								
									
										70
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| # tkr | ||||
| 
 | ||||
| A bare-bones status feed for self-hosted personal websites. | ||||
| 
 | ||||
| Currently very much a work in progress, but it's baically functional. | ||||
| 
 | ||||
| ## How it works | ||||
| 
 | ||||
| Deploy the `/tkr` directory to a web server that supports php. It will work either as the root of a (sub)domain (e.g. tky.mydomain.com) or if served from a subdirectory (e.g. mydomain.com/tkr). | ||||
| 
 | ||||
| If you serve it from a subdirectory, set the value of `$basePath` in `/app/config.php` to the subdirectory name, excluding the trailing slash (e.g. `/tkr`) | ||||
| 
 | ||||
| It provides an rss feed at `/rss` relative to where it's being served (e.g. `/tkr/rss` if served from `/tkr/`). Each rss entry links to an individual post (which I call "ticks"). | ||||
| 
 | ||||
| ## Serving | ||||
| 
 | ||||
| The document root should be `/PATH/TO/tkr/public`. This will ensure that only the files that need to be accessible from the internet are served by your web server. | ||||
| 
 | ||||
| ## Storage | ||||
| 
 | ||||
| Ticks are stored in files on the filesystem under `/tkr/ticks`. This directory must be writable by the web server user and so SHOULD NOT be served by the web server. If you set your document root to `/tkr/public/`, then you'll be fine. | ||||
| 
 | ||||
| The file structure is `YYYY/MM/DD.txt`. That is, each day's ticks are located in a file whose full path is `/tkr/ticks/YEAR/MONTH/DAY.txt`. This is to prevent any single file from getting too large. | ||||
| 
 | ||||
| Each entry takes the form `TIMESTAMP|TICK`, where `TIMESTAMP` is the time that the entry was made and `TICK` is the text of the entry. | ||||
| 
 | ||||
| For illustration, here's a sample from the file `/tkr/ticks/2025/05/25` on my test system. | ||||
| 
 | ||||
| ```sh | ||||
| # cat /tkr/ticks/2025/05/25.txt | ||||
| 23:27:37|some stuff | ||||
| 23:27:45|some more, stuff | ||||
| ``` | ||||
| 
 | ||||
| ## Initial config | ||||
| 
 | ||||
| I'll write this up when I improve it. | ||||
| 
 | ||||
| ## Sample images | ||||
| 
 | ||||
| Logged out | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| Logged in | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| RSS | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| Single tick | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## TODO | ||||
| 
 | ||||
| * An actual setup script | ||||
| * Validate CSRF token on tick submission | ||||
| * Let people set the time zone for ticks | ||||
| * Support basic custom styling | ||||
| * Do that config improvement I implied in the previous section | ||||
| * See if I can get individual ticks to resolve as urls (e.g. /2025/05/26/00/12) rather than doing the query string thing | ||||
| * Clean up the nginx configs | ||||
| * Add an .htaccess example | ||||
| * Maybe h-feed or JSON feed? | ||||
| * Microformat support? | ||||
| * A little more profile info? | ||||
| * Probably a bunch of other stuff I'm not thinking of | ||||
							
								
								
									
										12
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| services: | ||||
|     web: | ||||
|         image: nginx:latest | ||||
|         ports: | ||||
|             - "80:80" | ||||
|         volumes: | ||||
|             - ./tkr-nginx-folder.conf:/etc/nginx/conf.d/default.conf | ||||
|             - ./tkr:/tkr | ||||
|     php: | ||||
|         image: php:fpm | ||||
|         volumes: | ||||
|             - ./tkr:/tkr | ||||
							
								
								
									
										33
									
								
								tkr-nginx-folder.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								tkr-nginx-folder.conf
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| server { | ||||
|     listen 80 default_server; | ||||
|     root /usr/share/nginx/html; | ||||
| 
 | ||||
|     location ^~ /tkr { | ||||
|         index index.php; | ||||
|         alias /tkr/public; | ||||
| 
 | ||||
|         #location ~ "^/tkr/([0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]{2}/[0-9]{2}/[0-9]{2})$" { | ||||
|         #    rewrite "^/tkr/([0-9/]{19})$ /tkr/tick.php?path=$1" break; | ||||
|         #} | ||||
| 
 | ||||
|         location ~ ^/tkr(/.+\.php)$ { | ||||
|             fastcgi_pass php:9000; | ||||
|             include fastcgi_params; | ||||
|             fastcgi_param SCRIPT_FILENAME /tkr/public/$1; | ||||
|             fastcgi_param SCRIPT_NAME $uri; | ||||
|             fastcgi_param REQUEST_METHOD  $request_method; | ||||
|             fastcgi_param CONTENT_TYPE    $content_type; | ||||
|             fastcgi_param CONTENT_LENGTH  $content_length; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     # Deny anything else | ||||
|     location / { | ||||
|         try_files $uri $uri/ =404; | ||||
|     } | ||||
| 
 | ||||
|     # Deny access to hidden or stray files | ||||
|     location ~* \.(htaccess|env|ini|log|bak)$ { | ||||
|         deny all; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								tkr-nginx.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tkr-nginx.conf
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| server { | ||||
|     listen 80 default_server; | ||||
|     root /app/public; | ||||
| 
 | ||||
|     index index.php index.html index.htm; | ||||
| 
 | ||||
|     location ~ \.php$ { | ||||
|         fastcgi_pass php:9000; | ||||
|         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; | ||||
|         fastcgi_param REQUEST_METHOD  $request_method; | ||||
|         fastcgi_param CONTENT_TYPE    $content_type; | ||||
|         fastcgi_param CONTENT_LENGTH  $content_length; | ||||
|         include fastcgi_params;      | ||||
|     } | ||||
| 
 | ||||
|     location / { | ||||
|         try_files $uri $uri/ =404; | ||||
|     } | ||||
| 
 | ||||
|     location ~* \.(htaccess|env|ini|log|bak)$ { | ||||
|         deny all; | ||||
|     } | ||||
| }  | ||||
							
								
								
									
										12
									
								
								tkr/config.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tkr/config.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| <?php | ||||
| $dbLocation = __DIR__ . '/db/tkr.sqlite'; | ||||
| $tickLocation = __DIR__ . '/ticks'; | ||||
| $basePath = '/tkr'; | ||||
| 
 | ||||
| try { | ||||
|     $pdo = new PDO("sqlite:$dbLocation"); | ||||
|     $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | ||||
|     $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); | ||||
| } catch (PDOException $e) { | ||||
|     die("Database connection failed: " . $e->getMessage()); | ||||
| } | ||||
							
								
								
									
										56
									
								
								tkr/create_db.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								tkr/create_db.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| <?php | ||||
| // TODO: Replace this whole thing with a setup.php
 | ||||
| 
 | ||||
| function prompt($prompt) { | ||||
|     echo $prompt; | ||||
|     return trim(fgets(STDIN)); | ||||
| } | ||||
| 
 | ||||
| function promptSilent($prompt = "Enter Password: ") { | ||||
|     if (strncasecmp(PHP_OS, 'WIN', 3) === 0) { | ||||
|         // Windows doesn't support shell-based hidden input
 | ||||
|         echo "Warning: Password input not hidden on Windows.\n"; | ||||
|         return prompt($prompt); | ||||
|     } else { | ||||
|         // Use shell to disable echo for password input
 | ||||
|         echo $prompt; | ||||
|         system('stty -echo'); | ||||
|         $password = rtrim(fgets(STDIN), "\n"); | ||||
|         system('stty echo'); | ||||
|         echo "\n"; | ||||
|         return $password; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| $dbFile = __DIR__ . '/tkr.sqlite'; | ||||
| 
 | ||||
| try { | ||||
|     $pdo = new PDO("sqlite:$dbFile"); | ||||
|     $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | ||||
| } catch (PDOException $e) { | ||||
|     die("Could not connect to DB: " . $e->getMessage() . "\n"); | ||||
| } | ||||
| 
 | ||||
| $pdo->exec("CREATE TABLE IF NOT EXISTS user (
 | ||||
|     id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
|     username TEXT UNIQUE NOT NULL, | ||||
|     password_hash TEXT NOT NULL | ||||
| )");
 | ||||
| 
 | ||||
| $username = prompt("Enter username: "); | ||||
| $password = promptSilent("Enter password: "); | ||||
| $confirm  = promptSilent("Confirm password: "); | ||||
| 
 | ||||
| if ($password !== $confirm) { | ||||
|     die("Error: Passwords do not match.\n"); | ||||
| } | ||||
| 
 | ||||
| $passwordHash = password_hash($password, PASSWORD_DEFAULT); | ||||
| 
 | ||||
| try { | ||||
|     $stmt = $pdo->prepare("INSERT INTO user(username, password_hash) VALUES (?, ?)"); | ||||
|     $stmt->execute([$username, $passwordHash]); | ||||
|     echo "User '$username' created successfully.\n"; | ||||
| } catch (PDOException $e) { | ||||
|     echo "Failed to create user: " . $e->getMessage() . "\n"; | ||||
| } | ||||
							
								
								
									
										0
									
								
								tkr/db/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tkr/db/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										61
									
								
								tkr/public/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								tkr/public/index.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| <?php | ||||
| define('APP_ROOT', realpath(__DIR__ . '/../')); | ||||
| define('ITEMS_PER_PAGE', 25); | ||||
| 
 | ||||
| require APP_ROOT . '/config.php'; | ||||
| require APP_ROOT . '/session.php'; | ||||
| require_once APP_ROOT . '/stream_ticks.php'; | ||||
| 
 | ||||
| $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; | ||||
| $limit = ITEMS_PER_PAGE; | ||||
| $offset = ($page - 1) * $limit; | ||||
| 
 | ||||
| $ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset)); | ||||
| ?>
 | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|     <head> | ||||
|         <title>My ticker</title> | ||||
|         <style> | ||||
|             body { font-family: sans-serif; margin: 2em; } | ||||
|             .tick { margin-bottom: 1em; } | ||||
|             .ticktime { color: gray; font-size: 0.9em; } | ||||
|             .ticktext {color: black; font-size: 1.0em; } | ||||
|             .pagination a { margin: 0 5px; text-decoration: none; } | ||||
|         </style> | ||||
|     </head> | ||||
|     <body> | ||||
|         <h2>Welcome! Here's what's ticking.</h2> | ||||
| 
 | ||||
| <?php foreach ($ticks as $tick): ?>
 | ||||
|         <div class="tick"> | ||||
|             <spam class="ticktime"><?= $tick['timestamp'] ?></span>
 | ||||
|             <span class="ticktext"><?= $tick['tick'] ?></spam>
 | ||||
|         </div> | ||||
| <?php endforeach; ?>
 | ||||
|    | ||||
|     <div class="pagination"> | ||||
| 
 | ||||
| <?php if ($page > 1): ?>
 | ||||
|         <a href="?page=<?= $page - 1 ?>">« Newer</a> | ||||
| <?php endif; ?>
 | ||||
| 
 | ||||
| <?php if (count($ticks) === $limit): ?>
 | ||||
|         <a href="?page=<?= $page + 1 ?>">Older »</a> | ||||
| <?php endif; ?>
 | ||||
|     </div> | ||||
| 
 | ||||
| <?php if (!$isLoggedIn): ?>
 | ||||
|     <p><a href="<?= $basePath ?>/login.php">Login</a></p> | ||||
| <?php else: ?>
 | ||||
|     <form action="save_tick.php" method="post"> | ||||
|         <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>"> | ||||
|         <label for="tick">What's ticking?</label> | ||||
|         <input name="tick" id="tick" type="text"> | ||||
|    | ||||
|         <button type="submit">Tick</button> | ||||
|     </form> | ||||
|     <p><a href="<?= $basePath ?>/logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
 | ||||
| <?php endif; ?>
 | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										50
									
								
								tkr/public/login.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								tkr/public/login.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| <?php | ||||
| define('APP_ROOT', realpath(__DIR__ . '/../')); | ||||
| 
 | ||||
| require APP_ROOT . '/config.php'; | ||||
| require APP_ROOT . '/session.php'; | ||||
| 
 | ||||
| $error = ''; | ||||
| 
 | ||||
| if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||
|     if (!validateCsrfToken($_POST['csrf_token'])) { | ||||
|         die('Invalid CSRF token'); | ||||
|     } | ||||
| 
 | ||||
|     $username = $_POST['username'] ?? ''; | ||||
|     $password = $_POST['password'] ?? ''; | ||||
| 
 | ||||
|     $stmt = $pdo->prepare("SELECT id, username, password_hash FROM user WHERE username = ?"); | ||||
|     $stmt->execute([$username]); | ||||
|     $user = $stmt->fetch(); | ||||
| 
 | ||||
|     if ($user && password_verify($password, $user['password_hash'])) { | ||||
|         session_regenerate_id(true); | ||||
|         $_SESSION['user_id'] = $user['id']; | ||||
|         $_SESSION['username'] = $user['username']; | ||||
|         header('Location: ' . $basePath . '/'); | ||||
|         exit; | ||||
|     } else { | ||||
|         $error = 'Invalid username or password'; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| $csrf_token = generateCsrfToken(); | ||||
| ?>
 | ||||
| 
 | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|     <head><title>Login</title></head> | ||||
|     <body> | ||||
|     <h2>Login</h2> | ||||
| <?php if ($error): ?>
 | ||||
|     <p style="color:red"><?=  htmlspecialchars($error) ?></p>
 | ||||
| <?php endif; ?>
 | ||||
|     <form method="post" action="<?= $basePath ?>/login.php"> | ||||
|         <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>"> | ||||
|         <label>Username: <input type="text" name="username" required></label><br> | ||||
|         <label>Password: <input type="password" name="password" required></label><br> | ||||
|         <button type="submit">Login</button> | ||||
|     </form> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										11
									
								
								tkr/public/logout.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tkr/public/logout.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| <?php | ||||
| define('APP_ROOT', realpath(__DIR__ . '/../')); | ||||
| 
 | ||||
| require APP_ROOT . '/config.php'; | ||||
| require APP_ROOT . '/session.php'; | ||||
| 
 | ||||
| $_SESSION = []; | ||||
| session_destroy(); | ||||
| 
 | ||||
| header('Location: ' . $basePath . '/'); | ||||
| exit; | ||||
							
								
								
									
										43
									
								
								tkr/public/rss/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								tkr/public/rss/index.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| <?php | ||||
| define('APP_ROOT', realpath(__DIR__ . '/../../')); | ||||
| define('ITEMS_PER_PAGE', 25); | ||||
| 
 | ||||
| require APP_ROOT . '/config.php'; | ||||
| require_once APP_ROOT . '/stream_ticks.php'; | ||||
| 
 | ||||
| header('Content-Type: application/rss+xml; charset=utf-8'); | ||||
| 
 | ||||
| $ticks = iterator_to_array(stream_ticks($tickLocation, ITEMS_PER_PAGE)); | ||||
| 
 | ||||
| echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; | ||||
| ?>
 | ||||
| <rss version="2.0"> | ||||
| <channel> | ||||
|     <title>My tkr</title> | ||||
|     <link rel="alternate" type="application/rss+xml" title="Tick RSS" href="/tkr/rss/"> | ||||
|     <description>My tkr</description> | ||||
|     <language>en-us</language> | ||||
|     <lastBuildDate><?php echo date(DATE_RSS); ?></lastBuildDate>
 | ||||
| 
 | ||||
| <?php foreach ($ticks as $tick): | ||||
|     [$date, $time] = explode(' ', $tick['timestamp']); | ||||
|     $dateParts = explode('-', $date); | ||||
|     $timeParts = explode(':', $time); | ||||
| 
 | ||||
|     [$year, $month, $day] = $dateParts; | ||||
|     [$hour, $minute, $second] = $timeParts; | ||||
| 
 | ||||
|     $tickPath = "$year/$month/$day/$hour/$minute/$second"; | ||||
| ?>
 | ||||
|      | ||||
|     <item> | ||||
|         <title><?php echo htmlspecialchars(date(DATE_RFC7231, strtotime($tick['timestamp']))); ?></title>
 | ||||
|         <link><?php echo htmlspecialchars("$basePath/tick.php?path=$tickPath"); ?></link>
 | ||||
|         <description><?php echo htmlspecialchars($tick['tick']); ?></description>
 | ||||
|         <pubDate><?php echo date(DATE_RSS, strtotime($tick['timestamp'])); ?></pubDate>
 | ||||
|         <guid><?php echo htmlspecialchars($tickPath); ?></guid>
 | ||||
|     </item> | ||||
|     <?php endforeach; ?>
 | ||||
| 
 | ||||
| </channel> | ||||
| </rss> | ||||
							
								
								
									
										44
									
								
								tkr/public/save_tick.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								tkr/public/save_tick.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| <?php | ||||
| define('APP_ROOT', realpath(__DIR__ . '/../')); | ||||
| 
 | ||||
| require APP_ROOT . '/config.php'; | ||||
| require APP_ROOT . '/session.php'; | ||||
| 
 | ||||
| // ticks must be sent via POST
 | ||||
| if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) { | ||||
|     // csrf check
 | ||||
|     if (!validateCsrfToken($_POST['csrf_token'])) { | ||||
|         die('Invalid CSRF token'); | ||||
|     } | ||||
| 
 | ||||
|     $tick = htmlspecialchars($_POST['tick'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); | ||||
| } else { | ||||
|     // just go back to the index
 | ||||
|     header('Location: index.php'); | ||||
|     exit; | ||||
| } | ||||
| 
 | ||||
| # write the tick to a new entry 
 | ||||
| $date = new DateTime(); | ||||
| 
 | ||||
| $year = $date->format('Y'); | ||||
| $month = $date->format('m'); | ||||
| $day = $date->format('d'); | ||||
| $time = $date->format('H:i:s'); | ||||
| 
 | ||||
| // build the full path to the tick file
 | ||||
| $dir = "$tickLocation/$year/$month"; | ||||
| $filename = "$dir/$day.txt"; | ||||
| 
 | ||||
| // create the directory if it doesn't exist
 | ||||
| if (!is_dir($dir)) { | ||||
|     mkdir($dir, 0770, true); | ||||
| } | ||||
| 
 | ||||
| // write the tick to the file (the file will be created if it doesn't exist)
 | ||||
| $content = $time . "|" . $tick . "\n"; | ||||
| file_put_contents($filename, $content, FILE_APPEND); | ||||
| 
 | ||||
| // go back to the index and show the latest tick
 | ||||
| header('Location: index.php'); | ||||
| exit; | ||||
							
								
								
									
										33
									
								
								tkr/public/tick.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								tkr/public/tick.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| <?php | ||||
| require '/app/config.php'; | ||||
| 
 | ||||
| $path = $_GET['path'] ?? ''; | ||||
| $parts = explode('/', $path); | ||||
| 
 | ||||
| if (count($parts) !== 6) { | ||||
|     http_response_code(400); | ||||
|     echo "Invalid tick path."; | ||||
|     exit; | ||||
| } | ||||
| 
 | ||||
| [$y, $m, $d, $H, $i, $s] = $parts; | ||||
| $timestamp = "$H:$i:$s"; | ||||
| $file = "$tickLocation/$y/$m/$d.txt"; | ||||
| 
 | ||||
| if (!file_exists($file)) { | ||||
|     http_response_code(404); | ||||
|     echo "Tick not found."; | ||||
|     exit; | ||||
| } | ||||
| 
 | ||||
| $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | ||||
| foreach ($lines as $line) { | ||||
|     if (str_starts_with($line, $timestamp)) { | ||||
|         echo "<h1>Tick from $timestamp on $y-$m-$d</h1>"; | ||||
|         echo "<p>" . htmlspecialchars(explode('|', $line)[1]) . "</p>"; | ||||
|         exit; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| http_response_code(404); | ||||
| echo "Tick not found."; | ||||
							
								
								
									
										17
									
								
								tkr/session.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tkr/session.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| <?php | ||||
| if (session_status() === PHP_SESSION_NONE) { | ||||
|     session_start(); | ||||
| } | ||||
| 
 | ||||
| $isLoggedIn = isset($_SESSION['username']); | ||||
| 
 | ||||
| function generateCsrfToken() { | ||||
|     if (empty($_SESSION['csrf_token'])) { | ||||
|         $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); | ||||
|     } | ||||
|     return $_SESSION['csrf_token']; | ||||
| } | ||||
| 
 | ||||
| function validateCsrfToken($token) { | ||||
|     return hash_equals($_SESSION['csrf_token'], $token); | ||||
| } | ||||
							
								
								
									
										47
									
								
								tkr/stream_ticks.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tkr/stream_ticks.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| <?php | ||||
| // display the requested block of ticks
 | ||||
| // without storing all ticks in an array
 | ||||
| function stream_ticks(string $tickLocation, int $limit, int $offset = 0): Generator { | ||||
|     $tick_files = glob($tickLocation . '/*/*/*.txt'); | ||||
|     usort($tick_files, fn($a, $b) => strcmp($b, $a)); // sort filenames in reverse chronological order
 | ||||
| 
 | ||||
|     $count = 0; | ||||
|     foreach ($tick_files as $file) { | ||||
|         // read all the ticks from the current file and reverse the order
 | ||||
|         // so the most recent ones are first
 | ||||
|         //
 | ||||
|         // each file is a single day, so we never hold more than
 | ||||
|         // one day's ticks in memory
 | ||||
|         $lines = array_reverse( | ||||
|             file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) | ||||
|         ); | ||||
| 
 | ||||
|         $pathParts = explode('/', str_replace('\\', '/', $file)); | ||||
|         $date = $pathParts[count($pathParts) - 3] . '-' . | ||||
|                 $pathParts[count($pathParts) - 2] . '-' . | ||||
|                 pathinfo($pathParts[count($pathParts) - 1], PATHINFO_FILENAME); | ||||
| 
 | ||||
|         foreach ($lines as $line) { | ||||
|             // just keep skipping ticks until we get to the starting point
 | ||||
|             if ($offset > 0) { | ||||
|                 $offset--; | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             // Ticks are pipe-delimited: timestamp|text
 | ||||
|             // But just in case a tick contains a pipe, only split on the first one that occurs
 | ||||
|             $tickParts = explode('|', $line, 2); | ||||
|             $time = $tickParts[0]; | ||||
|             $tick = $tickParts[1]; | ||||
| 
 | ||||
|             yield [ | ||||
|                 'timestamp' => $date . ' ' . $time, | ||||
|                 'tick' => $tick, | ||||
|             ]; | ||||
| 
 | ||||
|             if (++$count >= $limit) { | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										0
									
								
								tkr/ticks/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tkr/ticks/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user