+8
-16
.gitignore
+8
-16
.gitignore
···
1
-
# binaries
2
-
main
3
-
clockwork
4
-
beep
5
-
*.exe
6
-
*.exe~
7
-
*.so
8
-
*.dylib
9
-
*.dll
10
-
bin/
1
+
# Binaries
2
+
/beep
3
+
/build/
11
4
12
-
# editor/system specific metadata
5
+
# Editor/system specific metadata
13
6
.DS_Store
14
-
.idea/
15
7
.vscode/
16
-
*.iml
17
8
18
-
# secrets
9
+
# Secrets
19
10
/config.real.maple
20
11
.env
21
12
22
-
# local v and clockwork install (from gitpod stuffs)
13
+
# Local V and Clockwork install (Gitpod)
14
+
/clockwork
23
15
/v/
24
16
/clockwork/
25
17
26
-
# quick notes i keep while developing
18
+
# Quick notes I keep while developing
27
19
/stickynote.md
+7
config.maple
+7
config.maple
···
13
13
14
14
// set this to '' if your instance is closed source (twt)
15
15
source = 'https://github.com/emmathemartian/beep'
16
+
17
+
// toggle to `true` to require that users have an invite code to register
18
+
invite_only = false
19
+
invite_code = ''
20
+
21
+
// toggle to `true` to allow any non-logged-in user to view data (posts, users, etc)
22
+
public_data = false
16
23
}
17
24
18
25
http = {
+26
license
+26
license
···
1
+
Copyright 2025 Emmeline Coats
2
+
3
+
Redistribution and use in source and binary forms, with or without
4
+
modification, are permitted provided that the following conditions are met:
5
+
6
+
1. Redistributions of source code must retain the above copyright notice, this
7
+
list of conditions and the following disclaimer.
8
+
9
+
2. Redistributions in binary form must reproduce the above copyright notice,
10
+
this list of conditions and the following disclaimer in the documentation
11
+
and/or other materials provided with the distribution.
12
+
13
+
3. Neither the name of the copyright holder nor the names of its contributors
14
+
may be used to endorse or promote products derived from this software
15
+
without specific prior written permission.
16
+
17
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
18
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-7
license.txt
-7
license.txt
···
1
-
Copyright 2024 EmmaTheMartian
2
-
3
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
-
5
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
-
7
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+49
readme
+49
readme
···
1
+
2
+
beep
3
+
====
4
+
5
+
> *a legendary land of lowercase lovers.*
6
+
7
+
A self-hosted "social-media-oriented" mini-blogger.
8
+
9
+
Technically made because I wanted to mess around with RSS,
10
+
but I also wanted a teensy little blog/slow-paced-chat-app
11
+
for myself and my friends.
12
+
13
+
hosting
14
+
-------
15
+
16
+
You'll need a PostgreSQL database somewhere, along with V
17
+
to compile beep:
18
+
19
+
$ git clone https://github.com/emmathemartian/beep
20
+
$ cd beep
21
+
$ v -prod .
22
+
23
+
Copy the `config.maple` as `config.real.maple`
24
+
25
+
$ cp config.maple config.real.maple
26
+
27
+
Edit `config.real.maple` to set the URL, port, DB username,
28
+
password, and name.
29
+
30
+
`config.real.maple` also has settings to configure the
31
+
default theme, post length, username length, welcome
32
+
messages, etc etc.
33
+
34
+
WARNING: DO NOT PUT SECRETS IN `config.maple`.
35
+
`config.maple` is intended to be pushed to Git as a
36
+
"template config." Instead, use `config.real.maple` if you
37
+
plan to push anywhere. It's gitignored by default, i.e, an
38
+
accidental exclusion in the gitignore and push won't expose
39
+
your keys.
40
+
41
+
$ ./beep config.real.maple
42
+
43
+
Then go to the configured port to view (default is
44
+
`http://localhost:8008`).
45
+
46
+
If you don't have a database, you can either self-host a
47
+
psql database on your machine, or you can find a free one
48
+
online. I like [neon.tech](https://neon.tech), their free
49
+
plan is pretty comfortable for a small beep instance!
-39
readme.md
-39
readme.md
···
1
-
# beep
2
-
3
-
> *a legendary land of lowercase lovers.*
4
-
5
-
a self-hosted "social-media-oriented" mini-blogger.
6
-
7
-
technically made because i wanted to mess around with rss, but i also wanted a
8
-
teensy little blog/slow-paced-chat-app for myself and my friends.
9
-
10
-
## hosting
11
-
12
-
you will need a postgresql database somewhere, along with v to compile beep:
13
-
14
-
copy the `config.maple` as `config.real.maple`
15
-
16
-
edit `config.real.maple` to set the url, port, username, password, and database
17
-
name.
18
-
19
-
> `config.real.maple` also has settings to configure the feel of your beep
20
-
> instance, post length, username length, welcome messages, etc etc.
21
-
22
-
> **do not put your secrets in `config.maple`**. it is intended to be pushed to
23
-
> git as a "template config." instead, use `config.real.maple` if you plan to
24
-
> push anywhere. it is gitignored already, meaning you do not have to fear about
25
-
> your secrets not being kept a secret.
26
-
27
-
```sh
28
-
git clone https://github.com/emmathemartian/beep
29
-
cd beep
30
-
v -prod .
31
-
./beep config.real.maple
32
-
```
33
-
34
-
then go to the configured url to view (default is `http://localhost:8008`).
35
-
36
-
if you do not have a database, you can either self-host a postgresql database on
37
-
your machine, or you can find a free one online. i use and like
38
-
[neon.tech](https://neon.tech), their free plan is pretty comfortable for a
39
-
small beep instance!
+3
-2
src/templates/partial/header.html
+3
-2
src/templates/partial/header.html
···
1
1
<!DOCTYPE html>
2
-
<html>
2
+
<html lang="en">
3
3
4
4
<head>
5
5
<meta charset="utf-8" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
<meta name="description" content="" />
8
+
7
9
<link rel="icon" href="/favicon.png" />
8
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
9
10
<title>@ctx.title</title>
10
11
11
12
@include 'assets/style.html'
+5
src/templates/register.html
+5
src/templates/register.html
···
34
34
required
35
35
>
36
36
<br>
37
+
@if app.config.instance.invite_only
38
+
<label for="invite-code">invite code:</label>
39
+
<input type="text" name="invite-code" id="invite-code" required>
40
+
<br>
41
+
@end
37
42
@if app.config.hcaptcha.enabled
38
43
<div class="h-captcha" data-sitekey="@{app.config.hcaptcha.site_key}"></div>
39
44
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
+45
-13
src/webapp/api.v
+45
-13
src/webapp/api.v
···
11
11
// people from requesting searches with huge limits and straining the SQL server
12
12
pub const search_hard_limit = 50
13
13
14
+
////// util ///////
15
+
16
+
const always_public_routes = [
17
+
'/api/user/register/',
18
+
]
19
+
20
+
fn (mut app App) public_data_check(mut ctx Context) ?veb.Result {
21
+
if !app.config.instance.public_data {
22
+
println(ctx.req.url)
23
+
if ctx.req.url in always_public_routes {
24
+
return none
25
+
}
26
+
_ := app.whoami(mut ctx) or {
27
+
ctx.error('not logged in')
28
+
return ctx.redirect('/login')
29
+
}
30
+
}
31
+
return none
32
+
}
33
+
14
34
////// user //////
15
35
16
36
struct HcaptchaResponse {
···
40
60
ctx.error('failed to verify hcaptcha: ${data}')
41
61
return ctx.redirect('/register')
42
62
}
63
+
}
64
+
65
+
if app.config.instance.invite_only && ctx.form['invite-code'] != app.config.instance.invite_code {
66
+
ctx.error('invalid invite code')
67
+
return ctx.redirect('/register')
43
68
}
44
69
45
70
if app.get_user_by_name(username) != none {
···
254
279
// validate
255
280
if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) {
256
281
ctx.error('invalid nickname')
257
-
return ctx.redirect('/me')
282
+
return ctx.redirect('/settings')
258
283
}
259
284
260
285
if !app.set_nickname(user.id, clean_nickname) {
261
286
eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
262
-
return ctx.redirect('/me')
287
+
return ctx.redirect('/settings')
263
288
}
264
289
265
-
return ctx.redirect('/me')
290
+
return ctx.redirect('/settings')
266
291
}
267
292
268
293
@['/api/user/set_muted'; post]
···
301
326
ctx.error('failed to set automated status.')
302
327
}
303
328
304
-
return ctx.redirect('/me')
329
+
return ctx.redirect('/settings')
305
330
}
306
331
307
332
@['/api/user/set_theme'; post]
308
333
fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result {
309
334
if !app.config.instance.allow_changing_theme {
310
335
ctx.error('this instance disallows changing themes :(')
311
-
return ctx.redirect('/me')
336
+
return ctx.redirect('/settings')
312
337
}
313
338
314
339
user := app.whoami(mut ctx) or {
···
323
348
324
349
if !app.set_theme(user.id, theme) {
325
350
ctx.error('failed to change theme')
326
-
return ctx.redirect('/me')
351
+
return ctx.redirect('/settings')
327
352
}
328
353
329
-
return ctx.redirect('/me')
354
+
return ctx.redirect('/settings')
330
355
}
331
356
332
357
@['/api/user/set_pronouns'; post]
···
339
364
clean_pronouns := pronouns.trim_space()
340
365
if !app.validators.pronouns.validate(clean_pronouns) {
341
366
ctx.error('invalid pronouns')
342
-
return ctx.redirect('/me')
367
+
return ctx.redirect('/settings')
343
368
}
344
369
345
370
if !app.set_pronouns(user.id, clean_pronouns) {
346
371
ctx.error('failed to change pronouns')
347
-
return ctx.redirect('/me')
372
+
return ctx.redirect('/settings')
348
373
}
349
374
350
-
return ctx.redirect('/me')
375
+
return ctx.redirect('/settings')
351
376
}
352
377
353
378
@['/api/user/set_bio'; post]
···
360
385
clean_bio := bio.trim_space()
361
386
if !app.validators.user_bio.validate(clean_bio) {
362
387
ctx.error('invalid bio')
363
-
return ctx.redirect('/me')
388
+
return ctx.redirect('/settings')
364
389
}
365
390
366
391
if !app.set_bio(user.id, clean_bio) {
367
392
eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
368
-
return ctx.redirect('/me')
393
+
return ctx.redirect('/settings')
369
394
}
370
395
371
-
return ctx.redirect('/me')
396
+
return ctx.redirect('/settings')
372
397
}
373
398
374
399
@['/api/user/get_name']
···
650
675
651
676
@['/api/post/get_title']
652
677
fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
678
+
if !app.config.instance.public_data {
679
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
680
+
}
653
681
post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
654
682
return ctx.text(post.title)
655
683
}
···
701
729
702
730
@['/api/post/get/<id>'; get]
703
731
fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result {
732
+
if !app.config.instance.public_data {
733
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
734
+
}
704
735
post := app.get_post_by_id(id) or { return ctx.text('no such post') }
705
736
return ctx.json[Post](post)
706
737
}
707
738
708
739
@['/api/post/search'; get]
709
740
fn (mut app App) api_post_search(mut ctx Context, query string, limit int, offset int) veb.Result {
741
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
710
742
if limit >= search_hard_limit {
711
743
return ctx.text('limit exceeds hard limit (${search_hard_limit})')
712
744
}
+6
src/webapp/config.v
+6
src/webapp/config.v
···
15
15
allow_changing_theme bool
16
16
version string
17
17
source string
18
+
invite_only bool
19
+
invite_code string
20
+
public_data bool
18
21
}
19
22
http struct {
20
23
pub mut:
···
82
85
config.instance.allow_changing_theme = loaded_instance.get('allow_changing_theme').to_bool()
83
86
config.instance.version = loaded_instance.get('version').to_str()
84
87
config.instance.source = loaded_instance.get('source').to_str()
88
+
config.instance.invite_only = loaded_instance.get('invite_only').to_bool()
89
+
config.instance.invite_code = loaded_instance.get('invite_code').to_str()
90
+
config.instance.public_data = loaded_instance.get('public_data').to_bool()
85
91
86
92
loaded_http := loaded.get('http')
87
93
config.http.port = loaded_http.get('port').to_int()
+22
-3
src/webapp/pages.v
+22
-3
src/webapp/pages.v
···
4
4
import entity { User }
5
5
6
6
fn (mut app App) index(mut ctx Context) veb.Result {
7
+
if !app.config.instance.public_data {
8
+
_ := app.whoami(mut ctx) or {
9
+
ctx.error('not logged in')
10
+
return ctx.redirect('/login')
11
+
}
12
+
}
13
+
7
14
ctx.title = app.config.instance.name
8
15
user := app.whoami(mut ctx) or { User{} }
9
16
recent_posts := app.get_recent_posts()
···
91
98
92
99
@['/user/:username']
93
100
fn (mut app App) user(mut ctx Context, username string) veb.Result {
101
+
if !app.config.instance.public_data {
102
+
_ := app.whoami(mut ctx) or {
103
+
ctx.error('not logged in')
104
+
return ctx.redirect('/login')
105
+
}
106
+
}
107
+
94
108
user := app.whoami(mut ctx) or { User{} }
95
109
viewing := app.get_user_by_name(username) or {
96
110
ctx.error('user not found')
···
103
117
104
118
@['/post/:post_id']
105
119
fn (mut app App) post(mut ctx Context, post_id int) veb.Result {
120
+
if !app.config.instance.public_data {
121
+
_ := app.whoami(mut ctx) or {
122
+
ctx.error('not logged in')
123
+
return ctx.redirect('/login')
124
+
}
125
+
}
126
+
106
127
post := app.get_post_by_id(post_id) or {
107
128
ctx.error('no such post')
108
129
return ctx.redirect('/')
···
114
135
mut replying_to_user := app.get_unknown_user()
115
136
116
137
if post.replying_to != none {
117
-
replying_to_post = app.get_post_by_id(post.replying_to) or {
118
-
app.get_unknown_post()
119
-
}
138
+
replying_to_post = app.get_post_by_id(post.replying_to) or { app.get_unknown_post() }
120
139
replying_to_user = app.get_user_by_id(replying_to_post.author_id) or {
121
140
app.get_unknown_user()
122
141
}