├── .cargo └── config.toml ├── .config ├── .well-known │ └── .gitkeep ├── config.example.toml ├── databeam │ └── config.example.toml ├── header.html ├── layouts │ └── default.json ├── plugins │ └── .gitkeep └── static ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── assets │ └── rainbeam-logo-full-primary-placard.svg └── workflows │ └── docs.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── biome.json ├── builder.json ├── crates ├── authbeam │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── reva.toml │ └── src │ │ ├── api │ │ ├── general.rs │ │ ├── ipbans.rs │ │ ├── ipblocks.rs │ │ ├── items.rs │ │ ├── labels.rs │ │ ├── me.rs │ │ ├── mod.rs │ │ ├── notifications.rs │ │ ├── profile.rs │ │ ├── relationships.rs │ │ └── warnings.rs │ │ ├── avif.rs │ │ ├── database.rs │ │ ├── layout.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── model.rs │ │ └── permissions.rs ├── builder │ ├── index.js │ ├── lib.js │ └── package.json ├── carp │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ ├── carp1.rs │ │ ├── carp2.rs │ │ ├── lib.rs │ │ └── model.rs ├── databeam │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── cache │ │ ├── mod.rs │ │ ├── moka.rs │ │ ├── oysters.rs │ │ └── redis.rs │ │ ├── config.rs │ │ ├── database.rs │ │ ├── lib.rs │ │ ├── prelude.rs │ │ ├── sql.rs │ │ └── utility.rs ├── langbeam │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ └── lib.rs ├── rainbeam-core │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── config.rs │ │ ├── database.rs │ │ ├── lib.rs │ │ └── model.rs ├── rainbeam │ ├── Cargo.toml │ ├── LICENSE │ ├── reva.toml │ ├── src │ │ └── main.rs │ ├── static │ │ ├── css │ │ │ ├── blocks.css │ │ │ ├── components.css │ │ │ ├── interactable.css │ │ │ ├── root.css │ │ │ └── style.css │ │ ├── favicon.example.svg │ │ ├── images │ │ │ ├── default-avatar.svg │ │ │ ├── default-banner.svg │ │ │ └── ui │ │ │ │ └── logo-example.svg │ │ ├── js │ │ │ ├── account_warnings.js │ │ │ ├── app.js │ │ │ ├── carp.js │ │ │ ├── classes │ │ │ │ ├── LayoutEditor.js │ │ │ │ └── PartialComponent.js │ │ │ ├── codemirror.js │ │ │ ├── comments.js │ │ │ ├── dialogs.js │ │ │ ├── items.js │ │ │ ├── loader.js │ │ │ ├── me.js │ │ │ ├── notifications.js │ │ │ ├── questions.js │ │ │ ├── reactions.js │ │ │ ├── reports.js │ │ │ ├── responses.js │ │ │ ├── search.js │ │ │ ├── tokens.js │ │ │ └── warnings.js │ │ ├── manifest.example.json │ │ └── site │ │ │ └── README.md │ └── templates │ │ ├── audit.html │ │ ├── auth │ │ ├── login.html │ │ └── sign_up.html │ │ ├── base.html │ │ ├── components │ │ ├── big_friend.html │ │ ├── comment.html │ │ ├── footer.html │ │ ├── global_question.html │ │ ├── more_response_options.html │ │ ├── notification.html │ │ ├── private_question.html │ │ ├── private_response.html │ │ ├── profile_card.html │ │ ├── response.html │ │ ├── response_inner.html │ │ ├── response_title.html │ │ ├── theming.html │ │ ├── user_note.html │ │ └── warning.html │ │ ├── error.html │ │ ├── fun │ │ ├── carp.html │ │ └── styled_profile_card.html │ │ ├── general_markdown_text.html │ │ ├── homepage.html │ │ ├── inbox.html │ │ ├── intents │ │ └── report.html │ │ ├── ipbans.html │ │ ├── market │ │ ├── base.html │ │ ├── components │ │ │ ├── form.html │ │ │ ├── listing.html │ │ │ └── price.html │ │ ├── homepage.html │ │ ├── item.html │ │ └── new.html │ │ ├── notifications.html │ │ ├── partials │ │ ├── README.md │ │ ├── components │ │ │ ├── compose.html │ │ │ ├── layout_playground.html │ │ │ └── theme_playground.html │ │ ├── profile │ │ │ └── feed.html │ │ ├── timelines │ │ │ ├── discover │ │ │ │ ├── questions_most.html │ │ │ │ ├── responses_most.html │ │ │ │ └── responses_top.html │ │ │ ├── global_questions.html │ │ │ └── timeline.html │ │ └── views │ │ │ ├── comments.html │ │ │ └── reactions.html │ │ ├── profile │ │ ├── base.html │ │ ├── embed.html │ │ ├── inbox.html │ │ ├── layout_components │ │ │ ├── ComponentName::About.html │ │ │ ├── ComponentName::Actions.html │ │ │ ├── ComponentName::Ask.html │ │ │ ├── ComponentName::Banner.html │ │ │ ├── ComponentName::Feed.html │ │ │ ├── ComponentName::Footer.html │ │ │ ├── ComponentName::Name.html │ │ │ ├── ComponentName::Tabs.html │ │ │ ├── README.md │ │ │ ├── free_renderer.html │ │ │ └── renderer.html │ │ ├── layout_editor.html │ │ ├── mod.html │ │ ├── outbox.html │ │ ├── profile.html │ │ ├── questions.html │ │ ├── social │ │ │ ├── blocks.html │ │ │ ├── followers.html │ │ │ ├── following.html │ │ │ ├── friend_request.html │ │ │ ├── friends.html │ │ │ ├── requests.html │ │ │ └── social_base.html │ │ └── warning.html │ │ ├── raw_base.html │ │ ├── reports.html │ │ ├── search │ │ ├── base.html │ │ ├── components │ │ │ └── search.html │ │ ├── homepage.html │ │ ├── questions.html │ │ ├── responses.html │ │ └── users.html │ │ ├── settings │ │ ├── account.html │ │ ├── base.html │ │ ├── coins.html │ │ ├── components │ │ │ ├── lang_picker.html │ │ │ ├── privacy_options.html │ │ │ └── profile_options.html │ │ ├── privacy.html │ │ ├── profile.html │ │ ├── sessions.html │ │ └── theme.html │ │ ├── timelines │ │ ├── discover.html │ │ ├── global_question_timeline.html │ │ ├── public_global_question_timeline.html │ │ ├── public_timeline.html │ │ └── timeline.html │ │ └── views │ │ ├── comment.html │ │ ├── question.html │ │ └── response.html ├── rb │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── reva.toml │ └── src │ │ ├── lib.rs │ │ └── routing │ │ ├── api │ │ ├── comments.rs │ │ ├── mod.rs │ │ ├── profiles.rs │ │ ├── questions.rs │ │ ├── reactions.rs │ │ ├── responses.rs │ │ └── util.rs │ │ ├── mod.rs │ │ └── pages │ │ ├── market.rs │ │ ├── mod.rs │ │ ├── models │ │ ├── comment.rs │ │ ├── mod.rs │ │ └── response.rs │ │ ├── profile.rs │ │ ├── search.rs │ │ └── settings.rs └── shared │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ ├── config.rs │ ├── fs.rs │ ├── hash.rs │ ├── lib.rs │ ├── process.rs │ ├── snow.rs │ └── ui.rs ├── jobs ├── reset_extra_user_caches.sh └── reset_reactions_caches.sh ├── justfile ├── langs ├── en-US.json ├── ko-KR.json └── zh-TW.json ├── rustfmt.toml └── sql ├── 2024-08-24.sql ├── 2024-08-25.sql ├── add_badges_col.sql ├── add_coins_col.sql ├── add_context_col.sql ├── add_edited_col.sql ├── add_ip_col.sql ├── add_ips_col.sql ├── add_labels_col.sql ├── add_layout_col.sql ├── add_links.sql ├── add_name_col.sql ├── add_profile_counts_cols.sql ├── add_reaction_count_col.sql ├── add_reply_col.sql ├── add_tier_col.sql ├── add_totp_col.sql ├── add_totp_recovery_codes_col.sql ├── char_set.sql ├── ghsa-5w2q-9jwp-cjrp.sql ├── indexes.sql ├── int_permissions.sql ├── moderation.sql └── update_labels_col.sql /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustdocflags = ["--default-theme=ayu"] 3 | -------------------------------------------------------------------------------- /.config/.well-known/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swmff/rainbeam/6894a828ba00844c47ee062263485f2d7c9010e4/.config/.well-known/.gitkeep -------------------------------------------------------------------------------- /.config/config.example.toml: -------------------------------------------------------------------------------- 1 | port = 8080 2 | secure = true 3 | host = "https://example.com" 4 | snowflake_server_id = 1234567890 5 | blocked_hosts = ["https://example.com", "https://example2.com"] 6 | media_dir = "./media" 7 | 8 | name = "Rainbeam" 9 | description = "My Rainbeam instance" 10 | 11 | real_ip_header = "CF-Connecting-IP" 12 | registration_enabled = true 13 | 14 | [templates] 15 | # to change this, we automatically git ignore the `./.config/templates` dir 16 | # please place your custom templates in there :) 17 | # 18 | # this example uses `./header.html` so that it can be demoed using the example! 19 | header = "header.html" # this is relative to the config file, so `./.config` 20 | 21 | [captcha] 22 | # CHANGE THIS 23 | site_key = "10000000-ffff-ffff-ffff-000000000001" 24 | secret = "0x0000000000000000000000000000000000000000" 25 | -------------------------------------------------------------------------------- /.config/databeam/config.example.toml: -------------------------------------------------------------------------------- 1 | # example configuration for "main.db" sqlite file 2 | [connection] 3 | type = "sqlite" 4 | host="" 5 | user="" 6 | pass="" 7 | name="main" 8 | -------------------------------------------------------------------------------- /.config/header.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.config/layouts/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": "flex", 3 | "options": { 4 | "direction": "col", 5 | "gap": "8", 6 | "class": "w-full" 7 | }, 8 | "children": [ 9 | { 10 | "component": "banner" 11 | }, 12 | { 13 | "component": "flex", 14 | "options": { 15 | "collapse": "yes", 16 | "gap": "8" 17 | }, 18 | "children": [ 19 | { 20 | "component": "flex", 21 | "options": { 22 | "gap": "4", 23 | "direction": "col", 24 | "class": "sm:w-full", 25 | "style": "width: 25rem; height: max-content", 26 | "id": "profile_box_inner" 27 | }, 28 | "children": [ 29 | { 30 | "component": "flex", 31 | "options": { 32 | "gap": "2", 33 | "direction": "col", 34 | "width": "full", 35 | "class": "card" 36 | }, 37 | "children": [ 38 | { 39 | "component": "name" 40 | }, 41 | { 42 | "component": "about" 43 | }, 44 | { 45 | "component": "actions" 46 | } 47 | ] 48 | }, 49 | { 50 | "component": "footer" 51 | } 52 | ] 53 | }, 54 | { 55 | "component": "flex", 56 | "options": { 57 | "direction": "col", 58 | "gap": "8", 59 | "width": "full" 60 | }, 61 | "children": [ 62 | { 63 | "component": "ask" 64 | }, 65 | { 66 | "component": "tabs" 67 | }, 68 | { 69 | "component": "feed" 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /.config/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swmff/rainbeam/6894a828ba00844c47ee062263485f2d7c9010e4/.config/plugins/.gitkeep -------------------------------------------------------------------------------- /.config/static: -------------------------------------------------------------------------------- 1 | ../crates/rainbeam/static -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # swmff sparkler 2 | * @trisuaso 3 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for your consideration in contributing to Sparkler! Contributions like yours help to make Sparkler more welcoming and more secure. 4 | 5 | ## Issues 6 | 7 | When creating an issue, please follow the given templates as best you can. Before you begin creating your issue, please make sure you have checked to verify that you aren't reporting something that has already been reported! (This also applies to feature requests.) 8 | 9 | ## Merge Requests 10 | 11 | When creating a merge request, please ensure that your code actually compiles and runs without unexpected bugs. Sparkler uses a [justfile](https://github.com/swmff/sparkler/blob/master/justfile) to provide premade commands that you can use to build and run the server. You can test your code using `just test`. 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: neospring 2 | patreon: neospring 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'bug:' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Specifications** 27 | Please describe the environment you are experiencing this in. This includes your device, OS, web browser, etc. 28 | 29 | If you also run the server this is occurring on, please include the server OS and the Sparkler commit hash you are running. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'feature:' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: [master] 5 | workflow_dispatch: 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | concurrency: 11 | group: deploy 12 | cancel-in-progress: false 13 | jobs: 14 | build: 15 | name: Rustdoc 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v4 20 | - name: Checkout repo submodules 21 | run: git submodule update --init --recursive 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | - name: Init Rust 26 | uses: dtolnay/rust-toolchain@stable 27 | - name: Configure cache 28 | uses: Swatinem/rust-cache@v2 29 | - name: Configure pages 30 | id: pages 31 | uses: actions/configure-pages@v4 32 | - name: Init builder 33 | run: cd crates/builder && npm i && cd ../../ 34 | - name: Run builder 35 | run: node ./crates/builder/index.js 36 | - name: Clean docs directory 37 | run: cargo clean --doc 38 | - name: Build documentation 39 | run: cargo doc --no-deps --document-private-items --workspace --exclude neospring-desktop 40 | - name: Create index.html 41 | run: echo '' > target/doc/index.html 42 | - name: Artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: target/doc 46 | deploy: 47 | name: Pages 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | needs: build 53 | steps: 54 | - name: Deploy to pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | node_modules/ 3 | package-lock.json 4 | bun.lockb 5 | perf.data* 6 | 7 | # prod 8 | crates/rainbeam/static/site/about.md 9 | crates/rainbeam/static/site/tos.md 10 | crates/rainbeam/static/site/privacy.md 11 | crates/rainbeam/static/fonts/ 12 | crates/rainbeam/static/manifest.json 13 | crates/rainbeam/static/images/logo/ 14 | crates/rainbeam/static/images/ui/logo.* 15 | crates/rainbeam/static/favicon.svg 16 | crates/rainbeam/static/build/ 17 | crates/rainbeam/templates_build/ 18 | *.db 19 | .config/config.toml 20 | .config/databeam/config.toml 21 | .config/templates/ 22 | .config/plugins/**/* 23 | !.config/plugins/.gitkeep 24 | media/ 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = [ 4 | "crates/rainbeam-core", 5 | "crates/rb", 6 | "crates/rainbeam", 7 | "crates/authbeam", 8 | "crates/databeam", 9 | "crates/shared", 10 | "crates/langbeam", 11 | "crates/carp", 12 | ] 13 | 14 | [profile.dev] 15 | incremental = true 16 | 17 | [profile.release] 18 | opt-level = "z" 19 | lto = true 20 | codegen-units = 1 21 | # panic = "abort" 22 | panic = "unwind" 23 | strip = true 24 | incremental = true 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security updates are only supported for the latest version of Rainbeam. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | You can report confidential security vulnerabilites to the support email, support@swmff.org. Please indicate that the email is reporting a critical security vulnerability in the email subject field. 10 | 11 | You may also report the vulnerability through Github at . 12 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatter": { 3 | "enabled": true, 4 | "formatWithErrors": false, 5 | "indentWidth": 4, 6 | "indentStyle": "space", 7 | "lineWidth": 80 8 | }, 9 | "linter": { 10 | "rules": { 11 | "complexity": { 12 | "useArrowFunction": "off", 13 | "useOptionalChain": "off" 14 | }, 15 | "style": { 16 | "useTemplate": "off", 17 | "noParameterAssign": "off" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "build_dir": "crates/rainbeam/static/build", 3 | "templates_build_dir": "crates/rainbeam/templates_build", 4 | "clear_build_dirs": ["js", "css"], 5 | 6 | "css_dir": "crates/rainbeam/static/css", 7 | "js_dir": "crates/rainbeam/static/js", 8 | "templates_dir": "crates/rainbeam/templates", 9 | 10 | "js_format_options": { 11 | "comments": false, 12 | "preamble": "'https://github.com/swmff/rainbeam';" 13 | }, 14 | 15 | "extra_icon_imports": [ 16 | "paint-bucket", 17 | "download", 18 | "upload", 19 | "loader-circle", 20 | "shapes", 21 | "check", 22 | "trash", 23 | "move-up", 24 | "move-down", 25 | "plus", 26 | "type", 27 | "code-xml", 28 | "binary" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /crates/authbeam/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.db 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /crates/authbeam/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "authbeam" 3 | version = "7.0.0" 4 | edition = "2021" 5 | description = "Authentication manager" 6 | authors = ["trisuaso", "swmff"] 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | 11 | [features] 12 | postgres = ["databeam/postgres"] 13 | mysql = ["databeam/mysql"] 14 | sqlite = ["databeam/sqlite"] 15 | redis = ["databeam/redis"] 16 | moka = ["databeam/moka"] 17 | oysters = ["databeam/oysters"] 18 | default = ["databeam/sqlite", "redis"] 19 | 20 | [dependencies] 21 | axum = { version = "0.8.4", features = ["macros"] } 22 | axum-macros = "0.5.0" 23 | serde = { version = "1.0.219", features = ["derive"] } 24 | serde_json = "1.0.140" 25 | tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } 26 | dotenv = "0.15.0" 27 | axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] } 28 | regex = "1.11.1" 29 | reqwest = { version = "0.12.18", features = ["stream"] } 30 | hcaptcha-no-wasm = { version = "3.0.1" } 31 | mime_guess = "2.0.5" 32 | rainbeam-shared = { version = "1.0.1" } 33 | databeam = { path = "../databeam", version = "2.0.0", default-features = false } 34 | image = "0.25.6" 35 | pathbufd = "0.1.4" 36 | bitflags = "2.9.1" 37 | # pathbufd = { path = "../../../pathbufd" } 38 | reva = { version = "0.13.2", features = ["with-axum"] } 39 | reva_axum = "0.5.1" 40 | langbeam = { path = "../langbeam" } 41 | totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] } 42 | 43 | [lib] 44 | doctest = false 45 | -------------------------------------------------------------------------------- /crates/authbeam/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/authbeam/README.md: -------------------------------------------------------------------------------- 1 | # authbeam 2 | 3 | Rainbeam authentication types. 4 | -------------------------------------------------------------------------------- /crates/authbeam/reva.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | dirs = ["../rainbeam/templates_build"] 3 | whitespace = "Minimize" 4 | -------------------------------------------------------------------------------- /crates/authbeam/src/api/ipbans.rs: -------------------------------------------------------------------------------- 1 | use crate::database::Database; 2 | use crate::model::{DatabaseError, IpBanCreate}; 3 | use databeam::prelude::DefaultReturn; 4 | 5 | use axum::response::IntoResponse; 6 | use axum::{ 7 | extract::{Path, State}, 8 | Json, 9 | }; 10 | use axum_extra::extract::cookie::CookieJar; 11 | 12 | /// Create a ipban 13 | pub async fn create_request( 14 | jar: CookieJar, 15 | State(database): State, 16 | Json(props): Json, 17 | ) -> impl IntoResponse { 18 | // get user from token 19 | let auth_user = match jar.get("__Secure-Token") { 20 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 21 | Ok(ua) => ua, 22 | Err(e) => return Json(e.to_json()), 23 | }, 24 | None => return Json(DatabaseError::NotAllowed.to_json()), 25 | }; 26 | 27 | // return 28 | match database.create_ipban(props, auth_user).await { 29 | Ok(_) => Json(DefaultReturn { 30 | success: true, 31 | message: "Acceptable".to_string(), 32 | payload: (), 33 | }), 34 | Err(e) => Json(e.to_json()), 35 | } 36 | } 37 | 38 | /// Delete an ipban 39 | pub async fn delete_request( 40 | jar: CookieJar, 41 | Path(id): Path, 42 | State(database): State, 43 | ) -> impl IntoResponse { 44 | // get user from token 45 | let auth_user = match jar.get("__Secure-Token") { 46 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 47 | Ok(ua) => ua, 48 | Err(e) => return Json(e.to_json()), 49 | }, 50 | None => return Json(DatabaseError::NotAllowed.to_json()), 51 | }; 52 | 53 | // return 54 | match database.delete_ipban(&id, auth_user).await { 55 | Ok(_) => Json(DefaultReturn { 56 | success: true, 57 | message: "Acceptable".to_string(), 58 | payload: (), 59 | }), 60 | Err(e) => Json(e.to_json()), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/authbeam/src/api/ipblocks.rs: -------------------------------------------------------------------------------- 1 | use crate::database::Database; 2 | use crate::model::{DatabaseError, IpBlockCreate}; 3 | use databeam::prelude::DefaultReturn; 4 | 5 | use axum::response::IntoResponse; 6 | use axum::{ 7 | extract::{Path, State}, 8 | Json, 9 | }; 10 | use axum_extra::extract::cookie::CookieJar; 11 | 12 | /// Create a ipblock 13 | pub async fn create_request( 14 | jar: CookieJar, 15 | State(database): State, 16 | Json(props): Json, 17 | ) -> impl IntoResponse { 18 | // get user from token 19 | let auth_user = match jar.get("__Secure-Token") { 20 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 21 | Ok(ua) => ua, 22 | Err(e) => return Json(e.to_json()), 23 | }, 24 | None => return Json(DatabaseError::NotAllowed.to_json()), 25 | }; 26 | 27 | // return 28 | match database.create_ipblock(props, auth_user).await { 29 | Ok(_) => Json(DefaultReturn { 30 | success: true, 31 | message: "Acceptable".to_string(), 32 | payload: (), 33 | }), 34 | Err(e) => Json(e.to_json()), 35 | } 36 | } 37 | 38 | /// Delete an ipblock 39 | pub async fn delete_request( 40 | jar: CookieJar, 41 | Path(id): Path, 42 | State(database): State, 43 | ) -> impl IntoResponse { 44 | // get user from token 45 | let auth_user = match jar.get("__Secure-Token") { 46 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 47 | Ok(ua) => ua, 48 | Err(e) => return Json(e.to_json()), 49 | }, 50 | None => return Json(DatabaseError::NotAllowed.to_json()), 51 | }; 52 | 53 | // return 54 | match database.delete_ipblock(&id, auth_user).await { 55 | Ok(_) => Json(DefaultReturn { 56 | success: true, 57 | message: "Acceptable".to_string(), 58 | payload: (), 59 | }), 60 | Err(e) => Json(e.to_json()), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/authbeam/src/api/labels.rs: -------------------------------------------------------------------------------- 1 | use crate::database::Database; 2 | use crate::model::{DatabaseError, LabelCreate, TokenPermission}; 3 | use databeam::prelude::DefaultReturn; 4 | 5 | use axum::response::IntoResponse; 6 | use axum::{ 7 | extract::{Path, State}, 8 | Json, 9 | }; 10 | use axum_extra::extract::cookie::CookieJar; 11 | 12 | /// Get a label 13 | pub async fn get_request( 14 | State(database): State, 15 | Path(id): Path, 16 | ) -> impl IntoResponse { 17 | // get label 18 | let label = match database.get_label(id).await { 19 | Ok(i) => i, 20 | Err(e) => return Json(e.to_json()), 21 | }; 22 | 23 | // return 24 | Json(DefaultReturn { 25 | success: true, 26 | message: id.to_string(), 27 | payload: Some(label), 28 | }) 29 | } 30 | 31 | /// Create a label 32 | pub async fn create_request( 33 | jar: CookieJar, 34 | State(database): State, 35 | Json(props): Json, 36 | ) -> impl IntoResponse { 37 | // get user from token 38 | let auth_user = match jar.get("__Secure-Token") { 39 | Some(c) => { 40 | let token = c.value_trimmed(); 41 | 42 | match database.get_profile_by_unhashed(token).await { 43 | Ok(ua) => { 44 | // check token permission 45 | if !ua 46 | .token_context_from_token(&token) 47 | .can_do(TokenPermission::ManageAssets) 48 | { 49 | return Json(DatabaseError::NotAllowed.to_json()); 50 | } 51 | 52 | // return 53 | ua 54 | } 55 | Err(e) => return Json(e.to_json()), 56 | } 57 | } 58 | None => return Json(DatabaseError::NotAllowed.to_json()), 59 | }; 60 | 61 | // return 62 | let label = match database 63 | .create_label(&props.name, props.id, &auth_user.id) 64 | .await 65 | { 66 | Ok(m) => m, 67 | Err(e) => return Json(e.to_json()), 68 | }; 69 | 70 | Json(DefaultReturn { 71 | success: true, 72 | message: "Label created".to_string(), 73 | payload: Some(label), 74 | }) 75 | } 76 | 77 | /// Delete a label 78 | pub async fn delete_request( 79 | jar: CookieJar, 80 | Path(id): Path, 81 | State(database): State, 82 | ) -> impl IntoResponse { 83 | // get user from token 84 | let auth_user = match jar.get("__Secure-Token") { 85 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 86 | Ok(ua) => ua, 87 | Err(e) => return Json(e.to_json()), 88 | }, 89 | None => return Json(DatabaseError::NotAllowed.to_json()), 90 | }; 91 | 92 | // return 93 | if let Err(e) = database.delete_label(id, auth_user).await { 94 | return Json(e.to_json()); 95 | } 96 | 97 | Json(DefaultReturn { 98 | success: true, 99 | message: "Label deleted".to_string(), 100 | payload: (), 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /crates/authbeam/src/api/notifications.rs: -------------------------------------------------------------------------------- 1 | use crate::database::Database; 2 | use crate::model::DatabaseError; 3 | use databeam::prelude::DefaultReturn; 4 | 5 | use axum::response::IntoResponse; 6 | use axum::{ 7 | extract::{Path, State}, 8 | Json, 9 | }; 10 | use axum_extra::extract::cookie::CookieJar; 11 | 12 | /// Delete a notification 13 | pub async fn delete_request( 14 | jar: CookieJar, 15 | Path(id): Path, 16 | State(database): State, 17 | ) -> impl IntoResponse { 18 | // get user from token 19 | let auth_user = match jar.get("__Secure-Token") { 20 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 21 | Ok(ua) => ua, 22 | Err(e) => return Json(e.to_json()), 23 | }, 24 | None => return Json(DatabaseError::NotAllowed.to_json()), 25 | }; 26 | 27 | // return 28 | if let Err(e) = database.delete_notification(&id, auth_user).await { 29 | return Json(e.to_json()); 30 | } 31 | 32 | Json(DefaultReturn { 33 | success: true, 34 | message: "Notification deleted".to_string(), 35 | payload: (), 36 | }) 37 | } 38 | 39 | /// Delete the current user's notifications 40 | pub async fn delete_all_request( 41 | jar: CookieJar, 42 | State(database): State, 43 | ) -> impl IntoResponse { 44 | // get user from token 45 | let auth_user = match jar.get("__Secure-Token") { 46 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 47 | Ok(ua) => ua, 48 | Err(e) => return Json(e.to_json()), 49 | }, 50 | None => return Json(DatabaseError::NotAllowed.to_json()), 51 | }; 52 | 53 | // return 54 | if let Err(e) = database 55 | .delete_notifications_by_recipient(&auth_user.id.clone(), auth_user) 56 | .await 57 | { 58 | return Json(e.to_json()); 59 | } 60 | 61 | Json(DefaultReturn { 62 | success: true, 63 | message: "Notifications cleared!".to_string(), 64 | payload: (), 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /crates/authbeam/src/api/warnings.rs: -------------------------------------------------------------------------------- 1 | use crate::database::Database; 2 | use crate::model::{DatabaseError, WarningCreate}; 3 | use databeam::prelude::DefaultReturn; 4 | 5 | use axum::response::IntoResponse; 6 | use axum::{ 7 | extract::{Path, State}, 8 | Json, 9 | }; 10 | use axum_extra::extract::cookie::CookieJar; 11 | 12 | /// Create a warning 13 | pub async fn create_request( 14 | jar: CookieJar, 15 | State(database): State, 16 | Json(props): Json, 17 | ) -> impl IntoResponse { 18 | // get user from token 19 | let auth_user = match jar.get("__Secure-Token") { 20 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 21 | Ok(ua) => ua, 22 | Err(e) => return Json(e.to_json()), 23 | }, 24 | None => return Json(DatabaseError::NotAllowed.to_json()), 25 | }; 26 | 27 | // return 28 | match database.create_warning(props, auth_user).await { 29 | Ok(_) => Json(DefaultReturn { 30 | success: true, 31 | message: "Acceptable".to_string(), 32 | payload: (), 33 | }), 34 | Err(e) => Json(e.to_json()), 35 | } 36 | } 37 | 38 | /// Delete a warning 39 | pub async fn delete_request( 40 | jar: CookieJar, 41 | Path(id): Path, 42 | State(database): State, 43 | ) -> impl IntoResponse { 44 | // get user from token 45 | let auth_user = match jar.get("__Secure-Token") { 46 | Some(c) => match database.get_profile_by_unhashed(c.value_trimmed()).await { 47 | Ok(ua) => ua, 48 | Err(e) => return Json(e.to_json()), 49 | }, 50 | None => return Json(DatabaseError::NotAllowed.to_json()), 51 | }; 52 | 53 | // return 54 | match database.delete_warning(&id, auth_user).await { 55 | Ok(_) => Json(DefaultReturn { 56 | success: true, 57 | message: "Acceptable".to_string(), 58 | payload: (), 59 | }), 60 | Err(e) => Json(e.to_json()), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/authbeam/src/avif.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Bytes, 3 | extract::{FromRequest, Request}, 4 | http::{header::CONTENT_TYPE, StatusCode}, 5 | }; 6 | use axum_extra::extract::Multipart; 7 | use std::{fs::File, io::BufWriter}; 8 | 9 | /// An image extractor accepting: 10 | /// * `multipart/form-data` 11 | /// * `image/png` 12 | /// * `image/jpeg` 13 | /// * `image/avif` 14 | /// * `image/webp` 15 | pub struct Image(pub Bytes); 16 | 17 | impl FromRequest for Image 18 | where 19 | Bytes: FromRequest, 20 | S: Send + Sync, 21 | { 22 | type Rejection = StatusCode; 23 | 24 | async fn from_request(req: Request, state: &S) -> Result { 25 | let Some(content_type) = req.headers().get(CONTENT_TYPE) else { 26 | return Err(StatusCode::BAD_REQUEST); 27 | }; 28 | 29 | let body = if content_type 30 | .to_str() 31 | .unwrap() 32 | .starts_with("multipart/form-data") 33 | { 34 | let mut multipart = Multipart::from_request(req, state) 35 | .await 36 | .map_err(|_| StatusCode::BAD_REQUEST)?; 37 | 38 | let Ok(Some(field)) = multipart.next_field().await else { 39 | return Err(StatusCode::BAD_REQUEST); 40 | }; 41 | 42 | field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)? 43 | } else if (content_type == "image/avif") 44 | | (content_type == "image/jpeg") 45 | | (content_type == "image/png") 46 | | (content_type == "image/webp") 47 | { 48 | Bytes::from_request(req, state) 49 | .await 50 | .map_err(|_| StatusCode::BAD_REQUEST)? 51 | } else { 52 | return Err(StatusCode::BAD_REQUEST); 53 | }; 54 | 55 | Ok(Self(body)) 56 | } 57 | } 58 | 59 | /// Create an AVIF buffer given an input of `bytes` 60 | pub fn save_avif_buffer(path: &str, bytes: Vec) -> std::io::Result<()> { 61 | let pre_img_buffer = match image::load_from_memory(&bytes) { 62 | Ok(i) => i, 63 | Err(_) => { 64 | return Err(std::io::Error::new( 65 | std::io::ErrorKind::InvalidData, 66 | "Image failed", 67 | )); 68 | } 69 | }; 70 | 71 | let file = File::create(path)?; 72 | let mut writer = BufWriter::new(file); 73 | 74 | if let Err(_) = pre_img_buffer.write_to(&mut writer, image::ImageFormat::Avif) { 75 | return Err(std::io::Error::new( 76 | std::io::ErrorKind::Other, 77 | "Image conversion failed", 78 | )); 79 | }; 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /crates/authbeam/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Authentication manager with user accounts and simple group-based permissions. 2 | #![doc = include_str!("../README.md")] 3 | #![doc(issue_tracker_base_url = "https://github.com/swmff/rainbeam/issues/")] 4 | pub mod api; 5 | pub mod avif; 6 | pub mod database; 7 | pub mod layout; 8 | pub mod macros; 9 | pub mod model; 10 | pub mod permissions; 11 | 12 | pub use database::{Database, ServerOptions}; 13 | pub use databeam::DatabaseOpts; 14 | -------------------------------------------------------------------------------- /crates/builder/index.js: -------------------------------------------------------------------------------- 1 | import build from "./lib.js"; 2 | import fs from "node:fs/promises"; 3 | 4 | (async () => { 5 | const __cwd = process.cwd(); 6 | 7 | const start = performance.now(); 8 | await build(JSON.parse(await fs.readFile(`${__cwd}/builder.json`))); 9 | console.log(`took ${Math.floor(performance.now() - start)}ms`); 10 | })(); 11 | -------------------------------------------------------------------------------- /crates/builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "builder", 3 | "version": "1.0.0", 4 | "description": "Static asset builder", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "lightningcss": "^1.28.1" 14 | }, 15 | "dependencies": { 16 | "@swc/core": "^1.9.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/carp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "carp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.219", features = ["derive"] } 8 | serde_json = { version = "1.0.140" } 9 | -------------------------------------------------------------------------------- /crates/carp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/carp/src/carp1.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use crate::model::{CarpGraph, Error, Result}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct Point( 6 | usize, 7 | usize, 8 | #[serde(default, skip_serializing_if = "Option::is_none")] Option, 9 | ); 10 | pub type Line = Vec; 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct ImageConfig { 14 | #[serde(alias = "w")] 15 | pub width: usize, 16 | #[serde(alias = "h")] 17 | pub height: usize, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | pub struct Graph { 22 | #[serde(alias = "i")] 23 | pub image: ImageConfig, 24 | #[serde(alias = "d")] 25 | pub data: Vec, 26 | } 27 | 28 | impl Graph { 29 | pub fn from_str(input: &str) -> Result { 30 | match serde_json::from_str(input) { 31 | Ok(de) => Ok(de), 32 | Err(e) => Err(Error::DeserializeError(e.to_string())), 33 | } 34 | } 35 | } 36 | 37 | impl CarpGraph for Graph { 38 | fn to_bytes(&self) -> Vec { 39 | serde_json::to_string(&self).unwrap().as_bytes().to_owned() 40 | } 41 | 42 | fn from_bytes(bytes: Vec) -> Self { 43 | Self::from_str(&String::from_utf8(bytes).expect("invalid input")) 44 | .expect("failed to deserialize") 45 | } 46 | 47 | fn to_svg(&self) -> String { 48 | let mut out: String = String::new(); 49 | out.push_str(&format!( 50 | "", 51 | self.image.width, self.image.height, self.image.width, self.image.height 52 | )); 53 | 54 | // add lines 55 | let mut stroke_size: i8 = 1; 56 | let mut stroke_color: String = "#000000".to_string(); 57 | 58 | for line in &self.data { 59 | let mut previous_x_y: Option<(usize, usize)> = None; 60 | let mut line_path = String::new(); 61 | 62 | for point in line { 63 | // adjust brush color/size 64 | if let Some(ref color_or_size) = point.2 { 65 | if color_or_size.starts_with("#") { 66 | stroke_color = color_or_size.to_string(); 67 | } else { 68 | stroke_size = color_or_size.parse::().unwrap(); 69 | } 70 | } 71 | 72 | // add to path string 73 | line_path.push_str(&format!( 74 | " M{} {}{}", 75 | point.0, 76 | point.1, 77 | if let Some(pxy) = previous_x_y { 78 | // line to there 79 | format!(" L{} {}", pxy.0, pxy.1) 80 | } else { 81 | String::new() 82 | } 83 | )); 84 | 85 | previous_x_y = Some((point.0, point.1)); 86 | 87 | // add circular point 88 | out.push_str(&format!( 89 | "", 90 | point.0, 91 | point.1, 92 | stroke_size / 2 // the size is technically the diameter of the circle 93 | )); 94 | } 95 | 96 | out.push_str(&format!( 97 | "" 98 | )); 99 | } 100 | 101 | // return 102 | format!("{out}") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/carp/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod carp1; 2 | pub mod carp2; 3 | pub mod model; 4 | 5 | pub use model::CarpGraph; 6 | pub use carp2::Graph; 7 | -------------------------------------------------------------------------------- /crates/carp/src/model.rs: -------------------------------------------------------------------------------- 1 | pub type Result = std::result::Result; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum Error { 5 | DeserializeError(String), 6 | Custom(String), 7 | } 8 | 9 | impl std::fmt::Display for Error { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | f.write_str(&format!("{:?}", self)) 12 | } 13 | } 14 | 15 | /// A simple carpgraph renderer. 16 | pub trait CarpGraph { 17 | /// Serialize an image to bytes. 18 | fn to_bytes(&self) -> Vec; 19 | /// Deserialize an image from bytes. 20 | fn from_bytes(bytes: Vec) -> Self; 21 | /// Convert an image to svg format. 22 | fn to_svg(&self) -> String; 23 | } 24 | -------------------------------------------------------------------------------- /crates/databeam/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /crates/databeam/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "databeam" 3 | version = "2.0.1" 4 | edition = "2021" 5 | description = "Database connection library" 6 | authors = ["trisuaso", "swmff"] 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | 11 | [features] 12 | postgres = [] 13 | mysql = [] 14 | sqlite = [] 15 | redis = ["dep:redis"] 16 | moka = ["dep:moka"] 17 | oysters = ["dep:oysters_client"] 18 | default = ["sqlite", "redis"] 19 | 20 | [dependencies] 21 | redis = { version = "0.31.0", optional = true } 22 | moka = { version = "0.12.10", features = ["future"], optional = true } 23 | oysters_client = { version = "0.1.5", default-features = false, optional = true } 24 | serde = { version = "1.0.219", features = ["derive"] } 25 | serde_json = "1.0.140" 26 | toml = "0.8.22" 27 | rainbeam-shared = "1.0.1" 28 | pathbufd = "0.1.4" 29 | 30 | [dependencies.sqlx] 31 | version = "0.8.6" 32 | features = [ 33 | "sqlite", 34 | "postgres", 35 | "mysql", 36 | "any", 37 | "runtime-tokio", 38 | "tls-native-tls", 39 | ] 40 | -------------------------------------------------------------------------------- /crates/databeam/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/databeam/README.md: -------------------------------------------------------------------------------- 1 | # databeam 2 | 3 | Rainbeam database connection manager. 4 | -------------------------------------------------------------------------------- /crates/databeam/src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(async_fn_in_trait)] 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | 4 | pub const EXPIRE_AT: i64 = 3_600_000; 5 | 6 | #[allow(type_alias_bounds)] 7 | pub type TimedObject = (i64, T); 8 | 9 | #[cfg(feature = "redis")] 10 | pub mod redis; 11 | 12 | #[cfg(feature = "moka")] 13 | pub mod moka; 14 | 15 | #[cfg(feature = "oysters")] 16 | pub mod oysters; 17 | 18 | /// A simple cache "database". 19 | pub trait Cache { 20 | type Item; 21 | type Client; 22 | 23 | /// Create a new [`Cache`]. 24 | async fn new() -> Self; 25 | /// Get a connection to the cache. 26 | async fn get_con(&self) -> Self::Client; 27 | 28 | /// Get a cache object by its identifier 29 | /// 30 | /// # Arguments 31 | /// * `id` - `String` of the object's id 32 | async fn get(&self, id: Self::Item) -> Option; 33 | /// Set a cache object by its identifier and content 34 | /// 35 | /// # Arguments 36 | /// * `id` - `String` of the object's id 37 | /// * `content` - `String` of the object's content 38 | async fn set(&self, id: Self::Item, content: Self::Item) -> bool; 39 | /// Update a cache object by its identifier and content 40 | /// 41 | /// # Arguments 42 | /// * `id` - `String` of the object's id 43 | /// * `content` - `String` of the object's content 44 | async fn update(&self, id: Self::Item, content: Self::Item) -> bool; 45 | /// Remove a cache object by its identifier 46 | /// 47 | /// # Arguments 48 | /// * `id` - `String` of the object's id 49 | async fn remove(&self, id: Self::Item) -> bool; 50 | /// Remove a cache object by its identifier('s start) 51 | /// 52 | /// # Arguments 53 | /// * `id` - `String` of the object's id('s start) 54 | async fn remove_starting_with(&self, id: Self::Item) -> bool; 55 | /// Increment a cache object by its identifier 56 | /// 57 | /// # Arguments 58 | /// * `id` - `String` of the object's id 59 | async fn incr(&self, id: Self::Item) -> bool; 60 | /// Decrement a cache object by its identifier 61 | /// 62 | /// # Arguments 63 | /// * `id` - `String` of the object's id 64 | async fn decr(&self, id: Self::Item) -> bool; 65 | 66 | /// Get a cache object by its identifier 67 | /// 68 | /// # Arguments 69 | /// * `id` - `String` of the object's id 70 | async fn get_timed( 71 | &self, 72 | id: Self::Item, 73 | ) -> Option>; 74 | /// Set a cache object by its identifier and content 75 | /// 76 | /// # Arguments 77 | /// * `id` - `String` of the object's id 78 | /// * `content` - `String` of the object's content 79 | async fn set_timed(&self, id: Self::Item, content: T) -> bool; 80 | } 81 | -------------------------------------------------------------------------------- /crates/databeam/src/cache/oysters.rs: -------------------------------------------------------------------------------- 1 | //! Oysters connection manager 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use oysters_client::Client as OystersClient; 4 | 5 | use super::{Cache, TimedObject, EXPIRE_AT}; 6 | 7 | #[derive(Clone)] 8 | pub struct OystersCache { 9 | pub client: OystersClient, 10 | } 11 | 12 | impl Cache for OystersCache { 13 | type Item = String; 14 | type Client = OystersClient; 15 | 16 | async fn new() -> Self { 17 | Self { 18 | client: OystersClient::new("http://localhost:5072".to_string()), 19 | } 20 | } 21 | 22 | async fn get_con(&self) -> Self::Client { 23 | OystersClient::new("http://localhost:5072".to_string()) 24 | } 25 | 26 | async fn get(&self, id: Self::Item) -> Option { 27 | let v = self.client.get(&id).await; 28 | 29 | if v.is_empty() { 30 | None 31 | } else { 32 | Some(v) 33 | } 34 | } 35 | 36 | async fn set(&self, id: Self::Item, content: Self::Item) -> bool { 37 | self.client.insert(&id, &content).await; 38 | true 39 | } 40 | 41 | async fn update(&self, id: Self::Item, content: Self::Item) -> bool { 42 | self.set(id, content).await 43 | } 44 | 45 | async fn remove(&self, id: Self::Item) -> bool { 46 | self.client.remove(&id).await; 47 | true 48 | } 49 | 50 | async fn remove_starting_with(&self, id: Self::Item) -> bool { 51 | let keys: Vec = self.client.filter_keys(&id).await; 52 | 53 | for key in keys { 54 | self.remove(key).await; 55 | } 56 | 57 | true 58 | } 59 | 60 | async fn incr(&self, id: Self::Item) -> bool { 61 | self.client.incr(&id).await; 62 | true 63 | } 64 | 65 | async fn decr(&self, id: Self::Item) -> bool { 66 | self.client.decr(&id).await; 67 | true 68 | } 69 | 70 | async fn get_timed( 71 | &self, 72 | id: Self::Item, 73 | ) -> Option> { 74 | let res: String = self.client.get(&id).await; 75 | match serde_json::from_str::>(&res) { 76 | Ok(d) => { 77 | // check time 78 | let now = rainbeam_shared::epoch_timestamp(2024); 79 | 80 | if now - d.0 >= EXPIRE_AT { 81 | // expired key, remove and return None 82 | self.remove(id).await; 83 | return None; 84 | } 85 | 86 | // return 87 | Some(d) 88 | } 89 | Err(_) => None, 90 | } 91 | } 92 | 93 | async fn set_timed(&self, id: Self::Item, content: T) -> bool { 94 | self.client 95 | .insert( 96 | &id, 97 | &match serde_json::to_string::>(&( 98 | rainbeam_shared::epoch_timestamp(2024), 99 | content, 100 | )) { 101 | Ok(s) => s, 102 | Err(_) => return false, 103 | }, 104 | ) 105 | .await; 106 | 107 | true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crates/databeam/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Application config manager 2 | use serde::{Deserialize, Serialize}; 3 | use rainbeam_shared::fs; 4 | use pathbufd::PathBufD; 5 | use std::io::Result; 6 | 7 | /// Configuration file 8 | #[derive(Clone, Serialize, Deserialize, Debug)] 9 | #[derive(Default)] 10 | pub struct Config { 11 | pub connection: crate::sql::DatabaseOpts, 12 | } 13 | 14 | 15 | impl Config { 16 | /// Read configuration file into [`Config`] 17 | pub fn read(contents: String) -> Self { 18 | toml::from_str::(&contents).unwrap() 19 | } 20 | 21 | /// Pull configuration file 22 | pub fn get_config() -> Self { 23 | let path = PathBufD::current().extend(&[".config", "databeam", "config.toml"]); 24 | 25 | match fs::read(path) { 26 | Ok(c) => Config::read(c), 27 | Err(_) => { 28 | Self::update_config(Self::default()).expect("failed to write default config"); 29 | Self::default() 30 | } 31 | } 32 | } 33 | 34 | /// Update configuration file 35 | pub fn update_config(contents: Self) -> Result<()> { 36 | let c = fs::canonicalize(".").unwrap(); 37 | let here = c.to_str().unwrap(); 38 | 39 | fs::write( 40 | format!("{here}/.config/databeam/config.toml"), 41 | toml::to_string_pretty::(&contents).unwrap(), 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/databeam/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![doc(issue_tracker_base_url = "https://github.com/swmff/rainbeam/issues/")] 3 | pub mod cache; 4 | pub mod config; 5 | pub mod database; 6 | pub mod prelude; 7 | pub mod sql; 8 | pub mod utility; 9 | 10 | pub use sql::DatabaseOpts; 11 | pub use sqlx::query; 12 | -------------------------------------------------------------------------------- /crates/databeam/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::cache::Cache; 2 | pub use crate::database::{DefaultReturn, StarterDatabase}; 3 | 4 | #[cfg(feature = "redis")] 5 | pub use crate::cache::redis::RedisCache; 6 | 7 | #[cfg(feature = "moka")] 8 | pub use crate::cache::moka::MokaCache; 9 | 10 | #[cfg(feature = "oysters")] 11 | pub use crate::cache::oysters::OystersCache; 12 | -------------------------------------------------------------------------------- /crates/databeam/src/utility.rs: -------------------------------------------------------------------------------- 1 | //! Basic utility functions 2 | pub use rainbeam_shared::{hash::hash, hash::random_id, hash::uuid, unix_epoch_timestamp}; 3 | -------------------------------------------------------------------------------- /crates/langbeam/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "langbeam" 3 | version = "2.0.0" 4 | edition = "2021" 5 | description = "Rainbeam language file manager" 6 | authors = ["trisuaso", "swmff"] 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | serde = { version = "1.0.219", features = ["serde_derive"] } 13 | serde_json = "1.0.140" 14 | rainbeam-shared = "1.0.1" 15 | pathbufd = "0.1.4" 16 | -------------------------------------------------------------------------------- /crates/langbeam/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/langbeam/README.md: -------------------------------------------------------------------------------- 1 | # Langbeam 2 | 3 | Versioned l10n (localization) files. 4 | 5 | ## `LangFile` 6 | 7 | Text is stored in a "LangFile". In a LangFile, you must define a `name`, `version`, and the `data` stored with in. 8 | 9 | The `name` of a LangFile follow this format: 10 | 11 | ``` 12 | {reverse-domain-name-notation id}:{ISO 639 language code}-{ISO 3166-1 alpha-2 country code} 13 | ``` 14 | 15 | The `version` of a LangFile should follow semantic versioning. 16 | 17 | An example LangFile may look similar to this: 18 | 19 | ```json 20 | { 21 | "name": "net.rainbeam.langs:en-US", 22 | "version": "1.0.0", 23 | "data": { 24 | "example_label": "Example Label" 25 | } 26 | } 27 | ``` 28 | 29 | The key of entries in `data` should always stay the same. Only the value of entries should be translated between language files. 30 | 31 | ## `langs` directory 32 | 33 | All language files should be pulled from `{cwd}/langs`. The file name of files does not matter, as the library only cares about the `name` field of each of the files. No files that aren't JSON files are allowed in this directory. 34 | -------------------------------------------------------------------------------- /crates/langbeam/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{LazyLock, RwLock}, 4 | }; 5 | use serde::{Serialize, Deserialize}; 6 | use rainbeam_shared::fs; 7 | use pathbufd::PathBufD; 8 | 9 | pub static ENGLISH_US: LazyLock> = LazyLock::new(RwLock::default); 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct LangFile { 13 | pub name: String, 14 | pub version: String, 15 | pub data: HashMap, 16 | } 17 | 18 | impl Default for LangFile { 19 | fn default() -> Self { 20 | Self { 21 | name: "net.rainbeam.langs.testing:aa-BB".to_string(), 22 | version: "0.0.0".to_string(), 23 | data: HashMap::new(), 24 | } 25 | } 26 | } 27 | 28 | impl LangFile { 29 | /// Check if a value exists in `data` (and isn't empty) 30 | pub fn exists(&self, key: &str) -> bool { 31 | if let Some(value) = self.data.get(key) { 32 | if value.is_empty() { 33 | return false; 34 | } 35 | 36 | return true; 37 | } 38 | 39 | false 40 | } 41 | 42 | /// Get a value from `data`, returns an empty string if it doesn't exist 43 | pub fn get(&self, key: &str) -> String { 44 | if !self.exists(key) { 45 | if (self.name == "net.rainbeam.langs.testing:aa-BB") 46 | | (self.name == "net.rainbeam.langs.testing:en-US") 47 | { 48 | return key.to_string(); 49 | } else { 50 | // load english instead 51 | let reader = ENGLISH_US 52 | .read() 53 | .expect("failed to pull reader for ENGLISH_US"); 54 | return reader.get(key); 55 | } 56 | } 57 | 58 | self.data.get(key).unwrap().to_owned() 59 | } 60 | } 61 | 62 | /// Read the `langs` directory and return a [`Hashmap`] containing all files 63 | pub fn read_langs() -> HashMap { 64 | let mut out = HashMap::new(); 65 | 66 | let langs_dir = PathBufD::current().join("langs"); 67 | if let Ok(files) = fs::read_dir(langs_dir) { 68 | for file in files.into_iter() { 69 | if file.is_err() { 70 | continue; 71 | } 72 | 73 | let de: LangFile = 74 | match serde_json::from_str(&match fs::read_to_string(file.unwrap().path()) { 75 | Ok(f) => f, 76 | Err(_) => continue, 77 | }) { 78 | Ok(de) => de, 79 | Err(_) => continue, 80 | }; 81 | 82 | if de.name.ends_with("en-US") { 83 | let mut writer = ENGLISH_US 84 | .write() 85 | .expect("failed to pull writer for ENGLISH_US"); 86 | *writer = de.clone(); 87 | drop(writer); 88 | } 89 | 90 | out.insert(de.name.clone(), de); 91 | } 92 | } 93 | 94 | // return 95 | out 96 | } 97 | -------------------------------------------------------------------------------- /crates/rainbeam-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rainbeam-core" 3 | version = "9.0.0" 4 | edition = "2021" 5 | authors = ["trisuaso", "swmff"] 6 | description = "Rainbeam backend core" 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | rust-version = "1.83" 11 | 12 | [features] 13 | postgres = ["databeam/postgres", "authbeam/postgres"] 14 | mysql = ["databeam/mysql", "authbeam/mysql"] 15 | sqlite = ["databeam/sqlite", "authbeam/sqlite"] 16 | redis = ["databeam/redis", "authbeam/redis"] 17 | moka = ["databeam/moka", "authbeam/moka"] 18 | oysters = ["databeam/oysters", "authbeam/oysters"] 19 | default = ["databeam/sqlite", "authbeam/sqlite"] 20 | 21 | [dependencies] 22 | axum = { version = "0.8.4", features = ["macros", "form"] } 23 | axum-extra = { version = "0.10.1", features = ["cookie"] } 24 | reqwest = { version = "0.12.18", features = ["json", "stream"] } 25 | serde = { version = "1.0.219", features = ["derive"] } 26 | tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } 27 | toml = "0.8.22" 28 | serde_json = "1.0.140" 29 | regex = "1.11.1" 30 | hcaptcha-no-wasm = { version = "3.0.1" } 31 | ammonia = "4.1.0" 32 | async-recursion = "1.1.1" 33 | tracing = "0.1.41" 34 | rainbeam-shared = "1.0.1" 35 | databeam = { path = "../databeam", version = "2.0.0", default-features = false } 36 | authbeam = { path = "../authbeam", default-features = false } 37 | langbeam = { path = "../langbeam" } 38 | mime_guess = "2.0.5" 39 | pathbufd = "0.1.4" 40 | # pathbufd = { path = "../../../pathbufd" } 41 | carp = { path = "../carp" } 42 | 43 | [lib] 44 | crate-type = ["cdylib", "lib"] 45 | path = "src/lib.rs" 46 | name = "rainbeam" 47 | test = false 48 | doctest = true 49 | -------------------------------------------------------------------------------- /crates/rainbeam-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/rainbeam-core/README.md: -------------------------------------------------------------------------------- 1 | # Rainbeam Core Library 2 | -------------------------------------------------------------------------------- /crates/rainbeam-core/src/config.rs: -------------------------------------------------------------------------------- 1 | pub use rainbeam_shared::config::*; 2 | -------------------------------------------------------------------------------- /crates/rainbeam-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod database; 3 | pub mod model; 4 | -------------------------------------------------------------------------------- /crates/rainbeam/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rainbeam" 3 | version = "9.0.0" 4 | edition = "2021" 5 | authors = ["trisuaso", "swmff"] 6 | description = "Ask, share, socialize!" 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | rust-version = "1.83" 11 | 12 | [features] 13 | postgres = ["databeam/postgres", "authbeam/postgres", "rb/postgres"] 14 | mysql = ["databeam/mysql", "authbeam/mysql", "rb/mysql"] 15 | sqlite = ["databeam/sqlite", "authbeam/sqlite", "rb/sqlite"] 16 | mimalloc = ["dep:mimalloc"] 17 | redis = ["databeam/redis", "authbeam/redis", "rb/redis"] 18 | moka = ["databeam/moka", "authbeam/moka", "rb/moka"] 19 | oysters = ["databeam/oysters", "authbeam/oysters", "rb/oysters"] 20 | default = ["sqlite", "redis"] 21 | 22 | [dependencies] 23 | reva = { version = "0.13.2", features = ["with-axum"] } 24 | reva_axum = "0.5.1" 25 | axum = { version = "0.8.4", features = ["macros", "form"] } 26 | axum-extra = { version = "0.10.1", features = ["cookie"] } 27 | reqwest = { version = "0.12.18", features = ["json", "stream"] } 28 | serde = { version = "1.0.219", features = ["derive"] } 29 | tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } 30 | toml = "0.8.22" 31 | tower-http = { version = "0.6.4", features = ["fs", "trace"] } 32 | serde_json = "1.0.140" 33 | regex = "1.11.1" 34 | ammonia = "4.1.0" 35 | async-recursion = "1.1.1" 36 | tracing = "0.1.41" 37 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 38 | rainbeam-shared = { path = "../shared" } 39 | databeam = { path = "../databeam", version = "2.0.0", default-features = false } 40 | authbeam = { path = "../authbeam", default-features = false } 41 | langbeam = { path = "../langbeam" } 42 | rb = { path = "../rb", default-features = false } 43 | mimalloc = { version = "0.1.46", optional = true } 44 | mime_guess = "2.0.5" 45 | pathbufd = "0.1.4" 46 | # pathbufd = { path = "../../../pathbufd" } 47 | 48 | [[bin]] 49 | path = "src/main.rs" 50 | name = "rainbeam" 51 | test = false 52 | -------------------------------------------------------------------------------- /crates/rainbeam/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/rainbeam/reva.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | dirs = ["templates_build"] 3 | whitespace = "Minimize" 4 | -------------------------------------------------------------------------------- /crates/rainbeam/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url("root.css"); 2 | @import url("interactable.css"); 3 | @import url("blocks.css"); 4 | @import url("components.css"); 5 | 6 | /* utility */ 7 | .flex { 8 | display: flex; 9 | } 10 | 11 | .flex-col { 12 | flex-direction: column; 13 | } 14 | 15 | .flex-rev-col { 16 | flex-direction: column-reverse; 17 | } 18 | 19 | .flex-row { 20 | flex-direction: row !important; 21 | } 22 | 23 | .flex-rev-row { 24 | flex-direction: row-reverse; 25 | } 26 | 27 | .flex-wrap { 28 | flex-wrap: wrap; 29 | } 30 | 31 | .justify-center { 32 | justify-content: center; 33 | } 34 | 35 | .justify-between { 36 | justify-content: space-between; 37 | } 38 | 39 | .justify-right { 40 | justify-content: right; 41 | } 42 | 43 | .justify-start { 44 | justify-content: flex-start; 45 | } 46 | 47 | .items-center { 48 | align-items: center; 49 | } 50 | 51 | .gap-1 { 52 | gap: 0.25rem; 53 | } 54 | 55 | .gap-2 { 56 | gap: 0.5rem; 57 | } 58 | 59 | .gap-4 { 60 | gap: 1rem; 61 | } 62 | 63 | .gap-8 { 64 | gap: 1.25rem; 65 | } 66 | 67 | .mobile { 68 | display: none !important; 69 | } 70 | 71 | @media screen and (max-width: 650px) { 72 | .desktop { 73 | display: none !important; 74 | } 75 | 76 | .mobile { 77 | display: flex !important; 78 | } 79 | } 80 | 81 | @media screen and (max-width: 900px) { 82 | .sm\:static { 83 | position: static !important; 84 | } 85 | 86 | .mobile.flex { 87 | display: flex !important; 88 | } 89 | 90 | .sm\:w-full { 91 | width: 100% !important; 92 | } 93 | 94 | .sm\:mt-2 { 95 | margin-top: 2rem !important; 96 | } 97 | 98 | .sm\:items-start { 99 | align-items: flex-start !important; 100 | } 101 | 102 | .sm\:contents { 103 | display: contents !important; 104 | } 105 | } 106 | 107 | .shadow { 108 | box-shadow: 0 0 8px var(--color-shadow); 109 | } 110 | 111 | .shadow-md { 112 | box-shadow: 0 8px 16px var(--color-shadow); 113 | } 114 | 115 | .round-sm { 116 | border-radius: calc(var(--radius) / 2) !important; 117 | } 118 | 119 | .round { 120 | border-radius: var(--radius) !important; 121 | } 122 | 123 | .round-md { 124 | border-radius: calc(var(--radius) * 2) !important; 125 | } 126 | 127 | .round-lg { 128 | border-radius: calc(var(--radius) * 4) !important; 129 | } 130 | 131 | .w-full { 132 | width: 100% !important; 133 | } 134 | 135 | .w-content { 136 | width: max-content !important; 137 | } 138 | 139 | .bold { 140 | font-weight: 600; 141 | } 142 | 143 | [disabled="fully"] { 144 | opacity: 75%; 145 | pointer-events: visible; 146 | cursor: not-allowed; 147 | user-select: none; 148 | } 149 | 150 | .fade, 151 | .CodeMirror-placeholder { 152 | opacity: 75%; 153 | transition: opacity 0.15s; 154 | } 155 | 156 | .ff-inherit { 157 | font-family: inherit; 158 | } 159 | 160 | .fs-md { 161 | font-size: 12px; 162 | } 163 | 164 | [align="center"] { 165 | text-align: center; 166 | } 167 | 168 | [align="right"] { 169 | text-align: right; 170 | } 171 | 172 | .red { 173 | color: var(--color-red) !important; 174 | } 175 | 176 | .green { 177 | color: var(--color-green) !important; 178 | } 179 | 180 | .hidden { 181 | display: none; 182 | } 183 | 184 | align { 185 | width: 100%; 186 | display: block; 187 | } 188 | 189 | align.center { 190 | text-align: center; 191 | } 192 | 193 | align.right { 194 | text-align: right; 195 | } 196 | -------------------------------------------------------------------------------- /crates/rainbeam/static/images/default-avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/rainbeam/static/images/default-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/rainbeam/static/images/ui/logo-example.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 14 | 15 | 23 | 24 | 25 | 34 | 35 | 41 | 42 | 43 | 44 | 48 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/account_warnings.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("account_warnings"); 3 | 4 | self.define("delete", async function (_, id) { 5 | if ( 6 | !(await trigger("app::confirm", [ 7 | "Are you sure you want to do this?", 8 | ])) 9 | ) { 10 | return; 11 | } 12 | 13 | fetch(`/api/v0/auth/warnings/${id}`, { 14 | method: "DELETE", 15 | }) 16 | .then((res) => res.json()) 17 | .then((res) => { 18 | trigger("app::shout", [ 19 | res.success ? "tip" : "caution", 20 | res.message || "Warning deleted!", 21 | ]); 22 | 23 | document 24 | .getElementById(`warning:${id}`) 25 | .setAttribute("disabled", "fully"); 26 | }); 27 | }); 28 | })(); 29 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/codemirror.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("codemirror"); 3 | 4 | // create_editor 5 | self.define( 6 | "create_editor", 7 | function ( 8 | _, 9 | bind_to, 10 | value, 11 | placeholder, 12 | global = "editor", 13 | mode = "markdown", 14 | inputStyle = "contenteditable", 15 | ) { 16 | globalThis[global] = CodeMirror(bind_to, { 17 | value: value || "", 18 | mode, 19 | lineWrapping: true, 20 | autoCloseBrackets: true, 21 | autofocus: true, 22 | viewportMargin: Number.POSITIVE_INFINITY, 23 | inputStyle, 24 | highlightFormatting: false, 25 | fencedCodeBlockHighlighting: false, 26 | xml: false, 27 | smartIndent: false, 28 | placeholder, 29 | extraKeys: { 30 | Home: "goLineLeft", 31 | End: "goLineRight", 32 | Enter: (cm) => { 33 | cm.replaceSelection("\n"); 34 | }, 35 | }, 36 | }); 37 | 38 | // ... 39 | for (const element of Array.from( 40 | document.querySelectorAll(".CodeMirror-code"), 41 | )) { 42 | element.setAttribute("spellcheck", "true"); 43 | } 44 | }, 45 | ["object", "string", "string"], 46 | ); 47 | 48 | // tabs 49 | self.define("init_tabs", ({ markdown }) => { 50 | const text_button = document.getElementById("text_button"); 51 | const text_tab = document.getElementById("text_tab"); 52 | 53 | const preview_button = document.getElementById("preview_button"); 54 | const preview_tab = document.getElementById("preview_tab"); 55 | 56 | if (text_button && preview_button) { 57 | text_button.addEventListener("click", () => { 58 | preview_button.classList.remove("primary"); 59 | text_button.classList.add("primary"); 60 | 61 | preview_tab.style.display = "none"; 62 | text_tab.style.display = "block"; 63 | }); 64 | 65 | preview_button.addEventListener("click", async () => { 66 | text_button.classList.remove("primary"); 67 | preview_button.classList.add("primary"); 68 | 69 | text_tab.style.display = "none"; 70 | preview_tab.style.display = "block"; 71 | 72 | // render 73 | preview_tab.innerHTML = ""; 74 | preview_tab.innerHTML = await ( 75 | await fetch("/api/v1/pages/_app/render", { 76 | method: "POST", 77 | headers: { 78 | "Content-Type": "application/json", 79 | }, 80 | body: JSON.stringify({ 81 | content: globalThis.editor.getValue(), 82 | }), 83 | }) 84 | ).text(); 85 | }); 86 | } 87 | }); 88 | })(); 89 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/dialogs.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("dialogs"); 3 | 4 | class Dialog { 5 | #id; 6 | #element; 7 | 8 | constructor(id) { 9 | this.#id = id; 10 | this.#element = document.getElementById(id); 11 | } 12 | 13 | open() { 14 | this.#element.showModal(); 15 | } 16 | 17 | close() { 18 | this.#element.close(); 19 | 20 | // run events 21 | for (const event of self._event_store.dialogs[this.#id]) { 22 | event(); 23 | } 24 | } 25 | } 26 | 27 | self.define("add", function ({ $ }, id) { 28 | // init dialogs 29 | if (!$.dialogs) { 30 | $.dialogs = {}; 31 | } 32 | 33 | // init event store 34 | if (!$._event_store) { 35 | $._event_store = {}; 36 | } 37 | 38 | if (!$._event_store.dialogs) { 39 | $._event_store.dialogs = {}; 40 | } 41 | 42 | // add dialog 43 | $.dialogs[id] = new Dialog(id); 44 | }); 45 | 46 | self.define("get", function ({ $ }, id) { 47 | return $.dialogs[id]; 48 | }); 49 | 50 | self.define("event:confirm", function ({ $ }, id, callback) { 51 | if (!$._event_store.dialogs[id]) { 52 | $._event_store.dialogs[id] = []; 53 | } 54 | 55 | $._event_store.dialogs[id].push(callback); 56 | }); 57 | })(); 58 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/me.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("me"); 3 | 4 | self.LOGIN_ACCOUNT_TOKENS = JSON.parse( 5 | window.localStorage.getItem("login_account_tokens") || "{}", 6 | ); 7 | 8 | self.define("me", async function (_) { 9 | globalThis.__user = ( 10 | await (await fetch("/api/v0/auth/me")).json() 11 | ).payload; 12 | 13 | return globalThis.__user; 14 | }); 15 | 16 | self.define("logout", async function ({ $ }) { 17 | if ( 18 | !(await trigger("app::confirm", [ 19 | "Are you sure you would like to do this?", 20 | ])) 21 | ) { 22 | return; 23 | } 24 | 25 | // whoami? 26 | const me = await $.me(); 27 | 28 | // remove token 29 | const tokens = $.LOGIN_ACCOUNT_TOKENS; 30 | 31 | if (tokens[me.username]) { 32 | delete tokens[me.username]; 33 | $.set_login_account_tokens(tokens); 34 | } 35 | 36 | // ... 37 | fetch("/api/v0/auth/untag", { method: "POST" }).then(() => { 38 | fetch("/api/v0/auth/logout", { method: "POST" }).then(() => { 39 | // get the first saved token and login as that 40 | const first = Object.keys(tokens)[0]; 41 | 42 | if (first) { 43 | $.login(first); 44 | return; 45 | } 46 | 47 | window.location.href = "/"; 48 | }); 49 | }); 50 | }); 51 | 52 | self.define( 53 | "set_login_account_tokens", 54 | ({ $ }, value) => { 55 | $.LOGIN_ACCOUNT_TOKENS = value; 56 | window.localStorage.setItem( 57 | "login_account_tokens", 58 | JSON.stringify(value), 59 | ); 60 | }, 61 | ["object"], 62 | ); 63 | 64 | self.define("login", function ({ $ }, username) { 65 | const token = self.LOGIN_ACCOUNT_TOKENS[username]; 66 | 67 | if (!token) { 68 | return; 69 | } 70 | 71 | window.location.href = `/api/v0/auth/callback?token=${token}`; 72 | }); 73 | 74 | self.define("ui::render", function ({ $ }, element) { 75 | element.innerHTML = ""; 76 | for (const token of Object.entries($.LOGIN_ACCOUNT_TOKENS)) { 77 | element.innerHTML += ``; 88 | } 89 | }); 90 | })(); 91 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/notifications.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("notifications", ["app"]); 3 | 4 | self.define("delete", async function ({ $, app }, id, conf) { 5 | // if (!conf) { 6 | // if (!confirm("Are you sure you want to do this?")) { 7 | // return; 8 | // } 9 | // } 10 | 11 | fetch(`/api/v0/auth/notifications/${id}`, { 12 | method: "DELETE", 13 | }) 14 | .then((res) => res.json()) 15 | .then((res) => { 16 | if (document.getElementById(`notif:${id}`)) { 17 | trigger("app::toast", [ 18 | res.success ? "success" : "error", 19 | res.success ? "Notification deleted!" : res.message, 20 | ]); 21 | 22 | app.smooth_remove( 23 | document.getElementById(`notif:${id}`), 24 | 500, 25 | ); 26 | } 27 | }); 28 | }); 29 | 30 | self.define("clear", async function (_, conf) { 31 | // if (!conf) { 32 | // if (!confirm("Are you sure you want to do this?")) { 33 | // return; 34 | // } 35 | // } 36 | 37 | fetch("/api/v0/auth/notifications/clear", { 38 | method: "DELETE", 39 | }) 40 | .then((res) => res.json()) 41 | .then((res) => { 42 | trigger("app::toast", [ 43 | res.success ? "success" : "error", 44 | res.success ? "Notifications cleared!" : res.message, 45 | ]); 46 | }); 47 | }); 48 | 49 | self.define("onopen", function ({ $ }, id) { 50 | if (window.localStorage.getItem("clear_notifs") === "true") { 51 | $.delete(id, true); 52 | } 53 | }); 54 | })(); 55 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/reactions.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("reactions", ["app"]); 3 | 4 | self.define("create", function (_, id, type) { 5 | fetch(`/api/v1/reactions/${id}`, { 6 | method: "POST", 7 | headers: { 8 | "Content-Type": "application/json", 9 | }, 10 | body: JSON.stringify({ 11 | type, 12 | }), 13 | }) 14 | .then((res) => res.json()) 15 | .then((res) => { 16 | trigger("app::toast", [ 17 | res.success ? "success" : "error", 18 | res.message || "Reaction added!", 19 | ]); 20 | }); 21 | }); 22 | 23 | self.define("delete", function (_, id) { 24 | fetch(`/api/v1/reactions/${id}`, { 25 | method: "DELETE", 26 | }) 27 | .then((res) => res.json()) 28 | .then((res) => { 29 | trigger("app::toast", [ 30 | res.success ? "success" : "error", 31 | res.message || "Reaction removed!", 32 | ]); 33 | }); 34 | }); 35 | 36 | self.define("has-reacted", function (_, id) { 37 | return new Promise((resolve, _) => { 38 | fetch(`/api/v1/reactions/${id}`, { 39 | method: "GET", 40 | }) 41 | .then((res) => res.json()) 42 | .then((res) => { 43 | return resolve(res.success); 44 | }); 45 | }); 46 | }); 47 | 48 | self.define("toggle", async function ({ $, app }, id, type, target) { 49 | await app.debounce("reactions::toggle"); 50 | const remove = (await $["has-reacted"](id)) === true; 51 | 52 | if (remove) { 53 | if (target) { 54 | target.classList.remove("green"); 55 | target.querySelector("svg").classList.remove("filled"); 56 | 57 | const count = target.querySelector(".notification"); 58 | 59 | if (count) { 60 | count.innerText = Number.parseInt(count.innerText) - 1; 61 | } else { 62 | const new_count = document.createElement("span"); 63 | new_count.className = "notification camo"; 64 | new_count.innerText = "1"; 65 | target.appendChild(new_count); 66 | } 67 | } 68 | 69 | return $.delete(id); 70 | } 71 | 72 | if (target) { 73 | target.classList.add("green"); 74 | target.querySelector("svg").classList.add("filled"); 75 | 76 | const count = target.querySelector(".notification"); 77 | 78 | if (count) { 79 | count.innerText = Number.parseInt(count.innerText) + 1; 80 | } else { 81 | const new_count = document.createElement("span"); 82 | new_count.className = "notification camo"; 83 | new_count.innerText = "1"; 84 | target.appendChild(new_count); 85 | } 86 | } 87 | 88 | return $.create(id, type); 89 | }); 90 | })(); 91 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/reports.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("reports"); 3 | 4 | self.define("fill", function ({ $ }, type, target) { 5 | $.type = type; 6 | $.target = target; 7 | }); 8 | 9 | self.define("bootstrap", function (_, type, target) { 10 | window.open(`/intents/report?type=${type}&target=${target}`); 11 | }); 12 | 13 | self.define("file", function ({ $ }, e) { 14 | e.preventDefault(); 15 | fetch(`/api/v1/${$.type}/${$.target}/report`, { 16 | method: "POST", 17 | headers: { 18 | "Content-Type": "application/json", 19 | }, 20 | body: JSON.stringify({ 21 | content: e.target.content.value, 22 | token: e.target.querySelector(".h-captcha textarea").value, 23 | }), 24 | }) 25 | .then((res) => res.json()) 26 | .then((res) => { 27 | if (res.success === true) { 28 | alert(res.message); 29 | window.close(); 30 | return; 31 | } 32 | 33 | trigger("app::shout", ["caution", res.message]); 34 | e.target.reset(); 35 | }); 36 | }); 37 | })(); 38 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/search.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("search", ["app"]); 3 | 4 | self.drivers = { 5 | responses: "/search/responses?q=", 6 | questions: "/search/questions?q=", 7 | users: "/search/users?q=", 8 | posts: "/search/posts?q=", 9 | tag: "/search/responses?tag=", 10 | }; 11 | 12 | self.define("run", function ({ $, app }, driver, query) { 13 | const loc = $.drivers[driver]; 14 | 15 | if (!loc) { 16 | return app.toast("error", "Invalid search driver"); 17 | } 18 | 19 | window.location.href = `${loc}${query}`; 20 | }); 21 | })(); 22 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/tokens.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("tokens"); 3 | 4 | // methods 5 | self.define("store_token", async function ({ $ }, token) { 6 | const tokens = $.tokens(); 7 | tokens.push(token); 8 | window.location.setItem("tokens", JSON.stringify(tokens)); 9 | return tokens; 10 | }); 11 | 12 | self.define("remove_token", async function ({ $ }, token) { 13 | const tokens = $.tokens(); 14 | const index = tokens.indexOf(token) || 0; 15 | tokens.splice(index, 1); 16 | window.location.setItem("tokens", JSON.stringify(tokens)); 17 | return tokens; 18 | }); 19 | 20 | self.define("tokens", async function (_) { 21 | return JSON.parse(window.location.getItem("tokens") || "[]"); 22 | }); 23 | 24 | // ui 25 | })(); 26 | -------------------------------------------------------------------------------- /crates/rainbeam/static/js/warnings.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const self = reg_ns("warnings", []); 3 | 4 | const accepted_warnings = JSON.parse( 5 | window.localStorage.getItem("accepted_warnings") || "{}", 6 | ); 7 | 8 | self.define( 9 | "open", 10 | async function ({ $ }, warning_id, warning_hash, warning_page = "") { 11 | // check localStorage for this warning_id 12 | if (accepted_warnings[warning_id] !== undefined) { 13 | // check hash 14 | if (accepted_warnings[warning_id] !== warning_hash) { 15 | // hash is not the same, show dialog again 16 | delete accepted_warnings[warning_id]; 17 | } else { 18 | // return 19 | return; 20 | } 21 | } 22 | 23 | // open page 24 | if (warning_page !== "") { 25 | window.location.href = warning_page; 26 | return; 27 | } 28 | }, 29 | ); 30 | 31 | self.define( 32 | "accept", 33 | function ({ _ }, warning_id, warning_hash, redirect = "/") { 34 | accepted_warnings[warning_id] = warning_hash; 35 | 36 | window.localStorage.setItem( 37 | "accepted_warnings", 38 | JSON.stringify(accepted_warnings), 39 | ); 40 | 41 | setTimeout(() => { 42 | window.location.href = redirect; 43 | }, 100); 44 | }, 45 | ); 46 | })(); 47 | -------------------------------------------------------------------------------- /crates/rainbeam/static/manifest.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Rainbeam", 3 | "name": "Rainbeam", 4 | "description": "Ask, share, socialize!", 5 | "start_url": "https://rainbeam.net", 6 | "scope": "https://rainbeam.net", 7 | "display": "standalone", 8 | "categories": [ 9 | "social" 10 | ], 11 | "lang": "en", 12 | "icons": [ 13 | { 14 | "src": "/static/favicon.svg", 15 | "type": "image/svg+xml", 16 | "sizes": "128x128" 17 | }, 18 | { 19 | "src": "/static/images/logo/logo_192x192.png", 20 | "type": "image/png", 21 | "sizes": "192x192" 22 | }, 23 | { 24 | "src": "/static/images/logo/logo_512x512.png", 25 | "type": "image/png", 26 | "sizes": "512x512" 27 | } 28 | ], 29 | "theme_color": "#b62f5a", 30 | "background_color": "#f2f2f2" 31 | } -------------------------------------------------------------------------------- /crates/rainbeam/static/site/README.md: -------------------------------------------------------------------------------- 1 | # static/site/ 2 | 3 | This directory is where you can place your site's static Markdown pages. This includes the following files: 4 | 5 | * `about.md` (`/site/about`) 6 | * `tos.md` (`/site/terms-of-service`) 7 | * `privacy.md` (`/site/privacy`) 8 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/audit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Audit log - {{ config.name }}{% 2 | endblock %} {% block head %} 3 | 4 | {% endblock %} {% block nav_left %} 5 | 6 | {{ icon "house" }} 7 | {{ text "general:link.timeline" }} 8 | 9 | 10 | 11 | {{ icon "inbox" }} 12 | 13 | {{ text "general:link.inbox" }} 14 | {% if unread != 0 %} 15 | {{ unread }} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | {{ icon "compass" }} 22 | 23 | {{ text "general:link.discover" }} 24 | 25 | 26 | {% endblock %} {% block nav_right %} 27 | 28 | {{ icon "bell" }} 29 | 30 | {% endblock %} {% block content %} 31 |
32 |
33 |
34 | My Inbox 35 | Audit Log 36 | Reports 37 |
38 | 39 |
40 | Mod Actions 41 | IP Bans 42 |
43 | 44 | {% if logs.len() == 0 %} 45 |
46 | {{ text "general:text.no_results" }} 47 |
48 | {% endif %} 49 | 50 | 51 | 52 | {% for notif in logs %} 53 | {% let show_mark_as_read = false %} 54 | {% include "components/notification.html" %} 55 | {% endfor %} 56 | 57 | 58 |
59 | {% if page > 0 %} 60 | {{ text "general:link.previous" }} 63 | {% else %} 64 |
65 | {% endif %} {% if logs.len() != 0 %} 66 | {{ text "general:link.next" }} 69 | {% endif %} 70 |
71 |
72 |
73 | {% call super() %} {% endblock %} 74 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/big_friend.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | @{{ user.username }} 12 | 13 | 14 | {% let use_static = false %} {% include "components/user_note.html" %} 15 |
16 |
17 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/footer.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/more_response_options.html: -------------------------------------------------------------------------------- 1 |
2 | {{ icon "ellipsis" }} 3 | 4 |
5 | 11 | 12 |

