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