a mini social media app for small communities

add instance about page and some stopwatches

Changed files
+165 -41
src
+6
config.maple
··· 7 7 8 8 default_theme = 'https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css' 9 9 allow_changing_theme = true 10 + 11 + // instance version 12 + version = '2025.01' 13 + 14 + // set this to '' if your instance is closed source (twt) 15 + source = 'https://github.com/emmathemartian/beep' 10 16 } 11 17 12 18 http = {
+76 -38
src/main.v
··· 7 7 import os 8 8 import webapp { App, Context, StringValidator } 9 9 import beep_sql 10 + import util 10 11 11 - fn main() { 12 - config := webapp.load_config_from(os.args[1]) 12 + pub const version = '25.01.0' 13 13 14 - println('-> connecting to db...') 15 - mut db := pg.connect(pg.Config{ 16 - host: config.postgres.host 17 - dbname: config.postgres.db 18 - user: config.postgres.user 19 - password: config.postgres.password 20 - port: config.postgres.port 21 - })! 22 - println('<- connected') 23 - 24 - defer { 25 - db.close() 14 + @[inline] 15 + fn connect(mut app App) { 16 + println('-> connecting to database...') 17 + app.db = pg.connect(pg.Config{ 18 + host: app.config.postgres.host 19 + dbname: app.config.postgres.db 20 + user: app.config.postgres.user 21 + password: app.config.postgres.password 22 + port: app.config.postgres.port 23 + }) or { 24 + panic('failed to connect to database: ${err}') 26 25 } 26 + } 27 27 28 - beep_sql.load(mut db) 29 - 30 - mut app := &App{ 31 - config: config 32 - db: db 33 - auth: auth.new(db) 34 - } 35 - 36 - // vfmt off 37 - app.validators.username = StringValidator.new(config.user.username_min_len, config.user.username_max_len, config.user.username_pattern) 38 - app.validators.password = StringValidator.new(config.user.username_min_len, config.user.username_max_len, config.user.username_pattern) 39 - app.validators.nickname = StringValidator.new(config.user.nickname_min_len, config.user.nickname_max_len, config.user.nickname_pattern) 40 - app.validators.user_bio = StringValidator.new(config.user.bio_min_len, config.user.bio_max_len, config.user.bio_pattern) 41 - app.validators.pronouns = StringValidator.new(config.user.pronouns_min_len, config.user.pronouns_max_len, config.user.pronouns_pattern) 42 - app.validators.post_title = StringValidator.new(config.post.title_min_len, config.post.title_max_len, config.post.title_pattern) 43 - app.validators.post_body = StringValidator.new(config.post.body_min_len, config.post.body_max_len, config.post.body_pattern) 44 - // vfmt on 45 - 46 - app.mount_static_folder_at(app.config.static_path, '/static')! 47 - 48 - println('-> initializing database...') 49 - sql db { 28 + @[inline] 29 + fn init_db(mut app App) { 30 + println('-> initializing database') 31 + sql app.db { 50 32 create table entity.Site 51 33 create table entity.User 52 34 create table entity.Post ··· 54 36 create table entity.LikeCache 55 37 create table entity.Notification 56 38 create table entity.SavedPost 57 - }! 58 - println('<- done') 39 + } or { 40 + panic('failed to initialize database: ${err}') 41 + } 42 + } 43 + 44 + @[inline] 45 + fn load_validators(mut app App) { 46 + app.validators.username = StringValidator.new(app.config.user.username_min_len, app.config.user.username_max_len, app.config.user.username_pattern) 47 + app.validators.password = StringValidator.new(app.config.user.username_min_len, app.config.user.username_max_len, app.config.user.username_pattern) 48 + app.validators.nickname = StringValidator.new(app.config.user.nickname_min_len, app.config.user.nickname_max_len, app.config.user.nickname_pattern) 49 + app.validators.user_bio = StringValidator.new(app.config.user.bio_min_len, app.config.user.bio_max_len, app.config.user.bio_pattern) 50 + app.validators.pronouns = StringValidator.new(app.config.user.pronouns_min_len, app.config.user.pronouns_max_len, app.config.user.pronouns_pattern) 51 + app.validators.post_title = StringValidator.new(app.config.post.title_min_len, app.config.post.title_max_len, app.config.post.title_pattern) 52 + app.validators.post_body = StringValidator.new(app.config.post.body_min_len, app.config.post.body_max_len, app.config.post.body_pattern) 53 + } 54 + 55 + fn main() { 56 + mut stopwatch := util.Stopwatch.new() 57 + 58 + config := webapp.load_config_from(os.args[1]) 59 + mut app := &App{ config: config } 60 + 61 + // connect to database 62 + util.time_it( 63 + it: fn [mut app] () { 64 + connect(mut app) 65 + } 66 + name: 'connect to db' 67 + log: true 68 + ) 69 + 70 + defer { app.db.close() } 71 + 72 + // add authenticator 73 + app.auth = auth.new(app.db) 74 + 75 + // load sql files kept in beep_sql/ 76 + util.time_it( 77 + it: fn [mut app] () { 78 + beep_sql.load(mut app.db) 79 + } 80 + name: 'load beep_sql' 81 + log: true 82 + ) 83 + 84 + // load validators 85 + load_validators(mut app) 86 + 87 + // mount static things 88 + app.mount_static_folder_at(app.config.static_path, '/static')! 89 + 90 + // initialize database 91 + util.time_it(it: fn [mut app] () { 92 + init_db(mut app) 93 + }, name: 'init db', log: true) 59 94 60 95 // make the website config, if it does not exist 61 96 app.get_or_create_site_config() ··· 63 98 if config.dev_mode { 64 99 println('\033[1;31mNOTE: YOU ARE IN DEV MODE\033[0m') 65 100 } 101 + 102 + stop := stopwatch.stop() 103 + println('-> took ${stop} to start app') 66 104 67 105 veb.run[App, Context](mut app, app.config.http.port) 68 106 }
+20
src/templates/about.html
··· 1 + @include 'partial/header.html' 2 + 3 + <h1>about this instance</h1> 4 + 5 + <div> 6 + <p>name: @{app.config.instance.name}</p> 7 + <p>version: @{app.config.instance.version} (commit: @{app.commit})</p> 8 + <p>built at @{app.built_at} (<span id="built_at">date n/a</span>)</p> 9 + <p>built using @{app.v_hash}</p> 10 + 11 + @if app.config.instance.source != '' 12 + <p>source: <a href="@{app.config.instance.source}">@{app.config.instance.source}</a></p> 13 + @end 14 + </div> 15 + 16 + <script> 17 + document.getElementById('built_at').innerText = new Date(@{app.built_at} * 1000).toLocaleString() 18 + </script> 19 + 20 + @include 'partial/footer.html'
+4 -2
src/templates/partial/footer.html
··· 1 1 </main> 2 2 3 3 <footer> 4 - @if ctx.is_logged_in() 5 4 <p> 5 + @if ctx.is_logged_in() 6 6 <a href="/settings">settings</a> 7 7 @if user.admin 8 8 - ··· 10 10 @end 11 11 - 12 12 <a href="/logout">log out</a> 13 + - 14 + @end 15 + <a href="/about">about</a> 13 16 </p> 14 - @end 15 17 16 18 <p>powered by <a href="https://github.com/emmathemartian/beep">beep</a></p> 17 19 </footer>
+44
src/util/stopwatch.v
··· 1 + module util 2 + 3 + import time 4 + 5 + @[noinit] 6 + pub struct Stopwatch { 7 + pub: 8 + start time.Time = time.now() 9 + pub mut: 10 + stop time.Time 11 + took ?time.Duration 12 + } 13 + 14 + @[inline] 15 + pub fn Stopwatch.new() Stopwatch { 16 + return Stopwatch{} 17 + } 18 + 19 + @[inline] 20 + pub fn (mut stopwatch Stopwatch) stop() time.Duration { 21 + stopwatch.stop = time.now() 22 + duration := stopwatch.stop - stopwatch.start 23 + stopwatch.took = duration 24 + return duration 25 + } 26 + 27 + @[params] 28 + pub struct TimeItParams { 29 + pub: 30 + it fn () @[required] 31 + name string 32 + log bool 33 + } 34 + 35 + @[inline] 36 + pub fn time_it(params TimeItParams) Stopwatch { 37 + mut stopwatch := Stopwatch.new() 38 + params.it() 39 + took := stopwatch.stop() 40 + if params.log { 41 + println('-> (time_it) ${params.name} took ${took}') 42 + } 43 + return stopwatch 44 + }
+4 -1
src/webapp/app.v
··· 11 11 veb.StaticHandler 12 12 DatabaseAccess 13 13 pub: 14 - config Config 14 + config Config 15 + commit string = @VMODHASH 16 + built_at string = @BUILD_TIMESTAMP 17 + v_hash string = @VHASH 15 18 pub mut: 16 19 auth auth.Auth[pg.DB] 17 20 validators struct {
+4
src/webapp/config.v
··· 13 13 welcome string 14 14 default_theme string 15 15 allow_changing_theme bool 16 + version string 17 + source string 16 18 } 17 19 http struct { 18 20 pub mut: ··· 72 74 config.instance.welcome = loaded_instance.get('welcome').to_str() 73 75 config.instance.default_theme = loaded_instance.get('default_theme').to_str() 74 76 config.instance.allow_changing_theme = loaded_instance.get('allow_changing_theme').to_bool() 77 + config.instance.version = loaded_instance.get('version').to_str() 78 + config.instance.source = loaded_instance.get('source').to_str() 75 79 76 80 loaded_http := loaded.get('http') 77 81 config.http.port = loaded_http.get('port').to_int()
+7
src/webapp/pages.v
··· 206 206 ctx.title = '${app.config.instance.name} - search' 207 207 return $veb.html('../templates/search.html') 208 208 } 209 + 210 + @['/about'] 211 + fn (mut app App) about(mut ctx Context) veb.Result { 212 + user := app.whoami(mut ctx) or { User{} } 213 + ctx.title = '${app.config.instance.name} - about' 214 + return $veb.html('../templates/about.html') 215 + }