Tags should be separated by a comma.

13 | 14 | 20 | 21 |

22 | Users must accept this warning to view your post's contents. This 23 | may be required if your post contains sensitive content. 24 |

25 | 26 | 32 | 33 |

34 | Putting the full ID of another response here will mark your response 35 | as a reply to the response you mentioned here. 36 |

37 | 38 |
39 | 40 | 41 | 42 | {% if let Some(user) = profile %} {% if 43 | user.metadata.is_true("sparkler:private_profile") | 44 | user.metadata.is_true("rainbeam:nsfw_profile") %} 45 | 48 | {% endif %} {% endif %} 49 |
50 | 51 |

52 | Unlisted responses will be hidden from public timelines. 53 |

54 |
55 |
56 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/notification.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{ rainbeam_shared::ui::render_markdown(notif.title)|safe }} 5 | 6 | {{ notif.timestamp }} 7 |
8 | 9 |
10 | 11 |
{{ rainbeam_shared::ui::render_markdown(notif.content)|safe }}
12 | 13 | 14 |
15 | {% if !notif.address.is_empty() %} 16 | 22 | {{ icon "external-link" }} {{ text "general:link.open" }} 23 | 24 | {% endif %} {% if show_mark_as_read %} 25 | 31 | {% endif %} 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/private_question.html: -------------------------------------------------------------------------------- 1 |
2 | {{ icon "lock-keyhole" }} 3 | Question author limits who can view their questions. 4 |
5 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/private_response.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/profile_card.html: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/response.html: -------------------------------------------------------------------------------- 1 | {% let question = response.0.clone() %} {% let comment_count = response.2 %} {% 2 | let reaction_count = response.3 %} {% let response = response.1.clone() %} 3 | 4 |
10 | {% include "components/response_inner.html" %} 11 |
12 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/user_note.html: -------------------------------------------------------------------------------- 1 | {% if let Some(profile) = profile %} {% if 2 | user.metadata.exists("sparkler:status_note") | (profile.id == user.id) %} {% let 3 | note = user.metadata.soft_get("sparkler:status_note") %} {% if !note.is_empty() 4 | | (profile.id == user.id) %} 5 | 22 | 23 | 24 |
25 |
26 | {{ user.username }} 27 |
28 | {% if profile.id == user.id %} 29 | 35 | {{ icon "pen" }} 36 | 37 | {% endif %} 38 | 39 | 73 | 74 | 82 |
83 |
84 | 85 |
86 | {{ rainbeam_shared::ui::render_markdown(note)|safe }} 87 |
88 |
89 | {% endif %} {% endif %} {% endif %} 90 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/components/warning.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 14 | 15 | 19 | {{ warning.moderator.username }} 20 | 21 | 22 | 23 | {{ warning.timestamp }} 24 |
25 | 26 |
27 | 28 | 60 |
61 |
62 |
63 | 64 |
65 | {{ rainbeam_shared::ui::render_markdown(warning.content)|safe }} 66 |
67 |
68 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Uh oh! 😿 - {{ config.name }}{% 2 | endblock %} {% block head %} 3 | 4 | {% endblock %} {% block content %} 5 | 20 | {% call super() %} {% endblock %} 21 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/fun/carp.html: -------------------------------------------------------------------------------- 1 | {% extends "raw_base.html" %} {% block title %}Carpgraph{% endblock %} {% block 2 | base %} 3 | 22 | 23 | 45 | {% call super() %} {% endblock %} 46 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/fun/styled_profile_card.html: -------------------------------------------------------------------------------- 1 | 2 | {% include "components/profile_card.html" %} 3 | {% let raw_metadata = crate::routing::pages::clean_metadata_raw(user.metadata) %} 4 | {% include "components/theming.html" %} 5 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/general_markdown_text.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}{{ title }} - {{ config.name }}{% 2 | endblock %} {% block head %} 3 | 4 | 7 | {% endblock %} {% block content %} 8 |
9 |
10 |
11 | {{ rainbeam_shared::ui::render_markdown(text)|safe }} 12 |
13 |
14 |
15 | 16 | 23 | {% call super() %} {% endblock %} 24 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}{{ config.name }}{% endblock %} {% 2 | block head %} 3 | 4 | {% endblock %} {% block content %} 5 |
6 | 7 |
8 |
9 |

