a mini social media app for small communities
1module main 2 3import veb 4import db.pg 5import auth 6import entity { LikeCache, Like, Post, Site, User } 7 8pub struct App { 9 veb.StaticHandler 10pub: 11 config Config 12pub mut: 13 db pg.DB 14 auth auth.Auth[pg.DB] 15 validators struct { 16 pub mut: 17 username StringValidator 18 password StringValidator 19 nickname StringValidator 20 pronouns StringValidator 21 user_bio StringValidator 22 post_title StringValidator 23 post_body StringValidator 24 } 25} 26 27pub fn (app &App) get_user_by_name(username string) ?User { 28 users := sql app.db { 29 select from User where username == username 30 } or { [] } 31 if users.len != 1 { 32 return none 33 } 34 return users[0] 35} 36 37pub fn (app &App) get_user_by_id(id int) ?User { 38 users := sql app.db { 39 select from User where id == id 40 } or { [] } 41 if users.len != 1 { 42 return none 43 } 44 return users[0] 45} 46 47pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User { 48 user_token := app.auth.find_token(token, ctx.ip()) or { 49 eprintln('no such user corresponding to token') 50 return none 51 } 52 return app.get_user_by_id(user_token.user_id) 53} 54 55pub fn (app &App) get_recent_posts() []Post { 56 posts := sql app.db { 57 select from Post order by posted_at desc limit 10 58 } or { [] } 59 return posts 60} 61 62// pub fn (app &App) get_popular_posts() []Post { 63// posts := sql app.db { 64// select from Post order by likes desc limit 10 65// } or { [] } 66// return posts 67// } 68 69pub fn (app &App) get_posts_from_user(user_id int) []Post { 70 posts := sql app.db { 71 select from Post where author_id == user_id order by posted_at desc 72 } or { [] } 73 return posts 74} 75 76pub fn (app &App) get_users() []User { 77 users := sql app.db { 78 select from User 79 } or { [] } 80 return users 81} 82 83pub fn (app &App) get_post_by_id(id int) ?Post { 84 posts := sql app.db { 85 select from Post where id == id limit 1 86 } or { [] } 87 if posts.len != 1 { 88 return none 89 } 90 return posts[0] 91} 92 93pub fn (app &App) get_pinned_posts() []Post { 94 posts := sql app.db { 95 select from Post where pinned == true 96 } or { [] } 97 return posts 98} 99 100pub fn (app &App) whoami(mut ctx Context) ?User { 101 token := ctx.get_cookie('token') or { return none }.trim_space() 102 if token == '' { 103 return none 104 } 105 if user := app.get_user_by_token(ctx, token) { 106 if user.username == '' || user.id == 0 { 107 eprintln('a user had a token for the blank user') 108 // Clear token 109 ctx.set_cookie( 110 name: 'token' 111 value: '' 112 same_site: .same_site_none_mode 113 secure: true 114 path: '/' 115 ) 116 return none 117 } 118 return user 119 } else { 120 eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)') 121 // Clear token 122 ctx.set_cookie( 123 name: 'token' 124 value: '' 125 same_site: .same_site_none_mode 126 secure: true 127 path: '/' 128 ) 129 return none 130 } 131} 132 133pub fn (app &App) get_unknown_user() User { 134 return User{ 135 username: 'unknown' 136 } 137} 138 139pub fn (app &App) logged_in_as(mut ctx Context, id int) bool { 140 if !ctx.is_logged_in() { 141 return false 142 } 143 return app.whoami(mut ctx) or { return false }.id == id 144} 145 146pub fn (app &App) does_user_like_post(user_id int, post_id int) bool { 147 likes := sql app.db { 148 select from Like where user_id == user_id && post_id == post_id 149 } or { [] } 150 if likes.len > 1 { 151 // something is very wrong lol 152 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 153 } else if likes.len == 0 { 154 return false 155 } 156 return likes.first().is_like 157} 158 159pub fn (app &App) does_user_dislike_post(user_id int, post_id int) bool { 160 likes := sql app.db { 161 select from Like where user_id == user_id && post_id == post_id 162 } or { [] } 163 if likes.len > 1 { 164 // something is very wrong lol 165 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 166 } else if likes.len == 0 { 167 return false 168 } 169 return !likes.first().is_like 170} 171 172pub fn (app &App) does_user_like_or_dislike_post(user_id int, post_id int) bool { 173 likes := sql app.db { 174 select from Like where user_id == user_id && post_id == post_id 175 } or { [] } 176 if likes.len > 1 { 177 // something is very wrong lol 178 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 179 } 180 return likes.len == 1 181} 182 183pub fn (app &App) get_net_likes_for_post(post_id int) int { 184 // check cache 185 cache := sql app.db { 186 select from LikeCache where post_id == post_id limit 1 187 } or { [] } 188 189 mut likes := 0 190 191 if cache.len != 1 { 192 println('calculating net likes for post: ${post_id}') 193 // calculate 194 db_likes := sql app.db { 195 select from Like where post_id == post_id 196 } or { [] } 197 198 for like in db_likes { 199 if like.is_like { 200 likes++ 201 } else { 202 likes-- 203 } 204 } 205 206 // cache 207 cached := LikeCache{ 208 post_id: post_id 209 likes: likes 210 } 211 sql app.db { 212 insert cached into LikeCache 213 } or { 214 eprintln('failed to cache like: ${cached}') 215 return likes 216 } 217 } else { 218 likes = cache.first().likes 219 } 220 221 return likes 222} 223 224pub fn (app &App) get_or_create_site_config() Site { 225 configs := sql app.db { 226 select from Site 227 } or { [] } 228 if configs.len == 0 { 229 // make the site config 230 site_config := Site{} 231 sql app.db { 232 insert site_config into Site 233 } or { panic('failed to create site config (${err})') } 234 } else if configs.len > 1 { 235 // this should never happen 236 panic('there are multiple site configs') 237 } 238 return configs[0] 239} 240 241pub fn (app &App) get_motd() string { 242 site := app.get_or_create_site_config() 243 return site.motd 244}