├── .gitignore ├── .idea ├── .gitignore ├── dataSources.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── sqldialects.xml ├── tafarn.iml └── vcs.xml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.md ├── README.md ├── build.sh ├── i18n.toml ├── i18n ├── cy │ └── tafarn.ftl ├── en-GB │ └── tafarn.ftl ├── nl-NL │ └── tafarn.ftl └── ru-RU │ └── tafarn.ftl ├── kube ├── POST ├── apply.sh ├── config.yaml ├── deploy.yaml ├── ingress.yaml ├── net.yaml ├── nginx.yaml ├── pvc.yaml └── svc.yaml ├── migrations ├── 2022-11-01-230545_sessions │ ├── down.sql │ └── up.sql ├── 2022-11-02-090527_apps │ ├── down.sql │ └── up.sql ├── 2022-11-02-105841_oauth_consents │ ├── down.sql │ └── up.sql ├── 2022-11-02-142118_oauth_tokens │ ├── down.sql │ └── up.sql ├── 2022-11-02-162440_accounts │ ├── down.sql │ └── up.sql ├── 2022-11-03-110414_web_push │ ├── down.sql │ └── up.sql ├── 2022-11-04-124154_public_key │ ├── down.sql │ └── up.sql ├── 2022-12-26-132253_relationships │ ├── down.sql │ └── up.sql ├── 2022-12-26-224527_web_push_update │ ├── down.sql │ └── up.sql ├── 2022-12-26-231644_notifications │ ├── down.sql │ └── up.sql ├── 2022-12-27-012154_group_account │ ├── down.sql │ └── up.sql ├── 2022-12-29-140619_account_images │ ├── down.sql │ └── up.sql ├── 2023-01-01-144406_pagination │ ├── down.sql │ └── up.sql ├── 2023-01-03-184435_pending_follow │ ├── down.sql │ └── up.sql ├── 2023-01-04-123932_media │ ├── down.sql │ └── up.sql ├── 2023-01-07-235744_web_push_policy │ ├── down.sql │ └── up.sql ├── 2023-01-08-021756_status │ ├── down.sql │ └── up.sql ├── 2023-01-08-143528_timelines │ ├── down.sql │ └── up.sql ├── 2023-01-09-171219_likes_bookmarks_pins │ ├── down.sql │ └── up.sql ├── 2023-01-10-094949_account_notes │ ├── down.sql │ └── up.sql ├── 2023-01-13-162247_media_attachments │ ├── down.sql │ └── up.sql ├── 2023-01-13-194040_account_deletion │ ├── down.sql │ └── up.sql ├── 2023-01-15-193307_status_mention │ ├── down.sql │ └── up.sql └── 2023-01-15-203532_follow_notify_and_reblogs │ ├── down.sql │ └── up.sql ├── src ├── bin │ ├── frontend.rs │ ├── tafarnctl.rs │ └── tasks.rs ├── csrf.rs ├── i18n.rs ├── lib.rs ├── models.rs ├── schema.rs ├── tasks │ ├── accounts.rs │ ├── collection.rs │ ├── delivery.rs │ ├── inbox.rs │ ├── mod.rs │ ├── notifications.rs │ ├── relationships.rs │ └── statuses.rs └── views │ ├── accounts.rs │ ├── activity_streams.rs │ ├── blocks.rs │ ├── bookmarks.rs │ ├── conversations.rs │ ├── domain_blocks.rs │ ├── favourites.rs │ ├── filters.rs │ ├── follow_requests.rs │ ├── instance.rs │ ├── lists.rs │ ├── media.rs │ ├── meta.rs │ ├── mod.rs │ ├── mutes.rs │ ├── nodeinfo.rs │ ├── notifications.rs │ ├── oauth.rs │ ├── objs.rs │ ├── oidc.rs │ ├── search.rs │ ├── statuses.rs │ ├── suggestions.rs │ ├── timelines.rs │ └── web_push.rs ├── static ├── header.png └── missing.png └── templates ├── base.html.tera ├── host-meta.tera ├── oauth-consent.html.tera └── oauth-error.html.tera /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/pycharm 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm 3 | 4 | ### PyCharm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # AWS User-specific 16 | .idea/**/aws.xml 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # SonarLint plugin 69 | .idea/sonarlint/ 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | # Editor-based Rest Client 78 | .idea/httpRequests 79 | 80 | # Android studio 3.1+ serialized cache file 81 | .idea/caches/build_file_checksums.ser 82 | 83 | ### PyCharm Patch ### 84 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 85 | 86 | # *.iml 87 | # modules.xml 88 | # .idea/misc.xml 89 | # *.ipr 90 | 91 | # Sonarlint plugin 92 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 93 | .idea/**/sonarlint/ 94 | 95 | # SonarQube Plugin 96 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 97 | .idea/**/sonarIssues.xml 98 | 99 | # Markdown Navigator plugin 100 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 101 | .idea/**/markdown-navigator.xml 102 | .idea/**/markdown-navigator-enh.xml 103 | .idea/**/markdown-navigator/ 104 | 105 | # Cache file creation bug 106 | # See https://youtrack.jetbrains.com/issue/JBR-2257 107 | .idea/$CACHE_FILE$ 108 | 109 | # CodeStream plugin 110 | # https://plugins.jetbrains.com/plugin/12206-codestream 111 | .idea/codestream.xml 112 | 113 | # Azure Toolkit for IntelliJ plugin 114 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij 115 | .idea/**/azureSettings.xml 116 | 117 | # End of https://www.toptal.com/developers/gitignore/api/pycharm 118 | 119 | /target 120 | Rocket.toml 121 | as_key.pem 122 | vapid_key.pem 123 | /media 124 | .DS_Store 125 | /kube/secrets.yaml -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | postgresql 6 | true 7 | org.postgresql.Driver 8 | jdbc:postgresql://localhost:5432/tafarn 9 | $ProjectFileDir$ 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/tafarn.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tafarn" 3 | version = "0.1.1" 4 | edition = "2021" 5 | repository = "https://github.com/TheEnbyperor/tafarn" 6 | 7 | [lib] 8 | name = "tafarn" 9 | path = "src/lib.rs" 10 | 11 | [[bin]] 12 | name = "frontend" 13 | path = "src/bin/frontend.rs" 14 | 15 | [[bin]] 16 | name = "tasks" 17 | path = "src/bin/tasks.rs" 18 | 19 | [[bin]] 20 | name = "tafarnctl" 21 | path = "src/bin/tafarnctl.rs" 22 | 23 | [dependencies] 24 | rocket = { version = "0.5.0-rc.2", features = ["secrets", "json"] } 25 | rocket_sync_db_pools = { version = "0.1.0-rc.2", features = ["diesel_postgres_pool"] } 26 | rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] } 27 | celery = "0.4.0-rcn.11" 28 | tokio = { version = "1", features = ["fs", "sync", "parking_lot"] } 29 | log = "0.4" 30 | pretty_env_logger = "0.4" 31 | serde = "1" 32 | serde_json = "1" 33 | openidconnect = "2" 34 | diesel = { version = "1", features = ["postgres", "uuidv07", "extras"] } 35 | diesel_migrations = "1" 36 | diesel-derive-enum = { version = "1", features = ["postgres"] } 37 | uuid = { version = "0.8", features = ["serde", "v4"] } 38 | chrono = "0.4" 39 | time = "0.3" 40 | base64 = "0.13" 41 | rand = "0.8" 42 | phf = { version = "0.11", features = ["macros"] } 43 | url = "2" 44 | rocket-basicauth = "2" 45 | jwt-simple = "0.10" 46 | web-push = "0.9" 47 | pkcs8 = "0.7" 48 | p256 = "0.10" 49 | lazy_static = "1" 50 | reqwest = { version = "0.11", features = ["json"] } 51 | r2d2 = "0.8" 52 | md5 = "0.7" 53 | sha1 = "0.10" 54 | sha2 = "0.10" 55 | clap = { version = "4", features = ["derive"] } 56 | futures = "0.3" 57 | itertools = "0.10" 58 | backoff = { version = "0.4", features = ["futures", "tokio"] } 59 | openssl = "0.10" 60 | regex = "1" 61 | image = "0.24" 62 | percent-encoding = "2" 63 | blurhash = "0.1" 64 | web_push_old = { package = "web-push", version = "0.7" } 65 | async-recursion = "1.0.0" 66 | sanitize_html = "0.7" 67 | comrak = "0.15" 68 | bytes = "1" 69 | fluent = "0.16" 70 | fluent-bundle = "0.15" 71 | i18n-embed = { version = "0.13.8", features = ["fluent-system"] } 72 | i18n-embed-fl = "0.6.5" 73 | rust-embed = "6" 74 | unic-langid = "0.9" 75 | accept-language = "2" 76 | 77 | [profile.release] 78 | strip = true 79 | opt-level = "z" 80 | lto = true 81 | panic = "abort" 82 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly AS builder 2 | RUN update-ca-certificates 3 | WORKDIR /usr/src/ 4 | 5 | RUN USER=root cargo new tafarn 6 | WORKDIR /usr/src/tafarn 7 | 8 | COPY Cargo.toml Cargo.lock . 9 | COPY src ./src 10 | COPY migrations ./migrations 11 | COPY i18n ./i18n 12 | COPY i18n.toml ./i18n.toml 13 | RUN cargo install --path . 14 | 15 | FROM debian:buster-slim 16 | 17 | RUN apt-get update && apt-get install -y libssl1.1 libpq5 ca-certificates p11-kit-modules \ 18 | && apt-get clean && rm -rf /var/lib/apt/lists/* 19 | RUN update-ca-certificates 20 | 21 | WORKDIR /tafarn 22 | 23 | COPY --from=builder --chown=0:0 /usr/local/cargo/bin/frontend /tafarn/frontend 24 | COPY --from=builder --chown=0:0 /usr/local/cargo/bin/tasks /tafarn/tasks 25 | COPY --from=builder --chown=0:0 /usr/local/cargo/bin/tafarnctl /tafrarn/tasks 26 | COPY --chown=0:0 static /tafarn/static 27 | COPY --chown=0:0 templates /tafarn/templates 28 | 29 | ENTRYPOINT ["/tafarn/frontend"] 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Q Misell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 9 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tafarn 2 | An ActivityPub home server written in Rust, implementing the Mastodon API. 3 | 4 | At present no web UI is provided, the API is the only way to interact with the server. 5 | 6 | ## Configuration 7 | 8 | Tafarn is based on Rocket, as such the configuration is handled through the Rocket configuration system. 9 | An example configuration file is provided below: 10 | 11 | ```toml 12 | [debug] 13 | secret_key = "" 14 | jwt_secret = "" 15 | celery = { amqp_url = "amqp://localhost:5672/tafarn" } 16 | uri = "" 17 | vapid_key = "./vapid_key.pem" 18 | # The system actor private key 19 | as_key = "./as_key.pem" 20 | media_path = "./media" 21 | 22 | [debug.databases.db] 23 | url = "postgres://postgres@localhost/tafarn" 24 | 25 | [debug.oidc] 26 | issuer_url = "" 27 | client_id = "tafarn" 28 | client_secret = "" 29 | ``` 30 | 31 | Secret keys can be generated by `openssl rand -base64 32`. 32 | A VAPID key can be generated by `openssl ecparam -genkey -name prime256v1 -outform pem -out vapid_key.pem`. 33 | A system actor key can be generated by `openssl genrsa 2048 -outform pem -out as_key.pem`. 34 | 35 | ### Authentication 36 | 37 | Tafarn does not have its own authentication system, instead it uses an external OIDC provider. 38 | The OIDC provider must support the `/.well-known/openid-configuration` configuration endpoint. 39 | On first login the display name will be set to the `name` claim, and the username to either the `preferred_username` 40 | or `given_name` claim, if `preferred_username` is not available. 41 | 42 | ## Localization 43 | 44 | Tafarn uses the Fluent localization system. To add a new language copy the `en-GB` folder in `i18n` as a starting point. 45 | 46 | Currently supported languages are: 47 | - English (en-GB) 48 | - Welsh (cy) 49 | - Dutch (nl-NL); thanks [flimpie](https://flimpie.net/@flimpie). 50 | - Russian (ru-RU); thanks [Сергей Ворон](https://github.com/vorons). 51 | 52 | ## ActivityPub implementation status 53 | 54 | ### Events the server can receive 55 | - Follow Person/Service/Organization/Application/Group 56 | - Accept follow 57 | - Reject follow 58 | - Undo follow 59 | - Update Person/Service/Organization/Application/Group 60 | - Create note 61 | - Announce note 62 | - Undo announce 63 | - Delete note 64 | - Like note 65 | - Undo like note 66 | 67 | ### Events the server can send 68 | - Follow Person/Service/Organization/Application/Group 69 | - Accept follow 70 | - Undo follow 71 | - Update Person/Application/Group 72 | - Create note 73 | - Announce note 74 | - Undo announce 75 | - Like note 76 | - Undo like note 77 | 78 | ## API endpoints implementation status 79 | 80 | ### apps 81 | - [x] POST /api/v1/apps 82 | - [ ] GET /api/v1/apps/verify_credentials 83 | - [x] GET /oauth/authorize 84 | - [x] POST /oauth/token 85 | - [x] POST /oauth/revoke 86 | - [ ] POST /api/v1/emails/confirmation 87 | 88 | ### accounts 89 | - [ ] POST /api/v1/accounts (likely never to be implemented) 90 | - [x] GET /api/v1/accounts/verify_credentials 91 | - [x] PATCH /api/v1/accounts/update_credentials 92 | - [x] GET /api/v1/accounts/:id 93 | - [x] GET /api/v1/accounts/:id/statuses 94 | - [x] GET /api/v1/accounts/:id/followers 95 | - [x] GET /api/v1/accounts/:id/following 96 | - [ ] GET /api/v1/accounts/:id/featured_tags 97 | - [x] POST /api/v1/accounts/:id/follow 98 | - [x] POST /api/v1/accounts/:id/unfollow 99 | - [ ] POST /api/v1/accounts/:id/remove_from_followers 100 | - [x] POST /api/v1/accounts/:id/note 101 | - [x] GET /api/v1/accounts/relationships 102 | - [x] GET /api/v1/accounts/familiar_followers 103 | - [ ] GET /api/v1/accounts/search 104 | - [x] GET /api/v1/accounts/lookup 105 | - [ ] GET /api/v1/accounts/:id/identity_proofs (likely never to be implemented) 106 | 107 | ### bookmarks 108 | - [x] GET /api/v1/bookmarks 109 | - [x] POST /api/v1/statuses/:id/bookmark 110 | - [x] POST /api/v1/statuses/:id/unbookmark 111 | 112 | ### favourites 113 | - [x] GET /api/v1/favourites 114 | - [x] POST /api/v1/statuses/:id/favourite 115 | - [x] POST /api/v1/statuses/:id/unfavourite 116 | 117 | ### mutes 118 | - [ ] GET /api/v1/mutes 119 | - [ ] POST /api/v1/accounts/:id/mute 120 | - [ ] POST /api/v1/accounts/:id/unmute 121 | 122 | ### blocks 123 | - [ ] GET /api/v1/blocks 124 | - [ ] POST /api/v1/accounts/:id/block 125 | - [ ] POST /api/v1/accounts/:id/unblock 126 | 127 | ### domain blocks 128 | - [ ] GET /api/v1/domain_blocks 129 | - [ ] POST /api/v1/domain_blocks 130 | - [ ] DELETE /api/v1/domain_blocks 131 | 132 | ### filters 133 | - [ ] GET /api/v2/filters 134 | - [ ] GET /api/v2/filters/:id 135 | - [ ] POST /api/v2/filters 136 | - [ ] PUT /api/v2/filters/:id 137 | - [ ] DELETE /api/v2/filters/:id 138 | - [ ] GET /api/v2/filters/:filter_id/keywords 139 | - [ ] POST /api/v2/filters/:filter_id/keywords 140 | - [ ] GET /api/v2/filters/keywords/:id 141 | - [ ] PUT /api/v2/filters/keywords/:id 142 | - [ ] DELETE /api/v2/filters/keywords/:id 143 | - [ ] GET /api/v2/filters/:filter_id/statuses 144 | - [ ] POST /api/v2/filters/:filter_id/statuses 145 | - [ ] GET /api/v2/filters/statuses/:id 146 | - [ ] DELETE /api/v2/filters/statuses/:id 147 | - [ ] GET /api/v1/filters 148 | - [ ] GET /api/v1/filters/:id 149 | - [ ] POST /api/v1/filters 150 | - [ ] PUT /api/v1/filters/:id 151 | - [ ] DELETE /api/v1/filters/:id 152 | 153 | ### reports 154 | - [ ] POST /api/v1/reports 155 | 156 | ### follow requests 157 | - [ ] GET /api/v1/follow_requests 158 | - [ ] POST /api/v1/follow_requests/:account_id/authorize 159 | - [ ] POST /api/v1/follow_requests/:account_id/reject 160 | 161 | ### endorsements 162 | - [ ] GET /api/v1/endorsements 163 | - [ ] POST /api/v1/accounts/:id/pin 164 | - [ ] POST /api/v1/accounts/:id/unpin 165 | 166 | ### featured tags 167 | - [ ] GET /api/v1/featured_tags 168 | - [ ] POST /api/v1/featured_tags 169 | - [ ] DELETE /api/v1/featured_tags/:id 170 | - [ ] GET /api/v1/featured_tags/suggestions 171 | 172 | ### preferences 173 | - [x] GET /api/v1/preferences 174 | 175 | ### followed tags 176 | - [ ] GET /api/v1/followed_tags 177 | - [ ] POST /api/v1/tags/:id/follow 178 | - [ ] POST /api/v1/tags/:id/unfollow 179 | 180 | ### suggestions 181 | - [ ] GET /api/v2/suggestions 182 | - [ ] DELETE /api/v1/suggestions/:account_id 183 | - [ ] GET /api/v1/suggestions 184 | 185 | ### tags 186 | - [ ] GET /api/v1/tags/:id 187 | 188 | ### statuses 189 | - [x] POST /api/v1/statuses 190 | - [x] GET /api/v1/statuses/:id 191 | - [x] DELETE /api/v1/statuses/:id 192 | - [x] GET /api/v1/statuses/:id/context 193 | - [x] GET /api/v1/statuses/:id/reblogged_by 194 | - [x] GET /api/v1/statuses/:id/favourited_by 195 | - [x] POST /api/v1/statuses/:id/reblog 196 | - [x] POST /api/v1/statuses/:id/unreblog 197 | - [ ] POST /api/v1/statuses/:id/mute 198 | - [ ] POST /api/v1/statuses/:id/unmute 199 | - [x] POST /api/v1/statuses/:id/pin 200 | - [x] POST /api/v1/statuses/:id/unpin 201 | - [ ] PUT /api/v1/statuses/:id 202 | - [ ] GET /api/v1/statuses/:id/history 203 | - [ ] GET /api/v1/statuses/:id/source 204 | 205 | ### media 206 | - [ ] POST /api/v2/media 207 | - [x] GET /api/v1/media/:id 208 | - [x] PUT /api/v1/media/:id 209 | - [x] POST /api/v1/media 210 | 211 | Only image uploads are currently supported, videos and audio will result in a HTTP 422 Unprocessable Entity response. 212 | 213 | ### polls 214 | - [ ] GET /api/v1/polls/:id 215 | - [ ] POST /api/v1/polls/:id/votes 216 | 217 | ### scheduled statuses 218 | - [ ] GET /api/v1/scheduled_statuses 219 | - [ ] GET /api/v1/scheduled_statuses/:id 220 | - [ ] PUT /api/v1/scheduled_statuses/:id 221 | - [ ] DELETE /api/v1/scheduled_statuses/:id 222 | 223 | ### timelines 224 | - [x] GET /api/v1/timelines/public 225 | - [ ] GET /api/v1/timelines/tag/:hashtag 226 | - [x] GET /api/v1/timelines/home 227 | - [ ] GET /api/v1/timelines/list/:list_id 228 | - [ ] GET /api/v1/timelines/direct 229 | 230 | ### conversations 231 | - [ ] GET /api/v1/conversations 232 | - [ ] DELETE /api/v1/conversations/:id 233 | - [ ] POST /api/v1/conversations/:id/read 234 | 235 | ### lists 236 | - [ ] GET /api/v1/lists 237 | - [ ] GET /api/v1/lists/:id 238 | - [ ] POST /api/v1/lists 239 | - [ ] PUT /api/v1/lists/:id 240 | - [ ] DELETE /api/v1/lists/:id 241 | - [ ] GET /api/v1/lists/:id/accounts 242 | - [ ] POST /api/v1/lists/:id/accounts 243 | - [ ] DELETE /api/v1/lists/:id/accounts 244 | - [ ] GET /api/v1/accounts/:id/lists 245 | 246 | ### markers 247 | - [ ] GET /api/v1/markers 248 | - [ ] POST /api/v1/markers 249 | 250 | ### streaming 251 | - [ ] GET /api/v1/streaming/health 252 | - [ ] GET /api/v1/streaming/user 253 | - [ ] GET /api/v1/streaming/user/notification 254 | - [ ] GET /api/v1/streaming/public 255 | - [ ] GET /api/v1/streaming/public/local 256 | - [ ] GET /api/v1/streaming/public/remote 257 | - [ ] GET /api/v1/streaming/hashtag 258 | - [ ] GET /api/v1/streaming/hashtag/local 259 | - [ ] GET /api/v1/streaming/list 260 | - [ ] GET /api/v1/streaming/direct 261 | - [ ] wss://<>/api/v1/streaming 262 | 263 | ### notifications 264 | - [x] GET /api/v1/notifications 265 | - [x] GET /api/v1/notification/:id 266 | - [ ] POST /api/v1/notifications/clear 267 | - [ ] POST /api/v1/notifications/:id/dismiss 268 | 269 | ### web push 270 | - [x] POST /api/v1/push/subscription 271 | - [x] GET /api/v1/push/subscription 272 | - [x] PUT /api/v1/push/subscription 273 | - [x] DELETE /api/v1/push/subscription 274 | 275 | ### search 276 | - [x] GET /api/v2/search 277 | 278 | Search currently only handles accounts, hashtags and statuses are always empty. 279 | 280 | ### instance 281 | - [x] GET /api/v2/instance 282 | - [x] GET /api/v1/instance 283 | - [ ] GET /api/v1/instance/peers 284 | - [ ] GET /api/v1/instance/activity 285 | - [ ] GET /api/v1/instance/rules 286 | - [ ] GET /api/v1/instance/domain_blocks 287 | - [ ] GET /api/v1/custom_emojis 288 | - [ ] GET /api/v1/directory 289 | 290 | ### trends 291 | - [ ] GET /api/v1/trends/tags 292 | - [ ] GET /api/v1/trends/statuses 293 | - [ ] GET /api/v1/trends/links 294 | 295 | ### announcements 296 | - [ ] GET /api/v1/announcements 297 | - [ ] POST /api/v1/announcements/:id/dismiss 298 | - [ ] PUT /api/v1/announcements/:id/reactions/:name 299 | - [ ] DELETE /api/v1/announcements/:id/reactions/:name 300 | 301 | ### oembed 302 | - [ ] GET /api/oembed 303 | 304 | ### admin 305 | No admin API endpoint is currently implemented. 306 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=$(sentry-cli releases propose-version || exit) 4 | 5 | docker buildx build --platform linux/amd64 --push -t "theenbyperor/tafarn:$VERSION" . || exit -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en-GB" 2 | 3 | [fluent] 4 | assets_dir = "i18n" 5 | -------------------------------------------------------------------------------- /i18n/cy/tafarn.ftl: -------------------------------------------------------------------------------- 1 | follow-notification = Dilynodd {$name} chi 2 | favourite-notification = Hoffodd {$name} eich tŵt 3 | reblog-notification = Bŵstiodd {$name} eich tŵt 4 | mention-notification = Sônodd {$name} amdanoch 5 | 6 | invalid-client-name = Enw cleient annilys 7 | invalid-website = Gwefan annilys 8 | invalid-scope = Cyfyngder annilys 9 | login-unauthorized = Nid oes genych yr hawl i fewngofnodi i'r gweinydd yma 10 | 11 | invalid-request = Erfyniad annilys 12 | client-not-found = Ni chanfuwyd y cleient 13 | invalid-redirect-uri = URI ailgyfeirio annilys 14 | invalid-consent = Caniatâd annilys 15 | invalid-csrf-token = Tocyn CSRF annilys 16 | invalid-state = Statws annilys 17 | 18 | oauth-login-to = Mewngofnodi i {$name} 19 | website = Gwefan 20 | oauth-consent = Hoffai {$name} cysylltu â'ch cyfrif. 21 | Gofynwyd am a y caniatâdau yma: 22 | consent = Cymeradwyo 23 | reject = Gwrthod 24 | 25 | scope-read = Mynediad darllen llawn i'ch cyfrif 26 | scope-read-accounts = Darllen gwybodaeth syml am eich cyfrif 27 | scope-read-blocks = Gweld eich blociau 28 | scope-read-bookmarks = Gweld eich llyrnodau 29 | scope-read-favourites = Gweld eich ffefrynnau 30 | scope-read-filters = Gweld eich hidlyddion 31 | scope-read-follows = Gweld eich dilynion 32 | scope-read-lists = Gweld eich rhestrau 33 | scope-read-mutes = Gweld eich tawelodion 34 | scope-read-notifications = Gweld eich hysbysiadau 35 | scope-read-search = Perfformio chwiliadau fel chi 36 | scope-read-statuses = Gweld eich tŵtiau 37 | scope-write = Mynediad ysgrifennu llawn i'ch cyfrif 38 | scope-write-accounts = Gweinyddu gwybodaeth syml am eich cyfrif 39 | scope-write-blocks = Gweinyddu eich blociau 40 | scope-write-bookmarks = Gweinyddu eich llyrnodau 41 | scope-write-conversations = Gweinyddu eich sgyrsiau 42 | scope-write-favourites = Gweinyddu eich ffefrynnau 43 | scope-write-filters = Gweinyddu eich hidlyddion 44 | scope-write-follows = Gweinyddu eich dilynion 45 | scope-write-lists = Gweinyddu eich rhestrau 46 | scope-write-media = Lanlwytho ffeiliau 47 | scope-write-mutes = Gweinyddu eich tawelodion 48 | scope-write-notifications = Gweinyddu eich hysbysiadau 49 | scope-write-reports = Danfon adroddiadau camddefnyd 50 | scope-write-statuses = Danfon tŵtiau 51 | scope-follow = Gweinyddu eich perthynas â chyfrifon eraill 52 | scope-push = Mynediad i'ch hysbysiadau gwthiadwy 53 | 54 | error-no-permission = Nid oes genych ganiatâd i wneud hyn 55 | account-not-found = Ni chanfuwyd y cyfrif 56 | error-status-not-found = Ni chanfuwyd y tŵt 57 | error-notification-not-found = Ni chanfuwyd y hysbysiad 58 | unsupported-media-type = Math ffeil ni allwn ei dderbyn 59 | internal-server-error = Gwall mewnol i'r gweinydd 60 | failed-to-decode-image = Methwyd â darllen y ddelwedd 61 | error-media-not-found = Ni chanfuwyd y ffeil 62 | error-db = Gwall mewnol a'r gronfa ddata 63 | error-invalid-language = Iaith annilys 64 | limit-too-large = Gofynwyd am ormod o ddata 65 | error-invalid-visibility = Gwelededd tŵt annilys 66 | error-invalid-status = Tŵt annilys 67 | service-unavailable = Nid yw'r gwasanaeth ar gael ar hyn o bryd 68 | status-deleted = Mae'r tŵt wedi ei ddileu -------------------------------------------------------------------------------- /i18n/en-GB/tafarn.ftl: -------------------------------------------------------------------------------- 1 | follow-notification = {$name} followed you 2 | favourite-notification = {$name} favourited your toot 3 | reblog-notification = {$name} boosted your toot 4 | mention-notification = {$name} mentioned you 5 | 6 | invalid-client-name = Invalid client name 7 | invalid-website = Invalid website 8 | invalid-scope = Invalid scope 9 | login-unauthorized = You don't have permission to login to this server 10 | 11 | invalid-request = Invalid request 12 | client-not-found = Client not found 13 | invalid-redirect-uri = Invalid redirect URI 14 | invalid-consent = Invalid consent 15 | invalid-csrf-token = Invalid CSRF token 16 | invalid-state = Invalid state 17 | 18 | oauth-login-to = Log in to {$name} 19 | website = Website 20 | oauth-consent = {$name} would like to authenticate with your account. 21 | It has requested the following permissions: 22 | consent = Consent 23 | reject = Reject 24 | 25 | scope-read = Full read access to your account 26 | scope-read-accounts = Read access basic account information 27 | scope-read-blocks = View your blocks 28 | scope-read-bookmarks = View your bookmarks 29 | scope-read-favourites = View your likes 30 | scope-read-filters = View your filters 31 | scope-read-follows = View your follows 32 | scope-read-lists = View your lists 33 | scope-read-mutes = View your mutes 34 | scope-read-notifications = View your notifications 35 | scope-read-search = Perform searches as you 36 | scope-read-statuses = View your statuses 37 | scope-write = Full write access to your account 38 | scope-write-accounts = Manage your account information 39 | scope-write-blocks = Manage your blocks 40 | scope-write-bookmarks = Manage your bookmarks 41 | scope-write-conversations = Manage direct messages 42 | scope-write-favourites = Manage your likes 43 | scope-write-filters = Manage your filters 44 | scope-write-follows = Manage your follows 45 | scope-write-lists = Manage your lists 46 | scope-write-media = Upload media 47 | scope-write-mutes = Manage your mutes 48 | scope-write-notifications = Manage your notifications 49 | scope-write-reports = Submit abuse reports 50 | scope-write-statuses = Post statuses 51 | scope-follow = Manage relationships with other accounts 52 | scope-push = Access to push notifications 53 | 54 | error-no-permission = You do not have permission to perform this action 55 | account-not-found = Account not found 56 | error-status-not-found = Toot not found 57 | error-notification-not-found = Notification not found 58 | unsupported-media-type = Unsupported media type 59 | internal-server-error = Internal server error 60 | failed-to-decode-image = Failed to read image 61 | error-media-not-found = Media not found 62 | error-db = Database error 63 | error-invalid-language = Invalid language 64 | limit-too-large = Limit too large 65 | error-invalid-visibility = Invalid toot visibility 66 | error-invalid-status = Invalid toot 67 | service-unavailable = Service unavailable 68 | status-deleted = Toot deleted -------------------------------------------------------------------------------- /i18n/nl-NL/tafarn.ftl: -------------------------------------------------------------------------------- 1 | follow-notification = {$name} volgt je 2 | favourite-notification = {$name} vindt je toot leuk 3 | reblog-notification = {$name} heeft je toot gedeeld 4 | mention-notification = {$name} heeft je genoemd 5 | 6 | invalid-client-name = Ongeldige clientnaam 7 | invalid-website = Ongeldige website 8 | invalid-scope = Ongeldige scope 9 | login-unauthorized = Je hebt geen toestemmimg om op deze server in te loggen 10 | 11 | invalid-request = Ongeldig verzoek 12 | client-not-found = Client niet gevonden 13 | invalid-redirect-uri = Ongeldige doorverwijs-URI 14 | invalid-consent = Ongeldige toestemming 15 | invalid-csrf-token = Ongeldige CSRF-token 16 | invalid-state = Ongeldige staat 17 | 18 | oauth-login-to = Log in op {$name} 19 | website = Website 20 | oauth-consent = {$name} wilt met je account inloggen. 21 | De applicatie heeft om de volgende toestemmingen gevraagd: 22 | consent = Toestemmimg geven 23 | reject = Weigeren 24 | 25 | scope-read = Volledige lees-toegang tot je account 26 | scope-read-accounts = Basisgegevens van je account inzien 27 | scope-read-blocks = Je blokkades inzien 28 | scope-read-bookmarks = Je bladwijzers inzien 29 | scope-read-favourites = Inzien welke toots je leuk vind 30 | scope-read-filters = Je filters inzien 31 | scope-read-follows = Inzien wie je volgt 32 | scope-read-lists = Je lijsten inzien 33 | scope-read-mutes = Inzien wie je hebt gedempt 34 | scope-read-notifications = Je notificaties zien 35 | scope-read-search = Namens jou zoekopdrachten uitvoeren 36 | scope-read-statuses = Je statussen inzien 37 | scope-write = Volledige schrijftoegang tot je account 38 | scope-write-accounts = Je accountinformatie bewerken 39 | scope-write-blocks = Je blokkades beheren 40 | scope-write-bookmarks = Je bladwijzers beheren 41 | scope-write-conversations = Je directe berichten beheren 42 | scope-write-favourites = Beheren welke toots je leuk vind 43 | scope-write-filters = Je filters beheren 44 | scope-write-follows = Beheren wie je volgt 45 | scope-write-lists = Je lijsten beheren 46 | scope-write-media = Media uploaden 47 | scope-write-mutes = Beheren wie je hebt gedempt 48 | scope-write-notifications = Je notificaties beheren 49 | scope-write-reports = Misbruikmeldingen indienen 50 | scope-write-statuses = Statussen schrijven 51 | scope-follow = Relaties met andere accounts beheren 52 | scope-push = Toegang tot pushnotificaties 53 | 54 | error-no-permission = Je hebt geen toestemming om deze handeling te verrichten 55 | account-not-found = Account niet gevonden 56 | error-status-not-found = Toot niet gevonden 57 | error-notification-not-found = Notificatie niet gevonden 58 | unsupported-media-type = Mediasoort wordt niet ondersteund 59 | internal-server-error = Interne serverfout 60 | failed-to-decode-image = Afbeelding laden mislukt 61 | error-media-not-found = Media niet gevonden 62 | error-db = Databasefout 63 | error-invalid-language = Ongeldige taal 64 | limit-too-large = Limiet te groot 65 | error-invalid-visibility = Ongeldige toot-zichtbaarheid 66 | error-invalid-status = Ongeldige toot 67 | service-unavailable = Dienst niet beschikbaar 68 | status-deleted = Toot verwijderd 69 | -------------------------------------------------------------------------------- /i18n/ru-RU/tafarn.ftl: -------------------------------------------------------------------------------- 1 | follow-notification = {$name} подписался на вас 2 | favourite-notification = {$name} добавил в избранное ваш пост 3 | reblog-notification = {$name} продвинул ваш пост 4 | mention-notification = {$name} упомянул вас 5 | 6 | invalid-client-name = Неверное имя клиента 7 | invalid-website = Неверный веб-сайт 8 | invalid-scope = Неверная область 9 | login-unauthorized = У вас нет прав для входа на этот сервер 10 | 11 | invalid-request = Неверный запрос 12 | client-not-found = Клиент не найден 13 | invalid-redirect-uri = Неверный URI перенаправления 14 | invalid-consent = Неверное разрешение 15 | invalid-csrf-token = Неверный токен CSRF 16 | invalid-state = Неверное состояние 17 | 18 | oauth-login-to = Войти как {$name} 19 | website = Веб-сайт 20 | oauth-consent = {$name} хотел бы пройти аутентификацию с помощью вашей учетной записи. 21 | Он запросил следующие разрешения: 22 | consent = Разрешить 23 | reject = Отклонить 24 | 25 | scope-read = Полный доступ для чтения к вашей учетной записи 26 | scope-read-accounts = Доступ к основной информации об учетной записи для чтения 27 | scope-read-blocks = Просмотр ваших блокировок 28 | scope-read-bookmarks = Просмотр ваших закладок 29 | scope-read-favourites = Просмотр понравившихся 30 | scope-read-filters = Просмотр ваших фильтров 31 | scope-read-follows = Просмотр ваших подписок 32 | scope-read-lists = Просмотр ваших списков 33 | scope-read-mutes = Просмотр игнорируемых 34 | scope-read-notifications = Просмотр ваших уведомлений 35 | scope-read-search = Выполнять поиск 36 | scope-read-statuses = Просмотр ваших статусов 37 | scope-write = Полный доступ на запись к вашей учетной записи 38 | scope-write-accounts = Управление информацией вашей учетной записи 39 | scope-write-blocks = Управление вашими блокировками 40 | scope-write-bookmarks = Управление вашими закладками 41 | scope-write-conversations = Управление вашими личными сообщениями 42 | scope-write-favourites = Управление вашими лайками 43 | scope-write-filters = Управление вашими фильтрами 44 | scope-write-follows = Управление вашими подписчиками 45 | scope-write-lists = Управление вашими списками 46 | scope-write-media = Загружать мультимедиа 47 | scope-write-mutes = Управление вашими игнорируемыми 48 | scope-write-notifications = Управление вашими уведомлениями 49 | scope-write-reports = Отправлять отчеты о нарушениях 50 | scope-write-statuses = Писать сообщения 51 | scope-follow = Управление отношениями с другими учетными записями 52 | scope-push = Доступ к push-уведомлениям 53 | 54 | error-no-permission = У вас нет разрешения на выполнение этого действия 55 | account-not-found = Учетная запись не найдена 56 | error-status-not-found = Запись не найдено 57 | error-notification-not-found = Уведомление не найдено 58 | unsupported-media-type = Неподдерживаемый тип мультимедиа 59 | internal-server-error = Внутренняя ошибка сервера 60 | failed-to-decode-image = Не удалось прочитать изображение 61 | error-media-not-found = Мультимедиа не найдено 62 | error-db = Ошибка базы данных 63 | error-invalid-language = Неверный язык 64 | limit-too-large = Лимит слишком велик 65 | error-invalid-visibility = Неверная видимость записи 66 | error-invalid-status = Неверная запись 67 | service-unavailable = Сервис недоступен 68 | status-deleted = Запись удалена 69 | -------------------------------------------------------------------------------- /kube/POST: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheEnbyperor/tafarn/916398174296c28405d2ed256c3d4987f9ce1805/kube/POST -------------------------------------------------------------------------------- /kube/apply.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=$(sentry-cli releases propose-version || exit) 4 | 5 | kubectl apply -f pvc.yaml || exit 6 | kubectl apply -f config.yaml || exit 7 | kubectl apply -f secrets.yaml || exit 8 | kubectl apply -f net.yaml || exit 9 | kubectl apply -f svc.yaml || exit 10 | sed -e "s/(version)/$VERSION/g" < deploy.yaml | kubectl apply -f - || exit 11 | kubectl apply -f nginx.yaml || exit 12 | kubectl apply -f ingress.yaml || exit 13 | -------------------------------------------------------------------------------- /kube/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: tafarn-conf 5 | namespace: toot 6 | data: 7 | IDENT: "false" 8 | URI: "tafarn.glauca.space" 9 | VAPID_KEY: "/keys/vapid_key.pem" 10 | AS_KEY: "/keys/as_key.pem" 11 | MEDIA_PATH: "/media" 12 | LIMITS: "{data-form = \"250MiB\", file = \"250MiB\"}" 13 | --- 14 | apiVersion: v1 15 | kind: ConfigMap 16 | metadata: 17 | name: tafarn-nginx-conf 18 | namespace: toot 19 | data: 20 | nginx.conf: | 21 | user nginx; 22 | worker_processes 4; 23 | 24 | error_log /var/log/nginx/error.log warn; 25 | pid /var/run/nginx.pid; 26 | 27 | events { 28 | worker_connections 4096; 29 | } 30 | 31 | http { 32 | include /etc/nginx/mime.types; 33 | default_type application/octet-stream; 34 | 35 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 36 | '$status $body_bytes_sent "$http_referer" ' 37 | '"$http_user_agent" "$http_x_forwarded_for"'; 38 | 39 | access_log /var/log/nginx/access.log main; 40 | tcp_nopush on; 41 | 42 | upstream backend { 43 | server tafarn-frontend:80 fail_timeout=0; 44 | 45 | keepalive 64; 46 | } 47 | 48 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g; 49 | 50 | server { 51 | listen 80; 52 | listen [::]:80; 53 | server_name tafarn.glauca.space; 54 | 55 | keepalive_timeout 70; 56 | sendfile on; 57 | client_max_body_size 80m; 58 | 59 | gzip on; 60 | gzip_disable "msie6"; 61 | gzip_vary on; 62 | gzip_proxied any; 63 | gzip_comp_level 6; 64 | gzip_buffers 16 8k; 65 | gzip_http_version 1.1; 66 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon; 67 | 68 | location / { 69 | proxy_set_header Host $host; 70 | proxy_set_header X-Real-IP $remote_addr; 71 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 72 | proxy_set_header X-Forwarded-Proto https; 73 | proxy_set_header Proxy ""; 74 | proxy_pass_header Server; 75 | 76 | proxy_pass http://backend; 77 | proxy_buffering on; 78 | proxy_redirect off; 79 | proxy_cache off; 80 | 81 | add_header X-Cached $upstream_cache_status; 82 | add_header Cache-Control 'private, no-store'; 83 | expires off; 84 | etag off; 85 | 86 | tcp_nodelay on; 87 | } 88 | 89 | location /media/ { 90 | add_header Cache-Control "public, max-age=31536000, immutable"; 91 | alias /media/; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /kube/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tafarn-frontend 5 | namespace: toot 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: tafarn 11 | part: frontend 12 | template: 13 | metadata: 14 | annotations: 15 | cni.projectcalico.org/ipv6pools: "[\"default-ipv6-ippool\"]" 16 | labels: 17 | app: tafarn 18 | part: frontend 19 | spec: 20 | volumes: 21 | - name: conf 22 | configMap: 23 | name: tafarn-conf 24 | - name: keys 25 | secret: 26 | secretName: tafarn-keys 27 | - name: media 28 | persistentVolumeClaim: 29 | claimName: tafarn-media 30 | dnsConfig: 31 | options: 32 | - name: ndots 33 | value: "1" 34 | containers: 35 | - name: frontend 36 | image: theenbyperor/tafarn:(version) 37 | imagePullPolicy: IfNotPresent 38 | command: 39 | - "/tafarn/frontend" 40 | env: 41 | - name: RUST_BACKTRACE 42 | value: "full" 43 | - name: RUST_LOG 44 | value: INFO 45 | - name: ROCKET_ENV 46 | value: production 47 | - name: ROCKET_PORT 48 | value: "80" 49 | - name: ROCKET_ADDRESS 50 | value: "::" 51 | envFrom: 52 | - prefix: "ROCKET_" 53 | configMapRef: 54 | name: tafarn-conf 55 | - prefix: "ROCKET_" 56 | secretRef: 57 | name: tafarn-oidc 58 | - prefix: "ROCKET_" 59 | secretRef: 60 | name: tafarn-celery 61 | - prefix: "ROCKET_" 62 | secretRef: 63 | name: tafarn-secrets 64 | - prefix: "ROCKET_" 65 | secretRef: 66 | name: tafarn-db 67 | volumeMounts: 68 | - mountPath: "/media" 69 | name: media 70 | - mountPath: "/keys" 71 | name: keys 72 | - mountPath: "/tafarn/Rocket.toml" 73 | name: conf 74 | subPath: "Rocket.toml" 75 | ports: 76 | - containerPort: 80 77 | - name: clatd 78 | image: as207960/clatd 79 | command: [ 80 | "/bin/bash", "-c", 81 | "/clatd/clatd clat-v6-addr=fd2e:ae7d:58e3:f6ab::1 v4-conncheck-enable=no 'script-up=ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE'" 82 | ] 83 | securityContext: 84 | privileged: true 85 | --- 86 | apiVersion: apps/v1 87 | kind: Deployment 88 | metadata: 89 | name: tafarn-tasks 90 | namespace: toot 91 | spec: 92 | replicas: 2 93 | selector: 94 | matchLabels: 95 | app: tafarn 96 | part: tasks 97 | template: 98 | metadata: 99 | annotations: 100 | cni.projectcalico.org/ipv6pools: "[\"default-ipv6-ippool\"]" 101 | labels: 102 | app: tafarn 103 | part: tasks 104 | spec: 105 | volumes: 106 | - name: conf 107 | configMap: 108 | name: tafarn-conf 109 | - name: keys 110 | secret: 111 | secretName: tafarn-keys 112 | - name: media 113 | persistentVolumeClaim: 114 | claimName: tafarn-media 115 | dnsConfig: 116 | options: 117 | - name: ndots 118 | value: "1" 119 | containers: 120 | - name: frontend 121 | image: theenbyperor/tafarn:(version) 122 | imagePullPolicy: IfNotPresent 123 | command: 124 | - "/tafarn/tasks" 125 | env: 126 | - name: RUST_BACKTRACE 127 | value: "full" 128 | - name: RUST_LOG 129 | value: INFO 130 | - name: ROCKET_ENV 131 | value: production 132 | - name: ROCKET_PORT 133 | value: "80" 134 | - name: ROCKET_ADDRESS 135 | value: "::" 136 | envFrom: 137 | - prefix: "ROCKET_" 138 | configMapRef: 139 | name: tafarn-conf 140 | - prefix: "ROCKET_" 141 | secretRef: 142 | name: tafarn-oidc 143 | - prefix: "ROCKET_" 144 | secretRef: 145 | name: tafarn-celery 146 | - prefix: "ROCKET_" 147 | secretRef: 148 | name: tafarn-secrets 149 | - prefix: "ROCKET_" 150 | secretRef: 151 | name: tafarn-db 152 | volumeMounts: 153 | - mountPath: "/media" 154 | name: media 155 | - mountPath: "/keys" 156 | name: keys 157 | - mountPath: "/tafarn/Rocket.toml" 158 | name: conf 159 | subPath: "Rocket.toml" 160 | ports: 161 | - containerPort: 80 162 | - name: clatd 163 | image: as207960/clatd 164 | command: [ 165 | "/bin/bash", "-c", 166 | "/clatd/clatd clat-v6-addr=fd2e:ae7d:58e3:f6ab::1 v4-conncheck-enable=no 'script-up=ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE'" 167 | ] 168 | securityContext: 169 | privileged: true -------------------------------------------------------------------------------- /kube/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: tafarn-ingress 5 | namespace: toot 6 | annotations: 7 | cert-manager.io/cluster-issuer: letsencrypt 8 | nginx.ingress.kubernetes.io/proxy-body-size: "0" 9 | spec: 10 | tls: 11 | - hosts: 12 | - tafarn.glauca.space 13 | secretName: tafarn-tls 14 | rules: 15 | - host: tafarn.glauca.space 16 | http: 17 | paths: 18 | - path: / 19 | pathType: Prefix 20 | backend: 21 | service: 22 | name: tafarn-nginx 23 | port: 24 | number: 80 -------------------------------------------------------------------------------- /kube/net.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: tafarn-frontend 5 | namespace: toot 6 | spec: 7 | ingress: 8 | - from: 9 | - podSelector: 10 | matchLabels: 11 | app: tafarn 12 | part: nginx 13 | ports: 14 | - port: 80 15 | protocol: TCP 16 | podSelector: 17 | matchLabels: 18 | app: tafarn 19 | part: frontend 20 | policyTypes: 21 | - Ingress 22 | --- 23 | apiVersion: networking.k8s.io/v1 24 | kind: NetworkPolicy 25 | metadata: 26 | name: tafarn-nginx 27 | namespace: toot 28 | spec: 29 | podSelector: 30 | matchLabels: 31 | app: tafarn 32 | part: nginx 33 | policyTypes: 34 | - Ingress 35 | ingress: 36 | - from: 37 | - namespaceSelector: 38 | matchLabels: 39 | app.kubernetes.io/name: ingress-nginx 40 | podSelector: 41 | matchLabels: 42 | app.kubernetes.io/name: ingress-nginx 43 | ports: 44 | - protocol: TCP 45 | port: 80 -------------------------------------------------------------------------------- /kube/nginx.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tafarn-nginx 5 | namespace: toot 6 | labels: 7 | app: tafarn 8 | part: nginx 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: tafarn 14 | part: nginx 15 | template: 16 | metadata: 17 | annotations: 18 | cni.projectcalico.org/ipv6pools: "[\"default-ipv6-ippool\"]" 19 | labels: 20 | app: tafarn 21 | part: nginx 22 | spec: 23 | volumes: 24 | - name: media 25 | persistentVolumeClaim: 26 | claimName: tafarn-media 27 | - name: conf 28 | configMap: 29 | name: tafarn-nginx-conf 30 | containers: 31 | - name: nginx 32 | image: nginx 33 | imagePullPolicy: IfNotPresent 34 | ports: 35 | - containerPort: 80 36 | volumeMounts: 37 | - mountPath: "/media" 38 | name: media 39 | - mountPath: "/etc/nginx/nginx.conf" 40 | name: conf 41 | subPath: "nginx.conf" -------------------------------------------------------------------------------- /kube/pvc.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: tafarn-media 5 | namespace: toot 6 | spec: 7 | storageClassName: standard 8 | accessModes: 9 | - ReadWriteMany 10 | resources: 11 | requests: 12 | storage: 10Gi -------------------------------------------------------------------------------- /kube/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: tafarn-frontend 5 | namespace: toot 6 | labels: 7 | app: tafarn 8 | part: frontend 9 | spec: 10 | selector: 11 | app: tafarn 12 | part: frontend 13 | ports: 14 | - port: 80 15 | targetPort: 80 16 | name: http 17 | --- 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: tafarn-nginx 22 | namespace: toot 23 | labels: 24 | app: tafran 25 | part: nginx 26 | spec: 27 | selector: 28 | app: tafarn 29 | part: nginx 30 | ports: 31 | - port: 80 32 | targetPort: 80 33 | name: http 34 | -------------------------------------------------------------------------------- /migrations/2022-11-01-230545_sessions/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE session;-- This file should undo anything in `up.sql` -------------------------------------------------------------------------------- /migrations/2022-11-01-230545_sessions/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE session 2 | ( 3 | id UUID PRIMARY KEY, 4 | access_token VARCHAR NOT NULL, 5 | expires_at TIMESTAMP, 6 | refresh_token VARCHAR, 7 | claims VARCHAR NOT NULL 8 | ); -------------------------------------------------------------------------------- /migrations/2022-11-02-090527_apps/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE app_scopes; 2 | DROP TABLE apps; -------------------------------------------------------------------------------- /migrations/2022-11-02-090527_apps/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE apps ( 2 | id UUID PRIMARY KEY, 3 | name TEXT NOT NULL, 4 | website TEXT, 5 | redirect_uri TEXT NOT NULL, 6 | client_secret VARCHAR NOT NULL 7 | ); 8 | 9 | CREATE TABLE app_scopes ( 10 | app_id UUID REFERENCES apps(id) ON DELETE CASCADE, 11 | scope TEXT NOT NULL, 12 | PRIMARY KEY (app_id, scope) 13 | ); 14 | 15 | CREATE INDEX app_scopes_app_id ON app_scopes (app_id); -------------------------------------------------------------------------------- /migrations/2022-11-02-105841_oauth_consents/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE oauth_consent_scopes; 2 | DROP TABLE oauth_consents; -------------------------------------------------------------------------------- /migrations/2022-11-02-105841_oauth_consents/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE oauth_consents ( 2 | id UUID PRIMARY KEY, 3 | app_id UUID REFERENCES apps(id), 4 | user_id VARCHAR NOT NULL, 5 | time TIMESTAMP NOT NULL 6 | ); 7 | 8 | CREATE TABLE oauth_consent_scopes ( 9 | consent_id UUID REFERENCES oauth_consents(id) ON DELETE CASCADE, 10 | scope VARCHAR NOT NULL, 11 | PRIMARY KEY (consent_id, scope) 12 | ); 13 | 14 | CREATE INDEX oauth_consent_scopes_consent_id ON oauth_consent_scopes(consent_id); -------------------------------------------------------------------------------- /migrations/2022-11-02-142118_oauth_tokens/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE oauth_token_scopes; 2 | DROP TABLE oauth_token; 3 | DROP TABLE oauth_code_scopes; 4 | DROP TABLE oauth_codes; -------------------------------------------------------------------------------- /migrations/2022-11-02-142118_oauth_tokens/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE oauth_codes 2 | ( 3 | id UUID PRIMARY KEY NOT NULL, 4 | time TIMESTAMP NOT NULL, 5 | redirect_uri TEXT NOT NULL, 6 | client_id UUID REFERENCES apps (id) ON DELETE CASCADE NOT NULL, 7 | user_id TEXT NOT NULL 8 | ); 9 | 10 | CREATE TABLE oauth_code_scopes 11 | ( 12 | code_id UUID REFERENCES oauth_codes (id) ON DELETE CASCADE NOT NULL, 13 | scope VARCHAR NOT NULL, 14 | PRIMARY KEY (code_id, scope) 15 | ); 16 | 17 | CREATE INDEX oauth_code_scopes_code_id ON oauth_code_scopes (code_id); 18 | 19 | CREATE TABLE oauth_token 20 | ( 21 | id UUID PRIMARY KEY NOT NULL, 22 | time TIMESTAMP NOT NULL, 23 | client_id UUID REFERENCES apps (id) ON DELETE CASCADE NOT NULL, 24 | user_id TEXT NOT NULL, 25 | revoked BOOLEAN NOT NULL 26 | ); 27 | 28 | CREATE TABLE oauth_token_scopes 29 | ( 30 | token_id UUID REFERENCES oauth_token (id) ON DELETE CASCADE NOT NULL, 31 | scope VARCHAR NOT NULL, 32 | PRIMARY KEY (token_id, scope) 33 | ); 34 | 35 | CREATE INDEX oauth_token_scopes ON oauth_token_scopes (token_id); -------------------------------------------------------------------------------- /migrations/2022-11-02-162440_accounts/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE account_fields; 2 | DROP TABLE accounts; -------------------------------------------------------------------------------- /migrations/2022-11-02-162440_accounts/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE accounts ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | actor TEXT NULL, 4 | username VARCHAR(255) NOT NULL, 5 | display_name TEXT NOT NULL, 6 | bio TEXT NOT NULL, 7 | locked BOOLEAN NOT NULL, 8 | bot BOOLEAN NOT NULL, 9 | created_at TIMESTAMP NOT NULL, 10 | updated_at TIMESTAMP NOT NULL, 11 | avatar TEXT NULL, 12 | header TEXT NULL, 13 | default_sensitive BOOL NULL, 14 | default_language CHAR(2) NULL, 15 | discoverable BOOL NULL, 16 | follower_count INT NOT NULL, 17 | following_count INT NOT NULL, 18 | statuses_count INT NOT NULL, 19 | owned_by TEXT NULL, 20 | private_key TEXT NULL, 21 | local BOOLEAN NOT NULL, 22 | inbox_url TEXT NULL, 23 | outbox_url TEXT NULL, 24 | shared_inbox_url TEXT NULL, 25 | url TEXT NULL 26 | ); 27 | 28 | CREATE INDEX accounts_username ON accounts (username); 29 | CREATE INDEX accounts_owned_by ON accounts (owned_by); 30 | 31 | CREATE TABLE account_fields ( 32 | id UUID PRIMARY KEY NOT NULL, 33 | account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 34 | name TEXT NOT NULL, 35 | value TEXT NOT NULL, 36 | sort_order INT NOT NULL 37 | ); 38 | 39 | CREATE INDEX account_fields_account_id ON account_fields (account_id); -------------------------------------------------------------------------------- /migrations/2022-11-03-110414_web_push/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE web_push_subscriptions; -------------------------------------------------------------------------------- /migrations/2022-11-03-110414_web_push/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE web_push_subscriptions ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | token_id UUID REFERENCES oauth_token(id) ON DELETE CASCADE UNIQUE, 4 | account_id UUID REFERENCES accounts(id) ON DELETE CASCADE, 5 | endpoint TEXT NOT NULL, 6 | p256dh TEXT NOT NULL, 7 | auth TEXT NOT NULL, 8 | follow BOOLEAN NOT NULL DEFAULT FALSE, 9 | favourite BOOLEAN NOT NULL DEFAULT FALSE, 10 | reblog BOOLEAN NOT NULL DEFAULT FALSE, 11 | mention BOOLEAN NOT NULL DEFAULT FALSE, 12 | poll BOOLEAN NOT NULL DEFAULT FALSE 13 | ); 14 | 15 | CREATE INDEX web_push_subscriptions_token_id ON web_push_subscriptions (token_id); 16 | CREATE INDEX web_push_subscriptions_account_id ON web_push_subscriptions (account_id); -------------------------------------------------------------------------------- /migrations/2022-11-04-124154_public_key/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public_keys; -------------------------------------------------------------------------------- /migrations/2022-11-04-124154_public_key/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public_keys ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | key_id TEXT NOT NULL, 4 | user_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 5 | key TEXT NOT NULL 6 | ); 7 | 8 | CREATE INDEX public_keys_key_id ON public_keys(key_id); -------------------------------------------------------------------------------- /migrations/2022-12-26-132253_relationships/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE following; -------------------------------------------------------------------------------- /migrations/2022-12-26-132253_relationships/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE following ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | follower UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 4 | followee UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 5 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 6 | UNIQUE (follower, followee) 7 | ); 8 | 9 | CREATE INDEX following_follower_followee ON following(follower, followee); -------------------------------------------------------------------------------- /migrations/2022-12-26-224527_web_push_update/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE web_push_subscriptions 2 | DROP COLUMN "status", 3 | DROP COLUMN "follow_request", 4 | DROP COLUMN "update", 5 | DROP COLUMN "admin_sign_up", 6 | DROP COLUMN "admin_report"; -------------------------------------------------------------------------------- /migrations/2022-12-26-224527_web_push_update/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE web_push_subscriptions 2 | ADD COLUMN "status" BOOLEAN NOT NULL DEFAULT FALSE, 3 | ADD COLUMN "follow_request" BOOLEAN NOT NULL DEFAULT FALSE, 4 | ADD COLUMN "update" BOOLEAN NOT NULL DEFAULT FALSE, 5 | ADD COLUMN "admin_sign_up" BOOLEAN NOT NULL DEFAULT FALSE, 6 | ADD COLUMN "admin_report" BOOLEAN NOT NULL DEFAULT FALSE; -------------------------------------------------------------------------------- /migrations/2022-12-26-231644_notifications/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE notifications; -------------------------------------------------------------------------------- /migrations/2022-12-26-231644_notifications/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE notifications ( 2 | id UUID NOT NULL PRIMARY KEY, 3 | notification_type VARCHAR NOT NULL, 4 | account UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 5 | cause UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 6 | status UUID NULL, 7 | created_at TIMESTAMP NOT NULL DEFAULT NOW() 8 | ) -------------------------------------------------------------------------------- /migrations/2022-12-27-012154_group_account/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts DROP COLUMN "group"; -------------------------------------------------------------------------------- /migrations/2022-12-27-012154_group_account/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts ADD COLUMN "group" BOOLEAN NOT NULL DEFAULT FALSE; -------------------------------------------------------------------------------- /migrations/2022-12-29-140619_account_images/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts 2 | DROP COLUMN avatar_file, 3 | DROP COLUMN avatar_content_type, 4 | DROP COLUMN avatar_remote_url, 5 | DROP COLUMN header_file, 6 | DROP COLUMN header_content_type, 7 | DROP COLUMN header_remote_url, 8 | ADD COLUMN avatar TEXT NULL, 9 | ADD COLUMN header TEXT NULL; -------------------------------------------------------------------------------- /migrations/2022-12-29-140619_account_images/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts 2 | ADD COLUMN avatar_file TEXT NULL, 3 | ADD COLUMN avatar_content_type VARCHAR(255) NULL, 4 | ADD COLUMN avatar_remote_url TEXT NULL, 5 | ADD COLUMN header_file TEXT NULL, 6 | ADD COLUMN header_content_type VARCHAR(255) NULL, 7 | ADD COLUMN header_remote_url TEXT NULL, 8 | DROP COLUMN avatar, 9 | DROP COLUMN header; -------------------------------------------------------------------------------- /migrations/2023-01-01-144406_pagination/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts DROP COLUMN iid; 2 | ALTER TABLE following DROP COLUMN iid; 3 | ALTER TABLE notifications DROP COLUMN iid; -------------------------------------------------------------------------------- /migrations/2023-01-01-144406_pagination/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts ADD COLUMN iid SERIAL; 2 | ALTER TABLE following ADD COLUMN iid SERIAL; 3 | ALTER TABLE notifications ADD COLUMN iid SERIAL; 4 | 5 | CREATE INDEX accounts_iid_idx ON accounts (iid); 6 | CREATE INDEX following_iid_idx ON following (iid); 7 | CREATE INDEX notifications_iid_idx ON notifications (iid); -------------------------------------------------------------------------------- /migrations/2023-01-03-184435_pending_follow/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE following DROP COLUMN pending; -------------------------------------------------------------------------------- /migrations/2023-01-03-184435_pending_follow/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE following ADD COLUMN pending BOOLEAN NOT NULL DEFAULT FALSE; -------------------------------------------------------------------------------- /migrations/2023-01-04-123932_media/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE media; -------------------------------------------------------------------------------- /migrations/2023-01-04-123932_media/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE media ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | iid SERIAL, 4 | media_type VARCHAR(255) NOT NULL, 5 | file TEXT NULL, 6 | content_type VARCHAR(255) NULL, 7 | remote_url TEXT NULL, 8 | preview_file TEXT NULL, 9 | preview_content_type VARCHAR(255) NULL, 10 | blurhash TEXT NULL, 11 | focus_x DOUBLE PRECISION NULL, 12 | focus_y DOUBLE PRECISION NULL, 13 | original_width INT NULL, 14 | original_height INT NULL, 15 | preview_width INT NULL, 16 | preview_height INT NULL, 17 | created_at TIMESTAMP NOT NULL, 18 | description TEXT NULL, 19 | owned_by TEXT NULL 20 | ); 21 | 22 | CREATE INDEX media_iid_idx ON media (iid); -------------------------------------------------------------------------------- /migrations/2023-01-07-235744_web_push_policy/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE web_push_subscriptions DROP COLUMN policy; -------------------------------------------------------------------------------- /migrations/2023-01-07-235744_web_push_policy/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE web_push_subscriptions ADD COLUMN policy TEXT NOT NULL DEFAULT 'all'; -------------------------------------------------------------------------------- /migrations/2023-01-08-021756_status/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE status_audiences; 2 | ALTER TABLE accounts DROP COLUMN follower_collection_url; 3 | DROP TABLE status_media_attachments; 4 | DROP TABLE statuses; -------------------------------------------------------------------------------- /migrations/2023-01-08-021756_status/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE statuses ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | iid SERIAL, 4 | url TEXT NOT NULL, 5 | uri TEXT NULL, 6 | text TEXT NOT NULL DEFAULT '', 7 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 8 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 9 | in_reply_to_id UUID NULL REFERENCES statuses(id) ON DELETE SET NULL, 10 | boost_of_id UUID NULL REFERENCES statuses(id) ON DELETE CASCADE, 11 | in_reply_to_url TEXT NULL, 12 | boost_of_url TEXT NULL, 13 | sensitive BOOLEAN NOT NULL DEFAULT FALSE, 14 | spoiler_text TEXT NOT NULL DEFAULT '', 15 | language TEXT NULL, 16 | local BOOLEAN NOT NULL DEFAULT TRUE, 17 | account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 18 | deleted_at TIMESTAMP NULL, 19 | edited_at TIMESTAMP NULL, 20 | "public" BOOLEAN NOT NULL DEFAULT FALSE, 21 | "visible" BOOLEAN NOT NULL DEFAULT TRUE 22 | ); 23 | 24 | CREATE INDEX statuses_iid_idx ON statuses (iid); 25 | 26 | CREATE TABLE status_media_attachments ( 27 | status_id UUID NOT NULL REFERENCES statuses(id) ON DELETE CASCADE, 28 | media_attachment_id UUID NOT NULL REFERENCES media(id) ON DELETE CASCADE, 29 | attachment_order INTEGER NOT NULL, 30 | PRIMARY KEY (status_id, media_attachment_id) 31 | ); 32 | 33 | ALTER TABLE accounts ADD COLUMN follower_collection_url TEXT NULL; 34 | 35 | CREATE TABLE status_audiences ( 36 | id UUID PRIMARY KEY NOT NULL, 37 | status_id UUID NOT NULL REFERENCES statuses(id) ON DELETE CASCADE, 38 | mention BOOLEAN NOT NULL DEFAULT FALSE, 39 | account UUID NULL REFERENCES accounts(id) ON DELETE CASCADE, 40 | account_followers UUID NULL REFERENCES accounts(id) ON DELETE CASCADE 41 | ); -------------------------------------------------------------------------------- /migrations/2023-01-08-143528_timelines/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public_timeline; 2 | DROP TABLE home_timeline; -------------------------------------------------------------------------------- /migrations/2023-01-08-143528_timelines/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE home_timeline ( 2 | id SERIAL PRIMARY KEY, 3 | account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 4 | status_id UUID NOT NULL REFERENCES statuses(id) ON DELETE CASCADE 5 | ); 6 | 7 | CREATE TABLE public_timeline ( 8 | id SERIAL PRIMARY KEY, 9 | status_id UUID NOT NULL REFERENCES statuses(id) ON DELETE CASCADE 10 | ); -------------------------------------------------------------------------------- /migrations/2023-01-09-171219_likes_bookmarks_pins/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE pins; 2 | DROP TABLE bookmarks; 3 | DROP TABLE likes; -------------------------------------------------------------------------------- /migrations/2023-01-09-171219_likes_bookmarks_pins/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE likes ( 2 | id UUID PRIMARY KEY NOT NULL, 3 | iid SERIAL, 4 | status UUID NULL REFERENCES statuses(id), 5 | account UUID NOT NULL REFERENCES accounts(id), 6 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | url TEXT NULL, 8 | local BOOLEAN NOT NULL DEFAULT FALSE, 9 | status_url TEXT NULL 10 | ); 11 | 12 | CREATE TABLE bookmarks ( 13 | id UUID PRIMARY KEY NOT NULL, 14 | iid SERIAL, 15 | status UUID NOT NULL REFERENCES statuses(id), 16 | account UUID NOT NULL REFERENCES accounts(id) 17 | ); 18 | 19 | CREATE TABLE pins ( 20 | id UUID PRIMARY KEY NOT NULL, 21 | iid SERIAL, 22 | status UUID NOT NULL REFERENCES statuses(id), 23 | account UUID NOT NULL REFERENCES accounts(id) 24 | ); -------------------------------------------------------------------------------- /migrations/2023-01-10-094949_account_notes/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE account_notes; -------------------------------------------------------------------------------- /migrations/2023-01-10-094949_account_notes/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE account_notes ( 2 | account UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 3 | owner UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, 4 | note TEXT NOT NULL, 5 | PRIMARY KEY (account, owner) 6 | ); -------------------------------------------------------------------------------- /migrations/2023-01-13-162247_media_attachments/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE media_attachments; 2 | 3 | ALTER TABLE statuses 4 | DROP COLUMN text_source, 5 | DROP COLUMN spoiler_text_sounce; -------------------------------------------------------------------------------- /migrations/2023-01-13-162247_media_attachments/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE media_attachments ( 2 | status UUID NOT NULL REFERENCES statuses(id) ON DELETE CASCADE, 3 | media UUID NOT NULL REFERENCES media(id) ON DELETE CASCADE, 4 | PRIMARY KEY (status, media) 5 | ); 6 | 7 | ALTER TABLE statuses 8 | ADD COLUMN text_source TEXT NULL, 9 | ADD COLUMN spoiler_text_source TEXT NULL; -------------------------------------------------------------------------------- /migrations/2023-01-13-194040_account_deletion/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts DROP COLUMN deleted_at; -------------------------------------------------------------------------------- /migrations/2023-01-13-194040_account_deletion/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE accounts ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL; -------------------------------------------------------------------------------- /migrations/2023-01-15-193307_status_mention/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE tags; 2 | DROP TABLE status_mentions; -------------------------------------------------------------------------------- /migrations/2023-01-15-193307_status_mention/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE status_mentions ( 2 | id UUID PRIMARY KEY, 3 | status UUID NOT NULL REFERENCES statuses(id) ON DELETE CASCADE, 4 | account UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE 5 | ); 6 | 7 | CREATE TABLE tags ( 8 | id UUID PRIMARY KEY, 9 | name TEXT NOT NULL UNIQUE 10 | ); -------------------------------------------------------------------------------- /migrations/2023-01-15-203532_follow_notify_and_reblogs/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE following 2 | DROP COLUMN notify, 3 | DROP COLUMN reblogs; -------------------------------------------------------------------------------- /migrations/2023-01-15-203532_follow_notify_and_reblogs/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE following 2 | ADD COLUMN notify BOOLEAN NOT NULL DEFAULT FALSE, 3 | ADD COLUMN reblogs BOOLEAN NOT NULL DEFAULT TRUE; -------------------------------------------------------------------------------- /src/bin/frontend.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use rocket_sync_db_pools::Poolable; 5 | 6 | pub struct CORS; 7 | 8 | #[rocket::async_trait] 9 | impl rocket::fairing::Fairing for CORS { 10 | fn info(&self) -> rocket::fairing::Info { 11 | rocket::fairing::Info { 12 | name: "CORS", 13 | kind: rocket::fairing::Kind::Response, 14 | } 15 | } 16 | 17 | async fn on_response<'r>(&self, _request: &'r rocket::Request<'_>, response: &mut rocket::Response<'r>) { 18 | response.set_header(rocket::http::Header::new("Access-Control-Allow-Origin", "*")); 19 | response.set_header(rocket::http::Header::new("Access-Control-Allow-Methods", "POST, GET, PATCH, OPTIONS")); 20 | response.set_header(rocket::http::Header::new("Access-Control-Allow-Headers", "*")); 21 | response.set_header(rocket::http::Header::new("Access-Control-Allow-Credentials", "true")); 22 | } 23 | } 24 | 25 | #[rocket::options("/<_..>")] 26 | fn all_options() {} 27 | 28 | #[tokio::main] 29 | async fn main() -> Result<(), rocket::Error> { 30 | pretty_env_logger::init(); 31 | 32 | info!("Tafarn frontend starting..."); 33 | 34 | let app = tafarn::setup().await; 35 | 36 | tafarn::tasks::CONFIG.write().unwrap().replace(tafarn::tasks::Config { 37 | db: std::sync::Arc::new(diesel::PgConnection::pool("db", &app.rocket).unwrap()), 38 | celery: std::sync::Arc::new(app.celery_app.clone()), 39 | uri: app.uri, 40 | vapid_key: app.vapid_key, 41 | web_push_client: std::sync::Arc::new(web_push_old::WebPushClient::new()), 42 | as_key: std::sync::Arc::new(app.as_key), 43 | media_path: std::sync::Arc::new(app.media_path.clone()) 44 | }); 45 | 46 | let _ = app.rocket 47 | .attach(CORS) 48 | .attach(tafarn::DbConn::fairing()) 49 | .attach(tafarn::csrf::CSRFFairing) 50 | .attach(rocket_dyn_templates::Template::custom(|engines| { 51 | engines.tera.register_function("fl", tafarn::i18n::TeraLocalizer::new()); 52 | })) 53 | .manage(app.celery_app) 54 | .mount("/static", rocket::fs::FileServer::from("./static")) 55 | .mount("/media", rocket::fs::FileServer::from(app.media_path)) 56 | .mount("/", rocket::routes![ 57 | all_options, 58 | 59 | tafarn::views::oidc::oidc_redirect, 60 | 61 | tafarn::views::meta::host_meta, 62 | tafarn::views::meta::web_finger, 63 | tafarn::views::meta::well_known_node_info, 64 | 65 | tafarn::views::nodeinfo::node_info_2_0, 66 | tafarn::views::nodeinfo::node_info_2_1, 67 | 68 | tafarn::views::oauth::api_apps_form, 69 | tafarn::views::oauth::api_apps_json, 70 | tafarn::views::oauth::oauth_authorize, 71 | tafarn::views::oauth::oauth_consent, 72 | tafarn::views::oauth::oauth_token_form, 73 | tafarn::views::oauth::oauth_token_json, 74 | tafarn::views::oauth::oauth_revoke, 75 | 76 | tafarn::views::instance::instance, 77 | tafarn::views::instance::instance_v2, 78 | tafarn::views::instance::instance_peers, 79 | tafarn::views::instance::instance_activity, 80 | tafarn::views::instance::custom_emoji, 81 | 82 | tafarn::views::accounts::verify_credentials, 83 | tafarn::views::accounts::user_preferences, 84 | tafarn::views::accounts::update_credentials, 85 | tafarn::views::accounts::account, 86 | tafarn::views::accounts::account_statuses, 87 | tafarn::views::accounts::account_following, 88 | tafarn::views::accounts::account_followers, 89 | tafarn::views::accounts::lists, 90 | tafarn::views::accounts::relationships, 91 | tafarn::views::accounts::familiar_followers, 92 | tafarn::views::accounts::follow_account, 93 | tafarn::views::accounts::unfollow_account, 94 | tafarn::views::accounts::note, 95 | tafarn::views::accounts::lookup_account, 96 | 97 | tafarn::views::timelines::timeline_home, 98 | tafarn::views::timelines::timeline_hashtag, 99 | tafarn::views::timelines::timeline_public, 100 | 101 | tafarn::views::conversations::conversations, 102 | tafarn::views::conversations::delete_conversation, 103 | tafarn::views::conversations::read_conversation, 104 | 105 | tafarn::views::lists::lists, 106 | tafarn::views::lists::list, 107 | tafarn::views::lists::create_list, 108 | tafarn::views::lists::update_list, 109 | tafarn::views::lists::delete_list, 110 | tafarn::views::lists::list_accounts, 111 | tafarn::views::lists::list_add_accounts, 112 | tafarn::views::lists::list_delete_accounts, 113 | 114 | tafarn::views::filters::filters, 115 | tafarn::views::filters::filter, 116 | tafarn::views::filters::create_filter, 117 | tafarn::views::filters::update_filter, 118 | tafarn::views::filters::delete_filter, 119 | 120 | tafarn::views::domain_blocks::domain_blocks, 121 | tafarn::views::domain_blocks::create_domain_block, 122 | tafarn::views::domain_blocks::delete_domain_block, 123 | 124 | tafarn::views::follow_requests::follow_requests, 125 | tafarn::views::follow_requests::accept_follow_request, 126 | tafarn::views::follow_requests::reject_follow_request, 127 | 128 | tafarn::views::suggestions::suggestions, 129 | tafarn::views::suggestions::delete_suggestion, 130 | 131 | tafarn::views::notifications::notifications, 132 | tafarn::views::notifications::notification, 133 | tafarn::views::notifications::clear_notifications, 134 | tafarn::views::notifications::dismiss_notification, 135 | 136 | tafarn::views::search::search, 137 | 138 | tafarn::views::mutes::mutes, 139 | tafarn::views::mutes::get_mute_account, 140 | tafarn::views::mutes::mute_account, 141 | tafarn::views::mutes::get_unmute_account, 142 | tafarn::views::mutes::unmute_account, 143 | 144 | tafarn::views::blocks::blocks, 145 | tafarn::views::blocks::get_block_account, 146 | tafarn::views::blocks::block_account, 147 | tafarn::views::blocks::get_unblock_account, 148 | tafarn::views::blocks::unblock_account, 149 | 150 | tafarn::views::media::upload_media, 151 | tafarn::views::media::get_media, 152 | tafarn::views::media::update_media, 153 | 154 | tafarn::views::statuses::create_status_form, 155 | tafarn::views::statuses::create_status_json, 156 | tafarn::views::statuses::get_status, 157 | tafarn::views::statuses::delete_status, 158 | tafarn::views::statuses::status_context, 159 | tafarn::views::statuses::status_boosted_by, 160 | tafarn::views::statuses::status_liked_by, 161 | tafarn::views::statuses::boost_status, 162 | tafarn::views::statuses::unboost_status, 163 | tafarn::views::statuses::pin_status, 164 | tafarn::views::statuses::unpin_status, 165 | 166 | tafarn::views::bookmarks::bookmarks, 167 | tafarn::views::bookmarks::bookmark_status, 168 | tafarn::views::bookmarks::unbookmark_status, 169 | 170 | tafarn::views::favourites::favourites, 171 | tafarn::views::favourites::like_status, 172 | tafarn::views::favourites::unlike_status, 173 | 174 | tafarn::views::web_push::create_subscription, 175 | tafarn::views::web_push::get_subscription, 176 | tafarn::views::web_push::update_subscription, 177 | tafarn::views::web_push::delete_subscription, 178 | 179 | tafarn::views::activity_streams::transient, 180 | tafarn::views::activity_streams::user, 181 | tafarn::views::activity_streams::get_inbox, 182 | tafarn::views::activity_streams::post_inbox, 183 | tafarn::views::activity_streams::get_outbox, 184 | tafarn::views::activity_streams::post_outbox, 185 | tafarn::views::activity_streams::get_shared_inbox, 186 | tafarn::views::activity_streams::post_shared_inbox, 187 | tafarn::views::activity_streams::system_actor, 188 | tafarn::views::activity_streams::status, 189 | tafarn::views::activity_streams::status_activity, 190 | tafarn::views::activity_streams::like, 191 | ]) 192 | .launch() 193 | .await?; 194 | Ok(()) 195 | } -------------------------------------------------------------------------------- /src/bin/tafarnctl.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser)] 4 | #[command(author, version)] 5 | struct Cli { 6 | #[command(subcommand)] 7 | command: Commands, 8 | } 9 | 10 | #[derive(Subcommand)] 11 | enum Commands { 12 | #[command(about = "Refreshes all remote profiles from their home servers")] 13 | RefreshProfiles { 14 | #[arg(short, long, help = "Disable fetching the profile's graph (followers and following)")] 15 | no_graph: bool, 16 | }, 17 | #[command(about = "Refreshes a specific profile from the home server")] 18 | RefreshProfile { 19 | uri: String, 20 | #[arg(short, long, help = "Disable fetching the profile's graph (followers and following)")] 21 | no_graph: bool, 22 | }, 23 | } 24 | 25 | #[tokio::main] 26 | async fn main() { 27 | pretty_env_logger::init(); 28 | 29 | let cli = Cli::parse(); 30 | 31 | let app = tafarn::setup().await; 32 | // let db_pool = diesel::PgConnection::pool("db", &app.rocket).unwrap(); 33 | 34 | match cli.command { 35 | Commands::RefreshProfile { uri, no_graph } => { 36 | app.celery_app.send_task(tafarn::tasks::accounts::update_account::new(uri.clone(), no_graph)).await.unwrap(); 37 | println!("Update requested for {}", uri); 38 | } 39 | Commands::RefreshProfiles { no_graph } => { 40 | app.celery_app.send_task(tafarn::tasks::accounts::update_accounts::new(no_graph)).await.unwrap(); 41 | println!("Update of all profiles requested"); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/bin/tasks.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use rocket_sync_db_pools::Poolable; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | pretty_env_logger::init(); 9 | 10 | info!("Tafarn task runner starting..."); 11 | 12 | let app = tafarn::setup().await; 13 | let db_pool = diesel::PgConnection::pool("db", &app.rocket).unwrap(); 14 | let celery_app = std::sync::Arc::new(app.celery_app); 15 | 16 | tafarn::tasks::CONFIG.write().unwrap().replace(tafarn::tasks::Config { 17 | db: std::sync::Arc::new(db_pool), 18 | celery: celery_app.clone(), 19 | uri: app.uri, 20 | vapid_key: app.vapid_key, 21 | web_push_client: std::sync::Arc::new(web_push_old::WebPushClient::new()), 22 | as_key: std::sync::Arc::new(app.as_key), 23 | media_path: std::sync::Arc::new(app.media_path) 24 | }); 25 | 26 | celery_app.consume().await.unwrap(); 27 | } -------------------------------------------------------------------------------- /src/csrf.rs: -------------------------------------------------------------------------------- 1 | pub struct CSRFFairing; 2 | #[derive(Debug)] 3 | pub struct CSRFToken(Vec); 4 | 5 | #[rocket::async_trait] 6 | impl rocket::fairing::Fairing for CSRFFairing { 7 | fn info(&self) -> rocket::fairing::Info { 8 | rocket::fairing::Info { 9 | name: "CSRF", 10 | kind: rocket::fairing::Kind::Request, 11 | } 12 | } 13 | 14 | async fn on_request(&self, request: &mut rocket::Request<'_>, _: &mut rocket::Data<'_>) { 15 | debug!("Request cookies: {:#?}", request.cookies()); 16 | if let Some(_) = request.cookies() 17 | .get_private("csrf_token") 18 | .and_then(|c| base64::decode_config(c.value(), base64::URL_SAFE).ok()) { 19 | return; 20 | } 21 | 22 | use rand::Rng; 23 | let values: Vec = rand::thread_rng() 24 | .sample_iter(rand::distributions::Standard) 25 | .take(64) 26 | .collect(); 27 | 28 | let encoded = base64::encode_config(&values[..], base64::URL_SAFE); 29 | 30 | request.cookies().add_private( 31 | rocket::http::Cookie::build("csrf_token", encoded) 32 | .http_only(true) 33 | .expires(time::OffsetDateTime::now_utc() + time::Duration::hours(6)) 34 | .finish(), 35 | ); 36 | } 37 | } 38 | 39 | #[rocket::async_trait] 40 | impl<'r> rocket::request::FromRequest<'r> for CSRFToken { 41 | type Error = (); 42 | 43 | async fn from_request(request: &'r rocket::Request<'_>) -> rocket::request::Outcome { 44 | let cookies = request.cookies(); 45 | let csrf_cookie = match cookies 46 | .get_private("csrf_token") 47 | .or_else(|| cookies.get_pending("csrf_token")) 48 | .and_then(|c| base64::decode_config(c.value(), base64::URL_SAFE).ok()) { 49 | Some(c) => c, 50 | None => { 51 | use rand::Rng; 52 | let values: Vec = rand::thread_rng() 53 | .sample_iter(rand::distributions::Standard) 54 | .take(64) 55 | .collect(); 56 | 57 | 58 | request.cookies().add_private( 59 | rocket::http::Cookie::build("csrf_token", base64::encode_config(&values[..], base64::URL_SAFE)) 60 | .http_only(true) 61 | .expires(time::OffsetDateTime::now_utc() + time::Duration::hours(6)) 62 | .finish(), 63 | ); 64 | 65 | values 66 | } 67 | }; 68 | 69 | rocket::request::Outcome::Success(CSRFToken(csrf_cookie)) 70 | } 71 | } 72 | 73 | impl CSRFToken { 74 | pub fn verify(&self, token: &str) -> bool { 75 | match base64::decode_config(token, base64::URL_SAFE).ok() { 76 | Some(t) => t == self.0, 77 | None => false 78 | } 79 | } 80 | } 81 | 82 | impl ToString for CSRFToken { 83 | fn to_string(&self) -> String { 84 | base64::encode_config(&self.0, base64::URL_SAFE) 85 | } 86 | } -------------------------------------------------------------------------------- /src/i18n.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | pub struct Languages(pub Vec); 4 | pub struct Localizer { 5 | pub localizer: i18n_embed::fluent::FluentLanguageLoader, 6 | pub languages: Vec 7 | } 8 | 9 | #[rocket::async_trait] 10 | impl<'r> rocket::request::FromRequest<'r> for Localizer { 11 | type Error = &'static str; 12 | 13 | async fn from_request(request: &'r rocket::Request<'_>) -> rocket::request::Outcome { 14 | let langs = match request.guard::().await { 15 | rocket::request::Outcome::Success(l) => l, 16 | rocket::request::Outcome::Failure(e) => return rocket::request::Outcome::Failure(e), 17 | rocket::request::Outcome::Forward(()) => return rocket::request::Outcome::Forward(()), 18 | }; 19 | 20 | rocket::request::Outcome::Success(Localizer { 21 | localizer: crate::LANGUAGE_LOADER.select_languages(&langs.0), 22 | languages: langs.0 23 | }) 24 | } 25 | } 26 | 27 | #[rocket::async_trait] 28 | impl<'r> rocket::request::FromRequest<'r> for Languages { 29 | type Error = &'static str; 30 | 31 | async fn from_request(request: &'r rocket::Request<'_>) -> rocket::request::Outcome { 32 | let headers = request.headers(); 33 | let langs = accept_language::parse(headers.get_one("Accept-Language").unwrap_or("en-GB")) 34 | .into_iter().map(|l| l.parse::()) 35 | .collect::, _>>().unwrap_or_default(); 36 | 37 | rocket::request::Outcome::Success(Languages(langs)) 38 | } 39 | } 40 | 41 | impl Localizer { 42 | pub fn get_lang(id: &str) -> Self { 43 | let langs = match id.parse::() { 44 | Ok(lang) => vec![lang], 45 | Err(_) => vec![], 46 | }; 47 | 48 | Localizer { 49 | localizer: crate::LANGUAGE_LOADER.select_languages(&langs), 50 | languages: langs 51 | } 52 | } 53 | 54 | pub fn get_lang_opt(id: Option<&str>) -> Self { 55 | let langs = match id { 56 | Some(id) => match id.parse::() { 57 | Ok(lang) => vec![lang], 58 | Err(_) => vec![] 59 | }, 60 | None => vec![] 61 | }; 62 | 63 | Localizer { 64 | localizer: crate::LANGUAGE_LOADER.select_languages(&langs), 65 | languages: langs 66 | } 67 | } 68 | } 69 | 70 | impl Deref for Localizer { 71 | type Target = i18n_embed::fluent::FluentLanguageLoader; 72 | 73 | fn deref(&self) -> &Self::Target { 74 | &self.localizer 75 | } 76 | } 77 | 78 | impl serde::Serialize for Localizer { 79 | fn serialize(&self, serializer: S) -> Result { 80 | match self.languages.first() { 81 | Some(lang) => serializer.serialize_str(&lang.to_string()), 82 | None => serializer.serialize_none() 83 | } 84 | } 85 | } 86 | 87 | pub struct TeraLocalizer { 88 | cache: std::sync::Arc>> 89 | } 90 | 91 | impl TeraLocalizer { 92 | pub fn new() -> Self { 93 | Self { 94 | cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())) 95 | } 96 | } 97 | } 98 | 99 | impl rocket_dyn_templates::tera::Function for TeraLocalizer { 100 | fn call( 101 | &self, args: &std::collections::HashMap 102 | ) -> rocket_dyn_templates::tera::Result { 103 | let mut args = args.clone(); 104 | let lang = match args.remove("lang").ok_or_else(|| rocket_dyn_templates::tera::Error::msg("lang parameter is required"))? { 105 | rocket_dyn_templates::tera::Value::String(s) => Some(s), 106 | rocket_dyn_templates::tera::Value::Null => None, 107 | _ => return Err(rocket_dyn_templates::tera::Error::msg("lang parameter must be a string")) 108 | }; 109 | let message_id = args.remove("id").ok_or_else(|| rocket_dyn_templates::tera::Error::msg("id parameter is required"))? 110 | .as_str().ok_or_else(|| rocket_dyn_templates::tera::Error::msg("id parameter must be a string"))?.to_string(); 111 | 112 | let args = args.into_iter().map(|(k, v)| { 113 | let v = match v { 114 | rocket_dyn_templates::tera::Value::String(s) => fluent_bundle::types::FluentValue::String(std::borrow::Cow::Owned(s)), 115 | rocket_dyn_templates::tera::Value::Number(n) => fluent_bundle::types::FluentValue::Number( 116 | fluent_bundle::types::FluentNumber::new(n.as_f64().unwrap_or(0.0), Default::default()) 117 | ), 118 | _ => fluent_bundle::types::FluentValue::Error 119 | }; 120 | (k, v) 121 | }).collect::>(); 122 | 123 | if let Some(lang) = lang { 124 | match lang.parse::() { 125 | Ok(lang) => { 126 | { 127 | let cache = self.cache.read().unwrap(); 128 | if let Some(localizer) = cache.get(&lang) { 129 | return Ok(rocket_dyn_templates::tera::Value::String(localizer.get_args(&message_id, args))); 130 | } 131 | } 132 | let l = crate::LANGUAGE_LOADER.select_languages(&[&lang]); 133 | let s = l.get_args(&message_id, args); 134 | self.cache.write().unwrap().insert(lang, l); 135 | Ok(rocket_dyn_templates::tera::Value::String(s)) 136 | }, 137 | Err(_) => Ok(rocket_dyn_templates::tera::Value::String(crate::LANGUAGE_LOADER.get_args(&message_id, args))) 138 | } 139 | } else { 140 | Ok(rocket_dyn_templates::tera::Value::String(crate::LANGUAGE_LOADER.get_args(&message_id, args))) 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | session (id) { 3 | id -> Uuid, 4 | access_token -> Varchar, 5 | expires_at -> Nullable, 6 | refresh_token -> Nullable, 7 | claims -> Varchar, 8 | } 9 | } 10 | 11 | table! { 12 | apps (id) { 13 | id -> Uuid, 14 | name -> Varchar, 15 | website -> Nullable, 16 | redirect_uri -> Varchar, 17 | client_secret -> Varchar, 18 | } 19 | } 20 | 21 | table! { 22 | app_scopes (app_id, scope) { 23 | app_id -> Uuid, 24 | scope -> Varchar, 25 | } 26 | } 27 | 28 | table! { 29 | oauth_consents (id) { 30 | id -> Uuid, 31 | app_id -> Uuid, 32 | user_id -> Varchar, 33 | time -> Timestamp, 34 | } 35 | } 36 | 37 | table! { 38 | oauth_consent_scopes (consent_id, scope) { 39 | consent_id -> Uuid, 40 | scope -> Varchar, 41 | } 42 | } 43 | 44 | table! { 45 | oauth_codes (id) { 46 | id -> Uuid, 47 | time -> Timestamp, 48 | redirect_uri -> Varchar, 49 | client_id -> Uuid, 50 | user_id -> Varchar, 51 | } 52 | } 53 | 54 | table! { 55 | oauth_code_scopes (code_id, scope) { 56 | code_id -> Uuid, 57 | scope -> Varchar, 58 | } 59 | } 60 | 61 | table! { 62 | oauth_token (id) { 63 | id -> Uuid, 64 | time -> Timestamp, 65 | client_id -> Uuid, 66 | user_id -> Varchar, 67 | revoked -> Bool, 68 | } 69 | } 70 | 71 | table! { 72 | oauth_token_scopes (token_id, scope) { 73 | token_id -> Uuid, 74 | scope -> Varchar, 75 | } 76 | } 77 | 78 | table! { 79 | accounts (id) { 80 | id -> Uuid, 81 | iid -> Int8, 82 | actor -> Nullable, 83 | username -> Varchar, 84 | display_name -> Varchar, 85 | bio -> Varchar, 86 | locked -> Bool, 87 | bot -> Bool, 88 | group -> Bool, 89 | created_at -> Timestamp, 90 | updated_at -> Timestamp, 91 | default_sensitive -> Nullable, 92 | default_language -> Nullable, 93 | discoverable -> Nullable, 94 | follower_count -> Integer, 95 | following_count -> Integer, 96 | statuses_count -> Integer, 97 | owned_by -> Nullable, 98 | private_key -> Nullable, 99 | local -> Bool, 100 | inbox_url -> Nullable, 101 | outbox_url -> Nullable, 102 | shared_inbox_url -> Nullable, 103 | url -> Nullable, 104 | avatar_file -> Nullable, 105 | avatar_content_type -> Nullable, 106 | avatar_remote_url -> Nullable, 107 | header_file -> Nullable, 108 | header_content_type -> Nullable, 109 | header_remote_url -> Nullable, 110 | follower_collection_url -> Nullable, 111 | deleted_at -> Nullable, 112 | } 113 | } 114 | 115 | table! { 116 | account_fields (id) { 117 | id -> Uuid, 118 | account_id -> Uuid, 119 | name -> VarChar, 120 | value -> Varchar, 121 | sort_order -> Integer, 122 | } 123 | } 124 | 125 | table! { 126 | web_push_subscriptions (id) { 127 | id -> Uuid, 128 | token_id -> Uuid, 129 | account_id -> Uuid, 130 | endpoint -> Varchar, 131 | p256dh -> Varchar, 132 | auth -> Varchar, 133 | follow -> Bool, 134 | favourite -> Bool, 135 | reblog -> Bool, 136 | mention -> Bool, 137 | poll -> Bool, 138 | status -> Bool, 139 | follow_request -> Bool, 140 | update -> Bool, 141 | admin_sign_up -> Bool, 142 | admin_report -> Bool, 143 | policy -> Varchar, 144 | } 145 | } 146 | 147 | table! { 148 | public_keys (id) { 149 | id -> Uuid, 150 | key_id -> Varchar, 151 | user_id -> Uuid, 152 | key -> Varchar, 153 | } 154 | } 155 | 156 | table! { 157 | following (id) { 158 | id -> Uuid, 159 | iid -> Int8, 160 | follower -> Uuid, 161 | followee -> Uuid, 162 | created_at -> Timestamp, 163 | pending -> Bool, 164 | notify -> Bool, 165 | reblogs -> Bool, 166 | } 167 | } 168 | 169 | table! { 170 | notifications (id) { 171 | id -> Uuid, 172 | iid -> Int8, 173 | notification_type -> Varchar, 174 | account -> Uuid, 175 | cause -> Uuid, 176 | status -> Nullable, 177 | created_at -> Timestamp, 178 | } 179 | } 180 | 181 | table! { 182 | media (id) { 183 | id -> Uuid, 184 | media_type -> Varchar, 185 | file -> Nullable, 186 | content_type -> Nullable, 187 | remote_url -> Nullable, 188 | preview_file -> Nullable, 189 | preview_content_type -> Nullable, 190 | blurhash -> Nullable, 191 | focus_x -> Nullable, 192 | focus_y -> Nullable, 193 | original_width -> Nullable, 194 | original_height -> Nullable, 195 | preview_width -> Nullable, 196 | preview_height -> Nullable, 197 | created_at -> Timestamp, 198 | description -> Nullable, 199 | owned_by -> Nullable, 200 | } 201 | } 202 | 203 | table! { 204 | statuses (id) { 205 | id -> Uuid, 206 | iid -> Int8, 207 | url -> Varchar, 208 | uri -> Nullable, 209 | text -> Varchar, 210 | created_at -> Timestamp, 211 | updated_at -> Timestamp, 212 | in_reply_to_id -> Nullable, 213 | boost_of_id -> Nullable, 214 | in_reply_to_url -> Nullable, 215 | boost_of_url -> Nullable, 216 | sensitive -> Bool, 217 | spoiler_text -> Varchar, 218 | language -> Nullable, 219 | local -> Bool, 220 | account_id -> Uuid, 221 | deleted_at -> Nullable, 222 | edited_at -> Nullable, 223 | public -> Bool, 224 | visible -> Bool, 225 | text_source -> Nullable, 226 | spoiler_text_source -> Nullable, 227 | } 228 | } 229 | 230 | table! { 231 | status_media_attachments (status_id, media_attachment_id) { 232 | status_id -> Uuid, 233 | media_attachment_id -> Uuid, 234 | attachment_order -> Int4, 235 | } 236 | } 237 | 238 | table! { 239 | status_audiences (id) { 240 | id -> Uuid, 241 | status_id -> Uuid, 242 | mention -> Bool, 243 | account -> Nullable, 244 | account_followers -> Nullable, 245 | } 246 | } 247 | 248 | table! { 249 | home_timeline (id) { 250 | id -> Int8, 251 | account_id -> Uuid, 252 | status_id -> Uuid, 253 | } 254 | } 255 | 256 | table! { 257 | public_timeline (id) { 258 | id -> Int8, 259 | status_id -> Uuid, 260 | } 261 | } 262 | 263 | table! { 264 | likes (id) { 265 | id -> Uuid, 266 | iid -> Int8, 267 | status -> Nullable, 268 | account -> Uuid, 269 | created_at -> Timestamp, 270 | url -> Nullable, 271 | local -> Bool, 272 | status_url -> Nullable, 273 | } 274 | } 275 | 276 | table! { 277 | bookmarks (id) { 278 | id -> Uuid, 279 | iid -> Int8, 280 | status -> Uuid, 281 | account -> Uuid, 282 | } 283 | } 284 | 285 | table! { 286 | pins (id) { 287 | id -> Uuid, 288 | iid -> Int8, 289 | status -> Uuid, 290 | account -> Uuid, 291 | } 292 | } 293 | 294 | table! { 295 | account_notes (account, owner) { 296 | account -> Uuid, 297 | owner -> Uuid, 298 | note -> Varchar, 299 | } 300 | } 301 | 302 | table! { 303 | media_attachments (status, media) { 304 | status -> Uuid, 305 | media -> Uuid, 306 | } 307 | } 308 | 309 | table! { 310 | status_mentions (id) { 311 | id -> Uuid, 312 | status -> Uuid, 313 | account -> Uuid, 314 | created_at -> Timestamp, 315 | } 316 | } 317 | 318 | table! { 319 | tags (id) { 320 | id -> Uuid, 321 | name -> Varchar, 322 | } 323 | } 324 | 325 | joinable!(app_scopes -> apps (app_id)); 326 | joinable!(oauth_consent_scopes -> oauth_consents (consent_id)); 327 | joinable!(oauth_code_scopes -> oauth_codes (code_id)); 328 | joinable!(oauth_token_scopes -> oauth_token (token_id)); 329 | joinable!(account_fields -> accounts (account_id)); 330 | joinable!(web_push_subscriptions -> oauth_token (token_id)); 331 | joinable!(web_push_subscriptions -> accounts (account_id)); 332 | joinable!(public_keys -> accounts (user_id)); 333 | joinable!(notifications -> accounts (account)); 334 | joinable!(statuses -> accounts (account_id)); 335 | joinable!(status_media_attachments -> media (media_attachment_id)); 336 | joinable!(status_media_attachments -> statuses (status_id)); 337 | joinable!(status_audiences -> statuses (status_id)); 338 | 339 | allow_tables_to_appear_in_same_query!( 340 | session, 341 | apps, 342 | app_scopes, 343 | oauth_consents, 344 | oauth_consent_scopes, 345 | oauth_codes, 346 | oauth_code_scopes, 347 | oauth_token, 348 | oauth_token_scopes, 349 | accounts, 350 | account_fields, 351 | web_push_subscriptions, 352 | public_keys, 353 | following, 354 | notifications, 355 | media, 356 | statuses, 357 | status_audiences, 358 | status_media_attachments, 359 | home_timeline, 360 | public_timeline, 361 | likes, 362 | bookmarks, 363 | pins, 364 | account_notes, 365 | media_attachments, 366 | status_mentions, 367 | tags 368 | ); -------------------------------------------------------------------------------- /src/tasks/collection.rs: -------------------------------------------------------------------------------- 1 | use celery::prelude::*; 2 | use crate::views::activity_streams; 3 | use super::{fetch_object, resolve_object}; 4 | 5 | pub struct CollectionStream { 6 | pub collection: activity_streams::Collection, 7 | page_cache: Vec>, 8 | next_page: Option>, 9 | resolve_fut: Option>>, 10 | fetch_link_fut: Option>>, 11 | } 12 | 13 | impl CollectionStream { 14 | fn poll_fetch_link( 15 | mut self: std::pin::Pin<&mut Self>, cx: &mut futures::task::Context<'_> 16 | ) -> futures::task::Poll>> { 17 | match std::future::Future::poll(self.fetch_link_fut.as_mut().unwrap().as_mut(), cx) { 18 | futures::task::Poll::Ready(page) => { 19 | if let Some(page) = page { 20 | self.next_page = page.next; 21 | if let Some(items) = page.common.items { 22 | self.page_cache.extend(items); 23 | } 24 | futures::task::Poll::Ready(self.page_cache.pop()) 25 | } else { 26 | warn!("Unable to fetch collection page"); 27 | futures::task::Poll::Ready(None) 28 | } 29 | } 30 | futures::task::Poll::Pending => futures::task::Poll::Pending 31 | } 32 | } 33 | 34 | fn poll_resolve_object( 35 | mut self: std::pin::Pin<&mut Self>, cx: &mut futures::task::Context<'_> 36 | ) -> futures::task::Poll>> { 37 | match std::future::Future::poll(self.resolve_fut.as_mut().unwrap().as_mut(), cx) { 38 | futures::task::Poll::Ready(page) => { 39 | self.resolve_fut = None; 40 | match page { 41 | Some(o) => { 42 | let page = match match o { 43 | activity_streams::CollectionPageOrLink::Link(l) => { 44 | if let Some(l) = l.href { 45 | println!("Fetching {}", l); 46 | let fut = fetch_object(l); 47 | self.fetch_link_fut = Some(Box::pin(fut)); 48 | return self.poll_fetch_link(cx); 49 | } else { 50 | None 51 | } 52 | } 53 | activity_streams::CollectionPageOrLink::CollectionPage(o) | 54 | activity_streams::CollectionPageOrLink::OrderedCollectionPage(o) => { 55 | Some(o) 56 | } 57 | } { 58 | Some(p) => p, 59 | None => { 60 | warn!("Unable to fetch collection page"); 61 | return futures::task::Poll::Ready(None); 62 | } 63 | }; 64 | self.next_page = page.next; 65 | if let Some(items) = page.common.items { 66 | self.page_cache.extend(items); 67 | } 68 | futures::task::Poll::Ready(self.page_cache.pop()) 69 | } 70 | None => { 71 | warn!("Unable to resolve object: {:?}", self.next_page); 72 | futures::task::Poll::Ready(None) 73 | } 74 | } 75 | } 76 | futures::task::Poll::Pending => futures::task::Poll::Pending 77 | } 78 | } 79 | } 80 | 81 | impl futures::stream::Stream for CollectionStream { 82 | type Item = activity_streams::ReferenceOrObject; 83 | 84 | fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut futures::task::Context<'_>) -> futures::task::Poll> { 85 | if let Some(item) = self.page_cache.pop() { 86 | futures::task::Poll::Ready(Some(item)) 87 | } else { 88 | if self.fetch_link_fut.is_some() { 89 | self.poll_fetch_link(cx) 90 | } else if self.resolve_fut.is_some() { 91 | self.poll_resolve_object(cx) 92 | } else { 93 | if let Some(obj) = &self.next_page { 94 | self.resolve_fut = Some(Box::pin(resolve_object(obj.clone()))); 95 | self.poll_resolve_object(cx) 96 | } else { 97 | futures::task::Poll::Ready(None) 98 | } 99 | } 100 | } 101 | } 102 | 103 | fn size_hint(&self) -> (usize, Option) { 104 | (self.page_cache.len(), self.collection.total_items.map(|t| t as usize)) 105 | } 106 | } 107 | 108 | pub fn fetch_entire_collection( 109 | collection: activity_streams::Object 110 | ) -> TaskResult { 111 | match collection { 112 | activity_streams::Object::Collection(c) | 113 | activity_streams::Object::OrderedCollection(c) => { 114 | if let Some(items) = &c.items { 115 | let items = items.clone(); 116 | Ok(CollectionStream { 117 | next_page: None, 118 | page_cache: items, 119 | collection: c, 120 | resolve_fut: None, 121 | fetch_link_fut: None, 122 | }) 123 | } else if let Some(first) = &c.first { 124 | Ok(CollectionStream { 125 | next_page: Some(first.clone()), 126 | page_cache: vec![], 127 | collection: c, 128 | resolve_fut: None, 129 | fetch_link_fut: None, 130 | }) 131 | } else { 132 | Ok(CollectionStream { 133 | next_page: None, 134 | page_cache: vec![], 135 | collection: c, 136 | resolve_fut: None, 137 | fetch_link_fut: None, 138 | }) 139 | } 140 | } 141 | o => Err(TaskError::UnexpectedError(format!("Not a collection: {:?}", o))) 142 | } 143 | } -------------------------------------------------------------------------------- /src/tasks/delivery.rs: -------------------------------------------------------------------------------- 1 | use crate::models; 2 | use crate::views::activity_streams; 3 | use celery::prelude::*; 4 | use itertools::Itertools; 5 | 6 | 7 | #[celery::task] 8 | pub async fn deliver_object(object: activity_streams::Object, inbox: String, account: models::Account) -> TaskResult<()> { 9 | let config = super::config(); 10 | let url = reqwest::Url::parse(&inbox).with_unexpected_err(|| "Invalid inbox URL")?; 11 | let host = url.host_str().map(|h| h.to_string()).ok_or(TaskError::UnexpectedError("Invalid inbox URL".to_string()))?; 12 | 13 | let body = object.to_json(); 14 | let body_hash = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), body.as_bytes()) 15 | .with_unexpected_err(|| "Unable to hash body")?; 16 | let date = chrono::Utc::now().naive_utc().format("%a, %d %h %Y %H:%M:%S GMT").to_string(); 17 | let mut req = crate::AS_CLIENT.post(url) 18 | .body(body) 19 | .header("Host", host) 20 | .header("Date", date) 21 | .header("Digest", format!("SHA-256={}", base64::encode(body_hash))) 22 | .header("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") 23 | .build().with_unexpected_err(|| "Unable to build request")?; 24 | 25 | if let Some(pkey) = account.private_key.as_ref().map(|k| match openssl::pkey::PKey::private_key_from_pem(k.as_bytes()) { 26 | Ok(pkey) => Ok(pkey), 27 | Err(_) => Err(TaskError::UnexpectedError("Invalid private key".to_string())), 28 | }).transpose()? { 29 | super::sign_request(&mut req, &pkey, account.key_id(&config.uri)) 30 | .map_err(|e| TaskError::UnexpectedError(e))?; 31 | } 32 | 33 | let r = crate::AS_CLIENT.execute(req).await 34 | .with_expected_err(|| "Unable to send request")?; 35 | 36 | let status = r.status(); 37 | if !status.is_success() { 38 | let text = r.text().await.with_expected_err(|| "Unable to read response")?; 39 | return Err(TaskError::UnexpectedError(format!("Delivery failed ({}): {}", status, text))); 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | pub async fn deliver_dedupe_inboxes( 46 | object: activity_streams::Object, audience: Vec, account: models::Account 47 | ) -> TaskResult<()> { 48 | let config = super::config(); 49 | let mut inboxes = vec![]; 50 | for a in audience { 51 | if !a.local { 52 | if let Some(inbox) = a.shared_inbox_url { 53 | inboxes.push(inbox); 54 | } else if let Some(inbox) = a.inbox_url { 55 | inboxes.push(inbox); 56 | } 57 | } 58 | } 59 | 60 | let inboxes: Vec<_> = inboxes.into_iter().unique().collect(); 61 | for inbox in inboxes { 62 | config.celery.send_task( 63 | deliver_object::new(object.clone(), inbox, account.clone()) 64 | ).await.with_expected_err(|| "Unable to submit delivery task")?; 65 | } 66 | 67 | Ok(()) 68 | } -------------------------------------------------------------------------------- /src/tasks/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::views::activity_streams; 2 | 3 | pub mod collection; 4 | pub mod inbox; 5 | pub mod accounts; 6 | pub mod relationships; 7 | pub mod delivery; 8 | pub mod notifications; 9 | pub mod statuses; 10 | 11 | const SIGNED_HEADERS: [&str; 4] = ["host", "date", "digest", "content-type"]; 12 | 13 | lazy_static::lazy_static! { 14 | pub static ref CONFIG: std::sync::RwLock> = std::sync::RwLock::new(None); 15 | } 16 | 17 | #[derive(Clone)] 18 | pub struct Config { 19 | pub db: std::sync::Arc>>, 20 | pub celery: std::sync::Arc, 21 | pub uri: String, 22 | pub vapid_key: Vec, 23 | pub web_push_client: std::sync::Arc, 24 | pub as_key: std::sync::Arc>, 25 | pub media_path: std::sync::Arc, 26 | } 27 | 28 | #[inline] 29 | pub(crate) fn config() -> Config { 30 | CONFIG.read().unwrap().as_ref().unwrap().clone() 31 | } 32 | 33 | fn sign_request(req: &mut reqwest::Request, pkey: &openssl::pkey::PKeyRef, key_id: String) -> Result<(), String> { 34 | let mut signed_data = vec![ 35 | format!("(request-target): {} {}", req.method().as_str().to_lowercase(), req.url().path()), 36 | ]; 37 | let mut signed_headers = vec!["(request-target)".to_string()]; 38 | for (header_name, header_value) in req.headers().iter() { 39 | if SIGNED_HEADERS.iter().any(|h| header_name == h) { 40 | signed_data.push(format!( 41 | "{}: {}", 42 | header_name.as_str().to_lowercase(), 43 | header_value.to_str().map_err(|e| format!("Unable to convert header to string: {}", e))? 44 | )); 45 | signed_headers.push(header_name.as_str().to_lowercase()); 46 | } 47 | } 48 | 49 | let signed_data = signed_data.join("\n").into_bytes(); 50 | let mut signer = openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), &pkey) 51 | .map_err(|e| format!("Unable to create signer: {}", e))?; 52 | let signature = signer.sign_oneshot_to_vec(&signed_data) 53 | .map_err(|e| format!("Unable to sign request: {}", e))?; 54 | req.headers_mut().insert("Signature", format!( 55 | "keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"{}\",signature=\"{}\"", 56 | key_id, signed_headers.join(" "), 57 | base64::encode(signature) 58 | ).parse().map_err(|e| format!("Unable to parse signature header: {}", e))?); 59 | 60 | Ok(()) 61 | } 62 | 63 | async fn authenticated_get(url: reqwest::Url) -> Result { 64 | let config = config(); 65 | let pkey = config.as_key; 66 | let host = url.host_str().map(|h| h.to_string()).ok_or("No host in URL")?; 67 | let date = chrono::Utc::now().naive_utc().format("%a, %d %h %Y %H:%M:%S GMT").to_string(); 68 | let mut req = crate::AS_CLIENT.get(url.clone()) 69 | .header("Host", &host) 70 | .header("Date", &date) 71 | .build().map_err(|e| format!("Unable to build request: {}", e))?; 72 | 73 | sign_request(&mut req, &pkey, format!("https://{}/as/system#key", config.uri))?; 74 | crate::AS_CLIENT.execute(req).await.map_err(|e| format!("Unable to send request: {}", e)) 75 | } 76 | 77 | async fn fetch_object<'a, T: serde::de::DeserializeOwned, U: Into>>(uri: U) -> Option { 78 | let uri = uri.into(); 79 | let url = match reqwest::Url::parse(&uri) { 80 | Ok(url) => url, 81 | Err(e) => { 82 | warn!("Unable to parse URL {}: {}", uri, e); 83 | return None; 84 | } 85 | }; 86 | 87 | match backoff::future::retry(backoff::ExponentialBackoff::default(), || async { 88 | match authenticated_get(url.clone()).await { 89 | Ok(r) => { 90 | match r.error_for_status() { 91 | Ok(r) => { 92 | match r.json::().await { 93 | Ok(r) => Ok(r), 94 | Err(e) => Err(backoff::Error::Permanent(e.to_string())) 95 | } 96 | }, 97 | Err(e) => { 98 | if e.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS) { 99 | info!("Too many requests on {}, retrying", uri); 100 | Err(backoff::Error::Transient { err: e.to_string(), retry_after: None }) 101 | } else { 102 | Err(backoff::Error::Permanent(e.to_string())) 103 | } 104 | } 105 | } 106 | } 107 | Err(e) => Err(backoff::Error::Permanent(e.to_string())), 108 | } 109 | }).await { 110 | Ok(r) => Some(r), 111 | Err(e) => { 112 | warn!("Failed to fetch object {}: {}", uri, e); 113 | None 114 | } 115 | } 116 | } 117 | 118 | #[inline] 119 | pub async fn resolve_object(obj: activity_streams::ReferenceOrObject) -> Option { 120 | match obj { 121 | activity_streams::ReferenceOrObject::Object(o) => Some(*o), 122 | activity_streams::ReferenceOrObject::Reference(uri) => fetch_object(&uri).await 123 | } 124 | } 125 | 126 | #[inline] 127 | pub async fn resolve_object_or_link(obj: activity_streams::ReferenceOrObject) -> Option { 128 | match resolve_object(obj).await? { 129 | activity_streams::ObjectOrLink::Object(o) => Some(o), 130 | activity_streams::ObjectOrLink::Link(uri) => fetch_object(&uri.href?).await 131 | } 132 | } 133 | 134 | #[inline] 135 | pub fn resolve_url(obj: activity_streams::URLOrLink) -> Option { 136 | match obj { 137 | activity_streams::URLOrLink::URL(u) => Some(u), 138 | activity_streams::URLOrLink::Link(l) => l.href 139 | } 140 | } -------------------------------------------------------------------------------- /src/tasks/notifications.rs: -------------------------------------------------------------------------------- 1 | use celery::prelude::*; 2 | use diesel::prelude::*; 3 | use crate::models; 4 | 5 | #[derive(Serialize, Deserialize, Clone)] 6 | struct NotificationData { 7 | title: String, 8 | body: String, 9 | preferred_locale: String, 10 | access_token: String, 11 | notification_id: i64, 12 | notification_type: String, 13 | icon: Option, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Clone)] 17 | pub struct Notification { 18 | endpoint: String, 19 | p256dh: String, 20 | auth: String, 21 | data: NotificationData, 22 | } 23 | 24 | #[celery::task] 25 | pub async fn notify(notification: models::Notification) -> TaskResult<()> { 26 | let config = super::config(); 27 | let db = config.db.clone(); 28 | 29 | let (account, cause, status, is_followed, is_following) = tokio::task::block_in_place(|| -> TaskResult<_> { 30 | let c = db.get().with_expected_err(|| "Unable to get DB pool connection")?; 31 | let a = crate::schema::accounts::dsl::accounts 32 | .filter(crate::schema::accounts::dsl::id.eq(notification.account)) 33 | .get_result::(&c).with_expected_err(|| "Unable to get account")?; 34 | let ca = crate::schema::accounts::dsl::accounts 35 | .filter(crate::schema::accounts::dsl::id.eq(notification.cause)) 36 | .get_result::(&c).with_expected_err(|| "Unable to get account")?; 37 | let s = match notification.status { 38 | Some(sid) => Some(crate::schema::statuses::dsl::statuses 39 | .filter(crate::schema::statuses::dsl::id.eq(sid)) 40 | .get_result::(&c).with_expected_err(|| "Unable to get status")?), 41 | None => None 42 | }; 43 | 44 | let is_followed = crate::schema::following::dsl::following 45 | .filter(crate::schema::following::dsl::follower.eq(notification.account)) 46 | .filter(crate::schema::following::dsl::followee.eq(notification.cause)) 47 | .count().get_result::(&c).with_expected_err(|| "Unable to check followed set")? > 0; 48 | let is_following = crate::schema::following::dsl::following 49 | .filter(crate::schema::following::dsl::followee.eq(notification.account)) 50 | .filter(crate::schema::following::dsl::follower.eq(notification.cause)) 51 | .count().get_result::(&c).with_expected_err(|| "Unable to check following set")? > 0; 52 | 53 | Ok((a, ca, s, is_followed, is_following)) 54 | })?; 55 | 56 | let subscriptions = match notification.notification_type.as_str() { 57 | "follow" => { 58 | tokio::task::block_in_place(|| -> TaskResult<_> { 59 | let c = db.get().with_expected_err(|| "Unable to get DB pool connection")?; 60 | crate::schema::web_push_subscriptions::dsl::web_push_subscriptions 61 | .filter(crate::schema::web_push_subscriptions::dsl::follow.eq(true)) 62 | .filter(crate::schema::web_push_subscriptions::dsl::account_id.eq(notification.account)) 63 | .get_results(&c).with_expected_err(|| "Unable to get subscriptions") 64 | })? 65 | } 66 | "favourite" => { 67 | tokio::task::block_in_place(|| -> TaskResult<_> { 68 | let c = db.get().with_expected_err(|| "Unable to get DB pool connection")?; 69 | crate::schema::web_push_subscriptions::dsl::web_push_subscriptions 70 | .filter(crate::schema::web_push_subscriptions::dsl::favourite.eq(true)) 71 | .filter(crate::schema::web_push_subscriptions::dsl::account_id.eq(notification.account)) 72 | .get_results::(&c).with_expected_err(|| "Unable to get subscriptions") 73 | })? 74 | } 75 | "reblog" => { 76 | tokio::task::block_in_place(|| -> TaskResult<_> { 77 | let c = db.get().with_expected_err(|| "Unable to get DB pool connection")?; 78 | crate::schema::web_push_subscriptions::dsl::web_push_subscriptions 79 | .filter(crate::schema::web_push_subscriptions::dsl::reblog.eq(true)) 80 | .filter(crate::schema::web_push_subscriptions::dsl::account_id.eq(notification.account)) 81 | .get_results::(&c).with_expected_err(|| "Unable to get subscriptions") 82 | })? 83 | } 84 | "mention" => { 85 | tokio::task::block_in_place(|| -> TaskResult<_> { 86 | let c = db.get().with_expected_err(|| "Unable to get DB pool connection")?; 87 | crate::schema::web_push_subscriptions::dsl::web_push_subscriptions 88 | .filter(crate::schema::web_push_subscriptions::dsl::mention.eq(true)) 89 | .filter(crate::schema::web_push_subscriptions::dsl::account_id.eq(notification.account)) 90 | .get_results::(&c).with_expected_err(|| "Unable to get subscriptions") 91 | })? 92 | } 93 | _ => { 94 | warn!("Unknown notification type: {}", notification.notification_type); 95 | return Ok(()); 96 | } 97 | }.into_iter().filter(|s| match s.policy.as_str() { 98 | "all" => true, 99 | "follower" => is_following, 100 | "followed" => is_followed, 101 | _ => false 102 | }).collect::>(); 103 | 104 | let localizer = crate::i18n::Localizer::get_lang_opt(account.default_language.as_deref()); 105 | 106 | let notification_data = match notification.notification_type.as_str() { 107 | "follow" => { 108 | NotificationData { 109 | notification_id: notification.iid, 110 | notification_type: "follow".to_string(), 111 | title: fl!(localizer, "follow-notification", name = account.display_name), 112 | icon: cause.avatar_file.as_ref().map(|f| format!("https://{}/media/{}", config.uri, f)), 113 | body: cause.bio, 114 | access_token: "".to_string(), 115 | preferred_locale: cause.default_language.clone().unwrap_or_else(|| "en".to_string()), 116 | } 117 | }, 118 | "favourite" => { 119 | NotificationData { 120 | notification_id: notification.iid, 121 | notification_type: "favourite".to_string(), 122 | title: fl!(localizer, "favourite-notification", name = account.display_name), 123 | icon: cause.avatar_file.as_ref().map(|f| format!("https://{}/media/{}", config.uri, f)), 124 | body: status.as_ref().map(|s| s.text.clone()).unwrap_or_else(|| "".to_string()), 125 | access_token: "".to_string(), 126 | preferred_locale: cause.default_language.clone().unwrap_or_else(|| "en".to_string()), 127 | } 128 | } 129 | "reblog" => { 130 | NotificationData { 131 | notification_id: notification.iid, 132 | notification_type: "reblog".to_string(), 133 | title: fl!(localizer, "reblog-notification", name = account.display_name), 134 | icon: cause.avatar_file.as_ref().map(|f| format!("https://{}/media/{}", config.uri, f)), 135 | body: status.as_ref().map(|s| s.text.clone()).unwrap_or_else(|| "".to_string()), 136 | access_token: "".to_string(), 137 | preferred_locale: cause.default_language.clone().unwrap_or_else(|| "en".to_string()), 138 | } 139 | } 140 | "mention" => { 141 | NotificationData { 142 | notification_id: notification.iid, 143 | notification_type: "mention".to_string(), 144 | title: fl!(localizer, "mention-notification", name = account.display_name), 145 | icon: cause.avatar_file.as_ref().map(|f| format!("https://{}/media/{}", config.uri, f)), 146 | body: status.as_ref().map(|s| s.text.clone()).unwrap_or_else(|| "".to_string()), 147 | access_token: "".to_string(), 148 | preferred_locale: cause.default_language.clone().unwrap_or_else(|| "en".to_string()), 149 | } 150 | } 151 | _ => unreachable!() 152 | }; 153 | 154 | for sub in subscriptions { 155 | config.celery.send_task(deliver_notification::new(Notification { 156 | data: notification_data.clone(), 157 | endpoint: sub.endpoint, 158 | p256dh: sub.p256dh, 159 | auth: sub.auth, 160 | }, sub.id)).await.with_expected_err(|| "Unable to submit notification delivery task")?; 161 | } 162 | 163 | Ok(()) 164 | } 165 | 166 | #[celery::task] 167 | pub async fn deliver_notification(notification: Notification, subscription_id: uuid::Uuid) -> TaskResult<()> { 168 | let config = super::config(); 169 | 170 | let subscription = web_push_old::SubscriptionInfo::new( 171 | notification.endpoint, 172 | notification.p256dh, 173 | notification.auth, 174 | ); 175 | let vapid_signature_builder = web_push_old::VapidSignatureBuilder::from_pem( 176 | config.vapid_key.as_slice(), &subscription 177 | ).with_expected_err(|| "Unable to create VAPID signature builder")?; 178 | let vapid_signature = vapid_signature_builder.build().with_expected_err(|| "Unable to build VAPID signature")?; 179 | 180 | let payload = serde_json::to_vec(¬ification.data).unwrap(); 181 | 182 | let mut builder = web_push_old::WebPushMessageBuilder::new(&subscription) 183 | .with_expected_err(|| "Unable to create WebPushMessageBuilder")?; 184 | builder.set_ttl(48 * 60 * 60); // 48 hours 185 | builder.set_vapid_signature(vapid_signature); 186 | builder.set_payload(web_push_old::ContentEncoding::AesGcm, &payload); 187 | 188 | let message = builder.build().with_unexpected_err(|| "Unable to build WebPushMessage")?; 189 | let req = build_request(message).with_unexpected_err(|| "Unable to build WebPushRequest")?; 190 | let res = crate::AS_CLIENT.execute(req).await.with_expected_err(|| "Unable to execute WebPushRequest")?; 191 | let status = res.status(); 192 | if status.is_success() { 193 | return Ok(()); 194 | } 195 | 196 | if status == reqwest::StatusCode::GONE || status == reqwest::StatusCode::NOT_FOUND 197 | || status == reqwest::StatusCode::FORBIDDEN { 198 | info!("Removing invalid subscription {}", subscription_id); 199 | tokio::task::block_in_place(|| -> TaskResult<_> { 200 | let c = config.db.get().with_expected_err(|| "Unable to get DB pool connection")?; 201 | diesel::delete(crate::schema::web_push_subscriptions::dsl::web_push_subscriptions 202 | .filter(crate::schema::web_push_subscriptions::dsl::id.eq(subscription_id)) 203 | ).execute(&c).with_expected_err(|| "Unable to delete subscription") 204 | })?; 205 | return Ok(()); 206 | } 207 | 208 | return Err(TaskError::ExpectedError(format!("Unable to send WebPushMessage: {}", status))); 209 | } 210 | 211 | fn build_request(message: web_push_old::WebPushMessage) -> reqwest::Result { 212 | let mut builder = crate::AS_CLIENT.post(message.endpoint.to_string()) 213 | .header("Urgency", "normal") 214 | .header("TTL", format!("{}", message.ttl).as_bytes()); 215 | 216 | if let Some(payload) = message.payload { 217 | builder = builder 218 | .header("Content-Encoding", payload.content_encoding) 219 | .header( 220 | "Content-Length", 221 | format!("{}", payload.content.len() as u64).as_bytes(), 222 | ) 223 | .header("Content-Type", "application/octet-stream"); 224 | 225 | for (k, v) in payload.crypto_headers.into_iter() { 226 | let v: &str = v.as_ref(); 227 | builder = builder.header(k, v); 228 | } 229 | 230 | builder = builder.body(payload.content) 231 | } else { 232 | builder = builder.body(""); 233 | } 234 | 235 | builder.build() 236 | } -------------------------------------------------------------------------------- /src/views/blocks.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/blocks")] 2 | pub async fn blocks( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read:blocks") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[get("/api/v1/accounts/<_account_id>/block")] 16 | pub async fn get_block_account( 17 | _account_id: &str 18 | ) -> rocket::http::Status { 19 | rocket::http::Status::MethodNotAllowed 20 | } 21 | 22 | #[post("/api/v1/accounts//block")] 23 | pub async fn block_account( 24 | user: super::oauth::TokenClaims, account_id: String, localizer: crate::i18n::Localizer 25 | ) -> Result, super::Error> { 26 | if !user.has_scope("write:blocks") { 27 | return Err(super::Error { 28 | code: rocket::http::Status::Forbidden, 29 | error: fl!(localizer, "error-no-permission") 30 | }); 31 | } 32 | 33 | let _account_id = match uuid::Uuid::parse_str(&account_id) { 34 | Ok(id) => id, 35 | Err(_) => return Err(super::Error { 36 | code: rocket::http::Status::NotFound, 37 | error: fl!(localizer, "account-not-found") 38 | }) 39 | }; 40 | 41 | Err(super::Error { 42 | code: rocket::http::Status::ServiceUnavailable, 43 | error: fl!(localizer, "service-unavailable") 44 | }) 45 | } 46 | 47 | #[get("/api/v1/accounts/<_account_id>/unblock")] 48 | pub async fn get_unblock_account( 49 | _account_id: &str 50 | ) -> rocket::http::Status { 51 | rocket::http::Status::MethodNotAllowed 52 | } 53 | 54 | #[post("/api/v1/accounts//unblock")] 55 | pub async fn unblock_account( 56 | user: super::oauth::TokenClaims, account_id: String, localizer: crate::i18n::Localizer 57 | ) -> Result, super::Error> { 58 | if !user.has_scope("write:blocks") { 59 | return Err(super::Error { 60 | code: rocket::http::Status::Forbidden, 61 | error: fl!(localizer, "error-no-permission") 62 | }); 63 | } 64 | 65 | let _account_id = match uuid::Uuid::parse_str(&account_id) { 66 | Ok(id) => id, 67 | Err(_) => return Err(super::Error { 68 | code: rocket::http::Status::NotFound, 69 | error: fl!(localizer, "account-not-found") 70 | }) 71 | }; 72 | 73 | Err(super::Error { 74 | code: rocket::http::Status::ServiceUnavailable, 75 | error: fl!(localizer, "service-unavailable") 76 | }) 77 | } -------------------------------------------------------------------------------- /src/views/bookmarks.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use futures::StreamExt; 3 | use crate::models; 4 | 5 | #[get("/api/v1/bookmarks?&&")] 6 | pub async fn bookmarks( 7 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 8 | limit: Option, max_id: Option, min_id: Option, 9 | host: &rocket::http::uri::Host<'_>, localizer: crate::i18n::Localizer 10 | ) -> Result>>, super::Error> { 11 | if !user.has_scope("read:bookmarks") { 12 | return Err(super::Error { 13 | code: rocket::http::Status::Forbidden, 14 | error: fl!(localizer, "error-no-permission") 15 | }); 16 | } 17 | 18 | let limit = limit.unwrap_or(20); 19 | if limit > 500 { 20 | return Err( super::Error { 21 | code: rocket::http::Status::UnprocessableEntity, 22 | error: fl!(localizer, "limit-too-large") 23 | }); 24 | } 25 | 26 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 27 | 28 | let statuses: Vec<(models::Bookmark, models::Status)> = 29 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 30 | let mut sel = crate::schema::bookmarks::dsl::bookmarks.order_by( 31 | crate::schema::bookmarks::dsl::iid.desc() 32 | ).filter( 33 | crate::schema::bookmarks::dsl::account.eq(&account.id) 34 | ).inner_join( 35 | crate::schema::statuses::table.on( 36 | crate::schema::bookmarks::dsl::status.eq(crate::schema::statuses::dsl::id) 37 | ) 38 | ).filter( 39 | crate::schema::statuses::dsl::deleted_at.is_null() 40 | ).filter( 41 | crate::schema::statuses::dsl::boost_of_url.is_null() 42 | ).limit(limit as i64).into_boxed(); 43 | if let Some(min_id) = min_id { 44 | sel = sel.filter(crate::schema::bookmarks::dsl::iid.gt(min_id)); 45 | } 46 | if let Some(max_id) = max_id { 47 | sel = sel.filter(crate::schema::bookmarks::dsl::iid.lt(max_id)); 48 | } 49 | sel.get_results(c) 50 | }).await?; 51 | 52 | let mut links = vec![]; 53 | 54 | if let Some(last_id) = statuses.last().map(|a| a.0.iid) { 55 | links.push(super::Link { 56 | rel: "next".to_string(), 57 | href: format!("https://{}/api/v1/bookmarks?max_id={}", host.to_string(), last_id) 58 | }); 59 | } 60 | if let Some(first_id) = statuses.first().map(|a| a.0.iid) { 61 | links.push(super::Link { 62 | rel: "prev".to_string(), 63 | href: format!("https://{}/api/v1/bookmarks?min_id={}", host.to_string(), first_id) 64 | }); 65 | } 66 | 67 | Ok(super::LinkedResponse { 68 | inner: rocket::serde::json::Json( 69 | futures::stream::iter(statuses).map(|status| { 70 | super::statuses::render_status(config, &db, status.1, &localizer, Some(&account)) 71 | }).buffered(10).collect::>().await 72 | .into_iter().collect::, _>>()? 73 | ), 74 | links 75 | }) 76 | } 77 | 78 | #[post("/api/v1/statuses//bookmark")] 79 | pub async fn bookmark_status( 80 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 81 | status_id: String, localizer: crate::i18n::Localizer 82 | ) -> Result, super::Error> { 83 | if !user.has_scope("write:bookmarks") { 84 | return Err(super::Error { 85 | code: rocket::http::Status::Forbidden, 86 | error: fl!(localizer, "error-no-permission") 87 | }); 88 | } 89 | 90 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 91 | let status = super::statuses::get_status_and_check_visibility(&status_id, Some(&account), &db, &localizer).await?; 92 | 93 | if crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 94 | crate::schema::bookmarks::dsl::bookmarks.filter( 95 | crate::schema::bookmarks::dsl::status.eq(status.id) 96 | ).filter( 97 | crate::schema::bookmarks::dsl::account.eq(&account.id) 98 | ).count().get_result::(c) 99 | }).await? > 0 { 100 | return Ok(rocket::serde::json::Json(super::statuses::render_status(config, &db, status, &localizer, Some(&account)).await?)); 101 | } 102 | 103 | let new_bookmark = models::NewBookmark { 104 | id: uuid::Uuid::new_v4(), 105 | account: account.id, 106 | status: status.id, 107 | }; 108 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 109 | diesel::insert_into(crate::schema::bookmarks::dsl::bookmarks) 110 | .values(new_bookmark) 111 | .execute(c) 112 | }).await?; 113 | 114 | Ok(rocket::serde::json::Json(super::statuses::render_status(config, &db, status, &localizer, Some(&account)).await?)) 115 | } 116 | 117 | #[post("/api/v1/statuses//unbookmark")] 118 | pub async fn unbookmark_status( 119 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 120 | status_id: String, localizer: crate::i18n::Localizer 121 | ) -> Result, super::Error> { 122 | if !user.has_scope("write:bookmarks") { 123 | return Err(super::Error { 124 | code: rocket::http::Status::Forbidden, 125 | error: fl!(localizer, "error-no-permission") 126 | }); 127 | } 128 | 129 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 130 | let status = super::statuses::get_status_and_check_visibility(&status_id, Some(&account), &db, &localizer).await?; 131 | 132 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 133 | diesel::delete(crate::schema::bookmarks::dsl::bookmarks.filter( 134 | crate::schema::bookmarks::dsl::status.eq(status.id) 135 | ).filter( 136 | crate::schema::bookmarks::dsl::account.eq(&account.id) 137 | )).execute(c) 138 | }).await?; 139 | 140 | Ok(rocket::serde::json::Json(super::statuses::render_status(config, &db, status, &localizer, Some(&account)).await?)) 141 | } 142 | -------------------------------------------------------------------------------- /src/views/conversations.rs: -------------------------------------------------------------------------------- 1 | use crate::AppConfig; 2 | 3 | #[get("/api/v1/conversations?&&&")] 4 | pub async fn conversations( 5 | _config: &rocket::State, max_id: Option, since_id: Option, 6 | min_id: Option, limit: Option, user: super::oauth::TokenClaims, 7 | localizer: crate::i18n::Localizer 8 | ) -> Result>, super::Error> { 9 | if !user.has_scope("read:statuses") { 10 | return Err(super::Error { 11 | code: rocket::http::Status::Forbidden, 12 | error: fl!(localizer, "error-no-permission") 13 | }); 14 | } 15 | 16 | Ok(rocket::serde::json::Json(vec![])) 17 | } 18 | 19 | #[delete("/api/v1/conversations/<_id>")] 20 | pub async fn delete_conversation( 21 | _config: &rocket::State, _id: &str, user: super::oauth::TokenClaims, 22 | localizer: crate::i18n::Localizer, 23 | ) -> Result, super::Error> { 24 | if !user.has_scope("write:conversations") { 25 | return Err(super::Error { 26 | code: rocket::http::Status::Forbidden, 27 | error: fl!(localizer, "error-no-permission") 28 | }); 29 | } 30 | 31 | Err(super::Error { 32 | code: rocket::http::Status::ServiceUnavailable, 33 | error: fl!(localizer, "service-unavailable") 34 | }) 35 | } 36 | 37 | #[post("/api/v1/conversations/<_id>/read")] 38 | pub async fn read_conversation( 39 | _config: &rocket::State, _id: &str, user: super::oauth::TokenClaims, 40 | localizer: crate::i18n::Localizer 41 | ) -> Result, super::Error> { 42 | if !user.has_scope("write:conversations") { 43 | return Err(super::Error { 44 | code: rocket::http::Status::Forbidden, 45 | error: fl!(localizer, "error-no-permission") 46 | }); 47 | } 48 | 49 | Err(super::Error { 50 | code: rocket::http::Status::ServiceUnavailable, 51 | error: fl!(localizer, "service-unavailable") 52 | }) 53 | } -------------------------------------------------------------------------------- /src/views/domain_blocks.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/domain_blocks")] 2 | pub async fn domain_blocks( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer, 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read:blocks") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[derive(Deserialize, FromForm)] 16 | pub struct DomainBlock { 17 | domain: String, 18 | } 19 | 20 | #[post("/api/v1/domain_blocks", data = "<_form>")] 21 | pub async fn create_domain_block( 22 | user: super::oauth::TokenClaims, _form: rocket::form::Form, 23 | localizer: crate::i18n::Localizer, 24 | ) -> Result, super::Error> { 25 | if !user.has_scope("write:filters") { 26 | return Err(super::Error { 27 | code: rocket::http::Status::Forbidden, 28 | error: fl!(localizer, "error-no-permission") 29 | }); 30 | } 31 | 32 | Err(super::Error { 33 | code: rocket::http::Status::ServiceUnavailable, 34 | error: fl!(localizer, "service-unavailable") 35 | }) 36 | } 37 | 38 | #[delete("/api/v1/domain_block", data = "<_form>")] 39 | pub async fn delete_domain_block( 40 | user: super::oauth::TokenClaims, _form: rocket::form::Form, 41 | localizer: crate::i18n::Localizer, 42 | ) -> Result, super::Error> { 43 | if !user.has_scope("write:lists") { 44 | return Err(super::Error { 45 | code: rocket::http::Status::Forbidden, 46 | error: fl!(localizer, "error-no-permission") 47 | }); 48 | } 49 | 50 | Err(super::Error { 51 | code: rocket::http::Status::ServiceUnavailable, 52 | error: fl!(localizer, "service-unavailable") 53 | }) 54 | } -------------------------------------------------------------------------------- /src/views/favourites.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use chrono::prelude::*; 3 | use futures::StreamExt; 4 | use crate::models; 5 | 6 | #[get("/api/v1/favourites?&&")] 7 | pub async fn favourites( 8 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 9 | limit: Option, max_id: Option, min_id: Option, 10 | host: &rocket::http::uri::Host<'_>, localizer: crate::i18n::Localizer 11 | ) -> Result>>, super::Error> { 12 | if !user.has_scope("read:favourites") { 13 | return Err(super::Error { 14 | code: rocket::http::Status::Forbidden, 15 | error: fl!(localizer, "error-no-permission") 16 | }); 17 | } 18 | 19 | let limit = limit.unwrap_or(20); 20 | if limit > 500 { 21 | return Err( super::Error { 22 | code: rocket::http::Status::UnprocessableEntity, 23 | error: fl!(localizer, "limit-too-large") 24 | }); 25 | } 26 | 27 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 28 | 29 | let statuses: Vec<(models::Like, models::Status)> = 30 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 31 | let mut sel = crate::schema::likes::dsl::likes.order_by( 32 | crate::schema::likes::dsl::iid.desc() 33 | ).filter( 34 | crate::schema::likes::dsl::account.eq(&account.id) 35 | ).inner_join( 36 | crate::schema::statuses::table.on( 37 | crate::schema::likes::dsl::status.eq(crate::schema::statuses::dsl::id.nullable()) 38 | ) 39 | ).filter( 40 | crate::schema::statuses::dsl::deleted_at.is_null() 41 | ).filter( 42 | crate::schema::statuses::dsl::boost_of_url.is_null() 43 | ).limit(limit as i64).into_boxed(); 44 | if let Some(min_id) = min_id { 45 | sel = sel.filter(crate::schema::likes::dsl::iid.gt(min_id)); 46 | } 47 | if let Some(max_id) = max_id { 48 | sel = sel.filter(crate::schema::likes::dsl::iid.lt(max_id)); 49 | } 50 | sel.get_results(c) 51 | }).await?; 52 | 53 | let mut links = vec![]; 54 | 55 | if let Some(last_id) = statuses.last().map(|a| a.0.iid) { 56 | links.push(super::Link { 57 | rel: "next".to_string(), 58 | href: format!("https://{}/api/v1/favourites?max_id={}", host.to_string(), last_id) 59 | }); 60 | } 61 | if let Some(first_id) = statuses.first().map(|a| a.0.iid) { 62 | links.push(super::Link { 63 | rel: "prev".to_string(), 64 | href: format!("https://{}/api/v1/favourites?min_id={}", host.to_string(), first_id) 65 | }); 66 | } 67 | 68 | Ok(super::LinkedResponse { 69 | inner: rocket::serde::json::Json( 70 | futures::stream::iter(statuses).map(|status| { 71 | super::statuses::render_status(config, &db, status.1, &localizer, Some(&account)) 72 | }).buffered(10).collect::>().await 73 | .into_iter().collect::, _>>()? 74 | ), 75 | links 76 | }) 77 | } 78 | 79 | #[post("/api/v1/statuses//favourite")] 80 | pub async fn like_status( 81 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 82 | status_id: String, celery: &rocket::State, localizer: crate::i18n::Localizer 83 | ) -> Result, super::Error> { 84 | if !user.has_scope("write:favourites") { 85 | return Err(super::Error { 86 | code: rocket::http::Status::Forbidden, 87 | error: fl!(localizer, "error-no-permission") 88 | }); 89 | } 90 | 91 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 92 | let status = super::statuses::get_status_and_check_visibility(&status_id, Some(&account), &db, &localizer).await?; 93 | 94 | if crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 95 | crate::schema::likes::dsl::likes.filter( 96 | crate::schema::likes::dsl::status.eq(status.id) 97 | ).filter( 98 | crate::schema::likes::dsl::account.eq(&account.id) 99 | ).count().get_result::(c) 100 | }).await? > 0 { 101 | return Ok(rocket::serde::json::Json(super::statuses::render_status(config, &db, status, &localizer, Some(&account)).await?)); 102 | } 103 | 104 | let new_like = models::NewLike { 105 | id: uuid::Uuid::new_v4(), 106 | account: account.id, 107 | status: Some(status.id), 108 | status_url: None, 109 | created_at: Utc::now().naive_utc(), 110 | local: true, 111 | url: None, 112 | }; 113 | let like = crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 114 | diesel::insert_into(crate::schema::likes::dsl::likes) 115 | .values(new_like) 116 | .get_result::(c) 117 | }).await?; 118 | 119 | match celery.send_task( 120 | super::super::tasks::statuses::deliver_like::new(like, status.clone(), account.clone()) 121 | ).await { 122 | Ok(_) => {} 123 | Err(err) => { 124 | error!("Failed to submit celery task: {:?}", err); 125 | return Err(super::Error { 126 | code: rocket::http::Status::InternalServerError, 127 | error: fl!(localizer, "internal-server-error") 128 | }); 129 | } 130 | }; 131 | 132 | Ok(rocket::serde::json::Json(super::statuses::render_status(config, &db, status, &localizer, Some(&account)).await?)) 133 | } 134 | 135 | #[post("/api/v1/statuses//unfavourite")] 136 | pub async fn unlike_status( 137 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 138 | status_id: String, celery: &rocket::State, localizer: crate::i18n::Localizer 139 | ) -> Result, super::Error> { 140 | if !user.has_scope("write:favourites") { 141 | return Err(super::Error { 142 | code: rocket::http::Status::Forbidden, 143 | error: fl!(localizer, "error-no-permission") 144 | }); 145 | } 146 | 147 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 148 | let status = super::statuses::get_status_and_check_visibility(&status_id, Some(&account), &db, &localizer).await?; 149 | 150 | if let Some(like) = crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 151 | crate::schema::likes::dsl::likes.filter( 152 | crate::schema::likes::dsl::status.eq(status.id) 153 | ).filter( 154 | crate::schema::likes::dsl::account.eq(&account.id) 155 | ).get_result::(c).optional() 156 | }).await? { 157 | let like_id = like.id; 158 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 159 | diesel::delete(crate::schema::likes::dsl::likes.find(like_id)) 160 | .execute(c) 161 | }).await?; 162 | 163 | match celery.send_task( 164 | super::super::tasks::statuses::deliver_undo_like::new(like, status.clone(), account.clone()) 165 | ).await { 166 | Ok(_) => {} 167 | Err(err) => { 168 | error!("Failed to submit celery task: {:?}", err); 169 | return Err(super::Error { 170 | code: rocket::http::Status::InternalServerError, 171 | error: fl!(localizer, "internal-server-error") 172 | }); 173 | } 174 | }; 175 | } 176 | 177 | Ok(rocket::serde::json::Json(super::statuses::render_status(config, &db, status, &localizer, Some(&account)).await?)) 178 | } 179 | -------------------------------------------------------------------------------- /src/views/filters.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/filters")] 2 | pub async fn filters( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer, 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read:filters") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[derive(Deserialize, FromForm)] 16 | pub struct FilterCreateForm { 17 | phrase: String, 18 | context: Vec, 19 | irreversible: Option, 20 | whole_word: Option, 21 | expires_in: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, FromFormField, Debug)] 25 | pub enum FilterContexts { 26 | #[serde(rename = "home")] 27 | Home, 28 | #[serde(rename = "notifications")] 29 | Notifications, 30 | #[serde(rename = "public")] 31 | Public, 32 | #[serde(rename = "thread")] 33 | Thread 34 | } 35 | 36 | #[post("/api/v1/filters", data = "<_form>")] 37 | pub async fn create_filter( 38 | user: super::oauth::TokenClaims, _form: rocket::form::Form, 39 | localizer: crate::i18n::Localizer, 40 | ) -> Result, super::Error> { 41 | if !user.has_scope("write:filters") { 42 | return Err(super::Error { 43 | code: rocket::http::Status::Forbidden, 44 | error: fl!(localizer, "error-no-permission") 45 | }); 46 | } 47 | 48 | Err(super::Error { 49 | code: rocket::http::Status::ServiceUnavailable, 50 | error: fl!(localizer, "service-unavailable") 51 | }) 52 | } 53 | 54 | #[get("/api/v1/filters/<_filter_id>")] 55 | pub async fn filter( 56 | user: super::oauth::TokenClaims, _filter_id: String, localizer: crate::i18n::Localizer 57 | ) -> Result, super::Error> { 58 | if !user.has_scope("read:filters") { 59 | return Err(super::Error { 60 | code: rocket::http::Status::Forbidden, 61 | error: fl!(localizer, "error-no-permission") 62 | }); 63 | } 64 | 65 | Ok(rocket::serde::json::Json(super::objs::Filter {})) 66 | } 67 | 68 | #[post("/api/v1/filters/<_filter_id>", data = "<_form>")] 69 | pub async fn update_filter( 70 | user: super::oauth::TokenClaims, _filter_id: String, _form: rocket::form::Form, 71 | localizer: crate::i18n::Localizer, 72 | ) -> Result, super::Error> { 73 | if !user.has_scope("write:lists") { 74 | return Err(super::Error { 75 | code: rocket::http::Status::Forbidden, 76 | error: fl!(localizer, "error-no-permission") 77 | }); 78 | } 79 | 80 | Err(super::Error { 81 | code: rocket::http::Status::ServiceUnavailable, 82 | error: fl!(localizer, "service-unavailable") 83 | }) 84 | } 85 | 86 | #[delete("/api/v1/filters/<_filter_id>")] 87 | pub async fn delete_filter( 88 | user: super::oauth::TokenClaims, _filter_id: String, localizer: crate::i18n::Localizer 89 | ) -> Result, super::Error> { 90 | if !user.has_scope("write:lists") { 91 | return Err(super::Error { 92 | code: rocket::http::Status::Forbidden, 93 | error: fl!(localizer, "error-no-permission") 94 | }); 95 | } 96 | 97 | Err(super::Error { 98 | code: rocket::http::Status::ServiceUnavailable, 99 | error: fl!(localizer, "service-unavailable") 100 | }) 101 | } -------------------------------------------------------------------------------- /src/views/follow_requests.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/follow_requests")] 2 | pub async fn follow_requests( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer, 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read:follows") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[post("/api/v1/follow_requests/<_acct_id>/accept")] 16 | pub async fn accept_follow_request( 17 | user: super::oauth::TokenClaims, _acct_id: String, localizer: crate::i18n::Localizer 18 | ) -> Result, super::Error> { 19 | if !user.has_scope("write:filters") { 20 | return Err(super::Error { 21 | code: rocket::http::Status::Forbidden, 22 | error: fl!(localizer, "error-no-permission") 23 | }); 24 | } 25 | 26 | Err(super::Error { 27 | code: rocket::http::Status::ServiceUnavailable, 28 | error: fl!(localizer, "service-unavailable") 29 | }) 30 | } 31 | 32 | #[post("/api/v1/follow_requests/<_acct_id>/reject")] 33 | pub async fn reject_follow_request( 34 | user: super::oauth::TokenClaims, _acct_id: String, localizer: crate::i18n::Localizer 35 | ) -> Result, super::Error> { 36 | if !user.has_scope("write:lists") { 37 | return Err(super::Error { 38 | code: rocket::http::Status::Forbidden, 39 | error: fl!(localizer, "error-no-permission") 40 | }); 41 | } 42 | 43 | Err(super::Error { 44 | code: rocket::http::Status::ServiceUnavailable, 45 | error: fl!(localizer, "service-unavailable") 46 | }) 47 | } -------------------------------------------------------------------------------- /src/views/instance.rs: -------------------------------------------------------------------------------- 1 | use crate::AppConfig; 2 | use crate::views::objs::InstanceV2Configuration; 3 | 4 | #[get("/api/v1/instance")] 5 | pub async fn instance(config: &rocket::State) -> rocket::serde::json::Json { 6 | rocket::serde::json::Json(super::objs::Instance { 7 | uri: config.uri.clone(), 8 | title: "Tafarn Test".to_string(), 9 | short_description: "".to_string(), 10 | description: "".to_string(), 11 | email: "test@example.com".to_string(), 12 | version: "4.0.2".to_string(), 13 | urls: super::objs::InstanceURLs { 14 | streaming_api: None, 15 | }, 16 | stats: super::objs::InstanceStats { 17 | user_count: 0, 18 | status_count: 0, 19 | domain_count: 0, 20 | }, 21 | thumbnail: None, 22 | languages: vec!["en".to_string()], 23 | registrations: true, 24 | approval_required: false, 25 | contact_account: None, 26 | invites_enabled: false, 27 | }) 28 | } 29 | 30 | #[get("/api/v2/instance")] 31 | pub async fn instance_v2(config: &rocket::State) -> rocket::serde::json::Json { 32 | rocket::serde::json::Json(super::objs::InstanceV2 { 33 | domain: config.uri.clone(), 34 | title: "Tafarn Test".to_string(), 35 | description: "".to_string(), 36 | version: "4.0.2".to_string(), 37 | source_url: "".to_string(), 38 | usage: super::objs::InstanceV2Usage { 39 | users: super::objs::InstanceV2UsageUsers { 40 | active_month: 0, 41 | }, 42 | }, 43 | configuration: InstanceV2Configuration { 44 | urls: super::objs::InstanceV2URLs { 45 | streaming_api: "".to_string(), 46 | }, 47 | accounts: super::objs::InstanceV2Accounts { 48 | max_featured_tags: 0 49 | }, 50 | statuses: super::objs::InstanceV2Statuses { 51 | max_characters: 500, 52 | max_media_attachments: 0, 53 | characters_reserved_per_url: 0 54 | }, 55 | media_attachments: super::objs::InstanceV2MediaAttachments { 56 | supported_mime_types: vec![], 57 | image_size_limit: 0, 58 | image_matrix_limit: 0, 59 | video_size_limit: 0, 60 | video_frame_rate_limit: 0, 61 | video_matrix_limit: 0 62 | }, 63 | polls: super::objs::InstanceV2Polls { 64 | max_options: 0, 65 | max_characters_per_option: 0, 66 | min_expiration: 0, 67 | max_expiration: 0 68 | }, 69 | translation: super::objs::InstanceV2Translation { 70 | enabled: false 71 | } 72 | }, 73 | thumbnail: super::objs::InstanceV2Thumbnail { 74 | url: "".to_string(), 75 | blurhash: None, 76 | versions: None, 77 | }, 78 | languages: vec!["en".to_string()], 79 | registrations: super::objs::InstanceV2Registrations { 80 | enabled: true, 81 | approval_required: false, 82 | message: None, 83 | }, 84 | contact: super::objs::InstanceV2Contact { 85 | email: "test@example.com".to_string(), 86 | account: None, 87 | }, 88 | rules: vec![] 89 | }) 90 | } 91 | 92 | #[get("/api/v1/instance/peers")] 93 | pub async fn instance_peers() -> rocket::serde::json::Json> { 94 | rocket::serde::json::Json(vec![]) 95 | } 96 | 97 | #[derive(Serialize)] 98 | pub struct Activity { 99 | week: i64, 100 | statuses: i64, 101 | logins: i64, 102 | registrations: i64 103 | } 104 | 105 | #[get("/api/v1/instance/activity")] 106 | pub async fn instance_activity() -> rocket::serde::json::Json> { 107 | rocket::serde::json::Json(vec![]) 108 | } 109 | 110 | #[get("/api/v1/custom_emojis")] 111 | pub async fn custom_emoji() -> rocket::serde::json::Json> { 112 | rocket::serde::json::Json(vec![]) 113 | } -------------------------------------------------------------------------------- /src/views/lists.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/lists")] 2 | pub async fn lists( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read:lists") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[derive(Deserialize, FromForm)] 16 | pub struct ListCreateForm { 17 | title: String, 18 | #[serde(default)] 19 | replies_policy: super::objs::ListRepliesPolicy 20 | } 21 | 22 | #[post("/api/v1/lists", data = "<_form>")] 23 | pub async fn create_list( 24 | user: super::oauth::TokenClaims, _form: rocket::form::Form, localizer: crate::i18n::Localizer 25 | ) -> Result, super::Error> { 26 | if !user.has_scope("write:lists") { 27 | return Err(super::Error { 28 | code: rocket::http::Status::Forbidden, 29 | error: fl!(localizer, "error-no-permission") 30 | }); 31 | } 32 | 33 | Err(super::Error { 34 | code: rocket::http::Status::ServiceUnavailable, 35 | error: fl!(localizer, "service-unavailable") 36 | }) 37 | } 38 | 39 | #[get("/api/v1/lists/<_list_id>")] 40 | pub async fn list( 41 | user: super::oauth::TokenClaims, _list_id: String, localizer: crate::i18n::Localizer 42 | ) -> Result, super::Error> { 43 | if !user.has_scope("read:lists") { 44 | return Err(super::Error { 45 | code: rocket::http::Status::Forbidden, 46 | error: fl!(localizer, "error-no-permission") 47 | }); 48 | } 49 | 50 | Ok(rocket::serde::json::Json(super::objs::List {})) 51 | } 52 | 53 | #[post("/api/v1/lists/<_list_id>", data = "<_form>")] 54 | pub async fn update_list( 55 | user: super::oauth::TokenClaims, _list_id: String, _form: rocket::form::Form, 56 | localizer: crate::i18n::Localizer 57 | ) -> Result, super::Error> { 58 | if !user.has_scope("write:lists") { 59 | return Err(super::Error { 60 | code: rocket::http::Status::Forbidden, 61 | error: fl!(localizer, "error-no-permission") 62 | }); 63 | } 64 | 65 | Err(super::Error { 66 | code: rocket::http::Status::ServiceUnavailable, 67 | error: fl!(localizer, "service-unavailable") 68 | }) 69 | } 70 | 71 | #[delete("/api/v1/lists/<_list_id>")] 72 | pub async fn delete_list( 73 | user: super::oauth::TokenClaims, _list_id: String, localizer: crate::i18n::Localizer 74 | ) -> Result, super::Error> { 75 | if !user.has_scope("write:lists") { 76 | return Err(super::Error { 77 | code: rocket::http::Status::Forbidden, 78 | error: fl!(localizer, "error-no-permission") 79 | }); 80 | } 81 | 82 | Err(super::Error { 83 | code: rocket::http::Status::ServiceUnavailable, 84 | error: fl!(localizer, "service-unavailable") 85 | }) 86 | } 87 | 88 | #[get("/api/v1/lists/<_list_id>/accounts")] 89 | pub async fn list_accounts( 90 | user: super::oauth::TokenClaims, _list_id: String, localizer: crate::i18n::Localizer 91 | ) -> Result>, super::Error> { 92 | if !user.has_scope("read:lists") { 93 | return Err(super::Error { 94 | code: rocket::http::Status::Forbidden, 95 | error: fl!(localizer, "error-no-permission") 96 | }); 97 | } 98 | 99 | Ok(rocket::serde::json::Json(vec![])) 100 | } 101 | 102 | #[derive(FromForm)] 103 | pub struct ListAccountsForm { 104 | account_ids: Vec 105 | } 106 | 107 | #[post("/api/v1/lists/<_list_id>/accounts", data = "<_form>")] 108 | pub async fn list_add_accounts( 109 | user: super::oauth::TokenClaims, _list_id: String, _form: rocket::form::Form, 110 | localizer: crate::i18n::Localizer 111 | ) -> Result, super::Error> { 112 | if !user.has_scope("write:lists") { 113 | return Err(super::Error { 114 | code: rocket::http::Status::Forbidden, 115 | error: fl!(localizer, "error-no-permission") 116 | }); 117 | } 118 | 119 | Err(super::Error { 120 | code: rocket::http::Status::ServiceUnavailable, 121 | error: fl!(localizer, "service-unavailable") 122 | }) 123 | } 124 | 125 | #[delete("/api/v1/lists/<_list_id>/accounts", data = "<_form>")] 126 | pub async fn list_delete_accounts( 127 | user: super::oauth::TokenClaims, _list_id: String, _form: rocket::form::Form, 128 | localizer: crate::i18n::Localizer 129 | ) -> Result, super::Error> { 130 | if !user.has_scope("write:lists") { 131 | return Err(super::Error { 132 | code: rocket::http::Status::Forbidden, 133 | error: fl!(localizer, "error-no-permission") 134 | }); 135 | } 136 | 137 | Err(super::Error { 138 | code: rocket::http::Status::ServiceUnavailable, 139 | error: fl!(localizer, "service-unavailable") 140 | }) 141 | } -------------------------------------------------------------------------------- /src/views/meta.rs: -------------------------------------------------------------------------------- 1 | use crate::AppConfig; 2 | use rocket_dyn_templates::{Template, context}; 3 | use diesel::prelude::*; 4 | 5 | #[get("/.well-known/host-meta")] 6 | pub async fn host_meta(config: &rocket::State) -> Template { 7 | Template::render("host-meta", context! { uri: config.uri.clone() }) 8 | } 9 | 10 | #[derive(Serialize, Deserialize)] 11 | pub struct JRD { 12 | pub subject: String, 13 | pub aliases: Vec, 14 | #[serde(default, skip_serializing_if = "Option::is_none")] 15 | pub properties: Option, 16 | pub links: Vec 17 | } 18 | 19 | impl<'r> rocket::response::Responder<'r, 'static> for JRD { 20 | fn respond_to(self, _req: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { 21 | let body = serde_json::to_string(&self).unwrap(); 22 | 23 | let mut res = rocket::Response::new(); 24 | res.set_status(rocket::http::Status::Ok); 25 | res.adjoin_raw_header("Content-Type", "application/jrd+json"); 26 | res.adjoin_raw_header("Access-Control-Allow-Methods", "GET, OPTIONS"); 27 | res.adjoin_raw_header("Access-Control-Allow-Origin", "*"); 28 | res.adjoin_raw_header("Access-Control-Allow-Credentials", "false"); 29 | res.set_sized_body(body.len(), std::io::Cursor::new(body)); 30 | Ok(res) 31 | } 32 | } 33 | 34 | #[derive(Serialize, Deserialize)] 35 | pub struct JRDProperties { 36 | 37 | } 38 | 39 | #[derive(Serialize, Deserialize)] 40 | pub struct JRDLink { 41 | pub rel: String, 42 | #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] 43 | pub type_: Option, 44 | #[serde(default, skip_serializing_if = "Option::is_none")] 45 | pub href: Option, 46 | #[serde(default, skip_serializing_if = "Option::is_none")] 47 | pub titles: Option>, 48 | #[serde(default, skip_serializing_if = "Option::is_none")] 49 | pub properties: Option 50 | } 51 | 52 | #[get("/.well-known/nodeinfo")] 53 | pub fn well_known_node_info( 54 | config: &rocket::State 55 | ) -> JRD { 56 | JRD { 57 | subject: config.uri.to_string(), 58 | aliases: vec![], 59 | properties: None, 60 | links: vec![JRDLink { 61 | rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".to_string(), 62 | type_: Some("application/json".to_string()), 63 | href: Some(format!("https://{}/nodeinfo/2.1", config.uri)), 64 | titles: None, 65 | properties: None 66 | }, JRDLink { 67 | rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(), 68 | type_: Some("application/json".to_string()), 69 | href: Some(format!("https://{}/nodeinfo/2.0", config.uri)), 70 | titles: None, 71 | properties: None 72 | }] 73 | } 74 | } 75 | 76 | #[get("/.well-known/webfinger?")] 77 | pub async fn web_finger( 78 | db: crate::DbConn, config: &rocket::State, resource: String, localizer: crate::i18n::Localizer 79 | ) -> Result { 80 | let (scheme, acct) = match resource.split_once(':') { 81 | Some((scheme, acct)) => (scheme, acct), 82 | None => return Err(rocket::http::Status::NotFound) 83 | }; 84 | 85 | if scheme != "acct" { 86 | return Err(rocket::http::Status::NotFound); 87 | } 88 | 89 | let (username, domain) = match acct.split_once('@') { 90 | Some((username, domain)) => (username.to_string(), domain), 91 | None => return Err(rocket::http::Status::NotFound) 92 | }; 93 | 94 | if domain != config.uri { 95 | return Err(rocket::http::Status::NotFound); 96 | } 97 | 98 | if username == config.uri { 99 | return Ok(JRD { 100 | subject: format!("acct:{}@{}", config.uri, config.uri), 101 | aliases: vec![], 102 | properties: None, 103 | links: vec![JRDLink { 104 | rel: "hhttp://webfinger.net/rel/profile-page".to_string(), 105 | type_: Some("text/html".to_string()), 106 | href: Some(config.uri.to_string()), 107 | titles: None, 108 | properties: None 109 | }, JRDLink { 110 | rel: "self".to_string(), 111 | type_: Some("application/activity+json".to_string()), 112 | href: Some(format!("https://{}/as/system", config.uri)), 113 | titles: None, 114 | properties: None 115 | }] 116 | }) 117 | } 118 | 119 | let account: Option = crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 120 | crate::schema::accounts::dsl::accounts.filter( 121 | crate::schema::accounts::dsl::username.eq(username) 122 | ).first(c).optional() 123 | }).await?; 124 | 125 | let account = match account { 126 | Some(account) => account, 127 | None => return Err(rocket::http::Status::NotFound) 128 | }; 129 | 130 | Ok(JRD { 131 | subject: format!("acct:{}@{}", account.username, config.uri), 132 | aliases: vec![], 133 | properties: None, 134 | links: vec![JRDLink { 135 | rel: "hhttp://webfinger.net/rel/profile-page".to_string(), 136 | type_: Some("text/html".to_string()), 137 | href: Some(format!("https://{}/users/{}", config.uri, account.id)), 138 | titles: None, 139 | properties: None 140 | }, JRDLink { 141 | rel: "self".to_string(), 142 | type_: Some("application/activity+json".to_string()), 143 | href: Some(format!("https://{}/as/users/{}", config.uri, account.id)), 144 | titles: None, 145 | properties: None 146 | }] 147 | }) 148 | } 149 | 150 | -------------------------------------------------------------------------------- /src/views/mod.rs: -------------------------------------------------------------------------------- 1 | use rocket::Request; 2 | 3 | pub mod oauth; 4 | pub mod timelines; 5 | pub mod meta; 6 | pub mod accounts; 7 | pub mod oidc; 8 | pub mod lists; 9 | pub mod filters; 10 | pub mod domain_blocks; 11 | pub mod follow_requests; 12 | pub mod suggestions; 13 | pub mod notifications; 14 | pub mod web_push; 15 | pub mod instance; 16 | pub mod conversations; 17 | pub mod search; 18 | pub mod mutes; 19 | pub mod blocks; 20 | pub mod media; 21 | pub mod statuses; 22 | pub mod bookmarks; 23 | pub mod favourites; 24 | pub mod objs; 25 | pub mod activity_streams; 26 | pub mod nodeinfo; 27 | 28 | pub fn parse_bool(s: Option<&str>, default: bool, localizer: &crate::i18n::Localizer) -> Result { 29 | Ok(match s { 30 | None => default, 31 | Some("true") => true, 32 | Some("1") => true, 33 | Some("0") => false, 34 | Some("false") => false, 35 | _ => return Err(Error { 36 | code: rocket::http::Status::BadRequest, 37 | error: fl!(localizer, "invalid-request") 38 | }) 39 | }) 40 | } 41 | 42 | pub struct LinkedResponse { 43 | pub inner: T, 44 | pub links: Vec 45 | } 46 | 47 | #[derive(Debug)] 48 | pub struct Link { 49 | pub rel: String, 50 | pub href: String 51 | } 52 | 53 | impl <'r, 'o: 'r, T: rocket::response::Responder<'r, 'o>> rocket::response::Responder<'r, 'o> for LinkedResponse { 54 | fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'o> { 55 | let mut response = self.inner.respond_to(request)?; 56 | if !self.links.is_empty() { 57 | let mut links = vec![]; 58 | for link in self.links { 59 | links.push(format!( 60 | "<{}>; rel=\"{}\"", 61 | percent_encoding::utf8_percent_encode(&link.href, percent_encoding::CONTROLS), 62 | percent_encoding::utf8_percent_encode(&link.rel, percent_encoding::CONTROLS) 63 | )); 64 | } 65 | response.set_raw_header("Link", links.join(", ")); 66 | } 67 | Ok(response) 68 | } 69 | } 70 | 71 | pub struct Error { 72 | pub code: rocket::http::Status, 73 | pub error: String, 74 | } 75 | 76 | #[derive(Serialize)] 77 | struct ErrorResponse { 78 | error: String 79 | } 80 | 81 | impl <'r, 'o: 'r> rocket::response::Responder<'r, 'o> for Error { 82 | fn respond_to(self, _: &'r Request<'_>) -> rocket::response::Result<'o> { 83 | let body = serde_json::to_vec(&ErrorResponse { 84 | error: self.error 85 | }).unwrap(); 86 | 87 | rocket::Response::build() 88 | .status(self.code) 89 | .sized_body(body.len(), std::io::Cursor::new(body)) 90 | .header(rocket::http::ContentType::JSON) 91 | .ok() 92 | } 93 | } 94 | 95 | impl From for rocket::http::Status { 96 | fn from(from: Error) -> rocket::http::Status { 97 | from.code 98 | } 99 | } -------------------------------------------------------------------------------- /src/views/mutes.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/mutes")] 2 | pub async fn mutes( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read:mutes") || !user.has_scope("follow") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[get("/api/v1/accounts/<_account_id>/mute")] 16 | pub async fn get_mute_account( 17 | _account_id: &str 18 | ) -> rocket::http::Status { 19 | rocket::http::Status::MethodNotAllowed 20 | } 21 | 22 | #[post("/api/v1/accounts//mute")] 23 | pub async fn mute_account( 24 | user: super::oauth::TokenClaims, account_id: String, localizer: crate::i18n::Localizer 25 | ) -> Result, super::Error> { 26 | if !user.has_scope("write:mutes") { 27 | return Err(super::Error { 28 | code: rocket::http::Status::Forbidden, 29 | error: fl!(localizer, "error-no-permission") 30 | }); 31 | } 32 | 33 | let _account_id = match uuid::Uuid::parse_str(&account_id) { 34 | Ok(id) => id, 35 | Err(_) => return Err(super::Error { 36 | code: rocket::http::Status::NotFound, 37 | error: fl!(localizer, "account-not-found") 38 | }) 39 | }; 40 | 41 | Err(super::Error { 42 | code: rocket::http::Status::ServiceUnavailable, 43 | error: fl!(localizer, "service-unavailable") 44 | }) 45 | } 46 | 47 | #[get("/api/v1/accounts/<_account_id>/unmute")] 48 | pub async fn get_unmute_account( 49 | _account_id: &str 50 | ) -> rocket::http::Status { 51 | rocket::http::Status::MethodNotAllowed 52 | } 53 | 54 | #[post("/api/v1/accounts//unmute")] 55 | pub async fn unmute_account( 56 | user: super::oauth::TokenClaims, account_id: String, localizer: crate::i18n::Localizer 57 | ) -> Result, super::Error> { 58 | if !user.has_scope("write:mutes") { 59 | return Err(super::Error { 60 | code: rocket::http::Status::Forbidden, 61 | error: fl!(localizer, "error-no-permission") 62 | }); 63 | } 64 | 65 | let _account_id = match uuid::Uuid::parse_str(&account_id) { 66 | Ok(id) => id, 67 | Err(_) => return Err(super::Error { 68 | code: rocket::http::Status::NotFound, 69 | error: fl!(localizer, "account-not-found") 70 | }) 71 | }; 72 | 73 | Err(super::Error { 74 | code: rocket::http::Status::ServiceUnavailable, 75 | error: fl!(localizer, "service-unavailable") 76 | }) 77 | } -------------------------------------------------------------------------------- /src/views/nodeinfo.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Serialize, Clone)] 4 | pub struct NodeInfo2_1 { 5 | pub version: String, 6 | pub software: Software2_1, 7 | pub protocols: Vec, 8 | pub services: Services2_0, 9 | #[serde(rename = "openRegistrations")] 10 | pub open_registrations: bool, 11 | pub usage: Usage2_0, 12 | pub metadata: HashMap 13 | } 14 | 15 | #[derive(Serialize, Clone)] 16 | pub struct Software2_1 { 17 | pub name: String, 18 | pub version: String, 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub repository: Option, 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub homepage: Option 23 | } 24 | 25 | #[derive(Serialize, Clone)] 26 | pub struct NodeInfo2_0 { 27 | pub version: String, 28 | pub software: Software2_0, 29 | pub protocols: Vec, 30 | pub services: Services2_0, 31 | #[serde(rename = "openRegistrations")] 32 | pub open_registrations: bool, 33 | pub usage: Usage2_0, 34 | pub metadata: HashMap 35 | } 36 | 37 | #[derive(Serialize, Clone)] 38 | pub struct Software2_0 { 39 | pub name: String, 40 | pub version: String, 41 | } 42 | 43 | #[derive(Serialize, Copy, Clone)] 44 | pub enum Protocols2_0 { 45 | #[serde(rename = "activitypub")] 46 | ActivityPub, 47 | #[serde(rename = "buddycloud")] 48 | BuddyCloud, 49 | #[serde(rename = "dfrn")] 50 | Dfrn, 51 | #[serde(rename = "diaspora")] 52 | Diaospora, 53 | #[serde(rename = "libertree")] 54 | Libertree, 55 | #[serde(rename = "ostatus")] 56 | OStatus, 57 | #[serde(rename = "pumpio")] 58 | PumpIo, 59 | #[serde(rename = "tent")] 60 | Tent, 61 | #[serde(rename = "xmpp")] 62 | Xmpp, 63 | #[serde(rename = "zot")] 64 | Zot, 65 | } 66 | 67 | #[derive(Serialize, Clone)] 68 | pub struct Services2_0 { 69 | pub inbound: Vec, 70 | pub outbound: Vec, 71 | } 72 | 73 | #[derive(Serialize, Copy, Clone)] 74 | pub enum NodeInfoServicesInbound2_0 { 75 | #[serde(rename = "atom1.0")] 76 | Atom1_0, 77 | #[serde(rename = "gnusocial")] 78 | GnuSocial, 79 | #[serde(rename = "imap")] 80 | Imap, 81 | #[serde(rename = "pnut")] 82 | Pnut, 83 | #[serde(rename = "pop3")] 84 | Pop3, 85 | #[serde(rename = "pumpio")] 86 | PumpIo, 87 | #[serde(rename = "rss2.0")] 88 | Rss2_0, 89 | #[serde(rename = "twitter")] 90 | Twitter 91 | } 92 | 93 | #[derive(Serialize, Copy, Clone)] 94 | pub enum NodeInfoServicesOutbound2_0 { 95 | #[serde(rename = "atom1.0")] 96 | Atom1_0, 97 | #[serde(rename = "blogger")] 98 | Blogger, 99 | #[serde(rename = "buddycloud")] 100 | Buddycloud, 101 | #[serde(rename = "diaspora")] 102 | Diaspora, 103 | #[serde(rename = "dreamwidth")] 104 | Dreamwidth, 105 | #[serde(rename = "drupal")] 106 | Drupal, 107 | #[serde(rename = "facebook")] 108 | Facebook, 109 | #[serde(rename = "frendica")] 110 | Friendica, 111 | #[serde(rename = "gnusocial")] 112 | GnuSocial, 113 | #[serde(rename = "google")] 114 | Google, 115 | #[serde(rename = "insanejournal")] 116 | InsaneJournal, 117 | #[serde(rename = "libertreaa")] 118 | Libertree, 119 | #[serde(rename = "linkedin")] 120 | LinkedIn, 121 | #[serde(rename = "livejournal")] 122 | LiveJournal, 123 | #[serde(rename = "mediagoblin")] 124 | Mediagoblin, 125 | #[serde(rename = "myspace")] 126 | MySpace, 127 | #[serde(rename = "pinterest")] 128 | Pinterest, 129 | #[serde(rename = "pnut")] 130 | Pnut, 131 | #[serde(rename = "posterus")] 132 | Posterous, 133 | #[serde(rename = "pumpio")] 134 | PumpIo, 135 | #[serde(rename = "redmatrix")] 136 | RedMatrix, 137 | #[serde(rename = "rss2.0")] 138 | Rss2_0, 139 | #[serde(rename = "smtp")] 140 | Smtp, 141 | #[serde(rename = "tent")] 142 | Tent, 143 | #[serde(rename = "tumblr")] 144 | Tumblr, 145 | #[serde(rename = "twitter")] 146 | Twitter, 147 | #[serde(rename = "wordpress")] 148 | Wordpress, 149 | #[serde(rename = "xmpp")] 150 | Xmpp 151 | } 152 | 153 | #[derive(Serialize, Copy, Clone)] 154 | pub struct Usage2_0 { 155 | pub users: Users2_0, 156 | #[serde(rename = "localPosts")] 157 | pub local_posts: Option, 158 | #[serde(rename = "localComments")] 159 | pub local_comments: Option 160 | } 161 | 162 | #[derive(Serialize, Copy, Clone)] 163 | pub struct Users2_0 { 164 | pub total: Option, 165 | #[serde(rename = "activeHalfyear")] 166 | pub active_half_year: Option, 167 | #[serde(rename = "activeMonth")] 168 | pub active_month: Option 169 | } 170 | 171 | pub struct NodeInfo { 172 | inner: T, 173 | profile: &'static str 174 | } 175 | 176 | impl<'r, T: serde::ser::Serialize> rocket::response::Responder<'r, 'static> for NodeInfo { 177 | fn respond_to(self, _req: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { 178 | let body = serde_json::to_string(&self.inner).unwrap(); 179 | 180 | let mut res = rocket::Response::new(); 181 | res.set_status(rocket::http::Status::Ok); 182 | res.adjoin_raw_header( 183 | "Content-Type", 184 | format!("application/json; profile=\"{}\"", self.profile)); 185 | res.adjoin_raw_header("Access-Control-Allow-Methods", "GET, OPTIONS"); 186 | res.adjoin_raw_header("Access-Control-Allow-Origin", "*"); 187 | res.adjoin_raw_header("Access-Control-Allow-Credentials", "false"); 188 | res.set_sized_body(body.len(), std::io::Cursor::new(body)); 189 | Ok(res) 190 | } 191 | } 192 | 193 | #[get("/nodeinfo/2.1")] 194 | pub fn node_info_2_1() -> NodeInfo { 195 | let repository = env!("CARGO_PKG_REPOSITORY").to_string(); 196 | let homepage = env!("CARGO_PKG_HOMEPAGE").to_string(); 197 | NodeInfo { 198 | inner: NodeInfo2_1 { 199 | version: "2.1".to_string(), 200 | software: Software2_1 { 201 | name: "Tafarn".to_string(), 202 | version: env!("CARGO_PKG_VERSION").to_string(), 203 | repository: if repository.is_empty() { 204 | None 205 | } else { 206 | Some(repository) 207 | }, 208 | homepage: if homepage.is_empty() { 209 | None 210 | } else { 211 | Some(homepage) 212 | } 213 | }, 214 | protocols: vec![Protocols2_0::ActivityPub], 215 | services: Services2_0 { inbound: vec![], outbound: vec![] }, 216 | open_registrations: true, 217 | usage: Usage2_0 { 218 | users: Users2_0 { 219 | total: None, 220 | active_half_year: None, 221 | active_month: None 222 | }, 223 | local_posts: None, 224 | local_comments: None 225 | }, 226 | metadata: Default::default() 227 | }, 228 | profile: "http://nodeinfo.diaspora.software/ns/schema/2.1#" 229 | } 230 | } 231 | 232 | #[get("/nodeinfo/2.0")] 233 | pub fn node_info_2_0() -> NodeInfo { 234 | NodeInfo { 235 | inner: NodeInfo2_0 { 236 | version: "2.1".to_string(), 237 | software: Software2_0 { 238 | name: "Tafarn".to_string(), 239 | version: env!("CARGO_PKG_VERSION").to_string(), 240 | }, 241 | protocols: vec![Protocols2_0::ActivityPub], 242 | services: Services2_0 { inbound: vec![], outbound: vec![] }, 243 | open_registrations: true, 244 | usage: Usage2_0 { 245 | users: Users2_0 { 246 | total: None, 247 | active_half_year: None, 248 | active_month: None 249 | }, 250 | local_posts: None, 251 | local_comments: None 252 | }, 253 | metadata: Default::default() 254 | }, 255 | profile: "http://nodeinfo.diaspora.software/ns/schema/2.0#" 256 | } 257 | } -------------------------------------------------------------------------------- /src/views/notifications.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use chrono::prelude::*; 3 | use futures::StreamExt; 4 | use crate::models; 5 | 6 | pub async fn render_notification( 7 | db: &crate::DbConn, config: &crate::AppConfig, notification: models::Notification, 8 | localizer: &crate::i18n::Localizer 9 | ) -> Result { 10 | let (account, status) = crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 11 | let a = crate::schema::accounts::dsl::accounts.find(notification.cause).get_result(c)?; 12 | let s = notification.status 13 | .map(|sid| crate::schema::statuses::dsl::statuses.find(sid) 14 | .get_result::(c)) 15 | .transpose()?; 16 | 17 | Ok((a, s)) 18 | }).await?; 19 | 20 | Ok(super::objs::Notification { 21 | id: notification.iid.to_string(), 22 | notification_type: notification.notification_type, 23 | created_at: Utc.from_utc_datetime(¬ification.created_at), 24 | status: match status { 25 | Some(s) => Some(super::statuses::render_status(config, &db, s, localizer, Some(&account)).await?), 26 | None => None 27 | }, 28 | account: super::accounts::render_account(config, &db, &localizer, account).await?, 29 | report: None, 30 | }) 31 | } 32 | 33 | #[get("/api/v1/notifications?&&&&&")] 34 | pub async fn notifications( 35 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 36 | min_id: Option, max_id: Option, limit: Option, 37 | types: Option>, exclude_types: Option>, 38 | account_id: Option, host: &rocket::http::uri::Host<'_>, localizer: crate::i18n::Localizer 39 | ) -> Result>>, super::Error> { 40 | if !user.has_scope("read:notifications") { 41 | return Err(super::Error { 42 | code: rocket::http::Status::Forbidden, 43 | error: fl!(localizer, "error-no-permission") 44 | }); 45 | } 46 | 47 | let limit = limit.unwrap_or(15); 48 | if limit > 500 { 49 | return Err(super::Error { 50 | code: rocket::http::Status::BadRequest, 51 | error: fl!(localizer, "limit-too-large") 52 | }); 53 | } 54 | 55 | let account_id = match account_id { 56 | Some(id) => match uuid::Uuid::parse_str(&id) { 57 | Ok(id) => Some(id), 58 | Err(_) => return Err(super::Error { 59 | code: rocket::http::Status::NotFound, 60 | error: fl!(localizer, "account-not-found") 61 | }) 62 | }, 63 | None => None 64 | }; 65 | 66 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 67 | let notifications: Vec = crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 68 | let mut q = crate::schema::notifications::dsl::notifications.filter( 69 | crate::schema::notifications::dsl::account.eq(&account.id) 70 | ).limit(limit as i64).order_by(crate::schema::notifications::created_at.desc()).into_boxed(); 71 | if let Some(types) = types { 72 | q = q.filter(crate::schema::notifications::dsl::notification_type.eq_any(types)); 73 | } 74 | if let Some(types) = exclude_types { 75 | q = q.filter(crate::schema::notifications::dsl::notification_type.ne_all(types)); 76 | } 77 | if let Some(account_id) = account_id { 78 | q = q.filter(crate::schema::notifications::dsl::cause.eq(account_id)); 79 | } 80 | if let Some(min_id) = min_id { 81 | q = q.filter(crate::schema::notifications::dsl::iid.gt(min_id)); 82 | } 83 | if let Some(max_id) = max_id { 84 | q = q.filter(crate::schema::notifications::dsl::iid.lt(max_id)); 85 | } 86 | q.load(c) 87 | }).await?; 88 | 89 | let mut links = vec![]; 90 | 91 | if let Some(last_id) = notifications.last().map(|a| a.iid) { 92 | links.push(super::Link { 93 | rel: "next".to_string(), 94 | href: format!("https://{}/api/v1/notifications?max_id={}", host.to_string(), last_id) 95 | }); 96 | } 97 | if let Some(first_id) = notifications.first().map(|a| a.iid) { 98 | links.push(super::Link { 99 | rel: "prev".to_string(), 100 | href: format!("https://{}/api/v1/notifications?min_id={}", host.to_string(), first_id) 101 | }); 102 | } 103 | 104 | Ok(super::LinkedResponse { 105 | inner: rocket::serde::json::Json( 106 | futures::stream::iter(notifications.into_iter()) 107 | .map(|n| render_notification(&db, config, n, &localizer)) 108 | .buffered(10) 109 | .collect::>().await.into_iter().collect::, _>>()? 110 | ), 111 | links 112 | }) 113 | } 114 | 115 | async fn get_notification_and_check_visibility( 116 | notification_id: &str, account: &models::Account, db: &crate::DbConn, localizer: &crate::i18n::Localizer 117 | ) -> Result { 118 | let notification_id = match notification_id.parse::() { 119 | Ok(id) => id, 120 | Err(_) => return Err(super::Error { 121 | code: rocket::http::Status::NotFound, 122 | error: fl!(localizer, "error-notification-not-found") 123 | }) 124 | }; 125 | 126 | let notification: crate::models::Notification = match crate::db_run(db, localizer, move |c| -> QueryResult<_> { 127 | crate::schema::notifications::dsl::notifications.filter( 128 | crate::schema::notifications::dsl::iid.eq(notification_id) 129 | ).get_result(c).optional() 130 | }).await? { 131 | Some(a) => a, 132 | None => return Err(super::Error { 133 | code: rocket::http::Status::NotFound, 134 | error: fl!(localizer, "error-notification-not-found") 135 | }) 136 | }; 137 | 138 | if notification.account != account.id { 139 | return Err(super::Error { 140 | code: rocket::http::Status::Forbidden, 141 | error: fl!(localizer, "error-no-permission") 142 | }); 143 | } 144 | 145 | Ok(notification) 146 | } 147 | 148 | #[get("/api/v1/notifications/")] 149 | pub async fn notification( 150 | db: crate::DbConn, config: &rocket::State, user: super::oauth::TokenClaims, 151 | notification_id: String, localizer: crate::i18n::Localizer 152 | ) -> Result, super::Error> { 153 | if !user.has_scope("read:notifications") { 154 | return Err(super::Error { 155 | code: rocket::http::Status::Forbidden, 156 | error: fl!(localizer, "error-no-permission") 157 | }); 158 | } 159 | 160 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 161 | let notification = get_notification_and_check_visibility(¬ification_id, &account, &db, &localizer).await?; 162 | 163 | Ok(rocket::serde::json::Json(render_notification(&db, config, notification, &localizer).await?)) 164 | } 165 | 166 | #[post("/api/v1/notifications/clear")] 167 | pub async fn clear_notifications( 168 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer 169 | ) -> Result, super::Error> { 170 | if !user.has_scope("write:notifications") { 171 | return Err(super::Error { 172 | code: rocket::http::Status::Forbidden, 173 | error: fl!(localizer, "error-no-permission") 174 | }); 175 | } 176 | 177 | Ok(rocket::serde::json::Json(())) 178 | } 179 | 180 | #[post("/api/v1/notifications//dimiss")] 181 | pub async fn dismiss_notification( 182 | db: crate::DbConn, user: super::oauth::TokenClaims, notification_id: String, 183 | localizer: crate::i18n::Localizer 184 | ) -> Result, super::Error> { 185 | if !user.has_scope("write:notifications") { 186 | return Err(super::Error { 187 | code: rocket::http::Status::Forbidden, 188 | error: fl!(localizer, "error-no-permission") 189 | }); 190 | } 191 | 192 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 193 | let _notification = get_notification_and_check_visibility(¬ification_id, &account, &db, &localizer).await?; 194 | 195 | Ok(rocket::serde::json::Json(())) 196 | } -------------------------------------------------------------------------------- /src/views/search.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | #[get("/api/v2/search?&&&&&")] 4 | pub async fn search( 5 | db: crate::DbConn, config: &rocket::State, 6 | user: Option, q: String, limit: Option, offset: Option, 7 | following: Option<&str>, resolve: Option<&str>, r#type: Option<&str>, localizer: crate::i18n::Localizer 8 | ) -> Result, super::Error> { 9 | let limit = limit.unwrap_or(20); 10 | if limit > 500 { 11 | return Err(super::Error { 12 | code: rocket::http::Status::BadRequest, 13 | error: fl!(localizer, "limit-too-large") 14 | }); 15 | } 16 | let following = super::parse_bool(following, false, &localizer)?; 17 | let resolve = super::parse_bool(resolve, false, &localizer)?; 18 | 19 | if resolve { 20 | if user.is_none() { 21 | return Err(super::Error { 22 | code: rocket::http::Status::Forbidden, 23 | error: fl!(localizer, "error-no-permission") 24 | }); 25 | } 26 | 27 | if let Some((domain, q)) = if let Ok(url) = url::Url::parse(&q) { 28 | url.domain().map(|d| (d.to_string(), url.to_string())) 29 | } else if let Some(cap) = crate::WEBFINGER_RE.captures(&q) { 30 | Some(( 31 | cap.name("domain").unwrap().as_str().to_string(), 32 | cap.name("acct").unwrap().as_str().to_string() 33 | )) 34 | } else { 35 | None 36 | } { 37 | let url = format!("https://{}/.well-known/webfinger?resource={}", domain, q); 38 | if let Ok(res) = crate::AS_CLIENT.get(&url).send().await { 39 | if let Ok(jrd) = res.json::().await { 40 | if let Some(actor) = jrd.links.into_iter() 41 | .filter(|l| l.rel == "self") 42 | .find(|l| l.type_.as_deref() == Some("application/activity+json")) 43 | .map(|l| l.href.as_ref().unwrap().clone()) { 44 | match crate::tasks::accounts::find_account( 45 | super::activity_streams::ReferenceOrObject::Reference(actor), true 46 | ).await { 47 | Ok(Some(account)) => { 48 | return Ok(rocket::serde::json::Json(super::objs::Search { 49 | accounts: vec![super::accounts::render_account(config, &db, &localizer, account).await?], 50 | hashtags: vec![], 51 | statuses: vec![] 52 | })); 53 | }, 54 | Ok(None) => {}, 55 | Err(e) => { 56 | warn!("Error resolving search: {}", e); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | let account = match &user { 66 | Some(user) => { 67 | if !user.has_scope("read:search") { 68 | return Err(super::Error { 69 | code: rocket::http::Status::Forbidden, 70 | error: fl!(localizer, "error-no-permission") 71 | }); 72 | } 73 | Some(super::accounts::get_account(&db, &localizer, user).await?) 74 | }, 75 | None => None, 76 | }; 77 | let only_following = match account { 78 | Some(account) => if following { 79 | Some(account.id) 80 | } else { 81 | None 82 | }, 83 | None => if following { 84 | return Err(super::Error { 85 | code: rocket::http::Status::Forbidden, 86 | error: fl!(localizer, "error-no-permission") 87 | }); 88 | } else { 89 | None 90 | } 91 | }; 92 | 93 | let accounts: Vec = if r#type.is_none() || r#type == Some("accounts") { 94 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 95 | let q = q.replace("%", "\\%").replace("_", "\\_"); 96 | let ilike = format!("%{}%", q); 97 | let ilike_sort = format!("{}%", q); 98 | let mut query = crate::schema::accounts::dsl::accounts.filter( 99 | crate::schema::accounts::dsl::username.ilike(&ilike) 100 | .or(crate::schema::accounts::dsl::display_name.ilike(&ilike)) 101 | ).order_by(( 102 | crate::schema::accounts::dsl::username.not_ilike(&ilike_sort), 103 | crate::schema::accounts::dsl::display_name.not_ilike(&ilike_sort), 104 | )).limit(limit as i64).into_boxed(); 105 | if let Some(following) = only_following { 106 | query = query.filter(crate::schema::accounts::dsl::id.eq_any( 107 | crate::schema::following::dsl::following.select( 108 | crate::schema::following::dsl::followee 109 | ).filter( 110 | crate::schema::following::dsl::follower.eq(following) 111 | ) 112 | )); 113 | } 114 | if let Some(offset) = offset { 115 | query = query.offset(offset as i64); 116 | } 117 | query.get_results(c) 118 | }).await? 119 | } else { 120 | vec![] 121 | }; 122 | 123 | Ok(rocket::serde::json::Json(super::objs::Search { 124 | accounts: futures::future::try_join_all( 125 | accounts.into_iter().map(|a| super::accounts::render_account(config, &db, &localizer, a)).collect::>() 126 | ).await?, 127 | hashtags: vec![], 128 | statuses: vec![], 129 | })) 130 | } -------------------------------------------------------------------------------- /src/views/suggestions.rs: -------------------------------------------------------------------------------- 1 | #[get("/api/v1/suggestions")] 2 | pub async fn suggestions( 3 | user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer 4 | ) -> Result>, super::Error> { 5 | if !user.has_scope("read") { 6 | return Err(super::Error { 7 | code: rocket::http::Status::Forbidden, 8 | error: fl!(localizer, "error-no-permission") 9 | }); 10 | } 11 | 12 | Ok(rocket::serde::json::Json(vec![])) 13 | } 14 | 15 | #[delete("/api/v1/suggestions/<_acct_id>")] 16 | pub async fn delete_suggestion( 17 | user: super::oauth::TokenClaims, _acct_id: String, localizer: crate::i18n::Localizer 18 | ) -> Result, super::Error> { 19 | if !user.has_scope("read") { 20 | return Err(super::Error { 21 | code: rocket::http::Status::Forbidden, 22 | error: fl!(localizer, "error-no-permission") 23 | }); 24 | } 25 | 26 | Err(super::Error { 27 | code: rocket::http::Status::ServiceUnavailable, 28 | error: fl!(localizer, "service-unavailable") 29 | }) 30 | } -------------------------------------------------------------------------------- /src/views/timelines.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use futures::StreamExt; 3 | 4 | #[get("/api/v1/timelines/home?&&&")] 5 | pub async fn timeline_home( 6 | config: &rocket::State, db: crate::DbConn, user: super::oauth::TokenClaims, 7 | max_id: Option, since_id: Option, min_id: Option, 8 | limit: Option, host: &rocket::http::uri::Host<'_>, localizer: crate::i18n::Localizer 9 | ) -> Result>>, super::Error> { 10 | if !user.has_scope("read:statuses") { 11 | return Err(super::Error { 12 | code: rocket::http::Status::Forbidden, 13 | error: fl!(localizer, "error-no-permission") 14 | }); 15 | } 16 | 17 | let limit = limit.unwrap_or(20); 18 | if limit > 500 { 19 | return Err(super::Error { 20 | code: rocket::http::Status::BadRequest, 21 | error: fl!(localizer, "limit-too-large") 22 | }); 23 | } 24 | 25 | let account = super::accounts::get_account(&db, &localizer, &user).await?; 26 | 27 | let statuses: Vec<(crate::models::HomeTimelineEntry, crate::models::Status)> = 28 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 29 | let mut sel = crate::schema::home_timeline::dsl::home_timeline.filter( 30 | crate::schema::home_timeline::dsl::account_id.eq(&account.id) 31 | ).filter( 32 | crate::schema::statuses::dsl::deleted_at.is_null() 33 | ).filter( 34 | crate::schema::statuses::dsl::boost_of_url.is_null() 35 | ).order_by( 36 | crate::schema::home_timeline::dsl::id.desc() 37 | ).limit(limit as i64).inner_join(crate::schema::statuses::table.on( 38 | crate::schema::statuses::dsl::id.eq(crate::schema::home_timeline::dsl::status_id) 39 | )).into_boxed(); 40 | if let Some(min_id) = min_id { 41 | sel = sel.filter(crate::schema::home_timeline::dsl::id.gt(min_id)); 42 | } 43 | if let Some(max_id) = max_id { 44 | sel = sel.filter(crate::schema::home_timeline::dsl::id.lt(max_id)); 45 | } 46 | if let Some(since_id) = since_id { 47 | sel = sel.filter(crate::schema::home_timeline::dsl::id.gt(since_id)); 48 | } 49 | sel.get_results(c) 50 | }).await?; 51 | 52 | let mut links = vec![]; 53 | 54 | if let Some(last_id) = statuses.last().map(|a| a.0.id) { 55 | links.push(super::Link { 56 | rel: "next".to_string(), 57 | href: format!("https://{}/api/v1/timelines/home?max_id={}", host.to_string(), last_id) 58 | }); 59 | } 60 | if let Some(first_id) = statuses.first().map(|a| a.0.id) { 61 | links.push(super::Link { 62 | rel: "prev".to_string(), 63 | href: format!("https://{}/api/v1/timelines/home?min_id={}", host.to_string(), first_id) 64 | }); 65 | } 66 | 67 | Ok(super::LinkedResponse { 68 | inner: rocket::serde::json::Json( 69 | futures::stream::iter(statuses).map(|status| { 70 | super::statuses::render_status(config, &db, status.1, &localizer, Some(&account)) 71 | }).buffered(10).collect::>().await 72 | .into_iter().collect::, _>>()? 73 | ), 74 | links 75 | }) 76 | } 77 | 78 | #[get("/api/v1/timelines/tag/?&&&&&&&&&")] 79 | pub async fn timeline_hashtag( 80 | _config: &rocket::State, hashtag: &str, any: Option>, all: Option>, 81 | none: Option>, local: Option<&str>, remote: Option<&str>, 82 | only_media: Option<&str>, max_id: Option, since_id: Option, 83 | min_id: Option, limit: Option, localizer: crate::i18n::Localizer 84 | ) -> Result>, super::Error> { 85 | let _local = super::parse_bool(local, false, &localizer)?; 86 | let _remote = super::parse_bool(remote, false, &localizer)?; 87 | let _only_media = super::parse_bool(only_media, false, &localizer)?; 88 | 89 | Ok(rocket::serde::json::Json(vec![])) 90 | } 91 | 92 | #[get("/api/v1/timelines/public?&&&&&&")] 93 | pub async fn timeline_public( 94 | config: &rocket::State, db: crate::DbConn, user: Option, 95 | local: Option<&str>, remote: Option<&str>, only_media: Option<&str>, 96 | max_id: Option, since_id: Option, min_id: Option, limit: Option, 97 | host: &rocket::http::uri::Host<'_>, localizer: crate::i18n::Localizer 98 | ) -> Result>>, super::Error> { 99 | let local = super::parse_bool(local, false, &localizer)?; 100 | let remote = super::parse_bool(remote, false, &localizer)?; 101 | let _only_media = super::parse_bool(only_media, false, &localizer)?; 102 | 103 | if let Some(user) = &user { 104 | if !user.has_scope("read:statuses") { 105 | return Err(super::Error { 106 | code: rocket::http::Status::Forbidden, 107 | error: fl!(localizer, "error-no-permission") 108 | }); 109 | } 110 | } 111 | 112 | let limit = limit.unwrap_or(20); 113 | if limit > 500 { 114 | return Err(super::Error { 115 | code: rocket::http::Status::BadRequest, 116 | error: fl!(localizer, "limit-too-large") 117 | }); 118 | } 119 | 120 | let account = match &user { 121 | Some(u) => Some(super::accounts::get_account(&db, &localizer, u).await?), 122 | None => None 123 | }; 124 | 125 | let statuses: Vec<(crate::models::PublicTimelineEntry, crate::models::Status)> = 126 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 127 | let mut sel = crate::schema::public_timeline::dsl::public_timeline.order_by( 128 | crate::schema::public_timeline::dsl::id.desc() 129 | ).filter( 130 | crate::schema::statuses::dsl::deleted_at.is_null() 131 | ).filter( 132 | crate::schema::statuses::dsl::boost_of_url.is_null() 133 | ).limit(limit as i64).inner_join(crate::schema::statuses::table.on( 134 | crate::schema::statuses::dsl::id.eq(crate::schema::public_timeline::dsl::status_id) 135 | )).into_boxed(); 136 | if let Some(min_id) = min_id { 137 | sel = sel.filter(crate::schema::public_timeline::dsl::id.gt(min_id)); 138 | } 139 | if let Some(max_id) = max_id { 140 | sel = sel.filter(crate::schema::public_timeline::dsl::id.lt(max_id)); 141 | } 142 | if let Some(since_id) = since_id { 143 | sel = sel.filter(crate::schema::public_timeline::dsl::id.gt(since_id)); 144 | } 145 | if local { 146 | sel = sel.filter(crate::schema::statuses::dsl::local.eq(true)); 147 | } 148 | if remote { 149 | sel = sel.filter(crate::schema::statuses::dsl::local.eq(false)); 150 | } 151 | sel.get_results(c) 152 | }).await?; 153 | 154 | let mut links = vec![]; 155 | 156 | if let Some(last_id) = statuses.last().map(|a| a.0.id) { 157 | links.push(super::Link { 158 | rel: "next".to_string(), 159 | href: format!("https://{}/api/v1/timelines/public?max_id={}", host.to_string(), last_id) 160 | }); 161 | } 162 | if let Some(first_id) = statuses.first().map(|a| a.0.id) { 163 | links.push(super::Link { 164 | rel: "prev".to_string(), 165 | href: format!("https://{}/api/v1/timelines/public?min_id={}", host.to_string(), first_id) 166 | }); 167 | } 168 | 169 | Ok(super::LinkedResponse { 170 | inner: rocket::serde::json::Json( 171 | futures::stream::iter(statuses).map(|status| { 172 | super::statuses::render_status(config, &db, status.1, &localizer, account.as_ref()) 173 | }).buffered(10).collect::>().await 174 | .into_iter().collect::, _>>()? 175 | ), 176 | links 177 | }) 178 | } -------------------------------------------------------------------------------- /src/views/web_push.rs: -------------------------------------------------------------------------------- 1 | use crate::AppConfig; 2 | use diesel::prelude::*; 3 | 4 | #[derive(Deserialize)] 5 | pub struct WebPushSubscription { 6 | subscription: WebPushSubscriptionData, 7 | data: WebPushData, 8 | #[serde(default)] 9 | policy: WebPushPolicy, 10 | } 11 | 12 | #[derive(Deserialize)] 13 | pub struct WebPushSubscriptionData { 14 | endpoint: String, 15 | keys: WebPushKeys, 16 | } 17 | 18 | #[derive(Deserialize)] 19 | pub struct WebPushKeys { 20 | p256dh: String, 21 | auth: String, 22 | } 23 | 24 | #[derive(Deserialize)] 25 | pub struct WebPushData { 26 | alerts: super::objs::WebPushAlerts, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub enum WebPushPolicy { 31 | #[serde(rename = "all")] 32 | All, 33 | #[serde(rename = "followed")] 34 | Followed, 35 | #[serde(rename = "follower")] 36 | Follower, 37 | #[serde(rename = "none")] 38 | None, 39 | } 40 | 41 | impl Default for WebPushPolicy { 42 | fn default() -> Self { 43 | Self::All 44 | } 45 | } 46 | 47 | impl ToString for WebPushPolicy { 48 | fn to_string(&self) -> String { 49 | match self { 50 | WebPushPolicy::All => "all".to_string(), 51 | WebPushPolicy::Followed => "followed".to_string(), 52 | WebPushPolicy::Follower => "follower".to_string(), 53 | WebPushPolicy::None => "none".to_string(), 54 | } 55 | } 56 | } 57 | 58 | fn render_subscription( 59 | subscription: crate::models::WebPushSubscription, config: &AppConfig 60 | ) -> rocket::serde::json::Json { 61 | rocket::serde::json::Json(super::objs::WebPushSubscription { 62 | id: subscription.id.to_string(), 63 | endpoint: subscription.endpoint, 64 | alerts: super::objs::WebPushAlerts { 65 | follow: subscription.follow, 66 | favourite: subscription.favourite, 67 | reblog: subscription.reblog, 68 | mention: subscription.mention, 69 | poll: subscription.poll, 70 | status: subscription.status, 71 | follow_request: subscription.follow_request, 72 | update: subscription.update, 73 | admin_sign_up: subscription.admin_sign_up, 74 | admin_report: subscription.admin_report, 75 | }, 76 | server_key: base64::encode_config(config.web_push_signature.get_public_key(), base64::URL_SAFE_NO_PAD), 77 | }) 78 | } 79 | 80 | #[post("/api/v1/push/subscription", data = "")] 81 | pub async fn create_subscription( 82 | db: crate::DbConn, config: &rocket::State, 83 | user: super::oauth::TokenClaims, data: rocket::serde::json::Json, 84 | localizer: crate::i18n::Localizer 85 | ) -> Result, super::Error> { 86 | if !user.has_scope("push") { 87 | return Err(super::Error { 88 | code: rocket::http::Status::Forbidden, 89 | error: fl!(localizer, "error-no-permission") 90 | }); 91 | } 92 | 93 | let account = user.get_account(&db, &localizer).await?; 94 | 95 | let new_subscription = crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 96 | let new_subscription = crate::models::WebPushSubscription { 97 | id: uuid::Uuid::new_v4(), 98 | token_id: user.json_web_token_id, 99 | account_id: account.id, 100 | endpoint: data.subscription.endpoint.clone(), 101 | p256dh: data.subscription.keys.p256dh.clone(), 102 | auth: data.subscription.keys.auth.clone(), 103 | follow: data.data.alerts.follow, 104 | favourite: data.data.alerts.favourite, 105 | reblog: data.data.alerts.reblog, 106 | mention: data.data.alerts.mention, 107 | poll: data.data.alerts.poll, 108 | status: data.data.alerts.status, 109 | follow_request: data.data.alerts.follow_request, 110 | update: data.data.alerts.update, 111 | admin_sign_up: data.data.alerts.admin_sign_up, 112 | admin_report: data.data.alerts.admin_sign_up, 113 | policy: data.policy.to_string(), 114 | }; 115 | 116 | c.transaction(|| -> diesel::result::QueryResult<_> { 117 | diesel::delete(crate::schema::web_push_subscriptions::dsl::web_push_subscriptions.filter( 118 | crate::schema::web_push_subscriptions::dsl::token_id.eq(user.json_web_token_id) 119 | )).execute(c)?; 120 | 121 | diesel::insert_into(crate::schema::web_push_subscriptions::dsl::web_push_subscriptions) 122 | .values(&new_subscription) 123 | .execute(c)?; 124 | 125 | Ok(()) 126 | })?; 127 | 128 | Ok(new_subscription) 129 | }).await?; 130 | 131 | Ok(render_subscription(new_subscription, config)) 132 | } 133 | 134 | #[get("/api/v1/push/subscription")] 135 | pub async fn get_subscription( 136 | db: crate::DbConn, user: super::oauth::TokenClaims, config: &rocket::State, 137 | localizer: crate::i18n::Localizer 138 | ) -> Result, super::Error> { 139 | if !user.has_scope("push") { 140 | return Err(super::Error { 141 | code: rocket::http::Status::Forbidden, 142 | error: fl!(localizer, "error-no-permission") 143 | }); 144 | } 145 | 146 | let subscription: crate::models::WebPushSubscription = crate::db_run(&db, &localizer, move |c| -> diesel::result::QueryResult<_> { 147 | crate::schema::web_push_subscriptions::dsl::web_push_subscriptions.filter( 148 | crate::schema::web_push_subscriptions::dsl::token_id.eq(user.json_web_token_id) 149 | ).get_result(c) 150 | }).await?; 151 | 152 | 153 | Ok(render_subscription(subscription, config)) 154 | } 155 | 156 | #[derive(Deserialize)] 157 | pub struct WebPushSubscriptionUpdate { 158 | data: WebPushData, 159 | policy: Option, 160 | } 161 | 162 | #[put("/api/v1/push/subscription", data = "")] 163 | pub async fn update_subscription( 164 | db: crate::DbConn, user: super::oauth::TokenClaims, config: &rocket::State, 165 | data: rocket::serde::json::Json, localizer: crate::i18n::Localizer 166 | ) -> Result, super::Error> { 167 | if !user.has_scope("push") { 168 | return Err(super::Error { 169 | code: rocket::http::Status::Forbidden, 170 | error: fl!(localizer, "error-no-permission") 171 | }); 172 | } 173 | 174 | use crate::schema::web_push_subscriptions; 175 | #[derive(AsChangeset)] 176 | #[table_name="web_push_subscriptions"] 177 | pub struct WebPushSubscriptionUpdate { 178 | follow: bool, 179 | favourite: bool, 180 | reblog: bool, 181 | mention: bool, 182 | poll: bool, 183 | status: bool, 184 | follow_request: bool, 185 | update: bool, 186 | admin_sign_up: bool, 187 | admin_report: bool, 188 | policy: Option, 189 | } 190 | 191 | let subscription: crate::models::WebPushSubscription = crate::db_run(&db, &localizer, move |c| -> diesel::result::QueryResult<_> { 192 | diesel::update(crate::schema::web_push_subscriptions::dsl::web_push_subscriptions.filter( 193 | crate::schema::web_push_subscriptions::dsl::token_id.eq(user.json_web_token_id) 194 | )) 195 | .set(WebPushSubscriptionUpdate { 196 | policy: data.policy.as_ref().map(|p| p.to_string()), 197 | follow: data.data.alerts.follow, 198 | favourite: data.data.alerts.favourite, 199 | reblog: data.data.alerts.reblog, 200 | mention: data.data.alerts.mention, 201 | poll: data.data.alerts.poll, 202 | status: data.data.alerts.status, 203 | follow_request: data.data.alerts.follow_request, 204 | update: data.data.alerts.update, 205 | admin_sign_up: data.data.alerts.admin_sign_up, 206 | admin_report: data.data.alerts.admin_report, 207 | }) 208 | .get_result(c) 209 | }).await?; 210 | 211 | 212 | Ok(render_subscription(subscription, config)) 213 | } 214 | 215 | #[delete("/api/v1/push/subscription")] 216 | pub async fn delete_subscription( 217 | db: crate::DbConn, user: super::oauth::TokenClaims, localizer: crate::i18n::Localizer 218 | ) -> Result, super::Error> { 219 | if !user.has_scope("push") { 220 | return Err(super::Error { 221 | code: rocket::http::Status::Forbidden, 222 | error: fl!(localizer, "error-no-permission") 223 | }); 224 | } 225 | 226 | crate::db_run(&db, &localizer, move |c| -> QueryResult<_> { 227 | diesel::delete(crate::schema::web_push_subscriptions::dsl::web_push_subscriptions.filter( 228 | crate::schema::web_push_subscriptions::dsl::token_id.eq(user.json_web_token_id) 229 | )).execute(c) 230 | }).await?; 231 | 232 | Ok(rocket::serde::json::Json(())) 233 | } -------------------------------------------------------------------------------- /static/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheEnbyperor/tafarn/916398174296c28405d2ed256c3d4987f9ce1805/static/header.png -------------------------------------------------------------------------------- /static/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheEnbyperor/tafarn/916398174296c28405d2ed256c3d4987f9ce1805/static/missing.png -------------------------------------------------------------------------------- /templates/base.html.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tafarn 9 | 10 | 11 | 27 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /templates/host-meta.tera: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/oauth-consent.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block content %} 3 |

{{ fl(id="oauth-login-to", lang=lang, name=name) }}

4 |

5 | {{ fl(id="website", lang=lang) }}: {{ website }} 6 |

7 | 8 |

9 | {{ fl(id="oauth-consent", lang=lang, name=name) }} 10 |

11 | 12 |
    13 | {% for scope in scopes %} 14 |
  • {{ fl(id=scope.description, lang=lang) }}
  • 15 | {% endfor %} 16 |
17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /templates/oauth-error.html.tera: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block content %} 3 |

{{ fl(id="invalid-request", lang=lang) }}

4 | 5 |
6 | {{ message }} 7 |
8 | {% endblock %} --------------------------------------------------------------------------------