10 | {{ config.name }} 11 |

12 | 13 |

14 | {{ config.description }} 15 |

16 |
17 | 18 | 47 |
48 | 49 | 53 | 54 | {% include "components/footer.html" %} {% call super() %} {% endblock %} 55 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/intents/report.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}{{ config.name }}{% endblock %} {% 2 | block head %} 3 | 4 | {% endblock %} {% block nav_left %} 5 | 8 | {% endblock %} {% block content %} 9 |
10 |
11 |
12 |
13 | 16 | 17 | 23 | 24 |

25 | {{ text "report.html:text.please_describe" }} 26 |

27 | 28 |

{{ text "report.html:text.details1" }}

29 |

{{ text "report.html:text.details2" }}

30 |
31 | 32 |
36 | 37 |
38 |
39 | 42 | 43 | 46 |
47 |
48 |
49 |
50 | 51 | 58 | 59 | {% include "components/footer.html" %} {% call super() %} {% endblock %} 60 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/ipbans.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Audit log - {{ config.name }}{% 2 | endblock %} {% block head %} 3 | 4 | {% endblock %} {% block nav_left %} 5 | 6 | {{ icon "house" }} 7 | {{ text "general:link.timeline" }} 8 | 9 | 10 | 11 | {{ icon "inbox" }} 12 | 13 | {{ text "general:link.inbox" }} 14 | {% if unread != 0 %} 15 | {{ unread }} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | {{ icon "compass" }} 22 | 23 | {{ text "general:link.discover" }} 24 | 25 | 26 | {% endblock %} {% block nav_right %} 27 | 28 | {{ icon "bell" }} 29 | 30 | {% endblock %} {% block content %} 31 |
32 |
33 |
34 | My Inbox 35 | Audit Log 36 | Reports 37 |
38 | 39 |
40 | Mod Actions 41 | IP Bans 44 |
45 | 46 | {% if bans.len() == 0 %} 47 |
48 | {{ text "general:text.no_results" }} 49 |
50 | {% endif %} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {% for ban in bans %} 64 | 65 | 70 | 75 | 76 | 77 | {% endfor %} 78 | 79 |
IPModeratorNote
66 | {{ ban.ip }} 69 | 71 | {{ ban.moderator.username }} 74 |

