a mini social media app for small communities
1module webapp 2 3import veb 4import db.pg 5import regex 6import auth 7import entity { LikeCache, Like, Post, Site, User, Notification } 8import database { DatabaseAccess } 9 10pub struct App { 11 veb.StaticHandler 12 DatabaseAccess 13pub: 14 config Config 15pub mut: 16 auth auth.Auth[pg.DB] 17 validators struct { 18 pub mut: 19 username StringValidator 20 password StringValidator 21 nickname StringValidator 22 pronouns StringValidator 23 user_bio StringValidator 24 post_title StringValidator 25 post_body StringValidator 26 } 27} 28 29// get_user_by_token returns a user by their token, returns none if the user was 30// not found. 31pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User { 32 user_token := app.auth.find_token(token, ctx.ip()) or { 33 eprintln('no such user corresponding to token') 34 return none 35 } 36 return app.get_user_by_id(user_token.user_id) 37} 38 39// whoami returns the current logged in user, or none if the user is not logged 40// in. 41pub fn (app &App) whoami(mut ctx Context) ?User { 42 token := ctx.get_cookie('token') or { return none }.trim_space() 43 if token == '' { 44 return none 45 } 46 if user := app.get_user_by_token(ctx, token) { 47 if user.username == '' || user.id == 0 { 48 eprintln('a user had a token for the blank user') 49 // Clear token 50 ctx.set_cookie( 51 name: 'token' 52 value: '' 53 same_site: .same_site_none_mode 54 secure: true 55 path: '/' 56 ) 57 return none 58 } 59 return user 60 } else { 61 eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)') 62 // Clear token 63 ctx.set_cookie( 64 name: 'token' 65 value: '' 66 same_site: .same_site_none_mode 67 secure: true 68 path: '/' 69 ) 70 return none 71 } 72} 73 74// logged_in_as returns true if the user is logged in as the provided user id. 75pub fn (app &App) logged_in_as(mut ctx Context, id int) bool { 76 if !ctx.is_logged_in() { 77 return false 78 } 79 return app.whoami(mut ctx) or { return false }.id == id 80} 81 82// get_motd returns the site's message of the day. 83@[inline] 84pub fn (app &App) get_motd() string { 85 site := app.get_or_create_site_config() 86 return site.motd 87} 88 89// get_notification_count_for_frontend returns the notification count for a 90// given user, formatted for usage on the frontend. 91pub fn (app &App) get_notification_count_for_frontend(user_id int, limit int) string { 92 count := app.get_notification_count(user_id, limit) 93 if count == 0 { 94 return '' 95 } else if count > limit { 96 return ' (${count}+)' 97 } else { 98 return ' (${count})' 99 } 100} 101 102// process_post_mentions parses a post's body to send notifications for mentions 103// or replies. 104pub fn (app &App) process_post_mentions(post &Post) { 105 author := app.get_user_by_id(post.author_id) or { 106 eprintln('process_post_mentioned called on a post with a non-existent author: ${post}') 107 return 108 } 109 author_name := author.get_name() 110 111 // used so we do not send more than one notification per post 112 mut notified_users := []int{} 113 114 // notify who we replied to, if applicable 115 if post.replying_to != none { 116 if x := app.get_post_by_id(post.replying_to) { 117 app.send_notification_to(x.author_id, '${author_name} replied to your post!', '${author_name} replied to *(${x.id})') 118 } 119 } 120 121 // find mentions 122 mut re := regex.regex_opt('@\\(${app.config.user.username_pattern}\\)') or { 123 eprintln('failed to compile regex for process_post_mentions (err: ${err})') 124 return 125 } 126 matches := re.find_all_str(post.body) 127 for mat in matches { 128 println('found mentioned user: ${mat}') 129 username := mat#[2..-1] 130 user := app.get_user_by_name(username) or { 131 continue 132 } 133 134 if user.id in notified_users || user.id == author.id { 135 continue 136 } 137 notified_users << user.id 138 139 app.send_notification_to( 140 user.id, 141 '${author_name} mentioned you!', 142 'you have been mentioned in this post: *(${post.id})' 143 ) 144 } 145}