a mini social media app for small communities

add invite-only and private data, clean up readme, change from MIT to BSD 3-clause

+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 }