{{ ban.reason }}

80 |
81 |
82 | 83 | 105 | {% call super() %} {% endblock %} 106 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/market/base.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} {% block title %}Market - {{ config.name }}{% 2 | endblock %} {% block nav_left %} 3 | 4 | {{ icon "house" }} 5 | {{ text "general:link.timeline" }} 6 | 7 | 8 | 9 | {{ icon "inbox" }} 10 | 11 | {{ text "general:link.inbox" }} 12 | {% if unread != 0 %} 13 | {{ unread }} 14 | {% endif %} 15 | 16 | 17 | 18 | 19 | {{ icon "compass" }} 20 | 21 | {{ text "general:link.discover" }} 22 | 23 | 24 | {% endblock %} {% block nav_right %} 25 | 26 | {{ icon "bell" }} {% if notifs != 0 %} 27 | {{ notifs }} 28 | {% endif %} 29 | 30 | {% endblock %} {% block content %} {% if let Some(profile) = profile %} {% let 31 | other = profile.clone() %} {% if profile.username == other.username %} 32 |
33 | {% endif %} {% let raw_metadata = 34 | crate::routing::pages::clean_metadata_raw(other.metadata) %} {% include 35 | "components/theming.html" %} {% endif %} {% if let Some(user) = profile %} 36 | 65 | {% endif %} {% call super() %} {% endblock %} 66 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/market/components/form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 13 |
14 | 15 |
16 | 17 | 26 |
27 | 28 |
29 | 30 | 38 |
39 | 40 |
41 | 42 | 51 |
52 | 53 |
54 | 60 | 63 |
64 | 65 |
66 | 67 |
68 | 71 | {{ text "general:dialog.cancel" }} 74 |
75 | 76 | 85 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/market/components/listing.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ item.name }} 4 |
5 | {{ item.type.to_string() }} 6 | {{ item.timestamp }} 7 |
8 |
9 | 10 |
11 | 12 | @{{ author.username }} 20 | {{ author.username }} 21 | 22 | 23 | 24 | {% include "market/components/price.html" %} 25 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/market/components/price.html: -------------------------------------------------------------------------------- 1 | {{ icon "tag" }} {% if item.cost == -1 %} 2 | Off-sale 3 | {% else if item.cost == 0 %} 4 | Free 5 | {% else %} 6 | C${{ item.cost }} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/market/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block m_nav_right %} 2 |
3 | 11 | 12 | 13 |
14 | {% endblock %} {% block panel %} 15 |
16 | 17 | {% if creator.is_empty() %} 18 | 49 | {% endif %} {% if items.len() == 0 %} 50 |
51 | {{ text "general:text.no_results" }} 52 |
53 | {% endif %} 54 | 55 | 56 |
57 | {% for (item, author) in items %} {% include "components/listing.html" 58 | %} {% endfor %} 59 |
60 | 61 | 62 |
63 | {% if page > 0 %} 64 | {{ text "general:link.previous" }} 69 | {% else %} 70 |
71 | {% endif %} {% if items.len() != 0 %} 72 | {{ text "general:link.next" }} 77 | {% endif %} 78 |
79 |
80 | {% call super() %} {% endblock %} 81 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/market/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block panel %} 2 |
6 | {% include "components/form.html" %} 7 |
8 | 9 | 34 | {% call super() %} {% endblock %} 35 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/README.md: -------------------------------------------------------------------------------- 1 | # partials 2 | 3 | The main section of most timeline pages for embedding with specific endpoints as well as page endpoints. 4 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/components/compose.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% include "components/more_response_options.html" %} 4 | 5 |
6 |
7 |
8 |
9 | 10 | 11 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/components/layout_playground.html: -------------------------------------------------------------------------------- 1 | {% extends "raw_base.html" %} {% block title %}Layout Playground{% endblock %} 2 | {% block base %} 3 | 20 | 21 |
22 |
23 |
{{ layout.render_block()|safe }}
24 |
25 |
26 | {% call super() %} {% endblock %} 27 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/components/theme_playground.html: -------------------------------------------------------------------------------- 1 | {% extends "raw_base.html" %} {% block title %}Theme Playground{% endblock %} {% 2 | block base %} 3 |
4 |
5 |
6 |

