a mini social media app for small communities
1module main 2 3import veb 4import db.pg 5import auth 6import entity { Site, User, Post, Like, LikeCache } 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: 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 { 102 return none 103 }.trim_space() 104 if token == '' { 105 return none 106 } 107 if user := app.get_user_by_token(ctx, token) { 108 if user.username == '' || user.id == 0 { 109 eprintln('a user had a token for the blank user') 110 // Clear token 111 ctx.set_cookie( 112 name: 'token' 113 value: '' 114 same_site: .same_site_none_mode 115 secure: true 116 path: '/' 117 ) 118 return none 119 } 120 return user 121 } else { 122 eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)') 123 // Clear token 124 ctx.set_cookie( 125 name: 'token' 126 value: '' 127 same_site: .same_site_none_mode 128 secure: true 129 path: '/' 130 ) 131 return none 132 } 133} 134 135pub fn (app &App) get_unknown_user() User { 136 return User{ username: 'unknown' } 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 entity.Site 227 } or { [] } 228 if configs.len == 0 { 229 // make the site config 230 site_config := entity.Site{ } 231 sql app.db { 232 insert site_config into entity.Site 233 } or { 234 panic('failed to create site config (${err})') 235 } 236 } else if configs.len > 1 { 237 // this should never happen 238 panic('there are multiple site configs') 239 } 240 return configs[0] 241} 242 243pub fn (app &App) get_motd() string { 244 site := app.get_or_create_site_config() 245 return site.motd 246}