+2
-1
src/database/database.v
+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
+1
-1
src/database/like.v
+4
-3
src/database/notification.v
+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
+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
+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
+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
src/entity/site.v
+5
-6
src/entity/user.v
+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
+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
+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
+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
+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
+1
src/webapp/config.v
+5
-1
src/webapp/validation.v
+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) }