Regular card Example link

7 | 8 |
9 |

Secondary card

10 | 11 |
12 | 13 | 16 | 17 | 18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 |
30 |

Regular card (shadow)

31 | 32 |

Heading 1

33 |

Heading 2

34 |

Heading 3

35 | 36 |
37 |

Secondary card (shadow)

38 | 39 |

Heading 4

40 |
Heading 5
41 |
Heading 6
42 |
43 |
44 |
45 |
46 | 47 | 50 | {% call super() %} {% endblock %} 51 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/profile/feed.html: -------------------------------------------------------------------------------- 1 | 2 | {% let anonymous_username = other.metadata.kv.get("sparkler:anonymous_username") %} 3 | {% let anonymous_avatar = other.metadata.kv.get("sparkler:anonymous_avatar") %} 4 | 5 | {% for response in responses %} 6 | {% let relationship = relationships.get(response.1.author.id).unwrap().to_owned() %} 7 | {% if (relationship != crate::model::RelationshipStatus::Friends 8 | && relationship != crate::model::RelationshipStatus::Blocked 9 | && response.1.author.metadata.is_true("sparkler:private_profile")) | (response.1.author.group == -1) %} 10 | {% include "components/private_response.html" %} 11 | {% else %} 12 | {% let is_pinned = false %} 13 | {% let show_pin_button = true %} 14 | {% let do_not_render_question = false %} 15 | {% let show_comments = true %} 16 | {% let do_render_nested = true %} 17 | {% include "components/response.html" %} 18 | {% endif %} 19 | {% endfor %} 20 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/timelines/discover/questions_most.html: -------------------------------------------------------------------------------- 1 | 2 | {% if users.len() == 0 %} 3 |
4 | {{ text "general:text.no_results" }} 5 |
6 | {% endif %} 7 | 8 | 9 | {% for (count, user) in users %} 10 | 42 | {% endfor %} 43 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/timelines/discover/responses_most.html: -------------------------------------------------------------------------------- 1 | 2 | {% if users.len() == 0 %} 3 |
4 | {{ text "general:text.no_results" }} 5 |
6 | {% endif %} 7 | 8 | 9 | {% for (count, user) in users %} 10 | 42 | {% endfor %} 43 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/timelines/discover/responses_top.html: -------------------------------------------------------------------------------- 1 | 2 | {% if responses.len() == 0 %} 3 |
4 | {{ text "general:text.no_results" }} 5 |
6 | {% endif %} 7 | 8 | 9 | {% for response in responses %} 10 | {% let relationship = relationships.get(response.1.author.id).unwrap().to_owned() %} 11 | {% if (relationship != crate::model::RelationshipStatus::Friends 12 | && response.1.author.metadata.is_true("sparkler:private_profile")) 13 | | (response.1.author.group == -1) 14 | | (relationship == crate::model::RelationshipStatus::Blocked) 15 | | response.1.author.has_label(authbeam::model::RESERVED_LABEL_QUARANTINE) %} 16 | {% include "components/private_response.html" %} 17 | {% else %} 18 | {% let is_pinned = false %} 19 | {% let show_pin_button = false %} 20 | {% let do_not_render_question = false %} 21 | {% let anonymous_username = Some("anonymous") %} 22 | {% let anonymous_avatar = Some("") %} 23 | {% let show_comments = true %} 24 | {% let do_render_nested = true %} 25 | {% include "components/response.html" %} 26 | {% endif %} 27 | {% endfor %} 28 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/timelines/global_questions.html: -------------------------------------------------------------------------------- 1 | 2 | {% for question in questions %} 3 | {% let relationship = relationships.get(question.0.author.id).unwrap().to_owned() %} 4 | {% if (relationship != crate::model::RelationshipStatus::Friends 5 | && question.0.author.metadata.is_true("sparkler:private_profile")) 6 | | (question.0.author.group == -1) 7 | | (relationship == crate::model::RelationshipStatus::Blocked) 8 | | question.0.author.has_label(authbeam::model::RESERVED_LABEL_QUARANTINE) %} 9 | {% include "components/private_question.html" %} 10 | {% else %} 11 | {% let show_responses = true %} 12 | {% include "components/global_question.html" %} 13 | {% endif %} 14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/timelines/timeline.html: -------------------------------------------------------------------------------- 1 | 2 | {% if responses.len() == 0 %} 3 |
4 | {{ text "general:text.no_results" }} 5 |
6 | {% endif %} 7 | 8 | 9 | {% let user = profile.as_ref().unwrap() %} 10 | {% for response in responses %} 11 | {% let relationship = relationships.get(response.1.author.id).unwrap().to_owned() %} 12 | {% if (relationship != crate::model::RelationshipStatus::Friends 13 | && response.1.author.metadata.is_true("sparkler:private_profile")) 14 | | (response.1.author.group == -1) 15 | | (relationship == crate::model::RelationshipStatus::Blocked) 16 | | response.1.author.has_label(authbeam::model::RESERVED_LABEL_QUARANTINE) %} 17 | {% include "components/private_response.html" %} 18 | {% else %} 19 | {% let is_pinned = false %} 20 | {% let show_pin_button = false %} 21 | {% let do_not_render_question = false %} 22 | {% let anonymous_username = user.metadata.kv.get("sparkler:anonymous_username") %} 23 | {% let anonymous_avatar = user.metadata.kv.get("sparkler:anonymous_avatar") %} 24 | {% let show_comments = true %} 25 | {% let do_render_nested = true %} 26 | {% include "components/response.html" %} 27 | {% endif %} 28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/views/comments.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% for comment in comments %} 4 | {% let show_replies = true %} 5 | {% include "components/comment.html" %} 6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/partials/views/reactions.html: -------------------------------------------------------------------------------- 1 | {% if reactions.len() > 0 %} 2 |
3 |
{{ icon "heart" }}
4 | 5 | {% for reaction in reactions %} 6 | 7 | @{{ reaction.user.username }} 15 | 16 | {% endfor %} 17 |
18 | {% endif %} 19 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/ComponentName::About.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {% if let Some(biography) = other.metadata.kv.get("sparkler:biography") %} 4 | {{ rainbeam_shared::ui::render_markdown(biography)|safe }} 5 | {% endif %} 6 |
7 | 8 | 9 | {% if let Some(sidebar) = other.metadata.kv.get("sparkler:sidebar") %} 10 | {% if !sidebar.is_empty() %} 11 | 14 | {% endif %} {% endif %} {% if other.links.len() > 0 %} 15 | 16 | 17 | {% for (k, v) in other.links %} 18 | 19 | 24 | 25 | {% endfor %} 26 | 27 | 28 | {% endif %} 29 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/ComponentName::Banner.html: -------------------------------------------------------------------------------- 1 | {% if let Some(fit) = other.metadata.kv.get("sparkler:banner_fit") %} 2 | 9 | {% else %} 10 | 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/ComponentName::Feed.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/ComponentName::Footer.html: -------------------------------------------------------------------------------- 1 |
{% include "components/footer.html" %}
2 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/ComponentName::Name.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 | 10 | 11 | {% if other.tier >= config.tiers.avatar_crown %} 12 |
👑
13 | {% endif %} 14 |
15 | 16 | 17 |
18 |
19 |

