a mini social media app for small communities

document almost all api functions and minor refactors

Changed files
+78 -66
src
+2 -1
src/database/database.v
··· 1 + // **all** interactions with the database should be handled in this module. 1 2 module database 2 3 3 4 import db.pg 4 5 5 - // all interactions with the database should be handled through this struct. 6 + // DatabaseAccess handles all interactions with the database. 6 7 pub struct DatabaseAccess { 7 8 pub mut: 8 9 db pg.DB
+1 -1
src/database/like.v
··· 2 2 3 3 import entity { Like, LikeCache } 4 4 5 - // returns the net likes of the given post 5 + // get_net_likes_for_post returns the net likes of the given post. 6 6 pub fn (app &DatabaseAccess) get_net_likes_for_post(post_id int) int { 7 7 // check cache 8 8 cache := sql app.db {
+4 -3
src/database/notification.v
··· 2 2 3 3 import entity { Notification } 4 4 5 - // get a list of notifications for the given user 5 + // get_notifications_for gets a list of notifications for the given user. 6 6 pub fn (app &DatabaseAccess) get_notifications_for(user_id int) []Notification { 7 7 notifications := sql app.db { 8 8 select from Notification where user_id == user_id ··· 10 10 return notifications 11 11 } 12 12 13 - // get the amount of notifications a user has, with a given limit 13 + // get_notification_count gets the amount of notifications a user has, with a 14 + // given limit. 14 15 pub fn (app &DatabaseAccess) get_notification_count(user_id int, limit int) int { 15 16 notifications := sql app.db { 16 17 select from Notification where user_id == user_id limit limit ··· 18 19 return notifications.len 19 20 } 20 21 21 - // send a notification to the given user 22 + // send_notification_to sends a notification to the given user. 22 23 pub fn (app &DatabaseAccess) send_notification_to(user_id int, summary string, body string) { 23 24 notification := Notification{ 24 25 user_id: user_id
+11 -8
src/database/post.v
··· 3 3 import time 4 4 import entity { Post, Like, LikeCache } 5 5 6 - // get a post by its id, returns none if it does not exist 6 + // get_post_by_id gets a post by its id, returns none if it does not exist. 7 7 pub fn (app &DatabaseAccess) get_post_by_id(id int) ?Post { 8 8 posts := sql app.db { 9 9 select from Post where id == id limit 1 ··· 14 14 return posts[0] 15 15 } 16 16 17 - // get a post by its author and timestamp, returns none if it does not exist 17 + // get_post_by_author_and_timestamp gets a post by its author and timestamp, 18 + // returns none if it does not exist 18 19 pub fn (app &DatabaseAccess) get_post_by_author_and_timestamp(author_id int, timestamp time.Time) ?Post { 19 20 posts := sql app.db { 20 21 select from Post where author_id == author_id && posted_at == timestamp order by posted_at desc limit 1 ··· 25 26 return posts[0] 26 27 } 27 28 28 - // get a list of posts given a tag. this performs sql string operations and 29 - // probably is not very efficient, use sparingly. 29 + // get_posts_with_tag gets a list of the 10 most recent posts with the given tag. 30 + // this performs sql string operations and probably is not very efficient, use 31 + // sparingly. 30 32 pub fn (app &DatabaseAccess) get_posts_with_tag(tag string, offset int) []Post { 31 33 posts := sql app.db { 32 34 select from Post where body like '%#(${tag})%' order by posted_at desc limit 10 offset offset ··· 34 36 return posts 35 37 } 36 38 37 - // returns a list of all pinned posts 39 + // get_pinned_posts returns a list of all pinned posts. 38 40 pub fn (app &DatabaseAccess) get_pinned_posts() []Post { 39 41 posts := sql app.db { 40 42 select from Post where pinned == true ··· 42 44 return posts 43 45 } 44 46 45 - // returns a list of the ten most recent posts. 47 + // get_recent_posts returns a list of the ten most recent posts. 46 48 pub fn (app &DatabaseAccess) get_recent_posts() []Post { 47 49 posts := sql app.db { 48 50 select from Post order by posted_at desc limit 10 ··· 50 52 return posts 51 53 } 52 54 53 - // returns a list of the ten most liked posts. 55 + // get_popular_posts returns a list of the ten most liked posts. 54 56 // TODO: make this time-gated (i.e, top ten liked posts of the day) 55 57 pub fn (app &DatabaseAccess) get_popular_posts() []Post { 56 58 cached_likes := sql app.db { ··· 65 67 return posts 66 68 } 67 69 68 - // returns a list of all posts from a user in descending order of date 70 + // get_posts_from_user returns a list of all posts from a user in descending 71 + // order by posting date. 69 72 pub fn (app &DatabaseAccess) get_posts_from_user(user_id int) []Post { 70 73 posts := sql app.db { 71 74 select from Post where author_id == user_id order by posted_at desc
+23 -20
src/database/user.v
··· 2 2 3 3 import entity { User, Notification, Like, Post } 4 4 5 - // creates a new user and returns their struct after creation. 5 + // new_user creates a new user and returns their struct after creation. 6 6 pub fn (app &DatabaseAccess) new_user(user User) ?User { 7 7 sql app.db { 8 8 insert user into User ··· 16 16 return app.get_user_by_name(user.username) 17 17 } 18 18 19 - // updates the given user's username, returns true if this succeeded and false 20 - // otherwise. 19 + // set_username sets the given user's username, returns true if this succeeded 20 + // and false otherwise. 21 21 pub fn (app &DatabaseAccess) set_username(user_id int, new_username string) bool { 22 22 sql app.db { 23 23 update User set username = new_username where id == user_id ··· 28 28 return true 29 29 } 30 30 31 - // updates the given user's password, returns true if this succeeded and false 32 - // otherwise. 31 + // set_password sets the given user's password, returns true if this succeeded 32 + // and false otherwise. 33 33 pub fn (app &DatabaseAccess) set_password(user_id int, hashed_new_password string) bool { 34 34 sql app.db { 35 35 update User set password = hashed_new_password where id == user_id ··· 40 40 return true 41 41 } 42 42 43 - // updates the given user's nickname, returns true if this succeeded and false 44 - // otherwise. 43 + // set_nickname sets the given user's nickname, returns true if this succeeded 44 + // and false otherwise. 45 45 pub fn (app &DatabaseAccess) set_nickname(user_id int, new_nickname ?string) bool { 46 46 sql app.db { 47 47 update User set nickname = new_nickname where id == user_id ··· 52 52 return true 53 53 } 54 54 55 - // updates the given user's muted status, returns true if this succeeded and 56 - // false otherwise. 55 + // set_muted sets the given user's muted status, returns true if this succeeded 56 + // and false otherwise. 57 57 pub fn (app &DatabaseAccess) set_muted(user_id int, muted bool) bool { 58 58 sql app.db { 59 59 update User set muted = muted where id == user_id ··· 64 64 return true 65 65 } 66 66 67 - // updates the given user's theme url, returns true if this succeeded and false 68 - // otherwise. 67 + // set_theme sets the given user's theme url, returns true if this succeeded and 68 + // false otherwise. 69 69 pub fn (app &DatabaseAccess) set_theme(user_id int, theme ?string) bool { 70 70 sql app.db { 71 71 update User set theme = theme where id == user_id ··· 76 76 return true 77 77 } 78 78 79 - // updates the given user's pronouns, returns true if this succeeded and false 80 - // otherwise. 79 + // set_pronouns sets the given user's pronouns, returns true if this succeeded 80 + // and false otherwise. 81 81 pub fn (app &DatabaseAccess) set_pronouns(user_id int, pronouns string) bool { 82 82 sql app.db { 83 83 update User set pronouns = pronouns where id == user_id ··· 88 88 return true 89 89 } 90 90 91 - // updates the given user's bio, returns true if this succeeded and false 91 + // set_bio sets the given user's bio, returns true if this succeeded and false 92 92 // otherwise. 93 93 pub fn (app &DatabaseAccess) set_bio(user_id int, bio string) bool { 94 94 sql app.db { ··· 100 100 return true 101 101 } 102 102 103 - // get a user by their username, returns none if the user was not found. 103 + // get_user_by_name gets a user by their username, returns none if the user was 104 + // not found. 104 105 pub fn (app &DatabaseAccess) get_user_by_name(username string) ?User { 105 106 users := sql app.db { 106 107 select from User where username == username ··· 111 112 return users[0] 112 113 } 113 114 114 - // get a user by their id, returns none if the user was not found. 115 + // get_user_by_id gets a user by their id, returns none if the user was not 116 + // found. 115 117 pub fn (app &DatabaseAccess) get_user_by_id(id int) ?User { 116 118 users := sql app.db { 117 119 select from User where id == id ··· 122 124 return users[0] 123 125 } 124 126 125 - // returns all users 127 + // get_users returns all users. 126 128 pub fn (app &DatabaseAccess) get_users() []User { 127 129 users := sql app.db { 128 130 select from User ··· 130 132 return users 131 133 } 132 134 133 - // returns true if a user likes the given post 135 + // does_user_like_post returns true if a user likes the given post. 134 136 pub fn (app &DatabaseAccess) does_user_like_post(user_id int, post_id int) bool { 135 137 likes := sql app.db { 136 138 select from Like where user_id == user_id && post_id == post_id ··· 144 146 return likes.first().is_like 145 147 } 146 148 147 - // returns true if a user dislikes the given post 149 + // does_user_dislike_post returns true if a user dislikes the given post. 148 150 pub fn (app &DatabaseAccess) does_user_dislike_post(user_id int, post_id int) bool { 149 151 likes := sql app.db { 150 152 select from Like where user_id == user_id && post_id == post_id ··· 158 160 return !likes.first().is_like 159 161 } 160 162 161 - // returns true if a user likes or dislikes the given post 163 + // does_user_like_or_dislike_post returns true if a user likes *or* dislikes the 164 + // given post. 162 165 pub fn (app &DatabaseAccess) does_user_like_or_dislike_post(user_id int, post_id int) bool { 163 166 likes := sql app.db { 164 167 select from Like where user_id == user_id && post_id == post_id
+2 -2
src/entity/likes.v
··· 1 1 module entity 2 2 3 - // stores like information for posts 3 + // Like stores like information for a post. 4 4 pub struct Like { 5 5 pub mut: 6 6 id int @[primary; sql: serial] ··· 9 9 is_like bool 10 10 } 11 11 12 - // Stores total likes per post 12 + // LikeCache stores the total likes for a post. 13 13 pub struct LikeCache { 14 14 pub mut: 15 15 id int @[primary; sql: serial]
+1
src/entity/site.v
··· 1 1 module entity 2 2 3 + // Site stores mutable site-wide config and data. 3 4 pub struct Site { 4 5 pub mut: 5 6 id int @[primary; sql: serial]
+5 -6
src/entity/user.v
··· 14 14 muted bool 15 15 admin bool 16 16 17 - theme ?string 17 + theme string 18 18 19 19 bio string 20 20 pronouns string ··· 22 22 created_at time.Time = time.now() 23 23 } 24 24 25 + // get_name returns the user's nickname if it is not none, if so then their 26 + // username is returned. 25 27 @[inline] 26 28 pub fn (user User) get_name() string { 27 29 return user.nickname or { user.username } 28 30 } 29 31 30 - @[inline] 31 - pub fn (user User) get_theme() string { 32 - return user.theme or { '' } 33 - } 34 - 32 + // to_str_without_sensitive_data returns the stringified data for the user with 33 + // their password and salt censored. 35 34 @[inline] 36 35 pub fn (user User) to_str_without_sensitive_data() string { 37 36 return user.str()
+8 -12
src/main.v
··· 7 7 import os 8 8 import webapp { App, Context, StringValidator } 9 9 10 - fn init_db(db pg.DB) ! { 11 - sql db { 12 - create table entity.Site 13 - create table entity.User 14 - create table entity.Post 15 - create table entity.Like 16 - create table entity.LikeCache 17 - create table entity.Notification 18 - }! 19 - } 20 - 21 10 fn main() { 22 11 config := webapp.load_config_from(os.args[1]) 23 12 ··· 54 43 app.mount_static_folder_at(app.config.static_path, '/static')! 55 44 56 45 println('-> initializing database...') 57 - init_db(db)! 46 + sql db { 47 + create table entity.Site 48 + create table entity.User 49 + create table entity.Post 50 + create table entity.Like 51 + create table entity.LikeCache 52 + create table entity.Notification 53 + }! 58 54 println('<- done') 59 55 60 56 // make the website config, if it does not exist
+2 -2
src/templates/partial/header.html
··· 10 10 11 11 @include 'assets/style.html' 12 12 13 - @if ctx.is_logged_in() && user.theme != none 14 - <link rel="stylesheet" href="@user.get_theme()"> 13 + @if ctx.is_logged_in() && user.theme != '' 14 + <link rel="stylesheet" href="@user.theme"> 15 15 @else if app.config.instance.default_theme != '' 16 16 <link rel="stylesheet" href="@app.config.instance.default_theme"> 17 17 @endif
+1 -1
src/templates/settings.html
··· 62 62 63 63 <form action="/api/user/set_theme" method="post"> 64 64 <label for="url">theme:</label> 65 - <input type="url" name="url" id="url" value="@{user.theme or { '' }}"> 65 + <input type="url" name="url" id="url" value="@user.theme"> 66 66 <input type="submit" value="save"> 67 67 </form> 68 68 @end
+12 -9
src/webapp/app.v
··· 26 26 } 27 27 } 28 28 29 - // get a user by their token, returns none if the user was not found. 29 + // get_user_by_token returns a user by their token, returns none if the user was 30 + // not found. 30 31 pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User { 31 32 user_token := app.auth.find_token(token, ctx.ip()) or { 32 33 eprintln('no such user corresponding to token') ··· 35 36 return app.get_user_by_id(user_token.user_id) 36 37 } 37 38 38 - // returns the current logged in user, or none if the user is not logged in. 39 + // whoami returns the current logged in user, or none if the user is not logged 40 + // in. 39 41 pub fn (app &App) whoami(mut ctx Context) ?User { 40 42 token := ctx.get_cookie('token') or { return none }.trim_space() 41 43 if token == '' { ··· 69 71 } 70 72 } 71 73 72 - // get a user representing an unknown user 74 + // get_unknown_user returns a user representing an unknown user 73 75 pub fn (app &App) get_unknown_user() User { 74 76 return User{ 75 77 username: 'unknown' 76 78 } 77 79 } 78 80 79 - // get a post representing an unknown post 81 + // get_unknown_post returns a post representing an unknown post 80 82 pub fn (app &App) get_unknown_post() Post { 81 83 return Post{ 82 84 title: 'unknown' 83 85 } 84 86 } 85 87 86 - // returns true if the user is logged in as the provided user id. 88 + // logged_in_as returns true if the user is logged in as the provided user id. 87 89 pub fn (app &App) logged_in_as(mut ctx Context, id int) bool { 88 90 if !ctx.is_logged_in() { 89 91 return false ··· 91 93 return app.whoami(mut ctx) or { return false }.id == id 92 94 } 93 95 94 - // get the site's message of the day. 96 + // get_motd returns the site's message of the day. 95 97 @[inline] 96 98 pub fn (app &App) get_motd() string { 97 99 site := app.get_or_create_site_config() 98 100 return site.motd 99 101 } 100 102 101 - // get the notification count for a given user, formatted for usage on the 102 - // frontend. 103 + // get_notification_count_for_frontend returns the notification count for a 104 + // given user, formatted for usage on the frontend. 103 105 pub fn (app &App) get_notification_count_for_frontend(user_id int, limit int) string { 104 106 count := app.get_notification_count(user_id, limit) 105 107 if count == 0 { ··· 111 113 } 112 114 } 113 115 114 - // processes a post's body to send notifications for mentions or replies. 116 + // process_post_mentions parses a post's body to send notifications for mentions 117 + // or replies. 115 118 pub fn (app &App) process_post_mentions(post &Post) { 116 119 author := app.get_user_by_id(post.author_id) or { 117 120 eprintln('process_post_mentioned called on a post with a non-existent author: ${post}')
+1
src/webapp/config.v
··· 2 2 3 3 import emmathemartian.maple 4 4 5 + // Config stores constant site-wide configuration data. 5 6 pub struct Config { 6 7 pub mut: 7 8 dev_mode bool
+5 -1
src/webapp/validation.v
··· 2 2 3 3 import regex 4 4 5 - // handles validation of user-input fields 5 + // StringValidator handles validation of user-input fields. 6 6 pub struct StringValidator { 7 7 pub: 8 8 min_len int ··· 10 10 pattern regex.RE 11 11 } 12 12 13 + // validate validates a given string and returns true if it succeeded and false 14 + // otherwise. 13 15 @[inline] 14 16 pub fn (validator StringValidator) validate(str string) bool { 15 17 return str.len > validator.min_len && str.len < validator.max_len 16 18 && validator.pattern.matches_string(str) 17 19 } 18 20 21 + // StringValidator.new creates a new StringValidator with the given min, max, 22 + // and pattern. 19 23 pub fn StringValidator.new(min int, max int, pattern string) StringValidator { 20 24 mut re := regex.new() 21 25 re.compile_opt(pattern) or { panic(err) }