20 | {% if let Some(display_name) = other.metadata.kv.get("sparkler:display_name") %} 21 | {% if !display_name.trim().is_empty() %} 22 | {{ display_name }} 23 | {% else %} 24 | {{ other.username }} 25 | {% endif %} 26 | {% else %} 27 | {{ other.username }} 28 | {% endif %} 29 |

30 | 31 | {% let use_static = true %} 32 | {% let user = other.clone() %} 33 | {% include "components/user_note.html" %} 34 |
35 | 36 |

{{ other.username }}

37 | 38 | {% if is_following_you == true %} 39 | Follows you 40 | {% endif %} 41 | 42 |
43 | {% for badge in other.badges %} 44 | 48 | {{ icon "award" }} 49 | {{ badge.0 }} 50 | 51 | {% endfor %} 52 | 53 | {% if other.tier >= config.tiers.profile_badge %} 54 | 58 | {{ icon "crown" }} 59 | Supporter 60 | 61 | {% endif %} 62 | 63 | {% if other.group == -1 %} 64 | 68 | {{ icon "shield-ban" }} 69 | Banned 70 | 71 | {% endif %} 72 |
73 |
74 |
75 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/ComponentName::Tabs.html: -------------------------------------------------------------------------------- 1 | 2 | {% if !hide_social | is_helper %} 3 | 4 | 20 | {% endif %} 21 | 22 | 28 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/README.md: -------------------------------------------------------------------------------- 1 | `crates/authbeam/src/layout.rs::ComponentName` 2 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/free_renderer.html: -------------------------------------------------------------------------------- 1 | {{ component.render(other)|safe }} 2 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/layout_components/renderer.html: -------------------------------------------------------------------------------- 1 | 2 | {% let rendered = component.render_with_junk( 3 | other, 4 | config, 5 | profile, 6 | lang, 7 | response_count.to_owned(), 8 | questions_count.to_owned(), 9 | followers_count.to_owned(), 10 | following_count.to_owned(), 11 | friends_count.to_owned(), 12 | is_following.to_owned(), 13 | is_following_you.to_owned(), 14 | relationship.to_owned(), 15 | lock_profile.to_owned(), 16 | disallow_anonymous.to_owned(), 17 | require_account.to_owned(), 18 | hide_social.to_owned(), 19 | is_powerful.to_owned(), 20 | is_helper.to_owned(), 21 | is_self.to_owned() 22 | ) %} 23 | 24 | {% if rendered == "ComponentName::Banner" %} 25 | {% include "profile/layout_components/ComponentName::Banner.html" %} 26 | {% else if rendered == "ComponentName::Feed" %} 27 | {% include "profile/layout_components/ComponentName::Feed.html" %} 28 | {% else if rendered == "ComponentName::Tabs" %} 29 | {% include "profile/layout_components/ComponentName::Tabs.html" %} 30 | {% else if rendered == "ComponentName::Ask" %} 31 | {% include "profile/layout_components/ComponentName::Ask.html" %} 32 | {% else if rendered == "ComponentName::Name" %} 33 | {% include "profile/layout_components/ComponentName::Name.html" %} 34 | {% else if rendered == "ComponentName::About" %} 35 | {% include "profile/layout_components/ComponentName::About.html" %} 36 | {% else if rendered == "ComponentName::Actions" %} 37 | {% include "profile/layout_components/ComponentName::Actions.html" %} 38 | {% else if rendered == "ComponentName::Footer" %} 39 | {% include "profile/layout_components/ComponentName::Footer.html" %} 40 | {% else %} 41 | {{ rendered|safe }} 42 | {% endif %} 43 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/questions.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block pillmenu %} 2 | 3 | {{ text "profile:link.feed" }} 5 | {{ response_count }} 7 | 8 | 9 | 10 | Questions {{ questions_count }} 11 | 12 | 13 | {% if is_helper %} 14 | 15 | {{ text "profile:link.manage" }} Mod 18 | 19 | {% endif %} {% endblock %} {% block search %} 20 | 21 |
22 | 23 | 24 | 31 | 32 |
33 |
34 | 37 |
38 |
39 | {% endblock %} {% block panel %} 40 | 41 | {% if is_self | is_powerful %} 42 | 63 | {% endif %} 64 | 65 | 66 |
67 | 68 | {% for question in questions %} 69 | {% let show_responses = true %} 70 | {% include "components/global_question.html" %} 71 | {% endfor %} 72 | 73 | 74 | {% if questions_count != 0 %} 75 |
76 | {% if page > 0 %} 77 | {{ text "general:link.previous" }} 80 | {% else %} 81 |
82 | {% endif %} {% if questions.len() != 0 %} 83 | {{ text "general:link.next" }} 86 | {% endif %} 87 |
88 | {% endif %} 89 |
90 | {% call super() %} {% endblock %} 91 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/social/blocks.html: -------------------------------------------------------------------------------- 1 | {% extends "./social_base.html" %} {% block pillmenu %} 2 | 3 | {{ text "profile:link.followers" }} 5 | {{ followers_count }} 7 | 8 | 9 | 10 | {{ text "profile:link.following" }} 12 | {{ following_count }} 14 | 15 | 16 | 17 | {{ text "general:link.friends" }} 19 | {{ friends_count }} 21 | 22 | 23 | {% if is_self | is_helper %} 24 | {{ text "general:link.requests" }} 27 | {% endif %} {% if is_helper %} 28 | {{ text "settings:account.html:title.blocks" }} 31 | {% endif %} {% endblock %} {% block panel %} 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% if let Some(user) = profile %} 43 | 44 | 45 | {% for block in blocks %} {% let outbound = block.0.id == user.id %} 46 | 47 | 48 | 49 | 60 | 61 | {% endfor %} 62 | 63 | {% endif %} 64 |
TypeUser
{% if outbound %}Outbound{% else %}Inbound{% endif %} 50 | {% if outbound %} 51 | 52 | {{ block.1.username }} 53 | 54 | {% else %} 55 | 56 | {{ block.0.username }} 57 | 58 | {% endif %} 59 |
65 | 66 | 67 | {% if blocks.len() != 0 %} 68 |
69 | {% if page > 0 %} 70 | {{ text "general:link.previous" }} 73 | {% else %} 74 |
75 | {% endif %} 76 | {{ text "general:link.next" }} 79 |
80 | {% endif %} 81 |
82 | {% call super() %} {% endblock %} 83 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/social/followers.html: -------------------------------------------------------------------------------- 1 | {% extends "./social_base.html" %} {% block pillmenu %} 2 | 3 | {{ text "profile:link.followers" }} 5 | {{ followers_count }} 7 | 8 | 9 | 10 | {{ text "profile:link.following" }} 12 | {{ following_count }} 14 | 15 | 16 | 17 | {{ text "general:link.friends" }} 19 | {{ friends_count }} 21 | 22 | 23 | {% if is_self | is_helper %} 24 | {{ text "general:link.requests" }} 27 | {% endif %} {% if is_helper %} 28 | {{ text "settings:account.html:title.blocks" }} 31 | {% endif %} {% endblock %} {% block panel %} 32 | 33 |
34 | 35 | {% for card in followers %} 36 | {% let user = card.1.clone() %} 37 | {% include "components/profile_card.html" %} 38 | {% endfor %} 39 | 40 | 41 | {% if followers_count != 0 %} 42 |
43 | {% if page > 0 %} 44 | {{ text "general:link.previous" }} 47 | {% else %} 48 |
49 | {% endif %} {% if followers.len() != 0 %} 50 | {{ text "general:link.next" }} 53 | {% endif %} 54 |
55 | {% endif %} 56 |
57 | {% call super() %} {% endblock %} 58 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/social/following.html: -------------------------------------------------------------------------------- 1 | {% extends "./social_base.html" %} {% block pillmenu %} 2 | 3 | {{ text "profile:link.followers" }} 5 | {{ followers_count }} 7 | 8 | 9 | 10 | {{ text "profile:link.following" }} 12 | {{ following_count }} 14 | 15 | 16 | 17 | {{ text "general:link.friends" }} 19 | {{ friends_count }} 21 | 22 | 23 | {% if is_self | is_helper %} 24 | {{ text "general:link.requests" }} 27 | {% endif %} {% if is_helper %} 28 | {{ text "settings:account.html:title.blocks" }} 31 | {% endif %} {% endblock %} {% block panel %} 32 | 33 |
34 | 35 | {% for card in following %} 36 | {% let user = card.2.clone() %} 37 | {% include "components/profile_card.html" %} 38 | {% endfor %} 39 | 40 | 41 | {% if following_count != 0 %} 42 |
43 | {% if page > 0 %} 44 | {{ text "general:link.previous" }} 47 | {% else %} 48 |
49 | {% endif %} {% if following.len() != 0 %} 50 | {{ text "general:link.next" }} 53 | {% endif %} 54 |
55 | {% endif %} 56 |
57 | {% call super() %} {% endblock %} 58 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/social/friend_request.html: -------------------------------------------------------------------------------- 1 | {% extends "../../base.html" %} {% block title %}Friend request - {{ config.name 2 | }}{% endblock %} {% block nav_left %} 3 | 4 | {{ icon "house" }} 5 | {{ text "general:link.timeline" }} 6 | 7 | 8 | 9 | {{ icon "inbox" }} 10 | 11 | {{ text "general:link.inbox" }} 12 | {% if unread != 0 %} 13 | {{ unread }} 14 | {% endif %} 15 | 16 | 17 | 18 | 19 | {{ icon "compass" }} 20 | 21 | {{ text "general:link.discover" }} 22 | 23 | 24 | {% endblock %} {% block nav_right %} 25 | 26 | {{ icon "bell" }} {% if notifs != 0 %} 27 | {{ notifs }} 28 | {% endif %} 29 | 30 | {% endblock %} {% block content %} 31 |
32 |
33 |
34 | 35 |
36 | Accept @{{ other.username }}'s friend request? 37 |
38 | 39 |
40 | 43 | 44 | 47 |
48 |
49 |
50 |
51 | 52 | 83 | 84 | {% if let Some(profile) = profile %} {% let other = profile.clone() %} 85 |
86 | {% let raw_metadata = crate::routing::pages::clean_metadata_raw(other.metadata) 87 | %} {% include "components/theming.html" %} {% endif %} {% call super() %} {% 88 | endblock %} 89 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/social/friends.html: -------------------------------------------------------------------------------- 1 | {% extends "./social_base.html" %} {% block pillmenu %} 2 | 3 | {{ text "profile:link.followers" }} 5 | {{ followers_count }} 7 | 8 | 9 | 10 | {{ text "profile:link.following" }} 12 | {{ following_count }} 14 | 15 | 16 | 17 | {{ text "general:link.friends" }} 19 | {{ friends_count }} 21 | 22 | 23 | {% if is_self | is_helper %} 24 | {{ text "general:link.requests" }} 27 | {% endif %} {% if is_helper %} 28 | {{ text "settings:account.html:title.blocks" }} 31 | {% endif %} {% endblock %} {% block panel %} 32 | 33 |
34 | 35 | {% for relationship in friends %} 36 | {% if other.id != relationship.0.id %} 37 | {% let user = relationship.0.clone () %} 38 | {% include "components/profile_card.html" %} 39 | {% else %} 40 | {% let user = relationship.1.clone () %} 41 | {% include "components/profile_card.html" %} 42 | {% endif %} 43 | {% endfor %} 44 | 45 | 46 | {% if friends_count != 0 %} 47 |
48 | {% if page > 0 %} 49 | {{ text "general:link.previous" }} 52 | {% else %} 53 |
54 | {% endif %} {% if friends.len() != 0 %} 55 | {{ text "general:link.next" }} 58 | {% endif %} 59 |
60 | {% endif %} 61 |
62 | {% call super() %} {% endblock %} 63 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/social/social_base.html: -------------------------------------------------------------------------------- 1 | {% extends "../../base.html" %} {% block title %}{{ other.username }} - {{ 2 | config.name }}{% endblock %} {% block head %} 3 | 4 | {% if let Some(biography) = other.metadata.kv.get("sparkler:biography") %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 22 | 23 | 27 | 28 | {% let biography = biography.replace("\"", "\\\\\"") %} 29 | 30 | 31 | {% endif %} {% endblock %} {% block nav_left %} {% if profile.is_some() %} 32 | 33 | {{ icon "house" }} 34 | {{ text "general:link.timeline" }} 35 | 36 | 37 | 38 | {{ icon "inbox" }} 39 | 40 | {{ text "general:link.inbox" }} 41 | {% if unread != 0 %} 42 | {{ unread }} 43 | {% endif %} 44 | 45 | 46 | 47 | 48 | {{ icon "compass" }} 49 | 50 | {{ text "general:link.discover" }} 51 | 52 | 53 | {% endif %} {% endblock %} {% block nav_right %} {% if profile.is_some() %} 54 | 55 | {{ icon "bell" }} {% if notifs != 0 %} 56 | {{ notifs }} 57 | {% endif %} 58 | 59 | {% endif %} {% endblock %} {% block content %} 60 | 77 | 78 | {% include "components/footer.html" %} 79 | 80 | 81 | {% if is_self %} 82 |
83 | {% endif %} {% let raw_metadata = 84 | crate::routing::pages::clean_metadata_raw(other.metadata) %} {% include 85 | "components/theming.html" %} {% call super() %} {% endblock %} 86 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/profile/warning.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} {% block title %}{{ other.username }} - {{ 2 | config.name }}{% endblock %} {% block content %} {% if let Some(warning) = 3 | other.metadata.kv.get("sparkler:warning") %} {% if !warning.is_empty() %} {% let 4 | warning_hash = rainbeam_shared::hash::hash(warning.to_string()) %} 5 |
6 |
7 |
8 |
9 | {{ text "profile:base.html:text.warning_title" }} 12 |
13 | 14 |
15 | {{ rainbeam_shared::ui::render_markdown(warning)|safe }} 16 |
17 |
18 | 19 | 20 | {{ text "profile:base.html:text.warning_continue" }} 21 | 22 | 23 |
24 | 30 | {{ text "general:dialog.cancel" }} 33 |
34 |
35 |
36 | 37 | 38 | 44 | {% endif %} {% endif %} {% if let Some(profile) = profile %} {% let other = 45 | profile.clone() %} 46 |
47 | {% let raw_metadata = crate::routing::pages::clean_metadata_raw(other.metadata) 48 | %} {% include "components/theming.html" %} {% endif %} {% call super() %} {% 49 | endblock %} 50 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/reports.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Reports - {{ config.name }}{% 2 | endblock %} {% block head %} 3 | 4 | {% endblock %} {% block nav_left %} 5 | 6 | {{ icon "house" }} 7 | {{ text "general:link.timeline" }} 8 | 9 | 10 | 11 | {{ icon "inbox" }} 12 | 13 | {{ text "general:link.inbox" }} 14 | {% if unread != 0 %} 15 | {{ unread }} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | {{ icon "compass" }} 22 | 23 | {{ text "general:link.discover" }} 24 | 25 | 26 | {% endblock %} {% block nav_right %} 27 | 28 | {{ icon "bell" }} 29 | 30 | {% endblock %} {% block content %} 31 |
32 |
33 |
34 | My Inbox 35 | Audit Log 36 | Reports 37 |
38 | 39 | {% if reports.len() == 0 %} 40 |
41 | {{ text "general:text.no_results" }} 42 |
43 | {% endif %} 44 | 45 | 46 | 47 | {% for notif in reports %} 48 | {% let show_mark_as_read = true %} 49 | {% include "components/notification.html" %} 50 | {% endfor %} 51 |
52 |
53 | {% call super() %} {% endblock %} 54 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/search/base.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} {% block title %}"{{ query }}" - Search (page {{ 2 | page }}) on {{ config.name }}{% endblock %} {% block head %} 3 | 4 | {% endblock %} {% block nav_left %} {% if profile.is_some() %} 5 | 6 | {{ icon "house" }} 7 | {{ text "general:link.timeline" }} 8 | 9 | 10 | 11 | {{ icon "inbox" }} 12 | 13 | {{ text "general:link.inbox" }} 14 | {% if unread != 0 %} 15 | {{ unread }} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | {{ icon "compass" }} 22 | 23 | {{ text "general:link.discover" }} 24 | 25 | 26 | {% endif %} {% endblock %} {% block nav_right %} {% if profile.is_some() %} 27 | 28 | {{ icon "bell" }} {% if notifs != 0 %} 29 | {{ notifs }} 30 | {% endif %} 31 | 32 | {% endif %} {% endblock %} {% block content %} 33 |
34 |
35 | 38 | 39 |
{% include "components/search.html" %}
40 | 41 | 42 | {% if results.len() == 0 %} 43 |
44 | {{ text "general:text.no_results" }} 45 |
46 | {% endif %} {% block results %}{% endblock %} 47 | 48 | 49 |
50 | {% if page > 0 %} 51 | 55 | Previous 56 | 57 | {% else %} 58 |
59 | {% endif %} {% if results.len() != 0 %} 60 | 64 | Next 65 | 66 | {% endif %} 67 |
68 |
69 |
70 | 71 | {% if let Some(profile) = profile %} {% let other = profile.clone() %} {% if 72 | profile.username == other.username %} 73 |
74 | {% endif %} {% let raw_metadata = 75 | crate::routing::pages::clean_metadata_raw(other.metadata) %} {% include 76 | "components/theming.html" %} {% endif %} {% call super() %} {% endblock %} 77 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/search/components/search.html: -------------------------------------------------------------------------------- 1 |
7 | 15 | 16 | 23 | 24 | 25 |
26 | 27 | 44 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/search/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} {% block title %}{{ config.name }}{% endblock %} {% 2 | block head %} 3 | 4 | {% endblock %} {% block nav_left %} {% if profile.is_some() %} 5 | 6 | {{ icon "house" }} 7 | {{ text "general:link.timeline" }} 8 | 9 | 10 | 11 | {{ icon "inbox" }} 12 | 13 | {{ text "general:link.inbox" }} 14 | {% if unread != 0 %} 15 | {{ unread }} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | {{ icon "compass" }} 22 | 23 | {{ text "general:link.discover" }} 24 | 25 | 26 | {% endif %} {% endblock %} {% block nav_right %} {% if profile.is_some() %} 27 | 28 | {{ icon "bell" }} {% if notifs != 0 %} 29 | {{ notifs }} 30 | {% endif %} 31 | 32 | {% endif %} {% endblock %} {% block content %} 33 |
34 |
35 |

Search

36 | 37 | 40 | 41 |
{% include "components/search.html" %}
42 |
43 |
44 | 45 | {% if let Some(profile) = profile %} {% let other = profile.clone() %} {% if 46 | profile.username == other.username %} 47 |
48 | {% endif %} {% let raw_metadata = 49 | crate::routing::pages::clean_metadata_raw(other.metadata) %} {% include 50 | "components/theming.html" %} {% endif %} {% call super() %} {% endblock %} 51 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/search/questions.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block results %} 2 | 3 | {% for question in results %} 4 | {% let relationship = relationships.get(question.0.author.id).unwrap().to_owned() %} 5 | {% if (relationship != crate::model::RelationshipStatus::Friends 6 | && question.0.author.metadata.is_true("sparkler:private_profile")) 7 | | (question.0.author.group == -1) 8 | | (relationship == crate::model::RelationshipStatus::Blocked) %} 9 | {% include "components/private_question.html" %} 10 | {% else %} 11 | {% let show_responses = true %} 12 | {% include "components/global_question.html" %} 13 | {% endif %} 14 | {% endfor %} 15 | {% call super() %} {% endblock %} 16 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/search/responses.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block results %} 2 | 3 | {% for response in results %} 4 | {% let relationship = relationships.get(response.1.author.id).unwrap().to_owned() %} 5 | {% if (relationship != crate::model::RelationshipStatus::Friends 6 | && response.1.author.metadata.is_true("sparkler:private_profile")) 7 | | (response.1.author.group == -1) 8 | | (relationship == crate::model::RelationshipStatus::Blocked) %} 9 | {% include "components/private_response.html" %} 10 | {% else %} 11 | {% let is_pinned = false %} 12 | {% let show_pin_button = false %} 13 | {% let do_not_render_question = false %} 14 | {% let anonymous_username = Some("anonymous") %} 15 | {% let anonymous_avatar = Some("") %} 16 | {% let show_comments = true %} 17 | {% let do_render_nested = true %} 18 | {% include "components/response.html" %} 19 | {% endif %} 20 | {% endfor %} 21 | {% call super() %} {% endblock %} 22 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/search/users.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block results %} 2 | 3 | {% for user in results %} 4 | {% include "components/profile_card.html" %} 5 | {% endfor %} 6 | {% call super() %} {% endblock %} 7 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/settings/components/lang_picker.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/settings/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block sidenav %} 2 | {{ icon "smile" }}{{ text "settings:link.account" }} 5 | {{ icon "cookie" }}{{ text "settings:link.sessions" }} 8 | {{ icon "user-round-pen" }}{{ text "settings:link.profile" }} 11 | {{ icon "palette" }}{{ text "settings:link.theme" }} 14 | {{ icon "lock" }}{{ text "settings:link.privacy" }} 17 | {{ icon "store" }}{{ text "settings:link.coins" }} 20 | {% endblock %} {% block panel %} 21 | 22 | {% include "components/privacy_options.html" %} 23 | {% call super() %} {% endblock %} 24 | -------------------------------------------------------------------------------- /crates/rainbeam/templates/settings/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block sidenav %} 2 | {{ icon "smile" }}{{ text "settings:link.account" }} 5 | {{ icon "cookie" }}{{ text "settings:link.sessions" }} 8 | {{ icon "user-round-pen" }}{{ text "settings:link.profile" }} 11 | {{ icon "palette" }}{{ text "settings:link.theme" }} 14 | {{ icon "lock" }}{{ text "settings:link.privacy" }} 17 | {{ icon "store" }}{{ text "settings:link.coins" }} 20 | {% endblock %} {% block panel %} 21 | 22 | {% include "components/profile_options.html" %} 23 | {% call super() %} {% endblock %} 24 | -------------------------------------------------------------------------------- /crates/rb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rb" 3 | version = "9.0.0" 4 | edition = "2021" 5 | authors = ["trisuaso", "swmff"] 6 | description = "Rainbeam Axum" 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | rust-version = "1.83" 11 | 12 | [features] 13 | postgres = ["databeam/postgres", "authbeam/postgres", "rainbeam-core/postgres"] 14 | mysql = ["databeam/mysql", "authbeam/mysql", "rainbeam-core/mysql"] 15 | sqlite = ["databeam/sqlite", "authbeam/sqlite", "rainbeam-core/sqlite"] 16 | mimalloc = [] 17 | redis = ["databeam/redis", "authbeam/redis", "rainbeam-core/redis"] 18 | moka = ["databeam/moka", "authbeam/moka", "rainbeam-core/moka"] 19 | oysters = ["databeam/oysters", "authbeam/oysters", "rainbeam-core/oysters"] 20 | default = ["databeam/sqlite", "authbeam/sqlite", "rainbeam-core/sqlite"] 21 | 22 | [dependencies] 23 | reva = { version = "0.13.2", features = ["with-axum"] } 24 | reva_axum = "0.5.1" 25 | axum = { version = "0.8.4", features = ["macros", "form"] } 26 | axum-extra = { version = "0.10.1", features = ["cookie"] } 27 | reqwest = { version = "0.12.18", features = ["json", "stream"] } 28 | serde = { version = "1.0.219", features = ["derive"] } 29 | tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } 30 | toml = "0.8.22" 31 | tower-http = { version = "0.6.4", features = ["fs", "trace"] } 32 | serde_json = "1.0.140" 33 | regex = "1.11.1" 34 | hcaptcha-no-wasm = { version = "3.0.1" } 35 | ammonia = "4.1.0" 36 | async-recursion = "1.1.1" 37 | tracing = "0.1.41" 38 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 39 | # rainbeam-shared = { path = "../shared" } 40 | rainbeam-shared = "1.0.1" 41 | databeam = { path = "../databeam", version = "2.0.0", default-features = false } 42 | authbeam = { path = "../authbeam", default-features = false } 43 | langbeam = { path = "../langbeam" } 44 | rainbeam-core = { path = "../rainbeam-core", default-features = false } 45 | mime_guess = "2.0.5" 46 | pathbufd = "0.1.4" 47 | # pathbufd = { path = "../../../pathbufd" } 48 | carp = { path = "../carp" } 49 | 50 | [lib] 51 | crate-type = ["cdylib", "lib"] 52 | path = "src/lib.rs" 53 | name = "rb" 54 | test = false 55 | doctest = true 56 | -------------------------------------------------------------------------------- /crates/rb/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/rb/README.md: -------------------------------------------------------------------------------- 1 | # Rainbeam Axum 2 | -------------------------------------------------------------------------------- /crates/rb/reva.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | dirs = ["../rainbeam/templates_build"] 3 | whitespace = "Minimize" 4 | -------------------------------------------------------------------------------- /crates/rb/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 🌈 Rainbeam! 2 | #![doc = include_str!("../../../README.md")] 3 | #![doc(issue_tracker_base_url = "https://github.com/swmff/rainbeam/issues")] 4 | #![doc(html_favicon_url = "https://rainbeam.net/static/favicon.svg")] 5 | #![doc(html_logo_url = "https://rainbeam.net/static/favicon.svg")] 6 | use reva_axum::Template; 7 | 8 | pub use rainbeam::database; 9 | pub use rainbeam::config; 10 | pub use rainbeam::model; 11 | pub mod routing; 12 | 13 | /// Trait to convert errors into HTML 14 | pub(crate) trait ToHtml { 15 | fn to_html(&self, database: database::Database) -> String; 16 | } 17 | 18 | impl ToHtml for model::DatabaseError { 19 | fn to_html(&self, database: database::Database) -> String { 20 | crate::routing::pages::ErrorTemplate { 21 | config: database.config.clone(), 22 | lang: database.lang("net.rainbeam.langs:en-US"), 23 | profile: None, 24 | message: self.to_string(), 25 | } 26 | .render() 27 | .unwrap() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/rb/src/routing/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod comments; 2 | pub mod profiles; 3 | pub mod questions; 4 | pub mod reactions; 5 | pub mod responses; 6 | pub mod util; 7 | 8 | use crate::database::Database; 9 | use axum::Router; 10 | use hcaptcha_no_wasm::Hcaptcha; 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Serialize, Deserialize, Hcaptcha)] 14 | pub struct CreateReport { 15 | content: String, 16 | #[captcha] 17 | token: String, 18 | } 19 | 20 | pub fn routes(database: Database) -> Router { 21 | Router::new() 22 | .nest("/util", util::routes(database.clone())) 23 | .nest("/questions", questions::routes(database.clone())) 24 | .nest("/responses", responses::routes(database.clone())) 25 | .nest("/comments", comments::routes(database.clone())) 26 | .nest("/reactions", reactions::routes(database.clone())) 27 | .nest("/profiles", profiles::routes(database.clone())) 28 | } 29 | -------------------------------------------------------------------------------- /crates/rb/src/routing/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod pages; 3 | -------------------------------------------------------------------------------- /crates/rb/src/routing/pages/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod comment; 2 | pub mod response; 3 | -------------------------------------------------------------------------------- /crates/shared/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rainbeam-shared" 3 | version = "1.0.1" 4 | edition = "2021" 5 | authors = ["trisuaso", "swmff"] 6 | description = "Shared utilities for Rainbeam" 7 | homepage = "https://rainbeam.net" 8 | repository = "https://github.com/swmff/rainbeam" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | ammonia = "4.1.0" 13 | chrono = "0.4.41" 14 | hex_fmt = "0.3.0" 15 | num-bigint = "0.4.6" 16 | rand = "0.9.1" 17 | serde = { version = "1.0.219", features = ["derive"] } 18 | sha2 = "0.10.9" 19 | uuid = { version = "1.17.0", features = ["v4"] } 20 | pathbufd = "0.1.4" 21 | toml = "0.8.22" 22 | markdown = "1.0.0" 23 | 24 | [lib] 25 | doctest = false 26 | -------------------------------------------------------------------------------- /crates/shared/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trisuaso, swmff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/shared/README.md: -------------------------------------------------------------------------------- 1 | # shared 2 | 3 | Shared Rainbeam utilities. 4 | -------------------------------------------------------------------------------- /crates/shared/src/fs.rs: -------------------------------------------------------------------------------- 1 | //! Fs access utilities 2 | //! 3 | //! This is essentially a wrapper around standard fs, so it's just to keep things similar. 4 | use std::path::Path; 5 | pub use std::{ 6 | fs::{ 7 | create_dir, read_dir, read_to_string, remove_dir_all, remove_file, write as std_write, 8 | read as std_read, canonicalize, metadata, Metadata, 9 | }, 10 | io::Result, 11 | }; 12 | 13 | /// Get a path's metadata 14 | /// 15 | /// # Arguments 16 | /// * `path` 17 | pub fn fstat

(path: P) -> Result 18 | where 19 | P: AsRef, 20 | { 21 | metadata(path) 22 | } 23 | 24 | /// Create a directory if it does not already exist 25 | /// 26 | /// # Arguments 27 | /// * `path` 28 | pub fn mkdir

(path: P) -> Result<()> 29 | where 30 | P: AsRef, 31 | { 32 | if read_dir(&path).is_err() { 33 | create_dir(path)? 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | /// `rm -r` for only directories 40 | /// 41 | /// # Arguments 42 | /// * `path` 43 | pub fn rmdirr>(path: P) -> Result<()> 44 | where 45 | P: AsRef, 46 | { 47 | if read_dir(&path).is_err() { 48 | return Ok(()); // doesn't exist, return ok since there was nothing to remove 49 | } 50 | 51 | remove_dir_all(path) 52 | } 53 | 54 | /// `rm` for only files 55 | /// 56 | /// # Arguments 57 | /// * `path` 58 | pub fn rm>(path: P) -> Result<()> 59 | where 60 | P: AsRef, 61 | { 62 | remove_file(path) 63 | } 64 | 65 | /// Write to a file given its path and data 66 | /// 67 | /// # Arguments 68 | /// * `path` 69 | /// * `data` 70 | pub fn write(path: P, data: D) -> Result<()> 71 | where 72 | P: AsRef, 73 | D: AsRef<[u8]>, 74 | { 75 | std_write(path, data) 76 | } 77 | 78 | /// `touch` 79 | /// 80 | /// # Arguments 81 | /// * `path` 82 | pub fn touch

(path: P) -> Result<()> 83 | where 84 | P: AsRef, 85 | { 86 | std_write(path, "") 87 | } 88 | 89 | /// Append to a file given its path and data 90 | /// 91 | /// # Arguments 92 | /// * `path` 93 | /// * `data` 94 | pub fn append(path: P, data: D) -> Result<()> 95 | where 96 | P: AsRef, 97 | D: AsRef<[u8]>, 98 | { 99 | let mut bytes = std_read(&path)?; // read current data as bytes 100 | bytes = [&bytes, data.as_ref()].concat(); // append 101 | std_write(path, bytes) // write 102 | } 103 | 104 | /// `cat` 105 | /// 106 | /// # Arguments 107 | /// * `path` 108 | /// 109 | /// # Returns 110 | /// * [`String`] 111 | pub fn read>(path: P) -> Result 112 | where 113 | P: AsRef, 114 | { 115 | read_to_string(path) 116 | } 117 | -------------------------------------------------------------------------------- /crates/shared/src/hash.rs: -------------------------------------------------------------------------------- 1 | //! Hashing and IDs 2 | use hex_fmt::HexFmt; 3 | use rand::{distr::Alphanumeric, rng, Rng}; 4 | use sha2::{Digest, Sha256}; 5 | use uuid::Uuid; 6 | 7 | // ids 8 | pub fn uuid() -> String { 9 | let uuid = Uuid::new_v4(); 10 | uuid.to_string() 11 | } 12 | 13 | pub fn hash(input: String) -> String { 14 | let mut hasher = ::new(); 15 | hasher.update(input.into_bytes()); 16 | 17 | let res = hasher.finalize(); 18 | HexFmt(res).to_string() 19 | } 20 | 21 | pub fn hash_salted(input: String, salt: String) -> String { 22 | let mut hasher = ::new(); 23 | hasher.update(format!("{salt}{input}").into_bytes()); 24 | 25 | let res = hasher.finalize(); 26 | HexFmt(res).to_string() 27 | } 28 | 29 | pub fn salt() -> String { 30 | rng() 31 | .sample_iter(&Alphanumeric) 32 | .take(16) 33 | .map(char::from) 34 | .collect() 35 | } 36 | 37 | pub fn random_id() -> String { 38 | hash(uuid()) 39 | } 40 | -------------------------------------------------------------------------------- /crates/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Xsu Utilities 2 | #![doc = include_str!("../README.md")] 3 | #![doc(issue_tracker_base_url = "https://github.com/swmff/rainbeam/issues/")] 4 | pub mod fs; 5 | pub mod hash; 6 | pub mod process; 7 | pub mod snow; 8 | pub mod ui; 9 | pub mod config; 10 | 11 | // ... 12 | use std::time::{SystemTime, UNIX_EPOCH}; 13 | use chrono::{TimeZone, Utc}; 14 | 15 | /// Get a [`u128`] timestamp 16 | pub fn unix_epoch_timestamp() -> u128 { 17 | let right_now = SystemTime::now(); 18 | let time_since = right_now 19 | .duration_since(UNIX_EPOCH) 20 | .expect("Time travel is not allowed"); 21 | 22 | time_since.as_millis() 23 | } 24 | 25 | /// Get a [`i64`] timestamp from the given `year` epoch 26 | pub fn epoch_timestamp(year: i32) -> i64 { 27 | let now = Utc::now().timestamp_millis(); 28 | let then = Utc 29 | .with_ymd_and_hms(year, 1, 1, 0, 0, 0) 30 | .unwrap() 31 | .timestamp_millis(); 32 | 33 | now - then 34 | } 35 | -------------------------------------------------------------------------------- /crates/shared/src/process.rs: -------------------------------------------------------------------------------- 1 | //! Process management utilities 2 | 3 | /// Error quit with message 4 | /// 5 | /// # Arguments 6 | /// * `msg` - result message 7 | pub fn no(msg: &str) { 8 | println!("\x1b[91m{}\x1b[0m", format!("error:\x1b[0m {msg}")); 9 | std::process::exit(1); 10 | } 11 | 12 | /// Success quit with message 13 | /// 14 | /// # Arguments 15 | /// * `msg` - result message 16 | pub fn yes(msg: &str) { 17 | println!("\x1b[92m{}\x1b[0m", format!("success:\x1b[0m {msg}")); 18 | std::process::exit(0); 19 | } 20 | -------------------------------------------------------------------------------- /crates/shared/src/snow.rs: -------------------------------------------------------------------------------- 1 | //! Almost Snowflake 2 | //! 3 | //! Random IDs which include timestamp information (like Twitter Snowflakes) 4 | //! 5 | //! IDs are generated with 41 bits of an epoch timestamp, 10 bits of a machine/server ID, and 12 bits of randomly generated numbers. 6 | //! 7 | //! ``` 8 | //! tttttttttttttttttttttttttttttttttttttttttiiiiiiiiiirrrrrrrrrrrr... 9 | //! Timestamp ID Seed 10 | //! ``` 11 | use serde::{Serialize, Deserialize}; 12 | use crate::epoch_timestamp; 13 | 14 | use num_bigint::BigInt; 15 | use rand::Rng; 16 | 17 | static SEED_LEN: usize = 12; 18 | // static ID_LEN: usize = 10; 19 | 20 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 21 | pub struct AlmostSnowflake(String); 22 | 23 | pub fn bigint(input: usize) -> BigInt { 24 | BigInt::from(input) 25 | } 26 | 27 | impl AlmostSnowflake { 28 | /// Create a new [`AlmostSnowflake`] 29 | pub fn new(server_id: usize) -> Self { 30 | // generate random bytes 31 | let mut bytes = String::new(); 32 | 33 | let mut rng = rand::rng(); 34 | for _ in 1..=SEED_LEN { 35 | bytes.push_str(&rng.random_range(0..10).to_string()) 36 | } 37 | 38 | // build id 39 | let mut id = bigint(epoch_timestamp(2024) as usize) << 22_u128; 40 | id |= bigint((server_id % 1024) << 12); 41 | id |= bigint((bytes.parse::().unwrap() + 1) % 4096); 42 | 43 | // return 44 | Self(id.to_string()) 45 | } 46 | } 47 | 48 | impl std::fmt::Display for AlmostSnowflake { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | write!(f, "{}", self.0) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/shared/src/ui.rs: -------------------------------------------------------------------------------- 1 | use ammonia::Builder; 2 | use markdown::{to_html_with_options, Options, CompileOptions, ParseOptions, Constructs}; 3 | use std::collections::HashSet; 4 | 5 | /// Render markdown input into HTML 6 | pub fn render_markdown(input: &str) -> String { 7 | let options = Options { 8 | compile: CompileOptions { 9 | allow_any_img_src: false, 10 | allow_dangerous_html: true, 11 | gfm_task_list_item_checkable: false, 12 | gfm_tagfilter: false, 13 | ..Default::default() 14 | }, 15 | parse: ParseOptions { 16 | constructs: Constructs { 17 | gfm_autolink_literal: true, 18 | ..Default::default() 19 | }, 20 | gfm_strikethrough_single_tilde: false, 21 | math_text_single_dollar: false, 22 | mdx_expression_parse: None, 23 | mdx_esm_parse: None, 24 | ..Default::default() 25 | }, 26 | }; 27 | 28 | let html = match to_html_with_options(input, &options) { 29 | Ok(h) => h, 30 | Err(e) => e.to_string(), 31 | }; 32 | 33 | let mut allowed_attributes = HashSet::new(); 34 | allowed_attributes.insert("id"); 35 | allowed_attributes.insert("class"); 36 | allowed_attributes.insert("ref"); 37 | allowed_attributes.insert("aria-label"); 38 | allowed_attributes.insert("lang"); 39 | allowed_attributes.insert("title"); 40 | allowed_attributes.insert("align"); 41 | allowed_attributes.insert("src"); 42 | 43 | Builder::default() 44 | .generic_attributes(allowed_attributes) 45 | .add_tags(&[ 46 | "video", "source", "img", "b", "span", "p", "i", "strong", "em", "a", 47 | ]) 48 | .rm_tags(&["script", "style", "link", "canvas"]) 49 | .add_tag_attributes("a", &["href", "target"]) 50 | .clean(&html) 51 | .to_string() 52 | .replace( 53 | "src=\"", 54 | "loading=\"lazy\" src=\"/api/v0/util/ext/image?img=", 55 | ) 56 | .replace("