A modified version of Wafrn used on https://wf.jbc.lol (mirror of https://git.jbc.lol/jbcrn/wf.jbc.lol which is a mirror of https://codeberg.org/jbcarreon123/wf.jbc.lol)

Compare changes

Choose any two refs to compare.

Changed files
+2380 -1193
.vscode
cache
docs
install
logs
packages
-8
.env.example
··· 54 POSTGRES_METRICS_DBNAME=pgwatch_metrics 55 GF_SECURITY_ADMIN_PASSWORD= 56 57 - # OpenID Connect auth 58 - # Only do this if you know what you are doing! See docs/openid.md for more info 59 - OIDC_ENABLED=false 60 - OIDC_ISSUER=https://auth.localhost 61 - OIDC_CLIENT_ID=wafrn 62 - OIDC_CLIENT_SECRET=secret 63 - OIDC_AUTH_NAME=OpenID 64 - 65 # Debugging 66 ENABLE_RAW_OUTPUT=false
··· 54 POSTGRES_METRICS_DBNAME=pgwatch_metrics 55 GF_SECURITY_ADMIN_PASSWORD= 56 57 # Debugging 58 ENABLE_RAW_OUTPUT=false
+3 -3
.gitignore
··· 8 /tmp 9 /out-tsc 10 11 12 # Only exists if Bazel was run 13 /bazel-out ··· 51 .lite_workspace.lua 52 53 # default docker compose 54 - nohup.out 55 - mydockersimages.list 56 - pds.env
··· 8 /tmp 9 /out-tsc 10 11 + # custom caddy config 12 + packages/caddy 13 14 # Only exists if Bazel was run 15 /bazel-out ··· 53 .lite_workspace.lua 54 55 # default docker compose 56 + docker-compose.yml
+1 -1
.vscode/settings.json
··· 1 { 2 - // "editor.formatOnSave": true 3 }
··· 1 { 2 + "editor.formatOnSave": true 3 }
cache/.gitkeep

This is a binary file and will not be displayed.

+296
docker-compose.advanced.metrics.yml
···
··· 1 + services: 2 + backend: &default_backend 3 + build: &default_backend_build 4 + context: . 5 + dockerfile: packages/backend/Dockerfile 6 + # these args configure private env vars for the backend and public env vars for the frontend 7 + depends_on: 8 + db: 9 + condition: service_healthy 10 + redis: 11 + condition: service_started 12 + frontend: 13 + condition: service_started 14 + migration: 15 + condition: service_completed_successfully 16 + restart: unless-stopped 17 + environment: &default_backend_env_vars 18 + NODE_ENV: production 19 + ADMIN_USER: ${ADMIN_USER} 20 + ADMIN_EMAIL: ${ADMIN_EMAIL} 21 + ADMIN_PASSWORD: ${ADMIN_PASSWORD} 22 + JWT_SECRET: ${JWT_SECRET} 23 + DOMAIN_NAME: ${DOMAIN_NAME} 24 + 25 + CACHE_DOMAIN: ${CACHE_DOMAIN} 26 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 27 + 28 + SMTP_HOST: ${SMTP_HOST} 29 + SMTP_USER: ${SMTP_USER} 30 + SMTP_PORT: ${SMTP_PORT} 31 + SMTP_PASSWORD: ${SMTP_PASSWORD} 32 + SMTP_FROM: ${SMTP_FROM} 33 + 34 + POSTGRES_USER: ${POSTGRES_USER} 35 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 36 + POSTGRES_DBNAME: ${POSTGRES_DBNAME} 37 + 38 + WEBPUSH_EMAIL: ${WEBPUSH_EMAIL} 39 + WEBPUSH_PRIVATE: ${WEBPUSH_PRIVATE} 40 + WEBPUSH_PUBLIC: ${WEBPUSH_PUBLIC} 41 + 42 + ENABLE_BSKY: ${ENABLE_BSKY} 43 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 44 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 45 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 46 + 47 + USE_WORKERS: false 48 + LOG_SQL_QUERIES: ${LOG_SQL_QUERIES:-} 49 + UPLOAD_LIMIT: ${UPLOAD_LIMIT:-} 50 + POSTS_PER_PAGE: ${POSTS_PER_PAGE:-} 51 + LOG_LEVEL: ${LOG_LEVEL:-} 52 + BLOCKLIST_URI: ${BLOCKLIST_URI:-} 53 + FRONTEND_PATH: ${FRONTEND_PATH:-} 54 + DISABLE_REQUIRE_SEND_EMAIL: ${DISABLE_REQUIRE_SEND_EMAIL:-} 55 + BLOCKED_IPS: ${BLOCKED_IPS:-} 56 + REVIEW_REGISTRATIONS: ${REVIEW_REGISTRATIONS:-} 57 + IGNORE_BLOCK_HOSTS: ${IGNORE_BLOCK_HOSTS:-} 58 + 59 + FRONTEND_LOGO: ${FRONTEND_LOGO:-} 60 + FRONTEND_API_URL: ${FRONTEND_API_URL:-} 61 + FRONTEND_MEDIA_URL: ${FRONTEND_MEDIA_URL:-} 62 + FRONTEND_CACHE_URL: ${FRONTEND_CACHE_URL:-} 63 + FRONTEND_CACHE_BACKUP_URLS: ${FRONTEND_CACHE_BACKUP_URLS:-} 64 + FRONTEND_SHORTEN_POSTS: ${FRONTEND_SHORTEN_POSTS:-} 65 + FRONTEND_DISABLE_PWA: ${FRONTEND_DISABLE_PWA:-} 66 + FRONTEND_MAINTENANCE: ${FRONTEND_MAINTENANCE:-} 67 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 68 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 69 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 70 + 71 + FRONTEND_FQDN_URL: https://${DOMAIN_NAME} 72 + 73 + ENABLE_RAW_OUTPUT: ${ENABLE_RAW_OUTPUT:-} 74 + deploy: 75 + mode: replicated 76 + replicas: 3 77 + volumes: 78 + - ./packages/backend/uploads:/app/packages/backend/uploads 79 + - ./packages/backend/cache:/app/packages/backend/cache 80 + - frontend:/app/packages/frontend:ro 81 + 82 + migration: 83 + <<: *default_backend 84 + depends_on: 85 + db: 86 + condition: service_healthy 87 + redis: 88 + condition: service_started 89 + frontend: 90 + condition: service_started 91 + restart: no 92 + deploy: 93 + mode: replicated 94 + replicas: 1 95 + command: "npm exec tsx migrate.ts init-container" 96 + 97 + frontend: 98 + restart: unless-stopped 99 + build: 100 + context: . 101 + dockerfile: packages/frontend/Dockerfile 102 + ports: 103 + - 80:80 104 + - 443:443 105 + environment: 106 + DOMAIN_NAME: ${DOMAIN_NAME} 107 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 108 + CACHE_DOMAIN: ${CACHE_DOMAIN} 109 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 110 + ACME_EMAIL: ${ACME_EMAIL} 111 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 112 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 113 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 114 + CACHE_HOST: "cache:9000" 115 + BACKEND_HOST: "wafrn-backend-1:9000 wafrn-backend-2:9000 wafrn-backend-3:9000" 116 + WEBSOCKET_HOST: "wafrn-websocket-1:9000" 117 + 118 + volumes: 119 + - "caddy:/data" 120 + - "frontend:/var/www/html/frontend" 121 + - ./packages/backend/uploads:/var/www/html/uploads 122 + - ./packages/caddy:/etc/caddy/config 123 + 124 + db: 125 + build: 126 + context: monitoring/database 127 + dockerfile: Dockerfile 128 + restart: unless-stopped 129 + shm_size: '2gb' 130 + environment: 131 + POSTGRES_USER: ${POSTGRES_USER} 132 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 133 + POSTGRES_DB: ${POSTGRES_DBNAME} 134 + POSTGRES_METRICS_USER: ${POSTGRES_METRICS_USER} 135 + POSTGRES_METRICS_PASSWORD: ${POSTGRES_METRICS_PASSWORD} 136 + POSTGRES_METRICS_DBNAME: ${POSTGRES_METRICS_DBNAME} 137 + volumes: 138 + - dbpg:/var/lib/postgresql/data 139 + 140 + adminer: 141 + image: adminer 142 + restart: unless-stopped 143 + 144 + redis: 145 + image: redis:7.2.4 146 + restart: unless-stopped 147 + volumes: 148 + - redis:/data 149 + 150 + pds: 151 + image: ghcr.io/bluesky-social/pds:0.4 152 + restart: unless-stopped 153 + profiles: 154 + - bluesky 155 + environment: 156 + PDS_HOSTNAME: ${PDS_DOMAIN_NAME} 157 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 158 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 159 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} 160 + PDS_DATA_DIRECTORY: /pds 161 + PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks 162 + PDS_BLOB_UPLOAD_LIMIT: 52428800 163 + PDS_DID_PLC_URL: "https://plc.directory" 164 + PDS_BSKY_APP_VIEW_URL: "https://api.bsky.app" 165 + PDS_BSKY_APP_VIEW_DID: "did:web:api.bsky.app" 166 + PDS_REPORT_SERVICE_URL: "https://mod.bsky.app" 167 + PDS_REPORT_SERVICE_DID: "did:plc:ar7c4by46qjdydhdevvrndac" 168 + PDS_CRAWLERS: "https://bsky.network, https://atproto.africa" 169 + PDS_EMAIL_SMTP_URL: "smtp://${SMTP_USER}:${SMTP_PASSWORD}@${SMTP_HOST}:${SMTP_PORT}" 170 + PDS_EMAIL_FROM_ADDRESS: "${SMTP_FROM}" 171 + LOG_ENABLED: true 172 + volumes: 173 + - pds:/pds 174 + 175 + pds_worker: 176 + <<: *default_backend 177 + deploy: 178 + mode: replicated 179 + replicas: 1 180 + profiles: 181 + - bluesky 182 + command: "npm exec tsx atproto.ts" 183 + 184 + cache: 185 + <<: *default_backend 186 + deploy: 187 + mode: replicated 188 + replicas: 1 189 + websocket: 190 + <<: *default_backend 191 + deploy: 192 + mode: replicated 193 + replicas: 1 194 + command: "npm exec tsx websocket.ts" 195 + 196 + workers: 197 + <<: *default_backend 198 + build: 199 + <<: *default_backend_build 200 + environment: 201 + <<: *default_backend_env_vars 202 + USE_WORKERS: true 203 + deploy: 204 + mode: replicated 205 + replicas: 3 206 + 207 + prometheus: 208 + restart: unless-stopped 209 + image: prom/prometheus:latest 210 + volumes: 211 + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml 212 + - prometheus_data:/prometheus 213 + command: 214 + - '--config.file=/etc/prometheus/prometheus.yml' 215 + - '--storage.tsdb.path=/prometheus' 216 + - '--web.console.libraries=/usr/share/prometheus/console_libraries' 217 + - '--web.console.templates=/usr/share/prometheus/consoles' 218 + 219 + cadvisor: 220 + restart: unless-stopped 221 + image: gcr.io/cadvisor/cadvisor:latest 222 + command: 223 + - '-port=8081' 224 + environment: 225 + CADVISOR_HEALTHCHECK_URL: http://localhost:8081/healthz 226 + volumes: 227 + - /:/rootfs:ro 228 + - /var/run:/var/run:rw 229 + - /sys:/sys:ro 230 + - /var/lib/docker/:/var/lib/docker:ro 231 + 232 + node-exporter: 233 + restart: unless-stopped 234 + image: prom/node-exporter:latest 235 + volumes: 236 + - /proc:/host/proc:ro 237 + - /sys:/host/sys:ro 238 + - /:/rootfs:ro 239 + command: 240 + - '--path.procfs=/host/proc' 241 + - '--path.sysfs=/host/sys' 242 + - '--collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"' 243 + 244 + grafana: 245 + build: 246 + context: monitoring/grafana 247 + dockerfile: Dockerfile 248 + volumes: 249 + - grafana_data:/var/lib/grafana 250 + restart: unless-stopped 251 + environment: 252 + GF_SERVER_HTTP_PORT: 2345 253 + GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD} 254 + GF_USERS_ALLOW_SIGN_UP: false 255 + 256 + GF_SMTP_ENABLED: true 257 + GF_SMTP_HOST: ${SMTP_HOST}:${SMTP_PORT} 258 + GF_SMTP_FROM_ADDRESS: ${SMTP_FROM} 259 + GF_SERVER_DOMAIN: ${DOMAIN_NAME} 260 + GF_SMTP_FROM_NAME: ${SMTP_FROM} 261 + GF_SMTP_USER: "${SMTP_USER}" 262 + GF_SMTP_PASSWORD: "${SMTP_PASSWORD}" 263 + 264 + POSTGRES_METRICS_USER: ${POSTGRES_METRICS_USER} 265 + POSTGRES_METRICS_PASSWORD: ${POSTGRES_METRICS_PASSWORD} 266 + POSTGRES_METRICS_DBNAME: ${POSTGRES_METRICS_DBNAME} 267 + 268 + pgwatch: 269 + build: 270 + context: monitoring/pgwatch 271 + dockerfile: Dockerfile 272 + args: 273 + POSTGRES_USER: ${POSTGRES_USER} 274 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 275 + POSTGRES_DB: ${POSTGRES_DBNAME} 276 + restart: unless-stopped 277 + environment: 278 + POSTGRES_METRICS_USER: ${POSTGRES_METRICS_USER} 279 + POSTGRES_METRICS_PASSWORD: ${POSTGRES_METRICS_PASSWORD} 280 + POSTGRES_METRICS_DBNAME: ${POSTGRES_METRICS_DBNAME} 281 + command: 282 + - "--web-disable=all" 283 + - "--sources=/sources.yaml" 284 + - "--sink=postgresql://${POSTGRES_METRICS_USER}:${POSTGRES_METRICS_PASSWORD}@db:5432/${POSTGRES_METRICS_DBNAME}" 285 + depends_on: 286 + db: 287 + condition: service_healthy 288 + 289 + volumes: 290 + dbpg: 291 + caddy: 292 + pds: 293 + frontend: 294 + redis: 295 + prometheus_data: 296 + grafana_data:
+207
docker-compose.advanced.yml
···
··· 1 + services: 2 + backend: &default_backend 3 + build: &default_backend_build 4 + context: . 5 + dockerfile: packages/backend/Dockerfile 6 + # these args configure private env vars for the backend and public env vars for the frontend 7 + depends_on: 8 + db: 9 + condition: service_healthy 10 + redis: 11 + condition: service_started 12 + frontend: 13 + condition: service_started 14 + migration: 15 + condition: service_completed_successfully 16 + restart: unless-stopped 17 + environment: &default_backend_env_vars 18 + NODE_ENV: production 19 + ADMIN_USER: ${ADMIN_USER} 20 + ADMIN_EMAIL: ${ADMIN_EMAIL} 21 + ADMIN_PASSWORD: ${ADMIN_PASSWORD} 22 + JWT_SECRET: ${JWT_SECRET} 23 + DOMAIN_NAME: ${DOMAIN_NAME} 24 + 25 + CACHE_DOMAIN: ${CACHE_DOMAIN} 26 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 27 + 28 + SMTP_HOST: ${SMTP_HOST} 29 + SMTP_USER: ${SMTP_USER} 30 + SMTP_PORT: ${SMTP_PORT} 31 + SMTP_PASSWORD: ${SMTP_PASSWORD} 32 + SMTP_FROM: ${SMTP_FROM} 33 + 34 + POSTGRES_USER: ${POSTGRES_USER} 35 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 36 + POSTGRES_DBNAME: ${POSTGRES_DBNAME} 37 + 38 + WEBPUSH_EMAIL: ${WEBPUSH_EMAIL} 39 + WEBPUSH_PRIVATE: ${WEBPUSH_PRIVATE} 40 + WEBPUSH_PUBLIC: ${WEBPUSH_PUBLIC} 41 + 42 + ENABLE_BSKY: ${ENABLE_BSKY} 43 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 44 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 45 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 46 + 47 + USE_WORKERS: false 48 + LOG_SQL_QUERIES: ${LOG_SQL_QUERIES:-} 49 + UPLOAD_LIMIT: ${UPLOAD_LIMIT:-} 50 + POSTS_PER_PAGE: ${POSTS_PER_PAGE:-} 51 + LOG_LEVEL: ${LOG_LEVEL:-} 52 + BLOCKLIST_URI: ${BLOCKLIST_URI:-} 53 + FRONTEND_PATH: ${FRONTEND_PATH:-} 54 + DISABLE_REQUIRE_SEND_EMAIL: ${DISABLE_REQUIRE_SEND_EMAIL:-} 55 + BLOCKED_IPS: ${BLOCKED_IPS:-} 56 + REVIEW_REGISTRATIONS: ${REVIEW_REGISTRATIONS:-} 57 + IGNORE_BLOCK_HOSTS: ${IGNORE_BLOCK_HOSTS:-} 58 + 59 + FRONTEND_LOGO: ${FRONTEND_LOGO:-} 60 + FRONTEND_API_URL: ${FRONTEND_API_URL:-} 61 + FRONTEND_MEDIA_URL: ${FRONTEND_MEDIA_URL:-} 62 + FRONTEND_CACHE_URL: ${FRONTEND_CACHE_URL:-} 63 + FRONTEND_CACHE_BACKUP_URLS: ${FRONTEND_CACHE_BACKUP_URLS:-} 64 + FRONTEND_SHORTEN_POSTS: ${FRONTEND_SHORTEN_POSTS:-} 65 + FRONTEND_DISABLE_PWA: ${FRONTEND_DISABLE_PWA:-} 66 + FRONTEND_MAINTENANCE: ${FRONTEND_MAINTENANCE:-} 67 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 68 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 69 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 70 + 71 + FRONTEND_FQDN_URL: https://${DOMAIN_NAME} 72 + 73 + ENABLE_RAW_OUTPUT: ${ENABLE_RAW_OUTPUT:-} 74 + deploy: 75 + mode: replicated 76 + replicas: 3 77 + volumes: 78 + - ./packages/backend/uploads:/app/packages/backend/uploads 79 + - ./packages/backend/cache:/app/packages/backend/cache 80 + - frontend:/app/packages/frontend:ro 81 + 82 + migration: 83 + <<: *default_backend 84 + depends_on: 85 + db: 86 + condition: service_started 87 + redis: 88 + condition: service_started 89 + frontend: 90 + condition: service_started 91 + restart: no 92 + deploy: 93 + mode: replicated 94 + replicas: 1 95 + command: "npm exec tsx migrate.ts init-container" 96 + 97 + frontend: 98 + restart: unless-stopped 99 + build: 100 + context: . 101 + dockerfile: packages/frontend/Dockerfile 102 + ports: 103 + - 80:80 104 + - 443:443 105 + environment: 106 + DOMAIN_NAME: ${DOMAIN_NAME} 107 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 108 + CACHE_DOMAIN: ${CACHE_DOMAIN} 109 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 110 + ACME_EMAIL: ${ACME_EMAIL} 111 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 112 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 113 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 114 + CACHE_HOST: "cache:9000" 115 + BACKEND_HOST: "wafrn-backend-1:9000 wafrn-backend-2:9000 wafrn-backend-3:9000" 116 + WEBSOCKET_HOST: "wafrn-websocket-1:9000" 117 + volumes: 118 + - "caddy:/data" 119 + - "frontend:/var/www/html/frontend" 120 + - ./packages/backend/uploads:/var/www/html/uploads 121 + - ./packages/caddy:/etc/caddy/config 122 + 123 + db: 124 + image: postgres:17 125 + restart: unless-stopped 126 + shm_size: '2gb' 127 + environment: 128 + POSTGRES_USER: ${POSTGRES_USER} 129 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 130 + POSTGRES_DB: ${POSTGRES_DBNAME} 131 + volumes: 132 + - dbpg:/var/lib/postgresql/data 133 + 134 + adminer: 135 + image: adminer 136 + restart: unless-stopped 137 + 138 + redis: 139 + image: redis:7.2.4 140 + restart: unless-stopped 141 + volumes: 142 + - redis:/data 143 + 144 + pds: 145 + image: ghcr.io/bluesky-social/pds:0.4 146 + restart: unless-stopped 147 + profiles: 148 + - bluesky 149 + environment: 150 + PDS_HOSTNAME: ${PDS_DOMAIN_NAME} 151 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 152 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 153 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} 154 + PDS_DATA_DIRECTORY: /pds 155 + PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks 156 + PDS_BLOB_UPLOAD_LIMIT: 52428800 157 + PDS_DID_PLC_URL: "https://plc.directory" 158 + PDS_BSKY_APP_VIEW_URL: "https://api.bsky.app" 159 + PDS_BSKY_APP_VIEW_DID: "did:web:api.bsky.app" 160 + PDS_REPORT_SERVICE_URL: "https://mod.bsky.app" 161 + PDS_REPORT_SERVICE_DID: "did:plc:ar7c4by46qjdydhdevvrndac" 162 + PDS_CRAWLERS: "https://bsky.network, https://atproto.africa" 163 + PDS_EMAIL_SMTP_URL: "smtp://${SMTP_USER}:${SMTP_PASSWORD}@${SMTP_HOST}:${SMTP_PORT}" 164 + PDS_EMAIL_FROM_ADDRESS: "${SMTP_FROM}" 165 + LOG_ENABLED: true 166 + volumes: 167 + - pds:/pds 168 + 169 + pds_worker: 170 + <<: *default_backend 171 + profiles: 172 + - bluesky 173 + deploy: 174 + mode: replicated 175 + replicas: 1 176 + command: "npm exec tsx atproto.ts" 177 + 178 + cache: 179 + <<: *default_backend 180 + deploy: 181 + mode: replicated 182 + replicas: 1 183 + 184 + websocket: 185 + <<: *default_backend 186 + deploy: 187 + mode: replicated 188 + replicas: 1 189 + command: "npm exec tsx websocket.ts" 190 + 191 + workers: 192 + <<: *default_backend 193 + build: 194 + <<: *default_backend_build 195 + environment: 196 + <<: *default_backend_env_vars 197 + USE_WORKERS: true 198 + deploy: 199 + mode: replicated 200 + replicas: 3 201 + 202 + volumes: 203 + dbpg: 204 + caddy: 205 + pds: 206 + frontend: 207 + redis:
+44
docker-compose.local.yml
···
··· 1 + services: 2 + frontend: 3 + image: caddy:2 4 + restart: unless-stopped 5 + command: caddy run --config ${PWD}/packages/frontend/Caddyfile --adapter caddyfile 6 + ports: 7 + - 80:80 8 + - 443:443 9 + - 2019:2019 10 + volumes: 11 + - "caddy:/data" 12 + - ${PWD}/packages:${PWD}/packages 13 + 14 + db: 15 + image: postgres:17 16 + restart: unless-stopped 17 + shm_size: '2gb' 18 + environment: 19 + POSTGRES_USER: postgres 20 + POSTGRES_PASSWORD: root 21 + POSTGRES_DB: wafrn 22 + ports: 23 + - 5432:5432 24 + volumes: 25 + - dbpg:/var/lib/postgresql/data 26 + 27 + adminer: 28 + image: adminer 29 + restart: unless-stopped 30 + 31 + redis: 32 + image: redis:7.2.4 33 + restart: unless-stopped 34 + ports: 35 + - 6379:6379 36 + volumes: 37 + - redis:/data 38 + 39 + volumes: 40 + dbpg: 41 + caddy: 42 + pds: 43 + frontend: 44 + redis:
+34
docker-compose.localBackendDebuggerDev.yml
···
··· 1 + # Ok this is a compose that does not include nor frontend nor backend. 2 + services: 3 + db: 4 + image: postgres:17 5 + restart: unless-stopped 6 + environment: 7 + POSTGRES_USER: root 8 + PGUSER: root 9 + POSTGRES_PASSWORD: root 10 + POSTGRES_DB: wafrn 11 + volumes: 12 + - dbpg:/var/lib/postgresql/data 13 + ports: 14 + - "5432:5432" 15 + 16 + adminer: 17 + image: adminer 18 + restart: unless-stopped 19 + ports: 20 + - "8080:8080" 21 + 22 + redis: 23 + image: redis:7.2.4 24 + restart: unless-stopped 25 + volumes: 26 + - redis:/data 27 + ports: 28 + - "8001:8001" 29 + - "8070:8070" 30 + - "6379:6379" 31 + 32 + volumes: 33 + dbpg: 34 + redis:
+33
docker-compose.localBackendDevelopment.yml
···
··· 1 + services: 2 + db: 3 + image: postgres:17 4 + restart: unless-stopped 5 + shm_size: '2gb' 6 + environment: 7 + POSTGRES_USER: root 8 + POSTGRES_PASSWORD: root 9 + POSTGRES_DB: wafrn 10 + volumes: 11 + - dbpg:/var/lib/postgresql/data 12 + ports: 13 + - "5432:5432" 14 + 15 + adminer: 16 + image: adminer 17 + restart: unless-stopped 18 + ports: 19 + - "8080:8080" 20 + 21 + redis: 22 + image: redis:7.2.4 23 + restart: unless-stopped 24 + volumes: 25 + - redis:/data 26 + ports: 27 + - "8001:8001" 28 + - "8070:8070" 29 + - "6379:6379" 30 + 31 + volumes: 32 + dbpg: 33 + redis:
+259
docker-compose.simple.metrics.yml
···
··· 1 + services: 2 + backend: &default_backend 3 + build: 4 + context: . 5 + dockerfile: packages/backend/Dockerfile 6 + depends_on: 7 + db: 8 + condition: service_healthy 9 + redis: 10 + condition: service_started 11 + frontend: 12 + condition: service_started 13 + migration: 14 + condition: service_completed_successfully 15 + restart: unless-stopped 16 + environment: 17 + NODE_ENV: production 18 + ADMIN_USER: ${ADMIN_USER} 19 + ADMIN_EMAIL: ${ADMIN_EMAIL} 20 + ADMIN_PASSWORD: ${ADMIN_PASSWORD} 21 + JWT_SECRET: ${JWT_SECRET} 22 + DOMAIN_NAME: ${DOMAIN_NAME} 23 + 24 + CACHE_DOMAIN: ${CACHE_DOMAIN} 25 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 26 + 27 + SMTP_HOST: ${SMTP_HOST} 28 + SMTP_USER: ${SMTP_USER} 29 + SMTP_PORT: ${SMTP_PORT} 30 + SMTP_PASSWORD: ${SMTP_PASSWORD} 31 + SMTP_FROM: ${SMTP_FROM} 32 + 33 + POSTGRES_USER: ${POSTGRES_USER} 34 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 35 + POSTGRES_DBNAME: ${POSTGRES_DBNAME} 36 + 37 + WEBPUSH_EMAIL: ${WEBPUSH_EMAIL} 38 + WEBPUSH_PRIVATE: ${WEBPUSH_PRIVATE} 39 + WEBPUSH_PUBLIC: ${WEBPUSH_PUBLIC} 40 + 41 + ENABLE_BSKY: ${ENABLE_BSKY} 42 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 43 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 44 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 45 + 46 + USE_WORKERS: true 47 + LOG_SQL_QUERIES: ${LOG_SQL_QUERIES:-} 48 + UPLOAD_LIMIT: ${UPLOAD_LIMIT:-} 49 + POSTS_PER_PAGE: ${POSTS_PER_PAGE:-} 50 + LOG_LEVEL: ${LOG_LEVEL:-} 51 + BLOCKLIST_URI: ${BLOCKLIST_URI:-} 52 + FRONTEND_PATH: ${FRONTEND_PATH:-} 53 + DISABLE_REQUIRE_SEND_EMAIL: ${DISABLE_REQUIRE_SEND_EMAIL:-} 54 + BLOCKED_IPS: ${BLOCKED_IPS:-} 55 + REVIEW_REGISTRATIONS: ${REVIEW_REGISTRATIONS:-} 56 + IGNORE_BLOCK_HOSTS: ${IGNORE_BLOCK_HOSTS:-} 57 + 58 + FRONTEND_LOGO: ${FRONTEND_LOGO:-} 59 + FRONTEND_API_URL: ${FRONTEND_API_URL:-} 60 + FRONTEND_MEDIA_URL: ${FRONTEND_MEDIA_URL:-} 61 + FRONTEND_CACHE_URL: ${FRONTEND_CACHE_URL:-} 62 + FRONTEND_CACHE_BACKUP_URLS: ${FRONTEND_CACHE_BACKUP_URLS:-} 63 + FRONTEND_SHORTEN_POSTS: ${FRONTEND_SHORTEN_POSTS:-} 64 + FRONTEND_DISABLE_PWA: ${FRONTEND_DISABLE_PWA:-} 65 + FRONTEND_MAINTENANCE: ${FRONTEND_MAINTENANCE:-} 66 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 67 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 68 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 69 + 70 + FRONTEND_FQDN_URL: https://${DOMAIN_NAME} 71 + 72 + ENABLE_RAW_OUTPUT: ${ENABLE_RAW_OUTPUT:-} 73 + volumes: 74 + - ./packages/backend/uploads:/app/packages/backend/uploads 75 + - ./packages/backend/cache:/app/packages/backend/cache 76 + - frontend:/app/packages/frontend:ro 77 + 78 + migration: 79 + <<: *default_backend 80 + depends_on: 81 + db: 82 + condition: service_healthy 83 + redis: 84 + condition: service_started 85 + frontend: 86 + condition: service_started 87 + restart: no 88 + command: "npm exec tsx migrate.ts init-container" 89 + 90 + frontend: 91 + restart: unless-stopped 92 + build: 93 + context: . 94 + dockerfile: packages/frontend/Dockerfile 95 + ports: 96 + - 80:80 97 + - 443:443 98 + environment: 99 + DOMAIN_NAME: ${DOMAIN_NAME} 100 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 101 + CACHE_DOMAIN: ${CACHE_DOMAIN} 102 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 103 + ACME_EMAIL: ${ACME_EMAIL} 104 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 105 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 106 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 107 + volumes: 108 + - "caddy:/data" 109 + - "frontend:/var/www/html/frontend" 110 + - ./packages/backend/uploads:/var/www/html/uploads 111 + - ./packages/caddy:/etc/caddy/config 112 + 113 + db: 114 + build: 115 + context: monitoring/database 116 + dockerfile: Dockerfile 117 + restart: unless-stopped 118 + shm_size: '2gb' 119 + environment: 120 + POSTGRES_USER: ${POSTGRES_USER} 121 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 122 + POSTGRES_DB: ${POSTGRES_DBNAME} 123 + POSTGRES_METRICS_USER: ${POSTGRES_METRICS_USER} 124 + POSTGRES_METRICS_PASSWORD: ${POSTGRES_METRICS_PASSWORD} 125 + POSTGRES_METRICS_DBNAME: ${POSTGRES_METRICS_DBNAME} 126 + volumes: 127 + - dbpg:/var/lib/postgresql/data 128 + 129 + adminer: 130 + image: adminer 131 + restart: unless-stopped 132 + 133 + redis: 134 + image: redis:7.2.4 135 + restart: unless-stopped 136 + volumes: 137 + - redis:/data 138 + 139 + pds: 140 + image: ghcr.io/bluesky-social/pds:0.4 141 + restart: unless-stopped 142 + profiles: 143 + - bluesky 144 + environment: 145 + PDS_HOSTNAME: ${PDS_DOMAIN_NAME} 146 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 147 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 148 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} 149 + PDS_DATA_DIRECTORY: /pds 150 + PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks 151 + PDS_BLOB_UPLOAD_LIMIT: 52428800 152 + PDS_DID_PLC_URL: "https://plc.directory" 153 + PDS_BSKY_APP_VIEW_URL: "https://api.bsky.app" 154 + PDS_BSKY_APP_VIEW_DID: "did:web:api.bsky.app" 155 + PDS_REPORT_SERVICE_URL: "https://mod.bsky.app" 156 + PDS_REPORT_SERVICE_DID: "did:plc:ar7c4by46qjdydhdevvrndac" 157 + PDS_CRAWLERS: "https://bsky.network, https://atproto.africa" 158 + PDS_EMAIL_SMTP_URL: "smtp://${SMTP_USER}:${SMTP_PASSWORD}@${SMTP_HOST}:${SMTP_PORT}" 159 + PDS_EMAIL_FROM_ADDRESS: "${SMTP_FROM}" 160 + LOG_ENABLED: true 161 + volumes: 162 + - pds:/pds 163 + 164 + pds_worker: 165 + <<: *default_backend 166 + profiles: 167 + - bluesky 168 + command: "npm exec tsx atproto.ts" 169 + 170 + prometheus: 171 + restart: unless-stopped 172 + image: prom/prometheus:latest 173 + volumes: 174 + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml 175 + - prometheus_data:/prometheus 176 + command: 177 + - '--config.file=/etc/prometheus/prometheus.yml' 178 + - '--storage.tsdb.path=/prometheus' 179 + - '--web.console.libraries=/usr/share/prometheus/console_libraries' 180 + - '--web.console.templates=/usr/share/prometheus/consoles' 181 + 182 + cadvisor: 183 + restart: unless-stopped 184 + image: gcr.io/cadvisor/cadvisor:latest 185 + command: 186 + - '-port=8081' 187 + environment: 188 + CADVISOR_HEALTHCHECK_URL: http://localhost:8081/healthz 189 + volumes: 190 + - /:/rootfs:ro 191 + - /var/run:/var/run:rw 192 + - /sys:/sys:ro 193 + - /var/lib/docker/:/var/lib/docker:ro 194 + 195 + node-exporter: 196 + restart: unless-stopped 197 + image: prom/node-exporter:latest 198 + volumes: 199 + - /proc:/host/proc:ro 200 + - /sys:/host/sys:ro 201 + - /:/rootfs:ro 202 + command: 203 + - '--path.procfs=/host/proc' 204 + - '--path.sysfs=/host/sys' 205 + - '--collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"' 206 + 207 + grafana: 208 + build: 209 + context: monitoring/grafana 210 + dockerfile: Dockerfile 211 + volumes: 212 + - grafana_data:/var/lib/grafana 213 + restart: unless-stopped 214 + environment: 215 + GF_SERVER_HTTP_PORT: 2345 216 + GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD} 217 + GF_USERS_ALLOW_SIGN_UP: false 218 + 219 + GF_SMTP_ENABLED: true 220 + GF_SMTP_HOST: ${SMTP_HOST}:${SMTP_PORT} 221 + GF_SMTP_FROM_ADDRESS: ${SMTP_FROM} 222 + GF_SERVER_DOMAIN: ${DOMAIN_NAME} 223 + GF_SMTP_FROM_NAME: ${SMTP_FROM} 224 + GF_SMTP_USER: "${SMTP_USER}" 225 + GF_SMTP_PASSWORD: "${SMTP_PASSWORD}" 226 + 227 + POSTGRES_METRICS_USER: ${POSTGRES_METRICS_USER} 228 + POSTGRES_METRICS_PASSWORD: ${POSTGRES_METRICS_PASSWORD} 229 + POSTGRES_METRICS_DBNAME: ${POSTGRES_METRICS_DBNAME} 230 + 231 + pgwatch: 232 + build: 233 + context: monitoring/pgwatch 234 + dockerfile: Dockerfile 235 + args: 236 + POSTGRES_USER: ${POSTGRES_USER} 237 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 238 + POSTGRES_DB: ${POSTGRES_DBNAME} 239 + restart: unless-stopped 240 + environment: 241 + POSTGRES_METRICS_USER: ${POSTGRES_METRICS_USER} 242 + POSTGRES_METRICS_PASSWORD: ${POSTGRES_METRICS_PASSWORD} 243 + POSTGRES_METRICS_DBNAME: ${POSTGRES_METRICS_DBNAME} 244 + command: 245 + - "--web-disable=all" 246 + - "--sources=/sources.yaml" 247 + - "--sink=postgresql://${POSTGRES_METRICS_USER}:${POSTGRES_METRICS_PASSWORD}@db:5432/${POSTGRES_METRICS_DBNAME}" 248 + depends_on: 249 + db: 250 + condition: service_healthy 251 + 252 + volumes: 253 + dbpg: 254 + caddy: 255 + pds: 256 + frontend: 257 + redis: 258 + prometheus_data: 259 + grafana_data:
+170
docker-compose.simple.yml
···
··· 1 + services: 2 + backend: &default_backend 3 + build: 4 + context: . 5 + dockerfile: packages/backend/Dockerfile 6 + depends_on: 7 + db: 8 + condition: service_healthy 9 + redis: 10 + condition: service_started 11 + frontend: 12 + condition: service_started 13 + migration: 14 + condition: service_completed_successfully 15 + restart: unless-stopped 16 + environment: 17 + NODE_ENV: production 18 + ADMIN_USER: ${ADMIN_USER} 19 + ADMIN_EMAIL: ${ADMIN_EMAIL} 20 + ADMIN_PASSWORD: ${ADMIN_PASSWORD} 21 + JWT_SECRET: ${JWT_SECRET} 22 + DOMAIN_NAME: ${DOMAIN_NAME} 23 + 24 + CACHE_DOMAIN: ${CACHE_DOMAIN} 25 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 26 + 27 + SMTP_HOST: ${SMTP_HOST} 28 + SMTP_USER: ${SMTP_USER} 29 + SMTP_PORT: ${SMTP_PORT} 30 + SMTP_PASSWORD: ${SMTP_PASSWORD} 31 + SMTP_FROM: ${SMTP_FROM} 32 + 33 + POSTGRES_USER: ${POSTGRES_USER} 34 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 35 + POSTGRES_DBNAME: ${POSTGRES_DBNAME} 36 + 37 + WEBPUSH_EMAIL: ${WEBPUSH_EMAIL} 38 + WEBPUSH_PRIVATE: ${WEBPUSH_PRIVATE} 39 + WEBPUSH_PUBLIC: ${WEBPUSH_PUBLIC} 40 + 41 + ENABLE_BSKY: ${ENABLE_BSKY} 42 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 43 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 44 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 45 + 46 + USE_WORKERS: true 47 + LOG_SQL_QUERIES: ${LOG_SQL_QUERIES:-} 48 + UPLOAD_LIMIT: ${UPLOAD_LIMIT:-} 49 + POSTS_PER_PAGE: ${POSTS_PER_PAGE:-} 50 + LOG_LEVEL: ${LOG_LEVEL:-} 51 + BLOCKLIST_URI: ${BLOCKLIST_URI:-} 52 + FRONTEND_PATH: ${FRONTEND_PATH:-} 53 + DISABLE_REQUIRE_SEND_EMAIL: ${DISABLE_REQUIRE_SEND_EMAIL:-} 54 + BLOCKED_IPS: ${BLOCKED_IPS:-} 55 + REVIEW_REGISTRATIONS: ${REVIEW_REGISTRATIONS:-} 56 + IGNORE_BLOCK_HOSTS: ${IGNORE_BLOCK_HOSTS:-} 57 + 58 + FRONTEND_LOGO: ${FRONTEND_LOGO:-} 59 + FRONTEND_API_URL: ${FRONTEND_API_URL:-} 60 + FRONTEND_MEDIA_URL: ${FRONTEND_MEDIA_URL:-} 61 + FRONTEND_CACHE_URL: ${FRONTEND_CACHE_URL:-} 62 + FRONTEND_CACHE_BACKUP_URLS: ${FRONTEND_CACHE_BACKUP_URLS:-} 63 + FRONTEND_SHORTEN_POSTS: ${FRONTEND_SHORTEN_POSTS:-} 64 + FRONTEND_DISABLE_PWA: ${FRONTEND_DISABLE_PWA:-} 65 + FRONTEND_MAINTENANCE: ${FRONTEND_MAINTENANCE:-} 66 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 67 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 68 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 69 + 70 + FRONTEND_FQDN_URL: https://${DOMAIN_NAME} 71 + 72 + ENABLE_RAW_OUTPUT: ${ENABLE_RAW_OUTPUT:-} 73 + volumes: 74 + - ./packages/backend/uploads:/app/packages/backend/uploads 75 + - ./packages/backend/cache:/app/packages/backend/cache 76 + - frontend:/app/packages/frontend:ro 77 + 78 + migration: 79 + <<: *default_backend 80 + depends_on: 81 + db: 82 + condition: service_started 83 + redis: 84 + condition: service_started 85 + frontend: 86 + condition: service_started 87 + restart: no 88 + command: "npm exec tsx migrate.ts init-container" 89 + 90 + frontend: 91 + restart: unless-stopped 92 + build: 93 + context: . 94 + dockerfile: packages/frontend/Dockerfile 95 + ports: 96 + - 80:80 97 + - 443:443 98 + environment: 99 + DOMAIN_NAME: ${DOMAIN_NAME} 100 + PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 101 + CACHE_DOMAIN: ${CACHE_DOMAIN} 102 + MEDIA_DOMAIN: ${MEDIA_DOMAIN} 103 + ACME_EMAIL: ${ACME_EMAIL} 104 + FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 105 + FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 106 + FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 107 + volumes: 108 + - "caddy:/data" 109 + - "frontend:/var/www/html/frontend" 110 + - ./packages/backend/uploads:/var/www/html/uploads 111 + - ./packages/caddy:/etc/caddy/config 112 + 113 + db: 114 + image: postgres:17 115 + restart: unless-stopped 116 + shm_size: '2gb' 117 + environment: 118 + POSTGRES_USER: ${POSTGRES_USER} 119 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 120 + POSTGRES_DB: ${POSTGRES_DBNAME} 121 + volumes: 122 + - dbpg:/var/lib/postgresql/data 123 + 124 + adminer: 125 + image: adminer 126 + restart: unless-stopped 127 + 128 + redis: 129 + image: redis:7.2.4 130 + restart: unless-stopped 131 + volumes: 132 + - redis:/data 133 + 134 + pds: 135 + image: ghcr.io/bluesky-social/pds:0.4 136 + restart: unless-stopped 137 + profiles: 138 + - bluesky 139 + environment: 140 + PDS_HOSTNAME: ${PDS_DOMAIN_NAME} 141 + PDS_JWT_SECRET: ${PDS_JWT_SECRET} 142 + PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 143 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} 144 + PDS_DATA_DIRECTORY: /pds 145 + PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks 146 + PDS_BLOB_UPLOAD_LIMIT: 52428800 147 + PDS_DID_PLC_URL: "https://plc.directory" 148 + PDS_BSKY_APP_VIEW_URL: "https://api.bsky.app" 149 + PDS_BSKY_APP_VIEW_DID: "did:web:api.bsky.app" 150 + PDS_REPORT_SERVICE_URL: "https://mod.bsky.app" 151 + PDS_REPORT_SERVICE_DID: "did:plc:ar7c4by46qjdydhdevvrndac" 152 + PDS_CRAWLERS: "https://bsky.network, https://atproto.africa" 153 + PDS_EMAIL_SMTP_URL: "smtp://${SMTP_USER}:${SMTP_PASSWORD}@${SMTP_HOST}:${SMTP_PORT}" 154 + PDS_EMAIL_FROM_ADDRESS: "${SMTP_FROM}" 155 + LOG_ENABLED: true 156 + volumes: 157 + - pds:/pds 158 + 159 + pds_worker: 160 + <<: *default_backend 161 + profiles: 162 + - bluesky 163 + command: "npm exec tsx atproto.ts" 164 + 165 + volumes: 166 + dbpg: 167 + caddy: 168 + pds: 169 + frontend: 170 + redis:
-174
docker-compose.yml
··· 1 - services: 2 - backend: &default_backend 3 - build: 4 - context: . 5 - dockerfile: packages/backend/Dockerfile 6 - depends_on: 7 - db: 8 - condition: service_started 9 - redis: 10 - condition: service_started 11 - frontend: 12 - condition: service_started 13 - migration: 14 - condition: service_completed_successfully 15 - restart: unless-stopped 16 - environment: 17 - NODE_ENV: production 18 - ADMIN_USER: ${ADMIN_USER} 19 - ADMIN_EMAIL: ${ADMIN_EMAIL} 20 - ADMIN_PASSWORD: ${ADMIN_PASSWORD} 21 - JWT_SECRET: ${JWT_SECRET} 22 - DOMAIN_NAME: ${DOMAIN_NAME} 23 - 24 - CACHE_DOMAIN: ${CACHE_DOMAIN} 25 - MEDIA_DOMAIN: ${MEDIA_DOMAIN} 26 - 27 - SMTP_HOST: ${SMTP_HOST} 28 - SMTP_USER: ${SMTP_USER} 29 - SMTP_PORT: ${SMTP_PORT} 30 - SMTP_PASSWORD: ${SMTP_PASSWORD} 31 - SMTP_FROM: ${SMTP_FROM} 32 - 33 - POSTGRES_USER: ${POSTGRES_USER} 34 - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 35 - POSTGRES_DBNAME: ${POSTGRES_DBNAME} 36 - 37 - WEBPUSH_EMAIL: ${WEBPUSH_EMAIL} 38 - WEBPUSH_PRIVATE: ${WEBPUSH_PRIVATE} 39 - WEBPUSH_PUBLIC: ${WEBPUSH_PUBLIC} 40 - 41 - ENABLE_BSKY: ${ENABLE_BSKY} 42 - PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 43 - PDS_JWT_SECRET: ${PDS_JWT_SECRET} 44 - PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 45 - 46 - USE_WORKERS: true 47 - LOG_SQL_QUERIES: ${LOG_SQL_QUERIES:-} 48 - UPLOAD_LIMIT: ${UPLOAD_LIMIT:-} 49 - POSTS_PER_PAGE: ${POSTS_PER_PAGE:-} 50 - LOG_LEVEL: ${LOG_LEVEL:-} 51 - BLOCKLIST_URI: ${BLOCKLIST_URI:-} 52 - FRONTEND_PATH: ${FRONTEND_PATH:-} 53 - DISABLE_REQUIRE_SEND_EMAIL: ${DISABLE_REQUIRE_SEND_EMAIL:-} 54 - BLOCKED_IPS: ${BLOCKED_IPS:-} 55 - REVIEW_REGISTRATIONS: ${REVIEW_REGISTRATIONS:-} 56 - IGNORE_BLOCK_HOSTS: ${IGNORE_BLOCK_HOSTS:-} 57 - 58 - FRONTEND_LOGO: ${FRONTEND_LOGO:-} 59 - FRONTEND_API_URL: ${FRONTEND_API_URL:-} 60 - FRONTEND_MEDIA_URL: ${FRONTEND_MEDIA_URL:-} 61 - FRONTEND_CACHE_URL: ${FRONTEND_CACHE_URL:-} 62 - FRONTEND_CACHE_BACKUP_URLS: ${FRONTEND_CACHE_BACKUP_URLS:-} 63 - FRONTEND_SHORTEN_POSTS: ${FRONTEND_SHORTEN_POSTS:-} 64 - FRONTEND_DISABLE_PWA: ${FRONTEND_DISABLE_PWA:-} 65 - FRONTEND_MAINTENANCE: ${FRONTEND_MAINTENANCE:-} 66 - FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 67 - FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 68 - FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 69 - 70 - FRONTEND_FQDN_URL: https://${DOMAIN_NAME} 71 - 72 - ENABLE_RAW_OUTPUT: ${ENABLE_RAW_OUTPUT:-} 73 - volumes: 74 - - ./packages/backend/uploads:/app/packages/backend/uploads 75 - - ./packages/backend/cache:/app/packages/backend/cache 76 - - frontend:/app/packages/frontend:ro 77 - 78 - migration: 79 - <<: *default_backend 80 - depends_on: 81 - db: 82 - condition: service_started 83 - redis: 84 - condition: service_started 85 - frontend: 86 - condition: service_started 87 - restart: no 88 - command: "npm exec tsx migrate.ts init-container" 89 - 90 - frontend: 91 - restart: unless-stopped 92 - build: 93 - context: . 94 - dockerfile: packages/frontend/Dockerfile 95 - ports: 96 - - "127.0.0.1:9237:9237" 97 - - "127.0.0.1:9238:9238" 98 - - "127.0.0.1:9239:9239" 99 - - "127.0.0.1:9240:9240" 100 - environment: 101 - DOMAIN_NAME: ${DOMAIN_NAME} 102 - PDS_DOMAIN_NAME: ${PDS_DOMAIN_NAME} 103 - CACHE_DOMAIN: ${CACHE_DOMAIN} 104 - MEDIA_DOMAIN: ${MEDIA_DOMAIN} 105 - ACME_EMAIL: ${ACME_EMAIL} 106 - FRONTEND_SHORT_TITLE: ${FRONTEND_SHORT_TITLE:-} 107 - FRONTEND_LONG_TITLE: ${FRONTEND_LONG_TITLE:-} 108 - FRONTEND_DESCRIPTION: ${FRONTEND_DESCRIPTION:-} 109 - volumes: 110 - - "caddy:/data" 111 - - "frontend:/var/www/html/frontend" 112 - - ./packages/backend/uploads:/var/www/html/uploads 113 - - ./packages/caddy:/etc/caddy/config 114 - 115 - db: 116 - image: postgres:17 117 - restart: unless-stopped 118 - shm_size: '2gb' 119 - environment: 120 - POSTGRES_USER: ${POSTGRES_USER} 121 - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 122 - POSTGRES_DB: ${POSTGRES_DBNAME} 123 - volumes: 124 - - dbpg:/var/lib/postgresql/data 125 - 126 - adminer: 127 - image: adminer 128 - restart: unless-stopped 129 - 130 - redis: 131 - image: redis:7.2.4 132 - restart: unless-stopped 133 - volumes: 134 - - redis:/data 135 - 136 - pds: 137 - image: ghcr.io/bluesky-social/pds:0.4 138 - restart: unless-stopped 139 - profiles: 140 - - bluesky 141 - environment: 142 - PDS_HOSTNAME: ${PDS_DOMAIN_NAME} 143 - PDS_JWT_SECRET: ${PDS_JWT_SECRET} 144 - PDS_ADMIN_PASSWORD: ${PDS_ADMIN_PASSWORD} 145 - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} 146 - PDS_DATA_DIRECTORY: /pds 147 - PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks 148 - PDS_BLOB_UPLOAD_LIMIT: 52428800 149 - PDS_DID_PLC_URL: "https://plc.directory" 150 - PDS_BSKY_APP_VIEW_URL: "https://api.bsky.app" 151 - PDS_BSKY_APP_VIEW_DID: "did:web:api.bsky.app" 152 - PDS_REPORT_SERVICE_URL: "https://mod.bsky.app" 153 - PDS_REPORT_SERVICE_DID: "did:plc:ar7c4by46qjdydhdevvrndac" 154 - PDS_CRAWLERS: "https://bsky.network, https://atproto.africa" 155 - PDS_EMAIL_SMTP_URL: "smtps://${SMTP_USER}:${SMTP_PASSWORD}@${SMTP_HOST}:${SMTP_PORT}" 156 - PDS_EMAIL_FROM_ADDRESS: "${SMTP_FROM}" 157 - LOG_ENABLED: true 158 - ports: 159 - - "127.0.0.1:3001:3000" 160 - volumes: 161 - - pds:/pds 162 - 163 - pds_worker: 164 - <<: *default_backend 165 - profiles: 166 - - bluesky 167 - command: "npm exec tsx atproto.ts" 168 - 169 - volumes: 170 - dbpg: 171 - caddy: 172 - pds: 173 - frontend: 174 - redis:
···
-37
docs/openid.md
··· 1 - # Setting up OpenID Connect (OIDC) authentication 2 - 3 - If you are handling multiple services aside from Wafrn, you can implement something called an identity provider (or an IdP) which is a service that provides a central account to log in on multiple services. 4 - 5 - Linking it on Wafrn is possible, if your provider supports the OpenID Connect specification. Implementing this shouldn't have any breaking changes on your instance. Signing up using OpenID is not implemented due to security issues, but it will prefill your email and username on the register screen. 6 - 7 - Here's how you can implement OIDC auth on your instance: 8 - 9 - 1. Create a authentication client on your identity provider of choice. I will use Keycloak on this guide but you can use anything else (like Authentik, Authelia, etc). 10 - 11 - 1. Set your client type to OpenID Connect (if possible) 12 - 13 - 2. Set the Client ID to anything you want. Take note of that ID, we will need that later. 14 - 15 - 3. Set the client name to 'Wafrn'. You can change that if you like, that will be the name shown if someone authenticates to your Wafrn instance for the first time. Optionally, set the description of it. 16 - 17 - 4. Enable both client authentication and authorization, and don't touch anything else. 18 - 19 - 5. Set the root URL to your Wafrn instance's homepage (e.g. `https://app.wafrn.net/`), and set the home URL with the same thing. 20 - 21 - 6. Set the redirect URL to `https://wafrn.example/api/login/oidc/callback*`. 22 - 23 - 7. Copy the provided Client secret. We will need that. 24 - 25 - 2. In your .env file, edit these values: 26 - 27 - - `OIDC_ENABLED` to `true` 28 - 29 - - `OIDC_ISSUER` to your IdP's issuer URL 30 - 31 - - `OIDC_CLIENT_ID` is your specified client ID 32 - 33 - - `OIDC_CLIENT_SECRET` to the provided client secret 34 - 35 - - `OIDC_AUTH_NAME` to the name you want to call your auth provider 36 - 37 - 3. Now, restart the container. To take effect easily, you should clear your browser local storage by running `localStorage.clear()` in your browser console.
···
+1 -1
install/bsky/add-insert-code.sh
··· 6 7 source "${SCRIPT_DIR}/../../.env" 8 9 - NEW_CODE=$(curl -s --header "Content-Type: application/json" -d '{"useCount":999999}' http://admin:${PDS_ADMIN_PASSWORD}@localhost:3001/xrpc/com.atproto.server.createInviteCode | jq --raw-output '.code') 10 11 echo Adding code ${NEW_CODE} 12
··· 6 7 source "${SCRIPT_DIR}/../../.env" 8 9 + NEW_CODE=$(docker exec wafrn-pds-1 wget -q -O - --header "Content-Type: application/json" --post-data '{"useCount":999999}' http://admin:${PDS_ADMIN_PASSWORD}@localhost:3000/xrpc/com.atproto.server.createInviteCode | jq --raw-output '.code') 10 11 echo Adding code ${NEW_CODE} 12
+2 -2
install/bsky/create-admin.sh
··· 10 11 echo Generating new invite code 12 13 - NEW_CODE=$(curl -s -d '{"useCount":1}' --header "Content-Type: application/json" http://admin:${PDS_ADMIN_PASSWORD}@localhost:3001/xrpc/com.atproto.server.createInviteCode | jq --raw-output '.code') 14 15 echo Invite code genrated: ${NEW_CODE} 16 ··· 20 21 echo Creating admin account 22 23 - RESULT=$(curl -s -d "{\"email\":\"${ADMIN_USER}@${DOMAIN_NAME}\", \"handle\":\"${NEW_USERNAME}.${PDS_DOMAIN_NAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${NEW_CODE}\"}" --header "Content-Type: application/json" http://localhost:3001/xrpc/com.atproto.server.createAccount) 24 25 echo $RESULT 26
··· 10 11 echo Generating new invite code 12 13 + NEW_CODE=$(docker exec wafrn-pds-1 wget -q -O - --header "Content-Type: application/json" --post-data '{"useCount":1}' http://admin:${PDS_ADMIN_PASSWORD}@localhost:3000/xrpc/com.atproto.server.createInviteCode | jq --raw-output '.code') 14 15 echo Invite code genrated: ${NEW_CODE} 16 ··· 20 21 echo Creating admin account 22 23 + RESULT=$(docker exec wafrn-pds-1 wget -q -O - --header "Content-Type: application/json" --post-data "{\"email\":\"${ADMIN_USER}@${DOMAIN_NAME}\", \"handle\":\"${NEW_USERNAME}.${PDS_DOMAIN_NAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${NEW_CODE}\"}" http://admin:${PDS_ADMIN_PASSWORD}@localhost:3000/xrpc/com.atproto.server.createAccount) 24 25 echo $RESULT 26
+1 -1
install/installer.sh
··· 30 echo "Ok now we need an email for the administrator user" 31 read ADMIN_EMAIL 32 echo 33 - echo "We now need a handle for your administrator user. If this will be a personal, single-user instance then you can enter the username you wish to use as your main." 34 echo "Otherwise 'admin' is a good choice. You can also have a separate 'admin' and personal account as well." 35 read ADMIN_USER 36 echo
··· 30 echo "Ok now we need an email for the administrator user" 31 read ADMIN_EMAIL 32 echo 33 + echo "We now need a handle for your administrator user. If this will be a personal single user instance, we still recommend creating an admin account for bluesky reasons" 34 echo "Otherwise 'admin' is a good choice. You can also have a separate 'admin' and personal account as well." 35 read ADMIN_USER 36 echo
logs/.gitkeep

This is a binary file and will not be displayed.

+4 -37
package-lock.json
··· 1 { 2 "name": "wafrn", 3 - "version": "2025.10.03-DEV+JBC", 4 "lockfileVersion": 3, 5 "requires": true, 6 "packages": { 7 "": { 8 "name": "wafrn", 9 - "version": "2025.10.03-DEV+JBC", 10 "license": "AGPL-3.0-or-later", 11 "workspaces": [ 12 "packages/frontend", ··· 14 ], 15 "dependencies": { 16 "cheerio": "^1.1.0", 17 - "openid-client": "^6.8.1", 18 "tsx": "^4.19.1" 19 }, 20 "devDependencies": { ··· 16674 "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", 16675 "license": "MIT" 16676 }, 16677 - "node_modules/jose": { 16678 - "version": "6.1.0", 16679 - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", 16680 - "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", 16681 - "license": "MIT", 16682 - "funding": { 16683 - "url": "https://github.com/sponsors/panva" 16684 - } 16685 - }, 16686 "node_modules/joycon": { 16687 "version": "3.1.1", 16688 "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", ··· 19463 "@angular/core": "^20.0.0" 19464 } 19465 }, 19466 - "node_modules/oauth4webapi": { 19467 - "version": "3.8.2", 19468 - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.2.tgz", 19469 - "integrity": "sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==", 19470 - "license": "MIT", 19471 - "funding": { 19472 - "url": "https://github.com/sponsors/panva" 19473 - } 19474 - }, 19475 "node_modules/object-assign": { 19476 "version": "4.1.1", 19477 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", ··· 19581 }, 19582 "funding": { 19583 "url": "https://github.com/sponsors/sindresorhus" 19584 - } 19585 - }, 19586 - "node_modules/openid-client": { 19587 - "version": "6.8.1", 19588 - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.1.tgz", 19589 - "integrity": "sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==", 19590 - "license": "MIT", 19591 - "dependencies": { 19592 - "jose": "^6.1.0", 19593 - "oauth4webapi": "^3.8.2" 19594 - }, 19595 - "funding": { 19596 - "url": "https://github.com/sponsors/panva" 19597 } 19598 }, 19599 "node_modules/optionator": { ··· 26039 }, 26040 "packages/backend": { 26041 "name": "wafrn-backend", 26042 - "version": "2025.10.03-DEV+JBC", 26043 "license": "MIT", 26044 "dependencies": { 26045 "@atcute/bluesky-richtext-builder": "^1.0.2", ··· 26111 "node-cron": "^4.2.1", 26112 "node-stream-zip": "^1.15.0", 26113 "nodemailer": "^6.7.2", 26114 - "openid-client": "^6.8.1", 26115 "otpauth": "^9.4.0", 26116 "pg": "^8.11.0", 26117 "pg-hstore": "^2.3.4", ··· 26384 }, 26385 "packages/frontend": { 26386 "name": "wafrn", 26387 - "version": "2025.10.03-DEV+JBC", 26388 "dependencies": { 26389 "@angular-eslint/schematics": "^20.0.0", 26390 "@angular/animations": "^20.0.2",
··· 1 { 2 "name": "wafrn", 3 + "version": "2025.10.03-DEV", 4 "lockfileVersion": 3, 5 "requires": true, 6 "packages": { 7 "": { 8 "name": "wafrn", 9 + "version": "2025.10.03-DEV", 10 "license": "AGPL-3.0-or-later", 11 "workspaces": [ 12 "packages/frontend", ··· 14 ], 15 "dependencies": { 16 "cheerio": "^1.1.0", 17 "tsx": "^4.19.1" 18 }, 19 "devDependencies": { ··· 16673 "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", 16674 "license": "MIT" 16675 }, 16676 "node_modules/joycon": { 16677 "version": "3.1.1", 16678 "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", ··· 19453 "@angular/core": "^20.0.0" 19454 } 19455 }, 19456 "node_modules/object-assign": { 19457 "version": "4.1.1", 19458 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", ··· 19562 }, 19563 "funding": { 19564 "url": "https://github.com/sponsors/sindresorhus" 19565 } 19566 }, 19567 "node_modules/optionator": { ··· 26007 }, 26008 "packages/backend": { 26009 "name": "wafrn-backend", 26010 + "version": "2025.10.03-DEV", 26011 "license": "MIT", 26012 "dependencies": { 26013 "@atcute/bluesky-richtext-builder": "^1.0.2", ··· 26079 "node-cron": "^4.2.1", 26080 "node-stream-zip": "^1.15.0", 26081 "nodemailer": "^6.7.2", 26082 "otpauth": "^9.4.0", 26083 "pg": "^8.11.0", 26084 "pg-hstore": "^2.3.4", ··· 26351 }, 26352 "packages/frontend": { 26353 "name": "wafrn", 26354 + "version": "2025.10.03-DEV", 26355 "dependencies": { 26356 "@angular-eslint/schematics": "^20.0.0", 26357 "@angular/animations": "^20.0.2",
+2 -3
package.json
··· 1 { 2 "name": "wafrn", 3 - "version": "2025.10.03-DEV+JBC", 4 "description": "wafrn", 5 "main": "index.ts", 6 - "scripts": { 7 "full:upgrade": "git pull && pm2 restart all && npm run frontend:deploy", 8 "backend:prettier-format": "cd packages/backend && prettier --config .prettierrc '**/*.ts' --write", 9 "backend:develop": "cd packages/backend && tsx watch index.ts", ··· 50 }, 51 "dependencies": { 52 "cheerio": "^1.1.0", 53 - "openid-client": "^6.8.1", 54 "tsx": "^4.19.1" 55 } 56 }
··· 1 { 2 "name": "wafrn", 3 + "version": "2025.10.03-DEV", 4 "description": "wafrn", 5 "main": "index.ts", 6 + "scripts": { 7 "full:upgrade": "git pull && pm2 restart all && npm run frontend:deploy", 8 "backend:prettier-format": "cd packages/backend && prettier --config .prettierrc '**/*.ts' --write", 9 "backend:develop": "cd packages/backend && tsx watch index.ts", ··· 50 }, 51 "dependencies": { 52 "cheerio": "^1.1.0", 53 "tsx": "^4.19.1" 54 } 55 }
+1
packages/backend/.gitignore
··· 12 logs 13 /cache 14 environment.prod.ts 15 /build 16 17 .env
··· 12 logs 13 /cache 14 environment.prod.ts 15 + environment.dev.ts 16 /build 17 18 .env
+30 -437
packages/backend/atproto/utils/getAtProtoThread.ts
··· 1 // returns the post id 2 import { getAtProtoSession } from './getAtProtoSession.js' 3 import { QueryParams } from '@atproto/sync/dist/firehose/lexicons.js' 4 - import { Media, Notification, Post, PostMentionsUserRelation, PostTag, Quotes, User } from '../../models/index.js' 5 import { Model, Op } from 'sequelize' 6 import { PostView, ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs.js' 7 import { getAtprotoUser } from './getAtprotoUser.js' ··· 19 import { MediaAttributes } from '../../models/media.js' 20 import { getAdminAtprotoSession } from '../../utils/atproto/getAdminAtprotoSession.js' 21 import { getPostThreadRecursive } from '../../utils/activitypub/getPostThreadRecursive.js' 22 23 const markdownConverter = new showdown.Converter({ 24 simplifiedAutoLink: true, ··· 29 emoji: true 30 }) 31 32 - const adminUser = User.findOne({ 33 - where: { 34 - url: completeEnvironment.adminUser 35 } 36 }) 37 38 async function getAtProtoThread( 39 uri: string, ··· 100 return await processSinglePost(thread.post, parentId) 101 } 102 103 - async function processSinglePost( 104 - post: PostView, 105 - parentId?: string, 106 - forceUpdate?: boolean 107 - ): Promise<string | undefined> { 108 - if (!post || !completeEnvironment.enableBsky) { 109 - return undefined 110 - } 111 - if (!forceUpdate) { 112 - const existingPost = await Post.findOne({ 113 - where: { 114 - bskyUri: post.uri 115 - } 116 - }) 117 - if (existingPost) { 118 - return existingPost.id 119 - } 120 - } 121 - let postCreator: User | undefined 122 - try { 123 - postCreator = await getAtprotoUser(post.author.did, (await adminUser) as User, post.author) 124 - } catch (error) { 125 - logger.debug({ 126 - message: `Problem obtaining user from post`, 127 - post, 128 - parentId, 129 - forceUpdate, 130 - error 131 - }) 132 - } 133 - let verifiedFedi: string | undefined; 134 - if ('fediverseUrl' in post.record || 'bridgyOriginalUrl' in post.record) { 135 - const record = post.record as any 136 - if ('bridgyOriginalUrl' in post.record) { 137 - const res = await fetch("https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc" + `?identifier=${post.author.did}`); 138 - if (res.ok) { 139 - const json = await res.json() as { pds: string }; 140 - if (json.pds.replace(/^https?:\/\//, '') === 'atproto.brid.gy') { 141 - // if user is on bridgy pds, verify it 142 - verifiedFedi = post.record.bridgyOriginalUrl as string; 143 - } 144 - } 145 - } else { 146 - // prob wafrn post, but lets verify it 147 - const res = await fetch(post.record.fediverseUrl as string, { 148 - headers: { 149 - 'Accept': 'application/json' 150 - } 151 - }) 152 - if (res.ok) { 153 - const json = await res.json() as { object: { bskyCid: string } }; 154 - if (json.object.bskyCid === post.cid) { 155 - verifiedFedi = post.record.fediverseUrl as string; 156 - } 157 - } 158 - } 159 - } 160 - if (verifiedFedi) { 161 - try { 162 - const remotePost = await getPostThreadRecursive(postCreator, verifiedFedi); 163 - if (remotePost) { 164 - await getPostThreadRecursive(postCreator, verifiedFedi, undefined, remotePost.id) 165 - remotePost.bskyCid = post.cid; 166 - remotePost.bskyUri = post.uri; 167 - Post.upsert(remotePost, { 168 - conflictFields: ['userId', 'bskyCid', 'bskyUri'] 169 - }) 170 - return remotePost.id 171 - } 172 - } catch (error) { 173 - logger.debug({ 174 - message: `Error in obtaining fedi post ${verifiedFedi}`, 175 - error 176 - }) 177 - } 178 - } 179 - if (!postCreator || !post) { 180 - const usr = postCreator ? postCreator : await User.findOne({ where: { url: completeEnvironment.deletedUser } }) 181 - 182 - const invalidPost = await Post.create({ 183 - userId: usr?.id, 184 - content: `Failed to get atproto post`, 185 - parentId: parentId, 186 - isDeleted: true, 187 - createdAt: new Date(0), 188 - updatedAt: new Date(0) 189 - }) 190 - return invalidPost.id 191 - } 192 - if (postCreator) { 193 - const medias = getPostMedias(post) 194 - let tags: string[] = [] 195 - let mentions: string[] = [] 196 - let record = post.record as any 197 - let postText = record.text 198 - let federatedWoot = false 199 - if (record.fullText || record.bridgyOriginalText) { 200 - federatedWoot = true 201 - tags = record.fullTags?.split('\n').filter((x: string) => !!x) ?? [] // also detect full tags 202 - postText = record.fullText ?? record.bridgyOriginalText 203 - } 204 - if (record.facets && record.facets.length > 0 && !federatedWoot) { 205 - // lets get mentions 206 - const mentionedDids = record.facets 207 - .flatMap((elem: any) => elem.features) 208 - .map((elem: any) => elem.did) 209 - .filter((elem: any) => elem) 210 - if (mentionedDids && mentionedDids.length > 0) { 211 - const mentionedUsers = await User.findAll({ 212 - where: { 213 - bskyDid: { 214 - [Op.in]: mentionedDids 215 - } 216 - } 217 - }) 218 - mentions = mentionedUsers.map((elem) => elem.id) 219 - } 220 - 221 - const rt = new RichText({ 222 - text: postText, 223 - facets: record.facets 224 - }) 225 - let text = '' 226 - 227 - for (const segment of rt.segments()) { 228 - if (segment.isLink()) { 229 - const href = segment.link?.uri 230 - text += `<a href="${href}" target="_blank">${href}</a>` 231 - } else if (segment.isMention()) { 232 - const href = `${completeEnvironment.frontendUrl}/blog/${segment.mention?.did}` 233 - text += `<a href="${href}" target="_blank">${segment.text}</a>` 234 - } else if (segment.isTag()) { 235 - const href = `${completeEnvironment.frontendUrl}/dashboard/search/${segment.text.substring(1)}` 236 - text += `<a href="${href}" target="_blank">${segment.text}</a>` 237 - tags.push(segment.text.substring(1)) 238 - } else { 239 - text += segment.text 240 - } 241 - } 242 - postText = text 243 - } 244 - if (!federatedWoot) postText = postText.replaceAll('\n', '<br>') 245 - 246 - const labels = getPostLabels(post) 247 - let cw = labels.length > 0 ? `Post is labeled as: ${labels.join(', ')}` : undefined 248 - if (!cw && postCreator.NSFW) { 249 - cw = 'This user has been marked as NSFW and the post has been labeled automatically as NSFW' 250 - } 251 - const newData = { 252 - userId: postCreator.id, 253 - bskyCid: post.cid, 254 - bskyUri: post.uri, 255 - content: postText, 256 - createdAt: new Date((post.record as any).createdAt), 257 - privacy: Privacy.Public, 258 - parentId: parentId, 259 - content_warning: cw, 260 - ...getPostInteractionLevels(post, parentId) 261 - } 262 - if (!parentId) { 263 - delete newData.parentId 264 - } 265 - 266 - if ((await getAllLocalUserIds()).includes(newData.userId) && !forceUpdate) { 267 - // dirty as hell but this should stop the duplication 268 - await wait(1500) 269 - } 270 - let [postToProcess, created] = await Post.findOrCreate({ where: { bskyUri: post.uri }, defaults: newData }) 271 - // do not update existing posts. But what if local user creates a post through bsky? then we force updte i guess 272 - if (!(await getAllLocalUserIds()).includes(postToProcess.userId) || created) { 273 - if (!created) { 274 - postToProcess.set(newData) 275 - await postToProcess.save() 276 - } 277 - if (medias) { 278 - await Media.destroy({ 279 - where: { 280 - postId: postToProcess.id 281 - } 282 - }) 283 - await Media.bulkCreate( 284 - medias.map((media: any) => { 285 - return { ...media, postId: postToProcess.id } 286 - }) 287 - ) 288 - } 289 - if (parentId) { 290 - const ancestors = await postToProcess.getAncestors({ 291 - attributes: ['userId'], 292 - where: { 293 - hierarchyLevel: { 294 - [Op.gt]: postToProcess.hierarchyLevel - 5 295 - } 296 - } 297 - }) 298 - mentions = mentions.concat(ancestors.map((elem) => elem.userId)) 299 - } 300 - mentions = [...new Set(mentions)] 301 - if (mentions.length > 0) { 302 - await Notification.destroy({ 303 - where: { 304 - notificationType: 'MENTION', 305 - postId: postToProcess.id 306 - } 307 - }) 308 - await PostMentionsUserRelation.destroy({ 309 - where: { 310 - postId: postToProcess.id 311 - } 312 - }) 313 - await bulkCreateNotifications( 314 - mentions.map((mnt) => ({ 315 - notificationType: 'MENTION', 316 - postId: postToProcess.id, 317 - notifiedUserId: mnt, 318 - userId: postToProcess.userId, 319 - createdAt: new Date(postToProcess.createdAt) 320 - })), 321 - { 322 - ignoreDuplicates: true, 323 - postContent: postText, 324 - userUrl: postCreator.url 325 - } 326 - ) 327 - await PostMentionsUserRelation.bulkCreate( 328 - mentions.map((mnt) => { 329 - return { 330 - userId: mnt, 331 - postId: postToProcess.id 332 - } 333 - }), 334 - { ignoreDuplicates: true } 335 - ) 336 - } 337 - if (tags.length > 0) { 338 - await PostTag.destroy({ 339 - where: { 340 - postId: postToProcess.id 341 - } 342 - }) 343 - await PostTag.bulkCreate( 344 - tags.map((tag) => { 345 - return { 346 - postId: postToProcess.id, 347 - tagName: tag 348 - } 349 - }) 350 - ) 351 - } 352 - const quotedPostUri = getQuotedPostUri(post) 353 - if (quotedPostUri) { 354 - const quotedPostId = await getAtProtoThread(quotedPostUri) 355 - if (quotedPostId) { 356 - const quotedPost = await Post.findByPk(quotedPostId) 357 - if (quotedPost) { 358 - await createNotification( 359 - { 360 - notificationType: 'QUOTE', 361 - notifiedUserId: quotedPost.userId, 362 - userId: postToProcess.userId, 363 - postId: postToProcess.id 364 - }, 365 - { 366 - postContent: postToProcess.content, 367 - userUrl: postCreator?.url 368 - } 369 - ) 370 - await Quotes.findOrCreate({ 371 - where: { 372 - quoterPostId: postToProcess.id, 373 - quotedPostId: quotedPostId 374 - } 375 - }) 376 - } 377 - } 378 - } 379 - } 380 - 381 - return postToProcess.id 382 - } 383 - } 384 - 385 - function getPostMedias(post: PostView) { 386 - let res: MediaAttributes[] = [] 387 - const labels = getPostLabels(post) 388 - const embed = (post.record as any).embed 389 - if (embed) { 390 - if (embed.external) { 391 - res = res.concat([ 392 - { 393 - mediaType: !embed.external.uri.startsWith('https://media.ternor.com/') ? 'text/html' : 'image/gif', 394 - description: embed.external.title, 395 - url: embed.external.uri, 396 - mediaOrder: 0, 397 - external: true 398 - } 399 - ]) 400 - } 401 - if (embed.images || embed.media) { 402 - // case with quote and gif / link preview 403 - if (embed.media?.external) { 404 - res = res.concat([ 405 - { 406 - mediaType: !embed.media.external.uri.startsWith('https://media.ternor.com/') ? 'text/html' : 'image/gif', 407 - description: embed.media.external.title, 408 - url: embed.media.external.uri, 409 - mediaOrder: 0, 410 - external: true 411 - } 412 - ]) 413 - } else { 414 - const thingToProcess = embed.images ? embed.images : embed.media.images 415 - if (thingToProcess) { 416 - const toConcat = thingToProcess.map((media: any, index: any) => { 417 - const cid = media.image.ref['$link'] ? media.image.ref['$link'] : media.image.ref.toString() 418 - const did = post.author.did 419 - return { 420 - mediaType: media.image.mimeType, 421 - description: media.alt, 422 - height: media.aspectRatio?.height, 423 - width: media.aspectRatio?.width, 424 - url: `?cid=${encodeURIComponent(cid)}&did=${encodeURIComponent(did)}`, 425 - mediaOrder: index, 426 - external: true 427 - } 428 - }) 429 - res = res.concat(toConcat) 430 - } else { 431 - logger.debug({ 432 - message: `Bsky problem getting medias on post ${post.uri}` 433 - }) 434 - } 435 - } 436 - } 437 - if (embed.video) { 438 - const video = embed.video 439 - const cid = video.ref['$link'] ? video.ref['$link'] : video.ref.toString() 440 - const did = post.author.did 441 - res = res.concat([ 442 - { 443 - mediaType: embed.video.mimeType, 444 - description: '', 445 - height: embed.aspectRatio?.height, 446 - width: embed.aspectRatio?.width, 447 - url: `?cid=${encodeURIComponent(cid)}&did=${encodeURIComponent(did)}`, 448 - mediaOrder: 0, 449 - external: true 450 - } 451 - ]) 452 - } 453 - } 454 - return res.map((m) => { 455 - return { 456 - ...m, 457 - NSFW: labels.length > 0 458 - } 459 - }) 460 - } 461 - 462 function getQuotedPostUri(post: PostView): string | undefined { 463 let res: string | undefined = undefined 464 const embed = (post.record as any).embed ··· 472 return res 473 } 474 475 - // TODO improve this so we get better nsfw messages lol 476 - function getPostLabels(post: PostView) { 477 - let labels = new Set<string>() 478 - if (post.labels) { 479 - for (const label of post.labels) { 480 - if (label.neg && labels.has(label.val)) { 481 - labels.delete(label.val) 482 - } else { 483 - labels.add(label.val) 484 - } 485 - } 486 - } 487 - return Array.from(labels) 488 - } 489 - 490 async function getPostThreadSafe(options: any) { 491 try { 492 const agent = await getAdminAtprotoSession() ··· 497 options: options, 498 error: error 499 }) 500 - } 501 - } 502 - 503 - function getPostInteractionLevels( 504 - post: PostView, 505 - parentId: string | undefined 506 - ): { 507 - replyControl: InteractionControlType 508 - likeControl: InteractionControlType 509 - reblogControl: InteractionControlType 510 - quoteControl: InteractionControlType 511 - } { 512 - let canQuote = InteractionControl.Anyone 513 - let canReply: InteractionControlType = InteractionControl.Anyone 514 - if (post.viewer && post.viewer.embeddingDisabled) { 515 - canQuote = InteractionControl.NoOne 516 - } 517 - if (parentId) { 518 - canReply = InteractionControl.SameAsOp 519 - canQuote = InteractionControl.SameAsOp 520 - } else if (post.threadgate && post.threadgate.record && (post.threadgate.record as any).allow) { 521 - const allowList = (post.threadgate.record as any).allow 522 - if (allowList.length == 0) { 523 - canReply = InteractionControl.NoOne 524 - } else { 525 - const mentiontypes: string[] = allowList 526 - .map((elem: any) => elem['$type']) 527 - .map((elem: string) => elem.split('app.bsky.feed.threadgate#')[1]) 528 - if (mentiontypes.includes('mentionRule')) { 529 - if (mentiontypes.includes('followingRule')) { 530 - canReply = mentiontypes.includes('followerRule') 531 - ? InteractionControl.FollowersFollowersAndMentioned 532 - : InteractionControl.FollowingAndMentioned 533 - } else { 534 - canReply = mentiontypes.includes('followerRule') 535 - ? InteractionControl.FollowersAndMentioned 536 - : InteractionControl.MentionedUsersOnly 537 - } 538 - } else { 539 - if (mentiontypes.includes('followingRule')) { 540 - canReply = mentiontypes.includes('followerRule') 541 - ? InteractionControl.FollowersAndFollowing 542 - : InteractionControl.Following 543 - } else { 544 - canReply = mentiontypes.includes('followerRule') ? InteractionControl.Followers : InteractionControl.NoOne 545 - } 546 - } 547 - } 548 - } 549 - 550 - if (canQuote === InteractionControl.Anyone && canReply != InteractionControl.Anyone) { 551 - canQuote = canReply 552 - } 553 - 554 - return { 555 - quoteControl: canQuote, 556 - replyControl: canReply, 557 - likeControl: InteractionControl.Anyone, 558 - reblogControl: InteractionControl.Anyone 559 } 560 } 561
··· 1 // returns the post id 2 import { getAtProtoSession } from './getAtProtoSession.js' 3 import { QueryParams } from '@atproto/sync/dist/firehose/lexicons.js' 4 + import { EmojiReaction, Media, Notification, Post, PostAncestor, PostMentionsUserRelation, PostReport, PostTag, QuestionPoll, Quotes, RemoteUserPostView, SilencedPost, User, UserBitesPostRelation, UserBookmarkedPosts, UserLikesPostRelations } from '../../models/index.js' 5 import { Model, Op } from 'sequelize' 6 import { PostView, ThreadViewPost } from '@atproto/api/dist/client/types/app/bsky/feed/defs.js' 7 import { getAtprotoUser } from './getAtprotoUser.js' ··· 19 import { MediaAttributes } from '../../models/media.js' 20 import { getAdminAtprotoSession } from '../../utils/atproto/getAdminAtprotoSession.js' 21 import { getPostThreadRecursive } from '../../utils/activitypub/getPostThreadRecursive.js' 22 + import { Queue, QueueEvents } from 'bullmq' 23 24 const markdownConverter = new showdown.Converter({ 25 simplifiedAutoLink: true, ··· 30 emoji: true 31 }) 32 33 + const processPostQueue = new Queue<{ post: PostView, parentId?: string, forceUpdate?: boolean }, string | undefined>('processSinglePost', { 34 + connection: completeEnvironment.bullmqConnection, 35 + defaultJobOptions: { 36 + removeOnComplete: true, 37 + attempts: 6, 38 + backoff: { 39 + type: 'exponential', 40 + delay: 25000 41 + }, 42 + removeOnFail: false 43 } 44 }) 45 + processPostQueue.setMaxListeners(0); 46 + 47 + const processPostQueueEvents = new QueueEvents('processSinglePost', { 48 + connection: completeEnvironment.bullmqConnection, 49 + }); 50 + processPostQueueEvents.setMaxListeners(0); 51 + 52 + async function processSinglePost( 53 + post: PostView, 54 + parentId?: string, 55 + forceUpdate?: boolean 56 + ): Promise<string | undefined> { 57 + const job = await processPostQueue.add('processSinglePost', { post, parentId, forceUpdate }) 58 + const finished = await job.waitUntilFinished(processPostQueueEvents, 60000).catch((err) => { 59 + logger.debug(err, "Error occured while getting atproto post") 60 + }); 61 + return finished ?? undefined 62 + } 63 64 async function getAtProtoThread( 65 uri: string, ··· 126 return await processSinglePost(thread.post, parentId) 127 } 128 129 function getQuotedPostUri(post: PostView): string | undefined { 130 let res: string | undefined = undefined 131 const embed = (post.record as any).embed ··· 139 return res 140 } 141 142 async function getPostThreadSafe(options: any) { 143 try { 144 const agent = await getAdminAtprotoSession() ··· 149 options: options, 150 error: error 151 }) 152 } 153 } 154
+17 -12
packages/backend/atproto/utils/postToAtproto.ts
··· 186 187 if (token.type === 'link') { 188 builder.addLink(token.text, token.url) 189 - } 190 - 191 - // only add a embed if theres no embed, bsky only supports 1 embed 192 - if (token.type === 'autolink' && !('embed' in res)) { 193 builder.addLink(token.url, token.url) 194 const shasum = crypto.createHash('sha1') 195 shasum.update(token.url.toLowerCase()) 196 const urlHash = shasum.digest('hex') 197 let linkPreview: { url: string; title: string; description: string } | undefined = JSON.parse(await redisCache.get('linkPreviewCache:' + urlHash) ?? '{}') 198 if (!linkPreview?.title) { 199 - linkPreview = await getLinkPreview(token.url, { 200 - followRedirects: 'follow', 201 - headers: { 'User-Agent': completeEnvironment.instanceUrl } 202 - }) as { url: string; title: string; description: string } | undefined; 203 - await redisCache.set('linkPreviewCache:' + urlHash, JSON.stringify(linkPreview), 'EX', linkPreview ? 3600 * 24 : 300) 204 } 205 206 if (linkPreview?.title) { ··· 209 external: { 210 uri: linkPreview.url, 211 title: linkPreview.title, 212 - description: linkPreview.description 213 } 214 } 215 } 216 - } 217 - else builder.addText(token.raw) 218 } 219 postText = builder.text 220
··· 186 187 if (token.type === 'link') { 188 builder.addLink(token.text, token.url) 189 + } else if (token.type === 'autolink' && medias.length === 0) { 190 + // only add a embed if theres no embed, bsky only supports 1 embed 191 builder.addLink(token.url, token.url) 192 const shasum = crypto.createHash('sha1') 193 shasum.update(token.url.toLowerCase()) 194 const urlHash = shasum.digest('hex') 195 let linkPreview: { url: string; title: string; description: string } | undefined = JSON.parse(await redisCache.get('linkPreviewCache:' + urlHash) ?? '{}') 196 if (!linkPreview?.title) { 197 + try { 198 + linkPreview = await getLinkPreview(token.url, { 199 + followRedirects: 'follow', 200 + headers: { 'User-Agent': completeEnvironment.instanceUrl } 201 + }) as { url: string; title: string; description: string } | undefined; 202 + await redisCache.set('linkPreviewCache:' + urlHash, JSON.stringify(linkPreview), 'EX', linkPreview ? 3600 * 24 : 300) 203 + } catch (error) { 204 + logger.trace({ 205 + message: `Error obtaining link ${token.url}`, 206 + error: error 207 + }) 208 + } 209 + 210 } 211 212 if (linkPreview?.title) { ··· 215 external: { 216 uri: linkPreview.url, 217 title: linkPreview.title, 218 + description: linkPreview.description ?? `from ${new URL(linkPreview.url).hostname}` 219 } 220 } 221 } 222 + } else builder.addText(token.raw) 223 } 224 postText = builder.text 225
+593
packages/backend/atproto/workers/processSinglePostWorker.ts
···
··· 1 + import { RichText } from "@atproto/api" 2 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs.js" 3 + import { Op } from "sequelize" 4 + import { EmojiReaction, Media, Notification, Post, PostAncestor, PostMentionsUserRelation, PostReport, PostTag, QuestionPoll, Quotes, RemoteUserPostView, SilencedPost, User, UserBitesPostRelation, UserBookmarkedPosts, UserLikesPostRelations } from '../../models/index.js' 5 + import { getPostThreadRecursive } from "../../utils/activitypub/getPostThreadRecursive.js" 6 + import { completeEnvironment } from "../../utils/backendOptions.js" 7 + import { getAllLocalUserIds } from "../../utils/cacheGetters/getAllLocalUserIds.js" 8 + import { logger } from "../../utils/logger.js" 9 + import { bulkCreateNotifications, createNotification } from "../../utils/pushNotifications.js" 10 + import { wait } from "../../utils/wait.js" 11 + import { getQuotedPostUri, getAtProtoThread } from "../utils/getAtProtoThread.js" 12 + import { getAtprotoUser } from "../utils/getAtprotoUser.js" 13 + import { MediaAttributes } from "../../models/media.js" 14 + import { Privacy, InteractionControlType, InteractionControl } from "../../models/post.js" 15 + import { getAdminUser } from "../../utils/getAdminAndDeletedUser.js" 16 + import { Job } from "bullmq" 17 + 18 + const adminUser = getAdminUser(); 19 + 20 + async function processSinglePostJob(job: Job): Promise<string | undefined> { 21 + if (!job.data.post) { 22 + return undefined; 23 + } 24 + let post = await processSinglePost(job.data.post, job.data.parentId, job.data.forceUpdate) 25 + return post 26 + } 27 + 28 + async function processSinglePost( 29 + post: PostView, 30 + parentId?: string, 31 + forceUpdate?: boolean 32 + ): Promise<string | undefined> { 33 + if (!post || !completeEnvironment.enableBsky) { 34 + return undefined 35 + } 36 + if (!forceUpdate) { 37 + const existingPost = await Post.findOne({ 38 + where: { 39 + bskyUri: post.uri 40 + } 41 + }) 42 + if (existingPost) { 43 + return existingPost.id 44 + } 45 + } 46 + let postCreator: User | undefined 47 + try { 48 + postCreator = await getAtprotoUser(post.author.did, (await adminUser) as User, post.author) 49 + } catch (error) { 50 + logger.debug({ 51 + message: `Problem obtaining user from post`, 52 + post, 53 + parentId, 54 + forceUpdate, 55 + error 56 + }) 57 + } 58 + let verifiedFedi: string | undefined; 59 + if ('fediverseId' in post.record || 'bridgyOriginalUrl' in post.record) { 60 + if ('bridgyOriginalUrl' in post.record) { 61 + const res = await fetch("https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc" + `?identifier=${post.author.did}`); 62 + if (res.ok) { 63 + const json = await res.json() as { pds: string }; 64 + if (json.pds.replace(/^https?:\/\//, '') === 'atproto.brid.gy') { 65 + // if user is on bridgy pds, verify it 66 + verifiedFedi = post.record.bridgyOriginalUrl as string; 67 + logger.info({ uri: post.uri, url: post.record.bridgyOriginalUrl }, 'fedi bridged post is bridgy fed'); 68 + } 69 + } 70 + } else { 71 + // prob wafrn post, but lets verify it 72 + try { 73 + const waf = await fetch(`https://${new URL(post.record.fediverseId as string).hostname}/api/environment`) 74 + if (waf.ok) { 75 + const res = await fetch((post.record.fediverseId as string).replace('fediverse/', 'api/v2/'), { 76 + headers: { 77 + 'Accept': 'application/json' 78 + } 79 + }) 80 + if (res.ok) { 81 + const json = await res.json() as { posts: { bskyCid: string }[] }; 82 + if (json.posts[0].bskyCid === post.cid) { 83 + verifiedFedi = post.record.fediverseId as string; 84 + logger.info({ uri: post.uri, url: post.record.fediverseId }, 'fedi bridged post is wafrn'); 85 + } 86 + } 87 + } 88 + } catch (error) { 89 + logger.debug(error, `Error in obtaining fedi post ${post.record.fediverseId}`) 90 + } 91 + } 92 + } 93 + if (verifiedFedi) { 94 + try { 95 + const remotePost = await getPostThreadRecursive(postCreator, verifiedFedi); 96 + if (remotePost) { 97 + await getPostThreadRecursive(postCreator, verifiedFedi, undefined, remotePost.id) 98 + remotePost.bskyCid = post.cid; 99 + remotePost.bskyUri = post.uri; 100 + // if there's already a bsky post about 101 + // this that doesn't have any fedi urls, delete it 102 + // and prob update the things 103 + let existingPost = await Post.findOne({ 104 + where: { 105 + bskyCid: post.cid, 106 + remotePostId: null 107 + } 108 + }) 109 + if (existingPost) { 110 + // very expensive updates! but only happens when user 111 + // searches existing post that is alr on db 112 + await EmojiReaction.update({ 113 + postId: remotePost.id 114 + }, { 115 + where: { 116 + postId: existingPost.id 117 + } 118 + }) 119 + await Notification.update({ 120 + postId: remotePost.id 121 + }, { 122 + where: { 123 + postId: existingPost.id 124 + } 125 + }) 126 + await PostReport.update({ 127 + postId: remotePost.id 128 + }, { 129 + where: { 130 + postId: existingPost.id 131 + } 132 + }) 133 + try { 134 + await PostAncestor.update({ 135 + postsId: remotePost.id 136 + }, { 137 + where: { 138 + postsId: existingPost.id 139 + } 140 + }) 141 + } catch {} 142 + await QuestionPoll.update({ 143 + postId: remotePost.id 144 + }, { 145 + where: { 146 + postId: existingPost.id 147 + } 148 + }) 149 + await Quotes.update({ 150 + quoterPostId: remotePost.id 151 + }, { 152 + where: { 153 + quoterPostId: existingPost.id 154 + } 155 + }) 156 + if (!await Quotes.findOne({ 157 + where: { 158 + quotedPostId: remotePost.id 159 + } 160 + })) { 161 + await Quotes.update({ 162 + quotedPostId: remotePost.id 163 + }, { 164 + where: { 165 + quotedPostId: existingPost.id 166 + } 167 + }) 168 + } 169 + await RemoteUserPostView.update({ 170 + postId: remotePost.id 171 + }, { 172 + where: { 173 + postId: existingPost.id 174 + } 175 + }) 176 + await SilencedPost.update({ 177 + postId: remotePost.id 178 + }, { 179 + where: { 180 + postId: existingPost.id 181 + } 182 + }) 183 + await SilencedPost.update({ 184 + postId: remotePost.id 185 + }, { 186 + where: { 187 + postId: existingPost.id 188 + } 189 + }) 190 + await UserBitesPostRelation.update({ 191 + postId: remotePost.id 192 + }, { 193 + where: { 194 + postId: existingPost.id 195 + } 196 + }) 197 + await UserBookmarkedPosts.update({ 198 + postId: remotePost.id 199 + }, { 200 + where: { 201 + postId: existingPost.id 202 + } 203 + }) 204 + await UserLikesPostRelations.update({ 205 + postId: remotePost.id 206 + }, { 207 + where: { 208 + postId: existingPost.id 209 + } 210 + }) 211 + await Post.update({ 212 + parentId: remotePost.id 213 + }, { 214 + where: { 215 + parentId: existingPost.id 216 + } 217 + }) 218 + 219 + await Post.destroy({ 220 + where: { 221 + bskyCid: post.cid, 222 + remotePostId: null 223 + } 224 + }) 225 + } 226 + await remotePost.save() 227 + return remotePost.id 228 + } 229 + } catch (error) { 230 + logger.debug({ 231 + message: `Error in obtaining fedi post ${verifiedFedi}`, 232 + error 233 + }) 234 + } 235 + } 236 + if (!postCreator || !post) { 237 + const usr = postCreator ? postCreator : await User.findOne({ where: { url: completeEnvironment.deletedUser } }) 238 + 239 + const invalidPost = await Post.create({ 240 + userId: usr?.id, 241 + content: `Failed to get atproto post`, 242 + parentId: parentId, 243 + isDeleted: true, 244 + createdAt: new Date(0), 245 + updatedAt: new Date(0) 246 + }) 247 + return invalidPost.id 248 + } 249 + if (postCreator) { 250 + const medias = getPostMedias(post) 251 + let tags: string[] = [] 252 + let mentions: string[] = [] 253 + let record = post.record as any 254 + let postText = record.text 255 + let federatedWoot = false 256 + if (record.fullText || record.bridgyOriginalText) { 257 + federatedWoot = true 258 + tags = record.fullTags?.split('\n').filter((x: string) => !!x) ?? [] // also detect full tags 259 + postText = record.fullText ?? record.bridgyOriginalText 260 + } 261 + if (record.facets && record.facets.length > 0 && !federatedWoot) { 262 + // lets get mentions 263 + const mentionedDids = record.facets 264 + .flatMap((elem: any) => elem.features) 265 + .map((elem: any) => elem.did) 266 + .filter((elem: any) => elem) 267 + if (mentionedDids && mentionedDids.length > 0) { 268 + const mentionedUsers = await User.findAll({ 269 + where: { 270 + bskyDid: { 271 + [Op.in]: mentionedDids 272 + } 273 + } 274 + }) 275 + mentions = mentionedUsers.map((elem) => elem.id) 276 + } 277 + 278 + const rt = new RichText({ 279 + text: postText, 280 + facets: record.facets 281 + }) 282 + let text = '' 283 + 284 + for (const segment of rt.segments()) { 285 + if (segment.isLink()) { 286 + const href = segment.link?.uri 287 + text += `<a href="${href}" target="_blank">${href}</a>` 288 + } else if (segment.isMention()) { 289 + const href = `${completeEnvironment.frontendUrl}/blog/${segment.mention?.did}` 290 + text += `<a href="${href}" target="_blank">${segment.text}</a>` 291 + } else if (segment.isTag()) { 292 + const href = `${completeEnvironment.frontendUrl}/dashboard/search/${segment.text.substring(1)}` 293 + text += `<a href="${href}" target="_blank">${segment.text}</a>` 294 + tags.push(segment.text.substring(1)) 295 + } else { 296 + text += segment.text 297 + } 298 + } 299 + postText = text 300 + } 301 + if (!federatedWoot) postText = postText.replaceAll('\n', '<br>') 302 + 303 + const labels = getPostLabels(post) 304 + let cw = labels.length > 0 ? `Post is labeled as: ${labels.join(', ')}` : undefined 305 + if (!cw && postCreator.NSFW) { 306 + cw = 'This user has been marked as NSFW and the post has been labeled automatically as NSFW' 307 + } 308 + const newData = { 309 + userId: postCreator.id, 310 + bskyCid: post.cid, 311 + bskyUri: post.uri, 312 + content: postText, 313 + createdAt: new Date((post.record as any).createdAt), 314 + privacy: Privacy.Public, 315 + parentId: parentId, 316 + content_warning: cw, 317 + ...getPostInteractionLevels(post, parentId) 318 + } 319 + if (!parentId) { 320 + delete newData.parentId 321 + } 322 + 323 + if ((await getAllLocalUserIds()).includes(newData.userId) && !forceUpdate) { 324 + // dirty as hell but this should stop the duplication 325 + await wait(1500) 326 + } 327 + let [postToProcess, created] = await Post.findOrCreate({ where: { bskyUri: post.uri }, defaults: newData }) 328 + // do not update existing posts. But what if local user creates a post through bsky? then we force updte i guess 329 + if (!(await getAllLocalUserIds()).includes(postToProcess.userId) || created) { 330 + if (!created) { 331 + postToProcess.set(newData) 332 + await postToProcess.save() 333 + } 334 + if (medias) { 335 + await Media.destroy({ 336 + where: { 337 + postId: postToProcess.id 338 + } 339 + }) 340 + await Media.bulkCreate( 341 + medias.map((media: any) => { 342 + return { ...media, postId: postToProcess.id } 343 + }) 344 + ) 345 + } 346 + if (parentId) { 347 + const ancestors = await postToProcess.getAncestors({ 348 + attributes: ['userId'], 349 + where: { 350 + hierarchyLevel: { 351 + [Op.gt]: postToProcess.hierarchyLevel - 5 352 + } 353 + } 354 + }) 355 + mentions = mentions.concat(ancestors.map((elem) => elem.userId)) 356 + } 357 + mentions = [...new Set(mentions)] 358 + if (mentions.length > 0) { 359 + await Notification.destroy({ 360 + where: { 361 + notificationType: 'MENTION', 362 + postId: postToProcess.id 363 + } 364 + }) 365 + await PostMentionsUserRelation.destroy({ 366 + where: { 367 + postId: postToProcess.id 368 + } 369 + }) 370 + await bulkCreateNotifications( 371 + mentions.map((mnt) => ({ 372 + notificationType: 'MENTION', 373 + postId: postToProcess.id, 374 + notifiedUserId: mnt, 375 + userId: postToProcess.userId, 376 + createdAt: new Date(postToProcess.createdAt) 377 + })), 378 + { 379 + ignoreDuplicates: true, 380 + postContent: postText, 381 + userUrl: postCreator.url 382 + } 383 + ) 384 + await PostMentionsUserRelation.bulkCreate( 385 + mentions.map((mnt) => { 386 + return { 387 + userId: mnt, 388 + postId: postToProcess.id 389 + } 390 + }), 391 + { ignoreDuplicates: true } 392 + ) 393 + } 394 + if (tags.length > 0) { 395 + await PostTag.destroy({ 396 + where: { 397 + postId: postToProcess.id 398 + } 399 + }) 400 + await PostTag.bulkCreate( 401 + tags.map((tag) => { 402 + return { 403 + postId: postToProcess.id, 404 + tagName: tag 405 + } 406 + }) 407 + ) 408 + } 409 + const quotedPostUri = getQuotedPostUri(post) 410 + if (quotedPostUri) { 411 + const quotedPostId = await getAtProtoThread(quotedPostUri) 412 + if (quotedPostId) { 413 + const quotedPost = await Post.findByPk(quotedPostId) 414 + if (quotedPost) { 415 + await createNotification( 416 + { 417 + notificationType: 'QUOTE', 418 + notifiedUserId: quotedPost.userId, 419 + userId: postToProcess.userId, 420 + postId: postToProcess.id 421 + }, 422 + { 423 + postContent: postToProcess.content, 424 + userUrl: postCreator?.url 425 + } 426 + ) 427 + await Quotes.findOrCreate({ 428 + where: { 429 + quoterPostId: postToProcess.id, 430 + quotedPostId: quotedPostId 431 + } 432 + }) 433 + } 434 + } 435 + } 436 + } 437 + 438 + return postToProcess.id 439 + } 440 + } 441 + 442 + function getPostMedias(post: PostView) { 443 + let res: MediaAttributes[] = [] 444 + const labels = getPostLabels(post) 445 + const embed = (post.record as any).embed 446 + if (embed) { 447 + if (embed.external) { 448 + res = res.concat([ 449 + { 450 + mediaType: !embed.external.uri.startsWith('https://media.ternor.com/') ? 'text/html' : 'image/gif', 451 + description: embed.external.title, 452 + url: embed.external.uri, 453 + mediaOrder: 0, 454 + external: true 455 + } 456 + ]) 457 + } 458 + if (embed.images || embed.media) { 459 + // case with quote and gif / link preview 460 + if (embed.media?.external) { 461 + res = res.concat([ 462 + { 463 + mediaType: !embed.media.external.uri.startsWith('https://media.ternor.com/') ? 'text/html' : 'image/gif', 464 + description: embed.media.external.title, 465 + url: embed.media.external.uri, 466 + mediaOrder: 0, 467 + external: true 468 + } 469 + ]) 470 + } else { 471 + const thingToProcess = embed.images ? embed.images : embed.media.images 472 + if (thingToProcess) { 473 + const toConcat = thingToProcess.map((media: any, index: any) => { 474 + const cid = media.image.ref['$link'] ? media.image.ref['$link'] : media.image.ref.toString() 475 + const did = post.author.did 476 + return { 477 + mediaType: media.image.mimeType, 478 + description: media.alt, 479 + height: media.aspectRatio?.height, 480 + width: media.aspectRatio?.width, 481 + url: `?cid=${encodeURIComponent(cid)}&did=${encodeURIComponent(did)}`, 482 + mediaOrder: index, 483 + external: true 484 + } 485 + }) 486 + res = res.concat(toConcat) 487 + } else { 488 + logger.debug({ 489 + message: `Bsky problem getting medias on post ${post.uri}` 490 + }) 491 + } 492 + } 493 + } 494 + if (embed.video) { 495 + const video = embed.video 496 + const cid = video.ref['$link'] ? video.ref['$link'] : video.ref.toString() 497 + const did = post.author.did 498 + res = res.concat([ 499 + { 500 + mediaType: embed.video.mimeType, 501 + description: '', 502 + height: embed.aspectRatio?.height, 503 + width: embed.aspectRatio?.width, 504 + url: `?cid=${encodeURIComponent(cid)}&did=${encodeURIComponent(did)}`, 505 + mediaOrder: 0, 506 + external: true 507 + } 508 + ]) 509 + } 510 + } 511 + return res.map((m) => { 512 + return { 513 + ...m, 514 + NSFW: labels.length > 0 515 + } 516 + }) 517 + } 518 + 519 + // TODO improve this so we get better nsfw messages lol 520 + function getPostLabels(post: PostView) { 521 + let labels = new Set<string>() 522 + if (post.labels) { 523 + for (const label of post.labels) { 524 + if (label.neg && labels.has(label.val)) { 525 + labels.delete(label.val) 526 + } else { 527 + labels.add(label.val) 528 + } 529 + } 530 + } 531 + return Array.from(labels) 532 + } 533 + 534 + function getPostInteractionLevels( 535 + post: PostView, 536 + parentId: string | undefined 537 + ): { 538 + replyControl: InteractionControlType 539 + likeControl: InteractionControlType 540 + reblogControl: InteractionControlType 541 + quoteControl: InteractionControlType 542 + } { 543 + let canQuote = InteractionControl.Anyone 544 + let canReply: InteractionControlType = InteractionControl.Anyone 545 + if (post.viewer && post.viewer.embeddingDisabled) { 546 + canQuote = InteractionControl.NoOne 547 + } 548 + if (parentId) { 549 + canReply = InteractionControl.SameAsOp 550 + canQuote = InteractionControl.SameAsOp 551 + } else if (post.threadgate && post.threadgate.record && (post.threadgate.record as any).allow) { 552 + const allowList = (post.threadgate.record as any).allow 553 + if (allowList.length == 0) { 554 + canReply = InteractionControl.NoOne 555 + } else { 556 + const mentiontypes: string[] = allowList 557 + .map((elem: any) => elem['$type']) 558 + .map((elem: string) => elem.split('app.bsky.feed.threadgate#')[1]) 559 + if (mentiontypes.includes('mentionRule')) { 560 + if (mentiontypes.includes('followingRule')) { 561 + canReply = mentiontypes.includes('followerRule') 562 + ? InteractionControl.FollowersFollowersAndMentioned 563 + : InteractionControl.FollowingAndMentioned 564 + } else { 565 + canReply = mentiontypes.includes('followerRule') 566 + ? InteractionControl.FollowersAndMentioned 567 + : InteractionControl.MentionedUsersOnly 568 + } 569 + } else { 570 + if (mentiontypes.includes('followingRule')) { 571 + canReply = mentiontypes.includes('followerRule') 572 + ? InteractionControl.FollowersAndFollowing 573 + : InteractionControl.Following 574 + } else { 575 + canReply = mentiontypes.includes('followerRule') ? InteractionControl.Followers : InteractionControl.NoOne 576 + } 577 + } 578 + } 579 + } 580 + 581 + if (canQuote === InteractionControl.Anyone && canReply != InteractionControl.Anyone) { 582 + canQuote = canReply 583 + } 584 + 585 + return { 586 + quoteControl: canQuote, 587 + replyControl: canReply, 588 + likeControl: InteractionControl.Anyone, 589 + reblogControl: InteractionControl.Anyone 590 + } 591 + } 592 + 593 + export { processSinglePostJob }
packages/backend/cache/.gitkeep

This is a binary file and will not be displayed.

+2 -3
packages/backend/contexts/litepub-0.1.jsonld
··· 5 { 6 "Emoji": "toot:Emoji", 7 "Hashtag": "as:Hashtag", 8 - "WafrnHashtag": "as:WafrnHashtag", 9 "PropertyValue": "schema:PropertyValue", 10 "atomUri": "ostatus:atomUri", 11 "conversation": { ··· 23 "value": "schema:value", 24 "sensitive": "as:sensitive", 25 "litepub": "http://litepub.social/ns#", 26 - "wafrn": "https://wafrn.net/ns#", 27 "invisible": "litepub:invisible", 28 "directMessage": "litepub:directMessage", 29 "listMessage": { ··· 50 } 51 } 52 ] 53 - }
··· 5 { 6 "Emoji": "toot:Emoji", 7 "Hashtag": "as:Hashtag", 8 + "WafrnHashtag":"as:WafrnHashtag", 9 "PropertyValue": "schema:PropertyValue", 10 "atomUri": "ostatus:atomUri", 11 "conversation": { ··· 23 "value": "schema:value", 24 "sensitive": "as:sensitive", 25 "litepub": "http://litepub.social/ns#", 26 "invisible": "litepub:invisible", 27 "directMessage": "litepub:directMessage", 28 "listMessage": { ··· 49 } 50 } 51 ] 52 + }
+1 -9
packages/backend/environment.dev.ts
··· 123 externalCacheurl: '/api/cache?media=', 124 shortenPosts: 3, 125 disablePWA: false, 126 - maintenance: false, 127 - oidcAuthName: 'OpenID', 128 - oidcEnabled: true, 129 - }, 130 - oidcConfig: { 131 - enabled: true, 132 - issuer: 'http://auth.localhost/', 133 - clientId: 'wafrn', 134 - clientSecret: "this is a secret" 135 } 136 }
··· 123 externalCacheurl: '/api/cache?media=', 124 shortenPosts: 3, 125 disablePWA: false, 126 + maintenance: false 127 } 128 }
+1 -9
packages/backend/environment.example.ts
··· 121 shortenPosts: ${{FRONTEND_SHORTEN_POSTS:-3}}, 122 disablePWA: ${{FRONTEND_DISABLE_PWA:-false}}, 123 maintenance: ${{FRONTEND_MAINTENANCE:-false}}, 124 - enableRawOutput: ${{ENABLE_RAW_OUTPUT:-false}}, 125 - oidcAuthName: '${{OIDC_AUTH_NAME:-OpenID}}', 126 - oidcEnabled: ${{OIDC_ENABLED:-false}} 127 - }, 128 - oidcConfig: { 129 - enabled: ${{OIDC_ENABLED:-false}}, 130 - issuer: '${{OIDC_ISSUER:-http://auth.localhost/}}', 131 - clientId: '${{OIDC_CLIENT_ID:-wafrn}}', 132 - clientSecret: '${{OIDC_CLIENT_SECRET:-secret}}' 133 } 134 }
··· 121 shortenPosts: ${{FRONTEND_SHORTEN_POSTS:-3}}, 122 disablePWA: ${{FRONTEND_DISABLE_PWA:-false}}, 123 maintenance: ${{FRONTEND_MAINTENANCE:-false}}, 124 + enableRawOutput: ${{ENABLE_RAW_OUTPUT:-false}} 125 } 126 }
-6
packages/backend/environment.local.example.ts
··· 119 disablePWA: false, 120 maintenance: false, 121 enableRawOutput: true 122 - }, 123 - oidcConfig: { 124 - enabled: true, 125 - issuer: 'http://auth.localhost/', 126 - clientId: 'wafrn', 127 - clientSecret: "this is a secret" 128 } 129 }
··· 119 disablePWA: false, 120 maintenance: false, 121 enableRawOutput: true 122 } 123 }
+1 -7
packages/backend/interfaces/environment.ts
··· 78 webpushPrivateKey: string 79 webpushPublicKey: string 80 webpushEmail: string 81 - frontendEnvironment: any, 82 - oidcConfig: { 83 - enabled: boolean, 84 - issuer: string, 85 - clientId: string, 86 - clientSecret: string 87 - } 88 }
··· 78 webpushPrivateKey: string 79 webpushPublicKey: string 80 webpushEmail: string 81 + frontendEnvironment: any 82 }
+1 -2
packages/backend/package.json
··· 1 { 2 "name": "wafrn-backend", 3 - "version": "2025.10.03-DEV+JBC", 4 "description": "wafrn backend", 5 "main": "index.js", 6 "type": "module", ··· 87 "node-cron": "^4.2.1", 88 "node-stream-zip": "^1.15.0", 89 "nodemailer": "^6.7.2", 90 - "openid-client": "^6.8.1", 91 "otpauth": "^9.4.0", 92 "pg": "^8.11.0", 93 "pg-hstore": "^2.3.4",
··· 1 { 2 "name": "wafrn-backend", 3 + "version": "2025.10.03-DEV", 4 "description": "wafrn backend", 5 "main": "index.js", 6 "type": "module", ··· 87 "node-cron": "^4.2.1", 88 "node-stream-zip": "^1.15.0", 89 "nodemailer": "^6.7.2", 90 "otpauth": "^9.4.0", 91 "pg": "^8.11.0", 92 "pg-hstore": "^2.3.4",
+1 -1
packages/backend/routes/activitypub/activitypub.ts
··· 56 getCheckFediverseSignatureFunction(false), 57 async (req: SignedRequest, res: Response) => { 58 const url = req.params.url.toLowerCase() 59 - if (req.headers['accept']?.includes('*') && !req.query.jsonld) { 60 res.redirect(`/blog/${url}`) 61 return 62 }
··· 56 getCheckFediverseSignatureFunction(false), 57 async (req: SignedRequest, res: Response) => { 58 const url = req.params.url.toLowerCase() 59 + if (req.headers['accept']?.includes('*')) { 60 res.redirect(`/blog/${url}`) 61 return 62 }
+3 -29
packages/backend/routes/activitypub/well-known.ts
··· 4 import { User, Post } from '../../models/index.js' 5 import { getAllLocalUserIds } from '../../utils/cacheGetters/getAllLocalUserIds.js' 6 import { return404 } from '../../utils/return404.js' 7 - import { getAdminUser } from '../../utils/getAdminAndDeletedUser.js' 8 import fs from 'fs' 9 10 // @ts-ignore cacher has no types ··· 15 const cacher = new Cacher() 16 17 function wellKnownRoutes(app: Application) { 18 - app.get('/.well-known/security.txt', (req, res) => { 19 - res.send( 20 - `Contact: mailto:jb@jbc.lol 21 - Expires: 2030-12-24T21:50:00Z` 22 - ) 23 - res.end() 24 - }) 25 - 26 // webfinger protocol 27 app.get('/.well-known/host-meta', (req: Request, res) => { 28 res.send( ··· 103 activated: true 104 } 105 }) 106 - const adminUser = await getAdminUser(); 107 const activeUsersSixMonths = await User.count({ 108 where: { 109 id: { ··· 147 version: '2.0', 148 software: { 149 name: 'wafrn', 150 - version: packageJsonFile.version, 151 - homepage: "https://codeberg.org/wafrn/wafrn", 152 - repository: "https://codeberg.org/wafrn/wafrn" 153 }, 154 protocols: ['activitypub'], 155 services: { 156 - outbound: [ 157 - "atom1.0" 158 - ], 159 inbound: [] 160 }, 161 usage: { ··· 173 } 174 }) 175 }, 176 - openRegistrations: false, 177 metadata: { 178 - nodeName: completeEnvironment.defaultSEOData.title, 179 - nodeDescription: completeEnvironment.defaultSEOData.description, 180 - nodeAdmins: [ 181 - { 182 - name: adminUser.url, 183 - email: adminUser.url + '@' + completeEnvironment.instanceUrl 184 - } 185 - ], 186 - maintainer: { 187 - name: adminUser.url, 188 - email: adminUser.url + '@' + completeEnvironment.instanceUrl 189 - }, 190 themeColor: '#96d8d1' 191 } 192 }
··· 4 import { User, Post } from '../../models/index.js' 5 import { getAllLocalUserIds } from '../../utils/cacheGetters/getAllLocalUserIds.js' 6 import { return404 } from '../../utils/return404.js' 7 import fs from 'fs' 8 9 // @ts-ignore cacher has no types ··· 14 const cacher = new Cacher() 15 16 function wellKnownRoutes(app: Application) { 17 // webfinger protocol 18 app.get('/.well-known/host-meta', (req: Request, res) => { 19 res.send( ··· 94 activated: true 95 } 96 }) 97 const activeUsersSixMonths = await User.count({ 98 where: { 99 id: { ··· 137 version: '2.0', 138 software: { 139 name: 'wafrn', 140 + version: packageJsonFile.version 141 }, 142 protocols: ['activitypub'], 143 services: { 144 + outbound: [], 145 inbound: [] 146 }, 147 usage: { ··· 159 } 160 }) 161 }, 162 + openRegistrations: true, 163 metadata: { 164 themeColor: '#96d8d1' 165 } 166 }
+2 -2
packages/backend/routes/bite.ts
··· 15 export default function biteRoutes(app: Application) { 16 app.post( 17 '/api/bitePost', 18 - // biteLimiter, // no 19 authenticateToken, 20 forceUpdateLastActive, 21 async (req: AuthorizedRequest, res: Response) => { ··· 79 80 app.post( 81 '/api/bite', 82 - // biteLimiter, no. 83 authenticateToken, 84 forceUpdateLastActive, 85 async (req: AuthorizedRequest, res: Response) => {
··· 15 export default function biteRoutes(app: Application) { 16 app.post( 17 '/api/bitePost', 18 + biteLimiter, 19 authenticateToken, 20 forceUpdateLastActive, 21 async (req: AuthorizedRequest, res: Response) => { ··· 79 80 app.post( 81 '/api/bite', 82 + biteLimiter, 83 authenticateToken, 84 forceUpdateLastActive, 85 async (req: AuthorizedRequest, res: Response) => {
+86 -23
packages/backend/routes/dashboard.ts
··· 46 let whereObject: any = { 47 privacy: Privacy.Public 48 } 49 switch (level) { 50 case 2: { 51 let hideReblogs = false ··· 61 } 62 const followedUsers = getFollowedsIds(posterId, true) 63 const nonFollowedUsers = getNonFollowedLocalUsersIds(posterId) 64 - whereObject = { 65 - [Op.or]: [ 66 - { 67 - privacy: { 68 - [Op.in]: [Privacy.Public, Privacy.FollowersOnly, Privacy.LocalOnly] 69 }, 70 - userId: { 71 - [Op.in]: await followedUsers 72 - } 73 - }, 74 - { 75 - privacy: { 76 - [Op.in]: req.jwtData?.userId ? [Privacy.Public, Privacy.LocalOnly] : [Privacy.Public] // only display public if not logged in 77 }, 78 - userId: { 79 - [Op.in]: await nonFollowedUsers 80 } 81 - }, 82 - { 83 - userId: posterId, 84 - privacy: { 85 - [Op.ne]: Privacy.DirectMessage 86 - } 87 - } 88 - ], 89 isReblog: { 90 [Op.in]: hideReblogs ? [false, null] : [true, false, null] 91 } 92 } 93 break 94 } 95 case 1: { ··· 98 userId: { [Op.in]: await getFollowedsIds(posterId) } 99 } 100 ] 101 const subscribedTags = await getFollowedHashtags(posterId) 102 if (subscribedTags && subscribedTags.length > 0) { 103 // query: get posts with hashtag thing 104 postsWithTags = PostTag.findAll({ ··· 131 order: [['createdAt', 'DESC']] 132 }) 133 } 134 whereObject = { 135 privacy: { [Op.in]: [Privacy.Public, Privacy.FollowersOnly, Privacy.LocalOnly, Privacy.Unlisted] }, 136 - [Op.or]: orConditions 137 } 138 break 139 } 140 case 0: { ··· 153 } 154 ] 155 } 156 break 157 } 158 case 10: {
··· 46 let whereObject: any = { 47 privacy: Privacy.Public 48 } 49 + 50 + const dbOptiondisableReplies = await UserOptions.findOne({ 51 + where: { 52 + userId: posterId, 53 + optionName: 'wafrn.disableReplies' 54 + } 55 + }) 56 + 57 + const disableReplies = dbOptiondisableReplies?.optionValue === 'true' 58 + const disableRepliesOr = [ 59 + { 60 + isReblog: true 61 + }, 62 + { 63 + parentId: null 64 + } 65 + ] 66 + 67 switch (level) { 68 case 2: { 69 let hideReblogs = false ··· 79 } 80 const followedUsers = getFollowedsIds(posterId, true) 81 const nonFollowedUsers = getNonFollowedLocalUsersIds(posterId) 82 + 83 + const and: any = [ 84 + { 85 + [Op.or]: [ 86 + { 87 + privacy: { 88 + [Op.in]: [Privacy.Public, Privacy.FollowersOnly, Privacy.LocalOnly] 89 + }, 90 + userId: { 91 + [Op.in]: await followedUsers 92 + } 93 }, 94 + { 95 + privacy: { 96 + [Op.in]: req.jwtData?.userId ? [Privacy.Public, Privacy.LocalOnly] : [Privacy.Public] // only display public if not logged in 97 + }, 98 + userId: { 99 + [Op.in]: await nonFollowedUsers 100 + } 101 }, 102 + { 103 + userId: posterId, 104 + privacy: { 105 + [Op.ne]: Privacy.DirectMessage 106 + } 107 } 108 + ] 109 + } 110 + ] 111 + 112 + if (disableReplies) { 113 + and.push({ 114 + [Op.or]: disableRepliesOr 115 + }) 116 + } 117 + 118 + whereObject = { 119 + [Op.and]: and, 120 isReblog: { 121 [Op.in]: hideReblogs ? [false, null] : [true, false, null] 122 } 123 } 124 + 125 break 126 } 127 case 1: { ··· 130 userId: { [Op.in]: await getFollowedsIds(posterId) } 131 } 132 ] 133 + 134 + const dbOptionDisableRewootsDashboard = await UserOptions.findOne({ 135 + where: { 136 + userId: posterId, 137 + optionName: 'wafrn.disableRewootsDashboard' 138 + } 139 + }) 140 + 141 + const hideReblogs = dbOptionDisableRewootsDashboard?.optionValue === 'true' 142 const subscribedTags = await getFollowedHashtags(posterId) 143 + 144 if (subscribedTags && subscribedTags.length > 0) { 145 // query: get posts with hashtag thing 146 postsWithTags = PostTag.findAll({ ··· 173 order: [['createdAt', 'DESC']] 174 }) 175 } 176 + 177 + const and: any = [ 178 + { 179 + [Op.or]: orConditions 180 + } 181 + ] 182 + 183 + if (disableReplies) { 184 + and.push({ 185 + [Op.or]: disableRepliesOr 186 + }) 187 + } 188 + 189 whereObject = { 190 privacy: { [Op.in]: [Privacy.Public, Privacy.FollowersOnly, Privacy.LocalOnly, Privacy.Unlisted] }, 191 + isReblog: { 192 + [Op.in]: hideReblogs ? [false, null] : [true, false, null] 193 + }, 194 + [Op.and]: and 195 } 196 + 197 break 198 } 199 case 0: { ··· 212 } 213 ] 214 } 215 + 216 + if (disableReplies) 217 + whereObject.parentId = null 218 + 219 break 220 } 221 case 10: {
+87 -167
packages/backend/routes/users.ts
··· 23 import sendEmail from '../utils/sendEmail.js' 24 import validateEmail from '../utils/validateEmail.js' 25 import bcrypt from 'bcrypt' 26 - import jwt, { JwtPayload } from 'jsonwebtoken' 27 import { sequelize } from '../models/index.js' 28 29 import optimizeMedia from '../utils/optimizeMedia.js' ··· 66 import { getAllLocalUserIds } from '../utils/cacheGetters/getAllLocalUserIds.js' 67 import { syncBskyFollowersAndFollowing } from '../utils/atproto/syncBskyFollowersAndFollowing.js' 68 import { getAdminUser } from '../utils/getAdminAndDeletedUser.js' 69 - import * as openId from 'openid-client' 70 71 const markdownConverter = new showdown.Converter({ 72 simplifiedAutoLink: true, ··· 96 ? completeEnvironment.bskyPds 97 : 'https://' + completeEnvironment.bskyPds 98 : '' 99 - 100 - const openIdConfig: openId.Configuration | undefined = (completeEnvironment.oidcConfig.enabled) ? await openId.discovery( 101 - new URL(completeEnvironment.oidcConfig.issuer), 102 - completeEnvironment.oidcConfig.clientId, 103 - completeEnvironment.oidcConfig.clientSecret, 104 - ) : undefined 105 106 const deletePostQueue = new Queue('deletePostQueue', { 107 connection: completeEnvironment.bullmqConnection, ··· 116 } 117 }) 118 119 function userRoutes(app: Application) { 120 app.post( 121 '/api/register', ··· 129 req.body?.email && 130 req.body.url && 131 req.body.url.match(/^[a-z0-9_A-Z]+([\_-]+[a-z0-9_A-Z]+)*$/i) && 132 - validateEmail(req.body.email) 133 ) { 134 const birthDate = new Date(req.body.birthDate) 135 const minimumAge = new Date() ··· 195 const emailSent = completeEnvironment.disableRequireSendEmail 196 ? true 197 : sendEmail({ 198 - email, 199 - subject: `Welcome to ${instanceHost}, please verify your email!`, 200 - body: `\ 201 <h1>Welcome to ${instanceUrl}</h1> 202 <p>To activate your account, <a href="${activationLink}">verify your email</a>.</p> 203 <br /> 204 <p>If you can't see the link above, copy this link: ${activationLink}</p> 205 ` 206 - }) 207 await Promise.all([userWithEmail, emailSent]) 208 await generateUserKeyPairQueue.add('generateUserKeyPair', { userId: (await userWithEmail).id }) 209 success = true ··· 594 } 595 }) 596 597 - app.get('/api/login/oidc', loginRateLimiter, async (req, res) => { 598 - if (!completeEnvironment.oidcConfig.enabled || !openIdConfig) { 599 - res.redirect('/login?error=openid_disabled') 600 - return 601 - } 602 - 603 - let redirectUri = new URL('/api/login/oidc/callback', completeEnvironment.frontendUrl).href 604 - let scope = 'openid email profile' 605 - 606 - const stateNonce = openId.randomNonce() 607 - const stateToken = openId.randomState() 608 - let state = jwt.sign( 609 - { 610 - openIdStep: 1, 611 - stateToken, 612 - stateNonce, 613 - }, 614 - completeEnvironment.jwtSecret, 615 - { expiresIn: '10m' } 616 - ) 617 - 618 - const decodedState = jwt.verify(`${state}`, completeEnvironment.jwtSecret, {}); 619 - console.log(state, decodedState); 620 - 621 - let parameters: Record<string, string> = { 622 - redirect_uri: redirectUri, 623 - state, 624 - scope, 625 - nonce: stateNonce 626 - } 627 - 628 - let redirectTo: URL = openId.buildAuthorizationUrl(openIdConfig, parameters) 629 - 630 - res.redirect(301, redirectTo.href); 631 - }) 632 - 633 - app.get('/api/login/oidc/callback', loginRateLimiter, async (req, res) => { 634 - if (!completeEnvironment.oidcConfig.enabled || !openIdConfig) { 635 - res.redirect('/login?error=openid_disabled') 636 - return 637 - } 638 - 639 - if (!req.query.code || !req.query.state) { 640 - return res.redirect('/login?error=missing_parameters'); 641 - } 642 - 643 - try { 644 - const stateToken = req.query.state as string; 645 - console.log(req.query); 646 - const decodedState = jwt.verify(`${stateToken}`, completeEnvironment.jwtSecret, { clockTolerance: 5 }); 647 - 648 - const url = new URL(req.url, `https://${req.headers.host ?? 'localhost'}`); 649 - 650 - const tokenSet: openId.TokenEndpointResponse = await openId.authorizationCodeGrant( 651 - openIdConfig, 652 - url, 653 - { 654 - expectedNonce: (decodedState as JwtPayload).stateNonce, 655 - expectedState: stateToken, 656 - } 657 - ) 658 - 659 - const userInfo = await openId.fetchUserInfo( 660 - openIdConfig, 661 - tokenSet.access_token, 662 - openId.skipSubjectCheck 663 - ) 664 - 665 - if (!userInfo.email) { 666 - res.redirect('/login?error=no_email_configured') 667 - return 668 - } 669 - 670 - const userWithEmail = await User.scope('full').findOne({ 671 - where: { 672 - email: userInfo.email.toLowerCase().trim(), 673 - banned: { 674 - [Op.ne]: true 675 - } 676 - } 677 - }) 678 - 679 - if (!userWithEmail) { 680 - res.redirect(`/register?email=${userInfo.email}&username=${userInfo.preferred_username}`) 681 - return 682 - } 683 - 684 - if (userWithEmail.activated) { 685 - const mfaEnabled = await MfaDetails.findAll({ 686 - where: { 687 - userId: userWithEmail.id, 688 - enabled: { 689 - [Op.eq]: true 690 - } 691 - } 692 - }) 693 - if (mfaEnabled.length > 0) { 694 - const token = jwt.sign( 695 - { 696 - mfaStep: 1, 697 - email: userWithEmail.email?.toLowerCase() 698 - }, 699 - completeEnvironment.jwtSecret, 700 - { expiresIn: '300s' } 701 - ) 702 - res.redirect(`/login?openid_token=${token}&mfa_required=true`) 703 - } else { 704 - const token = jwt.sign( 705 - { 706 - userId: userWithEmail.id, 707 - email: userWithEmail.email?.toLowerCase(), 708 - birthDate: userWithEmail.birthDate, 709 - url: userWithEmail.url, 710 - role: userWithEmail.role 711 - }, 712 - completeEnvironment.jwtSecret, 713 - { expiresIn: '31536000s' } 714 - ) 715 - res.redirect(`/login?openid_token=${token}`) 716 - userWithEmail.lastLoginIp = getIp(req, true) 717 - await userWithEmail.save() 718 - } 719 - } else { 720 - res.redirect('/login?error=user_not_activated') 721 - } 722 - } catch (e) { 723 - console.error(e); 724 - res.redirect('/login?error=openid_failed_auth') 725 - } 726 - }) 727 - 728 app.post('/api/login/mfa', [loginRateLimiter, optionalAuthentication], async (req: AuthorizedRequest, res: any) => { 729 let success = false 730 try { ··· 1004 let followed = blog.isRemoteUser 1005 ? blog.followingCount 1006 : Follows.count({ 1007 - where: { 1008 - followerId: blog.id, 1009 - accepted: true 1010 - } 1011 - }) 1012 let followers = blog.isRemoteUser 1013 ? blog.followerCount 1014 : Follows.count({ 1015 - where: { 1016 - followedId: blog.id, 1017 - accepted: true 1018 - } 1019 - }) 1020 const publicOptions = UserOptions.findAll({ 1021 where: { 1022 userId: blog.id, ··· 1055 1056 const postCount = blog 1057 ? await Post.count({ 1058 - where: { 1059 - userId: blog.id 1060 - } 1061 - }) 1062 : 0 1063 1064 followed = await followed ··· 1557 if (req.body.anonymous) { 1558 req.jwtData = undefined 1559 } 1560 const lastHourAsks = await Ask.count({ 1561 where: { 1562 creationIp: getIp(req), ··· 1814 message = `Alias not detected` 1815 } 1816 } 1817 - } catch (error) { } 1818 } 1819 1820 res.status(success ? 200 : 500) ··· 1902 }) 1903 userOption 1904 ? await userOption.update({ 1905 - optionValue: option.value, 1906 - public: option.public == true 1907 - }) 1908 : await UserOptions.create({ 1909 - userId: posterId, 1910 - optionName: option.name, 1911 - optionValue: option.value, 1912 - public: option.public == true 1913 - }) 1914 } 1915 } 1916 }
··· 23 import sendEmail from '../utils/sendEmail.js' 24 import validateEmail from '../utils/validateEmail.js' 25 import bcrypt from 'bcrypt' 26 + import jwt from 'jsonwebtoken' 27 import { sequelize } from '../models/index.js' 28 29 import optimizeMedia from '../utils/optimizeMedia.js' ··· 66 import { getAllLocalUserIds } from '../utils/cacheGetters/getAllLocalUserIds.js' 67 import { syncBskyFollowersAndFollowing } from '../utils/atproto/syncBskyFollowersAndFollowing.js' 68 import { getAdminUser } from '../utils/getAdminAndDeletedUser.js' 69 70 const markdownConverter = new showdown.Converter({ 71 simplifiedAutoLink: true, ··· 95 ? completeEnvironment.bskyPds 96 : 'https://' + completeEnvironment.bskyPds 97 : '' 98 99 const deletePostQueue = new Queue('deletePostQueue', { 100 connection: completeEnvironment.bullmqConnection, ··· 109 } 110 }) 111 112 + 113 + const slurs = [ 114 + "chinaman", 115 + "chinamen", 116 + "chink", 117 + "coolie", 118 + "coon", 119 + "eskimo", 120 + "golliwog", 121 + "gook", 122 + "gyp", 123 + "gypsy", 124 + "half-breed", 125 + "halfbreed", 126 + "heeb", 127 + "jap", 128 + "kaffer", 129 + "kaffir", 130 + "kaffir", 131 + "kaffre", 132 + "kafir", 133 + "kike", 134 + "kraut", 135 + "negress", 136 + "negro", 137 + "nig", 138 + "nig-nog", 139 + "nigga", 140 + "nigger", 141 + "nigguh", 142 + "pajeet", 143 + "paki", 144 + "pickaninnie", 145 + "pickaninny", 146 + "raghead", 147 + "retard", 148 + "sambo", 149 + "shemale", 150 + "soyboy", 151 + "spade", 152 + "sperg", 153 + "spic", 154 + "squaw", 155 + "tard", 156 + "wetback", 157 + "wigger", 158 + "wop", 159 + "yid", 160 + ] 161 + 162 function userRoutes(app: Application) { 163 app.post( 164 '/api/register', ··· 172 req.body?.email && 173 req.body.url && 174 req.body.url.match(/^[a-z0-9_A-Z]+([\_-]+[a-z0-9_A-Z]+)*$/i) && 175 + validateEmail(req.body.email) && 176 + !slurs.includes(req.body.url.toLowerCase() && 177 + slurs.every(elem => !req.body.url.includes(elem))) 178 ) { 179 const birthDate = new Date(req.body.birthDate) 180 const minimumAge = new Date() ··· 240 const emailSent = completeEnvironment.disableRequireSendEmail 241 ? true 242 : sendEmail({ 243 + email, 244 + subject: `Welcome to ${instanceHost}, please verify your email!`, 245 + body: `\ 246 <h1>Welcome to ${instanceUrl}</h1> 247 <p>To activate your account, <a href="${activationLink}">verify your email</a>.</p> 248 <br /> 249 <p>If you can't see the link above, copy this link: ${activationLink}</p> 250 ` 251 + }) 252 await Promise.all([userWithEmail, emailSent]) 253 await generateUserKeyPairQueue.add('generateUserKeyPair', { userId: (await userWithEmail).id }) 254 success = true ··· 639 } 640 }) 641 642 app.post('/api/login/mfa', [loginRateLimiter, optionalAuthentication], async (req: AuthorizedRequest, res: any) => { 643 let success = false 644 try { ··· 918 let followed = blog.isRemoteUser 919 ? blog.followingCount 920 : Follows.count({ 921 + where: { 922 + followerId: blog.id, 923 + accepted: true 924 + } 925 + }) 926 let followers = blog.isRemoteUser 927 ? blog.followerCount 928 : Follows.count({ 929 + where: { 930 + followedId: blog.id, 931 + accepted: true 932 + } 933 + }) 934 const publicOptions = UserOptions.findAll({ 935 where: { 936 userId: blog.id, ··· 969 970 const postCount = blog 971 ? await Post.count({ 972 + where: { 973 + userId: blog.id 974 + } 975 + }) 976 : 0 977 978 followed = await followed ··· 1471 if (req.body.anonymous) { 1472 req.jwtData = undefined 1473 } 1474 + if(req.body.question) { 1475 + if(slurs.includes(req.body.question.toLowerCase()) || req.body.question.toLowerCase().includes('kill yourself')) { 1476 + res.status(400) 1477 + return res.send({error: true, message: 'Your ask seems to be harmful. Fuck you.'}) 1478 + } 1479 + } 1480 const lastHourAsks = await Ask.count({ 1481 where: { 1482 creationIp: getIp(req), ··· 1734 message = `Alias not detected` 1735 } 1736 } 1737 + } catch (error) {} 1738 } 1739 1740 res.status(success ? 200 : 500) ··· 1822 }) 1823 userOption 1824 ? await userOption.update({ 1825 + optionValue: option.value, 1826 + public: option.public == true 1827 + }) 1828 : await UserOptions.create({ 1829 + userId: posterId, 1830 + optionName: option.name, 1831 + optionValue: option.value, 1832 + public: option.public == true 1833 + }) 1834 } 1835 } 1836 }
+7 -12
packages/backend/routes/websocket.ts
··· 49 const token = msgAsObject.object 50 jwt.verify(token, completeEnvironment.jwtSecret, async (err: any, jwtData: any) => { 51 if (err) { 52 - ws.close(); 53 - console.error('websocket error', err); 54 } 55 if (!jwtData?.userId) { 56 - console.error('websocket error', jwtData) 57 ws.close() 58 } else { 59 authorized = true ··· 64 } 65 }) 66 } else { 67 - console.error('websocket error', msgAsObject); 68 ws.close() 69 } 70 break ··· 79 } 80 }) 81 82 - ws.on('close', (msg: string) => { 83 - logger.info('ws closed', msg); 84 - }) 85 86 // if it has been one second and user has not started the auth process, time to kill this process 87 // if user failed auth and for any destiny's reason we are still on it 88 // in case of failure the auth part would take care of killing the connection 89 - // setTimeout(() => { 90 - // if (!authorized && !procesingAuth) { 91 - // ws.close() 92 - // } 93 - // }, 1000) 94 }) 95 }
··· 49 const token = msgAsObject.object 50 jwt.verify(token, completeEnvironment.jwtSecret, async (err: any, jwtData: any) => { 51 if (err) { 52 + ws.close() 53 } 54 if (!jwtData?.userId) { 55 ws.close() 56 } else { 57 authorized = true ··· 62 } 63 }) 64 } else { 65 ws.close() 66 } 67 break ··· 76 } 77 }) 78 79 + ws.on('close', (msg: string) => {}) 80 81 // if it has been one second and user has not started the auth process, time to kill this process 82 // if user failed auth and for any destiny's reason we are still on it 83 // in case of failure the auth part would take care of killing the connection 84 + setTimeout(() => { 85 + if (!authorized && !procesingAuth) { 86 + ws.close() 87 + } 88 + }, 1000) 89 }) 90 }
packages/backend/uploads/.gitkeep

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/.gitkeep

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_cool.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_evil.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_evil_3c.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_heart.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_party.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_pc.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_pride.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_sad.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_shocked.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_sip.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_thumbsup.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/waffy_woozy.png

This is a binary file and will not be displayed.

packages/backend/uploads/emojipacks/waffy/yeah_waffy.png

This is a binary file and will not be displayed.

packages/backend/uploads/themes/.gitkeep

This is a binary file and will not be displayed.

+1 -9
packages/backend/utils/activitypub/contexts.ts
··· 554 value: 'schema:value', 555 sensitive: 'as:sensitive', 556 litepub: 'http://litepub.social/ns#', 557 - wafrn: 'https://wafrn.net/ns#', 558 invisible: 'litepub:invisible', 559 directMessage: 'litepub:directMessage', 560 listMessage: { ··· 631 listenbrainz: 'sharkey:listenbrainz', 632 enableRss: 'sharkey:enableRss', 633 // vcard 634 - vcard: 'http://www.w3.org/2006/vcard/ns#', 635 - // wafrn 636 - wafrn: 'https://wafrn.net/ns#', 637 - bskyDid: 'wafrn:bskyDid', 638 - allowAsks: 'wafrn:allowAsks', 639 - allowAnonymousAsks: 'wafrn:allowAnonymousAsks', 640 - bskyUri: 'wafrn:bskyUri', 641 - bskyCid: 'wafrn:bskyCid' 642 } satisfies Context 643 644 export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition]
··· 554 value: 'schema:value', 555 sensitive: 'as:sensitive', 556 litepub: 'http://litepub.social/ns#', 557 invisible: 'litepub:invisible', 558 directMessage: 'litepub:directMessage', 559 listMessage: { ··· 630 listenbrainz: 'sharkey:listenbrainz', 631 enableRss: 'sharkey:enableRss', 632 // vcard 633 + vcard: 'http://www.w3.org/2006/vcard/ns#' 634 } satisfies Context 635 636 export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition]
+3 -9
packages/backend/utils/activitypub/getPostThreadRecursive.ts
··· 167 const tags = dom('a.hashtag').html('') 168 postTextContent = dom.html() 169 } 170 - 171 if ( 172 postPetition.attachment && 173 postPetition.attachment.length > 0 && ··· 210 content_warning: postPetition.summary 211 ? postPetition.summary 212 : remoteUser.NSFW 213 - ? 'User is marked as NSFW by this instance staff. Possible NSFW without tagging' 214 - : '', 215 createdAt: createdAt, 216 updatedAt: createdAt, 217 userId: remoteUserServerBaned || remoteUser.banned ? (await deletedUser)?.id : remoteUser.id, 218 remotePostId: postPetition.id, 219 - privacy: privacy, 220 - 221 - // if this is there, we're probably talking to another Wafrn instance 222 - // or something that sends this (but mostly Wafrn... well until Bridgy works with this) 223 - bskyUri: postPetition.bskyUri, 224 - bskyCid: postPetition.bskyCid 225 } 226 227 if (postPetition.name) {
··· 167 const tags = dom('a.hashtag').html('') 168 postTextContent = dom.html() 169 } 170 if ( 171 postPetition.attachment && 172 postPetition.attachment.length > 0 && ··· 209 content_warning: postPetition.summary 210 ? postPetition.summary 211 : remoteUser.NSFW 212 + ? 'User is marked as NSFW by this instance staff. Possible NSFW without tagging' 213 + : '', 214 createdAt: createdAt, 215 updatedAt: createdAt, 216 userId: remoteUserServerBaned || remoteUser.banned ? (await deletedUser)?.id : remoteUser.id, 217 remotePostId: postPetition.id, 218 + privacy: privacy 219 } 220 221 if (postPetition.name) {
+6 -4
packages/backend/utils/activitypub/postToJSONLD.ts
··· 207 published: new Date(post.createdAt).toISOString(), 208 updated: new Date(post.updatedAt).toISOString(), 209 url: post.bskyUri 210 - ? [`${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, post.bskyUri] 211 : `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, 212 attributedTo: `${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}`, 213 to: usersToSend.to, ··· 219 quoteUrl: misskeyQuoteURL, 220 _misskey_quote: misskeyQuoteURL, 221 quoteUri: misskeyQuoteURL, 222 - bskyUri: post.bskyUri, 223 - bskyCid: post.bskyCid, 224 // conversation: conversationString, 225 content: (processedContent + tagsAndQuotes).replace(lineBreaksAtEndRegex, ''), 226 attachment: postMedias ··· 230 return { 231 type: 'Document', 232 mediaType: media.mediaType, 233 - url: (media.url.startsWith('?cid') || media.external) ? 234 completeEnvironment.externalCacheurl + encodeURIComponent(media.url) : 235 (completeEnvironment.mediaUrl + media.url), 236 sensitive: media.NSFW ? true : false,
··· 207 published: new Date(post.createdAt).toISOString(), 208 updated: new Date(post.updatedAt).toISOString(), 209 url: post.bskyUri 210 + ? [`${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, { 211 + type: "Link", 212 + rel: "alternate", 213 + href: post.bskyUri 214 + }] 215 : `${completeEnvironment.frontendUrl}/fediverse/post/${post.id}`, 216 attributedTo: `${completeEnvironment.frontendUrl}/fediverse/blog/${localUser.url.toLowerCase()}`, 217 to: usersToSend.to, ··· 223 quoteUrl: misskeyQuoteURL, 224 _misskey_quote: misskeyQuoteURL, 225 quoteUri: misskeyQuoteURL, 226 // conversation: conversationString, 227 content: (processedContent + tagsAndQuotes).replace(lineBreaksAtEndRegex, ''), 228 attachment: postMedias ··· 232 return { 233 type: 'Document', 234 mediaType: media.mediaType, 235 + url: (media.url.startsWith('?cid') || media.external) ? 236 completeEnvironment.externalCacheurl + encodeURIComponent(media.url) : 237 (completeEnvironment.mediaUrl + media.url), 238 sensitive: media.NSFW ? true : false,
+2 -4
packages/backend/utils/activitypub/userToJSONLD.ts
··· 19 let alsoKnownAsList = userOptions.find((elem) => elem.optionName === 'fediverse.public.alsoKnownAs') 20 if (alsoKnownAsList?.optionValue) { 21 try { 22 - const parsedValue = JSON.parse(alsoKnownAsList?.optionValue) 23 if (typeof parsedValue === 'string') { 24 for (let elem of parsedValue.split(',')) { 25 let url = new URL(elem) ··· 62 manuallyApprovesFollowers: user.manuallyAcceptsFollows, 63 discoverable: true, 64 alsoKnownAs: alsoKnownAs, 65 - bskyDid: user.bskyDid, 66 published: user.createdAt, 67 tag: emojis.map((emoji: any) => emojiToAPTag(emoji)), 68 endpoints: { ··· 90 id: `${completeEnvironment.frontendUrl}/fediverse/blog/${user.url.toLowerCase()}#main-key`, 91 owner: `${completeEnvironment.frontendUrl}/fediverse/blog/${user.url.toLowerCase()}`, 92 publicKeyPem: user.publicKey 93 - }, 94 - 95 } 96 97 if (user.userMigratedTo) {
··· 19 let alsoKnownAsList = userOptions.find((elem) => elem.optionName === 'fediverse.public.alsoKnownAs') 20 if (alsoKnownAsList?.optionValue) { 21 try { 22 + const parsedValue = alsoKnownAsList?.optionValue 23 if (typeof parsedValue === 'string') { 24 for (let elem of parsedValue.split(',')) { 25 let url = new URL(elem) ··· 62 manuallyApprovesFollowers: user.manuallyAcceptsFollows, 63 discoverable: true, 64 alsoKnownAs: alsoKnownAs, 65 published: user.createdAt, 66 tag: emojis.map((emoji: any) => emojiToAPTag(emoji)), 67 endpoints: { ··· 89 id: `${completeEnvironment.frontendUrl}/fediverse/blog/${user.url.toLowerCase()}#main-key`, 90 owner: `${completeEnvironment.frontendUrl}/fediverse/blog/${user.url.toLowerCase()}`, 91 publicKeyPem: user.publicKey 92 + } 93 } 94 95 if (user.userMigratedTo) {
-1
packages/backend/utils/queueProcessors/getRemoteActorIdProcessor.ts
··· 96 remoteId: actorUrl, 97 activated: true, 98 federatedHostId: federatedHost.id, 99 - bskyDid: userPetition.bskyDid, 100 remoteMentionUrl: remoteMentionUrl, 101 followersCollectionUrl: userPetition.followers, 102 followingCollectionUrl: userPetition.following,
··· 96 remoteId: actorUrl, 97 activated: true, 98 federatedHostId: federatedHost.id, 99 remoteMentionUrl: remoteMentionUrl, 100 followersCollectionUrl: userPetition.followers, 101 followingCollectionUrl: userPetition.following,
+24 -8
packages/backend/utils/workers.ts
··· 12 import { generateUserKeyPair } from './queueProcessors/generateUserKeyPair.js' 13 import { completeEnvironment } from './backendOptions.js' 14 import { sendPostBsky } from './queueProcessors/sendPostBsky.js' 15 16 logger.info('started worker') 17 const workerInbox = new Worker('inbox', (job: Job) => inboxWorker(job), { ··· 95 96 const workerProcessFirehose = completeEnvironment.enableBsky 97 ? new Worker('firehoseQueue', async (job: Job) => await processFirehose(job), { 98 - connection: completeEnvironment.bullmqConnection, 99 - metrics: { 100 - maxDataPoints: MetricsTime.ONE_WEEK * 2 101 - }, 102 - concurrency: completeEnvironment.workers.medium, 103 - // up to one minute 104 - lockDuration: 60000 105 - }) 106 : null 107 108 const workerSendPushNotification = new Worker( ··· 156 ] 157 if (completeEnvironment.enableBsky) { 158 workers.push(workerProcessFirehose as Worker) 159 workers.push(workerSendPostBsky as Worker) 160 } 161 ··· 186 ] 187 if (completeEnvironment.enableBsky) { 188 workersToLogFail.push(workerProcessFirehose as Worker) 189 workersToLogFail.push(workerSendPostBsky as Worker) 190 } 191 ··· 207 workerProcessRemotePostView, 208 workerProcessRemoteMediaData, 209 workerProcessFirehose, 210 workerSendPushNotification, 211 workerCheckPushNotificationDelivery, 212 workerGenerateUserKeyPair,
··· 12 import { generateUserKeyPair } from './queueProcessors/generateUserKeyPair.js' 13 import { completeEnvironment } from './backendOptions.js' 14 import { sendPostBsky } from './queueProcessors/sendPostBsky.js' 15 + import { processSinglePostJob } from '../atproto/workers/processSinglePostWorker.js' 16 17 logger.info('started worker') 18 const workerInbox = new Worker('inbox', (job: Job) => inboxWorker(job), { ··· 96 97 const workerProcessFirehose = completeEnvironment.enableBsky 98 ? new Worker('firehoseQueue', async (job: Job) => await processFirehose(job), { 99 + connection: completeEnvironment.bullmqConnection, 100 + metrics: { 101 + maxDataPoints: MetricsTime.ONE_WEEK * 2 102 + }, 103 + concurrency: completeEnvironment.workers.medium, 104 + // up to one minute 105 + lockDuration: 60000 106 + }) 107 + : null 108 + 109 + const workerProcessSinglePost = completeEnvironment.enableBsky 110 + ? new Worker('processSinglePost', async (job: Job) => await processSinglePostJob(job), { 111 + connection: completeEnvironment.bullmqConnection, 112 + metrics: { 113 + maxDataPoints: MetricsTime.ONE_WEEK * 2 114 + }, 115 + concurrency: 25, 116 + // up to one minute 117 + lockDuration: 60000 118 + }) 119 : null 120 121 const workerSendPushNotification = new Worker( ··· 169 ] 170 if (completeEnvironment.enableBsky) { 171 workers.push(workerProcessFirehose as Worker) 172 + workers.push(workerProcessSinglePost as Worker) 173 workers.push(workerSendPostBsky as Worker) 174 } 175 ··· 200 ] 201 if (completeEnvironment.enableBsky) { 202 workersToLogFail.push(workerProcessFirehose as Worker) 203 + workersToLogFail.push(workerProcessSinglePost as Worker) 204 workersToLogFail.push(workerSendPostBsky as Worker) 205 } 206 ··· 222 workerProcessRemotePostView, 223 workerProcessRemoteMediaData, 224 workerProcessFirehose, 225 + workerProcessSinglePost, 226 workerSendPushNotification, 227 workerCheckPushNotificationDelivery, 228 workerGenerateUserKeyPair,
-1
packages/caddy/global/disable_https.conf
··· 1 - auto_https disable_redirects
···
+2
packages/frontend/.gitignore
··· 10 # settings 11 Caddyfile 12 13 14 # dependencies 15 /node_modules
··· 10 # settings 11 Caddyfile 12 13 + # override directory 14 + /overrides 15 16 # dependencies 17 /node_modules
+11 -15
packages/frontend/Caddyfile.example
··· 11 12 admin 0.0.0.0:2019 13 14 import /etc/caddy/config/global/* ${{DOMAIN_NAME}} 15 } 16 17 - :9239 { 18 import /etc/caddy/config/media_domain_pre/* ${{DOMAIN_NAME}} ${{MEDIA_DOMAIN}} 19 20 handle { ··· 26 import /etc/caddy/config/media_domain_post/* ${{DOMAIN_NAME}} ${{MEDIA_DOMAIN}} 27 } 28 29 - :9238 { 30 import /etc/caddy/config/cache_domain_pre/* ${{DOMAIN_NAME}} ${{CACHE_DOMAIN}} 31 32 handle /api/cache* { ··· 36 import /etc/caddy/config/cache_domain_post/* ${{DOMAIN_NAME}} ${{CACHE_DOMAIN}} 37 } 38 39 - :9237 { 40 import /etc/caddy/config/main_domain_pre/* ${{DOMAIN_NAME}} 41 42 header * { ··· 70 import /etc/caddy/config/main_domain_post/* ${{DOMAIN_NAME}} 71 } 72 73 - :9241 { 74 import /etc/caddy/config/monitoring_domain_pre/* ${{DOMAIN_NAME}} 75 76 reverse_proxy ${{GRAFANA_HOST:-grafana:2345}} ··· 78 import /etc/caddy/config/monitoring_domain_post/* ${{DOMAIN_NAME}} 79 } 80 81 - :9240 { 82 import /etc/caddy/config/pds_domain_pre/* ${{DOMAIN_NAME}} ${{PDS_DOMAIN_NAME}} 83 84 - handle / { 85 - root * /pds-homepage 86 - try_files {path} /pds.txt 87 - file_server 88 - } 89 - 90 - handle / { 91 - root * /pds-homepage 92 - try_files {path} /pds.txt 93 - file_server 94 } 95 96 handle / {
··· 11 12 admin 0.0.0.0:2019 13 14 + on_demand_tls { 15 + ask http://${{PDS_HOST:-pds:3000}}/tls-check 16 + } 17 + 18 import /etc/caddy/config/global/* ${{DOMAIN_NAME}} 19 } 20 21 + ${{MEDIA_DOMAIN}} { 22 import /etc/caddy/config/media_domain_pre/* ${{DOMAIN_NAME}} ${{MEDIA_DOMAIN}} 23 24 handle { ··· 30 import /etc/caddy/config/media_domain_post/* ${{DOMAIN_NAME}} ${{MEDIA_DOMAIN}} 31 } 32 33 + ${{CACHE_DOMAIN}} { 34 import /etc/caddy/config/cache_domain_pre/* ${{DOMAIN_NAME}} ${{CACHE_DOMAIN}} 35 36 handle /api/cache* { ··· 40 import /etc/caddy/config/cache_domain_post/* ${{DOMAIN_NAME}} ${{CACHE_DOMAIN}} 41 } 42 43 + ${{DOMAIN_NAME}} { 44 import /etc/caddy/config/main_domain_pre/* ${{DOMAIN_NAME}} 45 46 header * { ··· 74 import /etc/caddy/config/main_domain_post/* ${{DOMAIN_NAME}} 75 } 76 77 + monitoring.${{DOMAIN_NAME}} { 78 import /etc/caddy/config/monitoring_domain_pre/* ${{DOMAIN_NAME}} 79 80 reverse_proxy ${{GRAFANA_HOST:-grafana:2345}} ··· 82 import /etc/caddy/config/monitoring_domain_post/* ${{DOMAIN_NAME}} 83 } 84 85 + ${{PDS_DOMAIN_NAME}} *.${{PDS_DOMAIN_NAME}} { 86 import /etc/caddy/config/pds_domain_pre/* ${{DOMAIN_NAME}} ${{PDS_DOMAIN_NAME}} 87 88 + tls { 89 + on_demand 90 } 91 92 handle / {
packages/frontend/overrides/assets/icons/icon-128x128.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-144x144.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-152x152.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-192x192.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-384x384.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-512x512.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-72x72.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/icons/icon-96x96.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/linkpreview.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/logo.avif

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/logo.png

This is a binary file and will not be displayed.

packages/frontend/overrides/assets/logo_w.png

This is a binary file and will not be displayed.

packages/frontend/overrides/favicon.ico

This is a binary file and will not be displayed.

+1 -1
packages/frontend/package.json
··· 1 { 2 "name": "wafrn", 3 - "version": "2025.10.03-DEV+JBC", 4 "scripts": { 5 "ng": "ng", 6 "start": "npm run prebuild && ng serve",
··· 1 { 2 "name": "wafrn", 3 + "version": "2025.10.03-DEV", 4 "scripts": { 5 "ng": "ng", 6 "start": "npm run prebuild && ng serve",
+2 -3
packages/frontend/src/app/app-routing.module.ts
··· 23 24 { 25 path: 'register', 26 - loadChildren: () => import('./pages/register/register.module').then((m) => m.RegisterModule), 27 - canActivate: [isAdminGuard] 28 }, 29 { 30 path: 'checkMail', ··· 143 providers: [{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy }], 144 exports: [RouterModule] 145 }) 146 - export class AppRoutingModule { }
··· 23 24 { 25 path: 'register', 26 + loadChildren: () => import('./pages/register/register.module').then((m) => m.RegisterModule) 27 }, 28 { 29 path: 'checkMail', ··· 142 providers: [{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy }], 143 exports: [RouterModule] 144 }) 145 + export class AppRoutingModule {}
+65 -22
packages/frontend/src/app/components/navigation-menu/navigation-menu.component.ts
··· 265 // JSON driven UI lmao 266 this.menuItems = [ 267 { 268 label: 'menu.login', 269 icon: faArrowRightToBracket, 270 visible: () => !this.loginService.loggedIn.value, ··· 279 divider: true 280 }, 281 { 282 - label: 'menu.dashboard', 283 - icon: faHouse, 284 - visible: () => this.loginService.loggedIn.value, 285 - routerLink: '/dashboard', 286 command: () => { 287 this.hideMenu() 288 } 289 }, 290 { 291 - label: 'menu.exploreFediverse', 292 - icon: faCompass, 293 visible: () => this.loginService.loggedIn.value, 294 - routerLink: '/dashboard/explore', 295 command: () => { 296 this.hideMenu() 297 } 298 }, 299 { 300 - label: 'menu.notifications', 301 - icon: faBell, 302 visible: () => this.loginService.loggedIn.value, 303 - badge: () => this.notificationsService.notifications(), 304 - routerLink: '/dashboard/notifications', 305 - command: () => { 306 - this.hideMenu() 307 - } 308 }, 309 { 310 label: 'menu.search', ··· 321 visible: () => this.loginService.loggedIn.value, 322 badge: () => this.inboxNotifications(), 323 items: [ 324 { 325 label: 'menu.privateMessages', 326 icon: faEnvelope, ··· 579 ], 580 [ 581 { 582 label: 'menu.login', 583 icon: faArrowRightToBracket, 584 visible: () => !this.loginService.loggedIn.value, ··· 698 699 this.menuLinks = [ 700 { 701 - label: 'menu.faq', 702 - url: 'https://wafrn.net/faq/overview.html' 703 }, 704 { 705 - label: 'menu.source', 706 - url: 'https://codeberg.org/wafrn/wafrn' 707 }, 708 { 709 - label: 'menu.status', 710 - url: 'https://uptime.jbc.lol/status/main' 711 }, 712 { 713 - label: 'menu.sourceModified', 714 - url: 'https://codeberg.org/jbcarreon123/wf.jbc.lol' 715 }, 716 { 717 label: 'menu.patreon',
··· 265 // JSON driven UI lmao 266 this.menuItems = [ 267 { 268 + label: 'menu.register', 269 + icon: faUserPlus, 270 + visible: () => !this.loginService.loggedIn.value, 271 + routerLink: '/register', 272 + command: () => { 273 + this.hideMenu() 274 + } 275 + }, 276 + { 277 label: 'menu.login', 278 icon: faArrowRightToBracket, 279 visible: () => !this.loginService.loggedIn.value, ··· 288 divider: true 289 }, 290 { 291 + label: 'menu.exploreWafrn', 292 + icon: faCompass, 293 + visible: () => !this.loginService.loggedIn.value, 294 + routerLink: '/dashboard/exploreLocal', 295 command: () => { 296 this.hideMenu() 297 } 298 }, 299 { 300 + label: 'menu.dashboard', 301 + icon: faHouse, 302 visible: () => this.loginService.loggedIn.value, 303 + routerLink: '/dashboard', 304 command: () => { 305 this.hideMenu() 306 } 307 }, 308 { 309 + label: 'menu.explore', 310 + icon: faCompass, 311 visible: () => this.loginService.loggedIn.value, 312 + items: [ 313 + { 314 + label: 'menu.exploreWafrn', 315 + icon: faServer, 316 + visible: () => this.loginService.loggedIn.value, 317 + routerLink: '/dashboard/exploreLocal', 318 + command: () => { 319 + this.hideMenu() 320 + } 321 + }, 322 + { 323 + label: 'menu.exploreFediverse', 324 + icon: faCompass, 325 + visible: () => this.loginService.loggedIn.value, 326 + routerLink: '/dashboard/explore', 327 + command: () => { 328 + this.hideMenu() 329 + } 330 + } 331 + ] 332 }, 333 { 334 label: 'menu.search', ··· 345 visible: () => this.loginService.loggedIn.value, 346 badge: () => this.inboxNotifications(), 347 items: [ 348 + { 349 + label: 'menu.notifications', 350 + icon: faBell, 351 + visible: () => this.loginService.loggedIn.value, 352 + badge: () => this.notificationsService.notifications(), 353 + routerLink: '/dashboard/notifications', 354 + command: () => { 355 + this.hideMenu() 356 + } 357 + }, 358 { 359 label: 'menu.privateMessages', 360 icon: faEnvelope, ··· 613 ], 614 [ 615 { 616 + label: 'menu.register', 617 + icon: faUserPlus, 618 + visible: () => !this.loginService.loggedIn.value, 619 + routerLink: '/register', 620 + command: () => { 621 + this.hideMenu() 622 + } 623 + }, 624 + { 625 label: 'menu.login', 626 icon: faArrowRightToBracket, 627 visible: () => !this.loginService.loggedIn.value, ··· 741 742 this.menuLinks = [ 743 { 744 + label: 'menu.about', 745 + routerLink: '/about' 746 }, 747 { 748 + label: 'menu.privacy', 749 + routerLink: '/privacy' 750 }, 751 { 752 + label: 'menu.faq', 753 + url: 'https://wafrn.net/faq/overview.html' 754 }, 755 { 756 + label: 'menu.source', 757 + url: 'https://codeberg.org/wafrn/wafrn' 758 }, 759 { 760 label: 'menu.patreon',
+1 -1
packages/frontend/src/app/components/post-actions/post-actions.component.html
··· 37 </span> 38 </a> 39 } 40 - @if (isExternalPost && externalUrl() !== bskyUrl()) { 41 <a [href]="externalUrl()" target="_blank" mat-menu-item> 42 <span class="post-actions-menu-span-content"> 43 {{ 'post-actions.viewOriginalPost' | translate }}
··· 37 </span> 38 </a> 39 } 40 + @if (isExternalPost && (externalUrl() !== bskyUrl())) { 41 <a [href]="externalUrl()" target="_blank" mat-menu-item> 42 <span class="post-actions-menu-span-content"> 43 {{ 'post-actions.viewOriginalPost' | translate }}
+2 -2
packages/frontend/src/app/components/post-actions/post-actions.component.ts
··· 59 bookmarked = computed(() => this.post().bookmarkers.includes(this.myId)) 60 61 bskyUrl = computed<string>(() => { 62 - this.settingsService.settingsModified() // evil fix to update correctly 63 const bskyUri = this.post().bskyUri 64 if (!bskyUri) return '' 65 const parts = bskyUri.split('/app.bsky.feed.post/') 66 const userDid = parts[0].split('at://')[1] 67 return `https://${this.settingsService.values().atprotoLinkDestination || 'bsky.app'}/profile/${userDid}/post/${parts[1]}` 68 }) 69 - externalUrl = computed<string>(() => (this.post().bskyUri ? this.bskyUrl() : this.post().remotePostId)) 70 71 // icons 72 shareIcon = faLink
··· 59 bookmarked = computed(() => this.post().bookmarkers.includes(this.myId)) 60 61 bskyUrl = computed<string>(() => { 62 + this.settingsService.settingsModified() // evil fix to update correctly if (!bskyUri) return '' 63 const bskyUri = this.post().bskyUri 64 if (!bskyUri) return '' 65 const parts = bskyUri.split('/app.bsky.feed.post/') 66 const userDid = parts[0].split('at://')[1] 67 return `https://${this.settingsService.values().atprotoLinkDestination || 'bsky.app'}/profile/${userDid}/post/${parts[1]}` 68 }) 69 + externalUrl = computed<string>(() => (this.post().remotePostId ?? this.bskyUrl())) 70 71 // icons 72 shareIcon = faLink
+1 -1
packages/frontend/src/app/guards/is-admin.guard.ts
··· 5 export const isAdminGuard: CanActivateFn = (route, state) => { 6 const res = inject(JwtService).adminToken() 7 if (!res) { 8 - inject(Router).navigate(['/login']) 9 } 10 return res 11 }
··· 5 export const isAdminGuard: CanActivateFn = (route, state) => { 6 const res = inject(JwtService).adminToken() 7 if (!res) { 8 + inject(Router).navigate(['/']) 9 } 10 return res 11 }
+1 -1
packages/frontend/src/app/guards/login-required.guard.ts
··· 5 export const loginRequiredGuard: CanActivateChildFn = (childRoute, state) => { 6 const res = inject(JwtService).tokenValid() 7 if (!res) { 8 - inject(Router).navigate(['/login']) 9 } 10 return res 11 }
··· 5 export const loginRequiredGuard: CanActivateChildFn = (childRoute, state) => { 6 const res = inject(JwtService).tokenValid() 7 if (!res) { 8 + inject(Router).navigate(['/register']) 9 } 10 return res 11 }
+20 -24
packages/frontend/src/app/pages/login/login.component.html
··· 1 <mat-card class="mb-6 lg:mx-4 mat-card-higher wafrn-container"> 2 <mat-card-header class="pb-3 wafrn-container-header"> 3 - <mat-card-title class="flex gap-2"><fa-icon [icon]="faArrowRightToBracket"></fa-icon><span class="font-medium">{{ 4 - 'login.title' | translate }}</span></mat-card-title> 5 </mat-card-header> 6 <section class="my-6 mx-3 text-center"> 7 <h1 class="text-5xl">{{ 'login.welcomeBack' | translate }}</h1> ··· 10 </p> 11 </section> 12 13 - @if (!isOpenId) { 14 <form class="px-3" [formGroup]="loginForm" (ngSubmit)="onSubmit()"> 15 <mat-form-field class="mb-1 w-full"> 16 <mat-label>{{ 'login.email' | translate }}</mat-label> ··· 20 <mat-form-field class="mb-1 w-full mat-form-field-no-padding"> 21 <mat-label>{{ 'login.password' | translate }}</mat-label> 22 <input formControlName="password" [type]="showPassword ? 'text' : 'password'" name="password" matInput /> 23 - <button mat-icon-button matSuffix type="button" (click)="togglePasswordVisibility()" 24 [attr.aria-label]="showPassword ? ('login.hidePassword' | translate) : ('login.showPassword' | translate)" 25 - [attr.aria-pressed]="showPassword"> 26 <fa-icon [icon]="showPassword ? faEyeSlash : faEye"></fa-icon> 27 </button> 28 </mat-form-field> ··· 31 </div> 32 33 <div class="flex"> 34 - <button [disabled]="!loginForm.valid" mat-flat-button color="primary" extended 35 - class="w-full border-round-md mt-4"> 36 <span class="flex gap-2">{{ 'login.loginButton' | translate }}<fa-icon [icon]="submitIcon"></fa-icon></span> 37 </button> 38 @if (loginForm.disabled) { 39 - <mat-spinner diameter="24" class="mt-5 mx-3"></mat-spinner> 40 } 41 </div> 42 - @if (oidcEnabled) { 43 - <div class="flex"> 44 - <button (click)="redirectToOidc()" type="button" mat-flat-button color="primary" extended 45 - class="w-full border-round-md mt-4"> 46 - <span class="flex gap-2">{{ 'login.oidcButton' | translate:{oidcAuthName} }}<fa-icon 47 - [icon]="submitIcon"></fa-icon></span> 48 - </button> 49 - </div> 50 - } 51 </form> 52 - } 53 - @else { 54 - <section class="text-center"> 55 - <h2>{{ 'login.oidcDialog' | translate:{oidcAuthName:oidcAuthName??'OpenID'} }}</h2> 56 - </section> 57 - } 58 <section class="p-3 mt-3 wafrn-container-footer"> 59 <p class="m-0"> 60 {{ 'login.dontHaveAccount' | translate }} 61 <a routerLink="/register">{{ 'login.register' | translate }}</a> 62 </p> 63 </section> 64 - </mat-card>
··· 1 <mat-card class="mb-6 lg:mx-4 mat-card-higher wafrn-container"> 2 <mat-card-header class="pb-3 wafrn-container-header"> 3 + <mat-card-title class="flex gap-2" 4 + ><fa-icon [icon]="faArrowRightToBracket"></fa-icon 5 + ><span class="font-medium">{{ 'login.title' | translate }}</span></mat-card-title 6 + > 7 </mat-card-header> 8 <section class="my-6 mx-3 text-center"> 9 <h1 class="text-5xl">{{ 'login.welcomeBack' | translate }}</h1> ··· 12 </p> 13 </section> 14 15 <form class="px-3" [formGroup]="loginForm" (ngSubmit)="onSubmit()"> 16 <mat-form-field class="mb-1 w-full"> 17 <mat-label>{{ 'login.email' | translate }}</mat-label> ··· 21 <mat-form-field class="mb-1 w-full mat-form-field-no-padding"> 22 <mat-label>{{ 'login.password' | translate }}</mat-label> 23 <input formControlName="password" [type]="showPassword ? 'text' : 'password'" name="password" matInput /> 24 + <button 25 + mat-icon-button 26 + matSuffix 27 + type="button" 28 + (click)="togglePasswordVisibility()" 29 [attr.aria-label]="showPassword ? ('login.hidePassword' | translate) : ('login.showPassword' | translate)" 30 + [attr.aria-pressed]="showPassword" 31 + > 32 <fa-icon [icon]="showPassword ? faEyeSlash : faEye"></fa-icon> 33 </button> 34 </mat-form-field> ··· 37 </div> 38 39 <div class="flex"> 40 + <button 41 + [disabled]="!loginForm.valid" 42 + mat-flat-button 43 + color="primary" 44 + extended 45 + class="w-full border-round-md mt-4" 46 + > 47 <span class="flex gap-2">{{ 'login.loginButton' | translate }}<fa-icon [icon]="submitIcon"></fa-icon></span> 48 </button> 49 @if (loginForm.disabled) { 50 + <mat-spinner diameter="24" class="mt-5 mx-3"></mat-spinner> 51 } 52 </div> 53 </form> 54 <section class="p-3 mt-3 wafrn-container-footer"> 55 <p class="m-0"> 56 {{ 'login.dontHaveAccount' | translate }} 57 <a routerLink="/register">{{ 'login.register' | translate }}</a> 58 </p> 59 </section> 60 + </mat-card>
-21
packages/frontend/src/app/pages/login/login.component.ts
··· 1 import { Component, OnInit } from '@angular/core' 2 import { LoginService } from 'src/app/services/login.service' 3 - import { EnvironmentService } from 'src/app/services/environment.service' 4 5 import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms' 6 import { MessageService } from 'src/app/services/message.service' ··· 29 faEye = faEye 30 faEyeSlash = faEyeSlash 31 submitIcon = faArrowRight 32 - oidcEnabled = EnvironmentService.environment.oidcEnabled 33 - oidcAuthName = EnvironmentService.environment.oidcAuthName 34 35 showPassword = false // Property to track password visibility 36 ··· 42 loginMessage = "you shouldn't see this ever" 43 loginReset = false 44 45 - isOpenId = window.location.search.includes('openid_token') 46 - 47 constructor( 48 private loginService: LoginService, 49 private messages: MessageService 50 ) { 51 this.newLoginMessage() 52 - 53 - console.log(EnvironmentService.environment) 54 - if (!this.oidcAuthName || !this.oidcEnabled) { 55 - this.oidcEnabled = EnvironmentService.environment.oidcEnabled 56 - this.oidcAuthName = EnvironmentService.environment.oidcAuthName 57 - this.ngOnInit(); 58 - } 59 - 60 - const searchParams = new URLSearchParams(window.location.search) 61 - if (searchParams.get("openid_token")) { 62 - this.loginService.logInWithOpenId(searchParams.get("openid_token") ?? '', searchParams.get("mfa_required") === "true") 63 - } 64 } 65 66 ngOnInit(): void { ··· 86 setTimeout(() => { 87 this.loginReset = true 88 }, 0) 89 - } 90 - 91 - redirectToOidc() { 92 - window.location.pathname = "/api/login/oidc" 93 } 94 95 async onSubmit() {
··· 1 import { Component, OnInit } from '@angular/core' 2 import { LoginService } from 'src/app/services/login.service' 3 4 import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms' 5 import { MessageService } from 'src/app/services/message.service' ··· 28 faEye = faEye 29 faEyeSlash = faEyeSlash 30 submitIcon = faArrowRight 31 32 showPassword = false // Property to track password visibility 33 ··· 39 loginMessage = "you shouldn't see this ever" 40 loginReset = false 41 42 constructor( 43 private loginService: LoginService, 44 private messages: MessageService 45 ) { 46 this.newLoginMessage() 47 } 48 49 ngOnInit(): void { ··· 69 setTimeout(() => { 70 this.loginReset = true 71 }, 0) 72 } 73 74 async onSubmit() {
+42 -21
packages/frontend/src/app/pages/register/register.component.html
··· 1 <mat-card class="pb-3 mb-6 lg:mx-4 mat-card-higher wafrn-container"> 2 <mat-card-header class="pb-3 wafrn-container-header"> 3 - <mat-card-title class="flex gap-2"><fa-icon [icon]="faUserPlus"></fa-icon><span class="font-medium">{{ 4 - 'register.title' | translate }}</span></mat-card-title> 5 </mat-card-header> 6 <section class="my-6 mx-3 text-center"> 7 <h1 class="mb-0 text-5xl">{{ 'register.welcome' | translate }}</h1> 8 </section> 9 10 @if (manuallyReview) { 11 - <section class="mx-3 mb-4"> 12 - <app-info-card type="info">{{ 'register.manualReviewInfo' | translate }}</app-info-card> 13 - </section> 14 } 15 16 <form class="px-3" [formGroup]="loginForm" (ngSubmit)="onSubmit()"> 17 <mat-form-field class="w-full"> 18 <mat-label>Email</mat-label> 19 - <input formControlName="email" type="email" matInput [(ngModel)]="email" /> 20 </mat-form-field> 21 22 <mat-form-field class="w-full"> ··· 30 31 <mat-form-field class="w-full mb-3" subscriptSizing="dynamic"> 32 <mat-label>Username</mat-label> 33 - <input formControlName="url" type="text" matInput [(ngModel)]="username" /> 34 <mat-hint>Right now we do not allow special characters or spaces</mat-hint> 35 <mat-error>Username contains invalid characters</mat-error> 36 </mat-form-field> ··· 43 44 <mat-form-field (click)="picker.open()" class="w-full mb-3" subscriptSizing="dynamic"> 45 <mat-label>Your birth date <b>(There is a minimum registration age!)</b></mat-label> 46 - <input formControlName="birthDate" matInput [min]="minDate" [max]="minimumRegistrationDate" 47 - [matDatepicker]="picker" /> 48 - <mat-hint>MM/DD/YYYY - Your birthday date is required for legal reasons in the european union and USA. Its not 49 - shared 50 - with anyone. Minimum registration age varies depending on the instance</mat-hint> 51 <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle> 52 <mat-datepicker #picker></mat-datepicker> 53 </mat-form-field> ··· 56 <mat-hint>This actually does not do anything at all</mat-hint> 57 <mat-select> 58 @for (gender of genders; track $index) { 59 - <mat-option [value]="gender" [innerHTML]="gender">{{ gender }}</mat-option> 60 } 61 </mat-select> 62 </mat-form-field> ··· 66 <button type="button" mat-stroked-button color="primary" (click)="avatarInput.click()"> 67 <fa-icon [icon]="faUpload" class="m-1"></fa-icon>Choose File 68 </button> 69 - <input #avatarInput formControlName="avatar" id="avatar" type="file" 70 - accept="image/jpeg,image/png,image/webp,image/gif" hidden (change)="imgSelected($event)" /> 71 <p class="m-auto ml-1">{{ selectedFileName }}</p> 72 </div> 73 </div> 74 75 <div class="flex"> 76 - <button color="primary" mat-flat-button extended [disabled]="!loginForm.valid" 77 - class="w-full border-round-md mt-4"> 78 - <span class="flex gap-2">{{ 'register.registerButton' | translate }} <fa-icon 79 - [icon]="submitIcon"></fa-icon></span> 80 </button> 81 @if (loginForm.disabled) { 82 - <mat-spinner diameter="24" class="mt-5 mx-3"></mat-spinner> 83 } 84 </div> 85 </form> 86 - </mat-card>
··· 1 <mat-card class="pb-3 mb-6 lg:mx-4 mat-card-higher wafrn-container"> 2 <mat-card-header class="pb-3 wafrn-container-header"> 3 + <mat-card-title class="flex gap-2" 4 + ><fa-icon [icon]="faUserPlus"></fa-icon 5 + ><span class="font-medium">{{ 'register.title' | translate }}</span></mat-card-title 6 + > 7 </mat-card-header> 8 <section class="my-6 mx-3 text-center"> 9 <h1 class="mb-0 text-5xl">{{ 'register.welcome' | translate }}</h1> 10 </section> 11 12 @if (manuallyReview) { 13 + <section class="mx-3 mb-4"> 14 + <app-info-card type="info">{{ 'register.manualReviewInfo' | translate }}</app-info-card> 15 + </section> 16 } 17 18 <form class="px-3" [formGroup]="loginForm" (ngSubmit)="onSubmit()"> 19 <mat-form-field class="w-full"> 20 <mat-label>Email</mat-label> 21 + <input formControlName="email" type="email" matInput /> 22 </mat-form-field> 23 24 <mat-form-field class="w-full"> ··· 32 33 <mat-form-field class="w-full mb-3" subscriptSizing="dynamic"> 34 <mat-label>Username</mat-label> 35 + <input formControlName="url" type="text" matInput /> 36 <mat-hint>Right now we do not allow special characters or spaces</mat-hint> 37 <mat-error>Username contains invalid characters</mat-error> 38 </mat-form-field> ··· 45 46 <mat-form-field (click)="picker.open()" class="w-full mb-3" subscriptSizing="dynamic"> 47 <mat-label>Your birth date <b>(There is a minimum registration age!)</b></mat-label> 48 + <input 49 + formControlName="birthDate" 50 + matInput 51 + [min]="minDate" 52 + [max]="minimumRegistrationDate" 53 + [matDatepicker]="picker" 54 + /> 55 + <mat-hint 56 + >MM/DD/YYYY - Your birthday date is required for legal reasons in the european union and USA. Its not shared 57 + with anyone. Minimum registration age varies depending on the instance</mat-hint 58 + > 59 <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle> 60 <mat-datepicker #picker></mat-datepicker> 61 </mat-form-field> ··· 64 <mat-hint>This actually does not do anything at all</mat-hint> 65 <mat-select> 66 @for (gender of genders; track $index) { 67 + <mat-option [value]="gender" [innerHTML]="gender">{{ gender }}</mat-option> 68 } 69 </mat-select> 70 </mat-form-field> ··· 74 <button type="button" mat-stroked-button color="primary" (click)="avatarInput.click()"> 75 <fa-icon [icon]="faUpload" class="m-1"></fa-icon>Choose File 76 </button> 77 + <input 78 + #avatarInput 79 + formControlName="avatar" 80 + id="avatar" 81 + type="file" 82 + accept="image/jpeg,image/png,image/webp,image/gif" 83 + hidden 84 + (change)="imgSelected($event)" 85 + /> 86 <p class="m-auto ml-1">{{ selectedFileName }}</p> 87 </div> 88 </div> 89 90 <div class="flex"> 91 + <button 92 + color="primary" 93 + mat-flat-button 94 + extended 95 + [disabled]="!loginForm.valid" 96 + class="w-full border-round-md mt-4" 97 + > 98 + <span class="flex gap-2" 99 + >{{ 'register.registerButton' | translate }} <fa-icon [icon]="submitIcon"></fa-icon 100 + ></span> 101 </button> 102 @if (loginForm.disabled) { 103 + <mat-spinner diameter="24" class="mt-5 mx-3"></mat-spinner> 104 } 105 </div> 106 </form> 107 + </mat-card>
+2 -12
packages/frontend/src/app/pages/register/register.component.ts
··· 5 6 import { faArrowRight, faEye, faEyeSlash, faUpload, faUserPlus } from '@fortawesome/free-solid-svg-icons' 7 import { EnvironmentService } from 'src/app/services/environment.service' 8 - import { ActivatedRoute, Router } from '@angular/router' 9 10 @Component({ 11 selector: 'app-register', ··· 270 avatar: new UntypedFormControl('', []) 271 }) 272 273 - searchParams = new URLSearchParams(window.location.search) 274 - email = this.searchParams.get('email') 275 - username = this.searchParams.get('username') 276 - 277 constructor( 278 private loginService: LoginService, 279 private messages: MessageService, 280 - private router: Router, 281 - private route: ActivatedRoute 282 ) { 283 - this.route.queryParams.subscribe(params => { 284 - this.email = params['email'] 285 - this.username = params['username'] 286 - }) 287 - 288 // minimum age: 14 289 this.minimumRegistrationDate = new Date() 290 this.minimumRegistrationDate.setFullYear(this.minimumRegistrationDate.getFullYear() - 18)
··· 5 6 import { faArrowRight, faEye, faEyeSlash, faUpload, faUserPlus } from '@fortawesome/free-solid-svg-icons' 7 import { EnvironmentService } from 'src/app/services/environment.service' 8 + import { Router } from '@angular/router' 9 10 @Component({ 11 selector: 'app-register', ··· 270 avatar: new UntypedFormControl('', []) 271 }) 272 273 constructor( 274 private loginService: LoginService, 275 private messages: MessageService, 276 + private router: Router 277 ) { 278 // minimum age: 14 279 this.minimumRegistrationDate = new Date() 280 this.minimumRegistrationDate.setFullYear(this.minimumRegistrationDate.getFullYear() - 18)
+3 -19
packages/frontend/src/app/services/login.service.ts
··· 94 return success 95 } 96 97 - async logInWithOpenId(token: string, mfaRequired = false): Promise<boolean> { 98 - let success = false 99 - if (mfaRequired) { 100 - success = true 101 - // HACK DO NOT TOUCH THE ASYNC. IM SERIOUS. IT WOULD SKIP THIS SCREEN 102 - // IF YOU TOUCH THIS CODE YOU NEED TO TEST LOGIN WITH AN MFA ACC 103 - await this.router.navigate(['/login/mfa']) 104 - await this.addToken(token) 105 - } else { 106 - await this.addToken(token) 107 - await this.handleSuccessfulLogin() 108 - await this.router.navigate(['/dashboard']) 109 - success = true 110 - } 111 - 112 - return success 113 - } 114 - 115 async logInMfa(loginMfaForm: UntypedFormGroup): Promise<boolean> { 116 let success = false 117 try { ··· 368 hideQuotes: 'wafrn.hideQuotes', 369 displayMentionsOfBlockedUsersFromOtherUsers: 'wafrn.displayMentionsOfBlockedUsersFromOtherUsers', 370 hideNoDescriptionMedia: 'wafrn.hideNoDescriptionMedia', 371 - disableRewootsExploreLocal: 'wafrn.disableRewootsExploreLocal' 372 } 373 374 try {
··· 94 return success 95 } 96 97 async logInMfa(loginMfaForm: UntypedFormGroup): Promise<boolean> { 98 let success = false 99 try { ··· 350 hideQuotes: 'wafrn.hideQuotes', 351 displayMentionsOfBlockedUsersFromOtherUsers: 'wafrn.displayMentionsOfBlockedUsersFromOtherUsers', 352 hideNoDescriptionMedia: 'wafrn.hideNoDescriptionMedia', 353 + disableRewootsExploreLocal: 'wafrn.disableRewootsExploreLocal', 354 + disableRewootsDashboard: 'wafrn.disableRewootsDashboard', 355 + disableReplies: 'wafrn.disableReplies' 356 } 357 358 try {
+21 -1
packages/frontend/src/app/services/settings.service.ts
··· 84 'confettiMultiplier', 85 'flatConfetti', 86 'disableRewootsExploreLocal', 87 'confirmOpenCw', 88 'confirmOpenCwAnnoyance', 89 'disableLinkPreviews' ··· 403 type: 'checkbox', 404 default: false 405 }, 406 automaticallyExpandPosts: { 407 key: 'automaticallyExpandPosts', 408 translationKey: 'settings.automaticallyExpandPosts', ··· 698 { type: 'header', value: 'settings.header.dashboardBehavior' }, 699 { type: 'key', value: 'defaultDashboard' }, 700 { type: 'key', value: 'disableRewootsExploreLocal' }, 701 { type: 'key', value: 'automaticallyExpandPosts' }, 702 { type: 'key', value: 'expandQuotes' }, 703 { type: 'key', value: 'disableLinkPreviews' }, ··· 826 try { 827 this.fediAttachments.length = 0 828 this.fediAttachments.push(...JSON.parse(rawAttachments.optionValue)) 829 - } catch (error) {} 830 831 if (this.fediAttachments.length === 0) { 832 this.fediAttachments.push({ name: '', value: '' })
··· 84 'confettiMultiplier', 85 'flatConfetti', 86 'disableRewootsExploreLocal', 87 + 'disableRewootsDashboard', 88 + 'disableReplies', 89 'confirmOpenCw', 90 'confirmOpenCwAnnoyance', 91 'disableLinkPreviews' ··· 405 type: 'checkbox', 406 default: false 407 }, 408 + disableRewootsDashboard: { 409 + key: 'disableRewootsDashboard', 410 + translationKey: 'settings.disableRewootsDashboard', 411 + serverKey: 'wafrn.disableRewootsDashboard', 412 + localStorageKey: 'disableRewootsDashboard', 413 + type: 'checkbox', 414 + default: false 415 + }, 416 + disableReplies: { 417 + key: 'disableReplies', 418 + translationKey: 'settings.disableReplies', 419 + serverKey: 'wafrn.disableReplies', 420 + localStorageKey: 'disableReplies', 421 + type: 'checkbox', 422 + default: false 423 + }, 424 automaticallyExpandPosts: { 425 key: 'automaticallyExpandPosts', 426 translationKey: 'settings.automaticallyExpandPosts', ··· 716 { type: 'header', value: 'settings.header.dashboardBehavior' }, 717 { type: 'key', value: 'defaultDashboard' }, 718 { type: 'key', value: 'disableRewootsExploreLocal' }, 719 + { type: 'key', value: 'disableRewootsDashboard' }, 720 + { type: 'key', value: 'disableReplies' }, 721 { type: 'key', value: 'automaticallyExpandPosts' }, 722 { type: 'key', value: 'expandQuotes' }, 723 { type: 'key', value: 'disableLinkPreviews' }, ··· 846 try { 847 this.fediAttachments.length = 0 848 this.fediAttachments.push(...JSON.parse(rawAttachments.optionValue)) 849 + } catch (error) { } 850 851 if (this.fediAttachments.length === 0) { 852 this.fediAttachments.push({ name: '', value: '' })
packages/frontend/src/assets/Censor_Mb 2.png

This is a binary file and will not be displayed.

packages/frontend/src/assets/Desktop - 1.png

This is a binary file and will not be displayed.

+4 -1
packages/frontend/src/assets/i18n/br.json
··· 370 "instanceOnly": "Somente Local", 371 "unlisted": "Não Listado" 372 }, 373 "mutedWords": "Palavras silenciadas", 374 "mutedWordsDescription": "Uma por linha. Woots com estas frases serão escondidos por trás de um aviso de conteúdo.", 375 "superMutedWords": "Palavras bloqueadas", ··· 573 "couldNotFind": "não leva a uma página", 574 "returnHome": "Ir à página inicial" 575 } 576 - }
··· 370 "instanceOnly": "Somente Local", 371 "unlisted": "Não Listado" 372 }, 373 + "disableRewootsExploreLocal": "Desabilitar rewoots no feed Explorar Local", 374 + "disableRewootsDashboard": "Desabilitar rewoots na Linha do Tempo", 375 + "disableReplies": "Desabilitar respostas nos feeds", 376 "mutedWords": "Palavras silenciadas", 377 "mutedWordsDescription": "Uma por linha. Woots com estas frases serão escondidos por trás de um aviso de conteúdo.", 378 "superMutedWords": "Palavras bloqueadas", ··· 576 "couldNotFind": "não leva a uma página", 577 "returnHome": "Ir à página inicial" 578 } 579 + }
+15 -16
packages/frontend/src/assets/i18n/en.json
··· 25 "showPassword": "Show password", 26 "hidePassword": "Hide password", 27 "loginButton": "Log in", 28 - "oidcButton": "Log in with {{ oidcAuthName }}", 29 - "oidcDialog": "Logging in with {{ oidcAuthName }}, please wait...", 30 "checkEmail": "Please check your email", 31 "checkEmailLine1": "We have sent you an email. Please do check your email and the spam folder", 32 "checkEmailLine2": "If you have any problem, write to the instance admin, whose email can be found <a href=\"/about\">here</a>", ··· 56 "writeWoot": "Woot", 57 "notifications": "Notifications", 58 "explore": "Explore", 59 - "exploreWafrn": "Explore wf.jbc.lol", 60 - "exploreFediverse": "wf.jbc.lol & friends", 61 "unansweredAsks": "Unanswered Asks", 62 "privateMessages": "Private messages", 63 "search": "Search", ··· 65 "myBlog": "My blog", 66 "about": "About", 67 "privacy": "Privacy", 68 - "source": "Source (original)", 69 - "status": "Status", 70 - "sourceModified": "Source", 71 "patreon": "Patreon", 72 "kofi": "Ko-fi", 73 "inbox": "Inbox", ··· 79 "faq": "FAQ", 80 "more": "More", 81 "silencedPosts": "Silenced Woots", 82 "settings": { 83 "title": "Settings", 84 "follows": "Manage followers", ··· 205 "notRecommendedOptions": "Not recommended options", 206 "disableNSFWFilter": "Disable NSFW images filter", 207 "automaticallyExpandAllPosts": "Automatically expand all woots", 208 - "displayMentionsOfBlockedUsersFromOtherUsers": "Do not hide woots containing mentions to users in the shadow realm", 209 "hideNoDescriptionMedia": "Automatically CW media with no alt text" 210 }, 211 "privacy": { ··· 370 "hideProfileNotLoggedIn": "Hide profile to users who are not logged in", 371 "hideProfileNotLoggedInDescription": "Only applies to your profile in this wafrn, not to your woots nor other fedi instances or bluesky.", 372 "hideFollows": "Hide my follows and followers", 373 - "displayMentionsOfBlockedUsersFromOtherUsers": "Show woots containing mentions to users in the shadow realm", 374 "defaultPostEditorPrivacy": "Default Woot Editor Privacy", 375 "postEditorPrivacyOptions": { 376 "public": "Public", ··· 379 "unlisted": "Unlisted" 380 }, 381 "disableRewootsExploreLocal": "Disable rewoots in Explore local feed", 382 "mutedWords": "Muted words", 383 "mutedWordsDescription": "One word per line. Woots with these phrases will be placed behind a CW.", 384 "superMutedWords": "Blocked words", ··· 451 "silenceReplyDescription": "Silence notifications from likes, rewoots, reacts, and replies of this woot.", 452 "muteAccountTitle": "Mute account", 453 "muteAccountDescription": "Hide woots and replies by this user. They will still be able to interact with your woots.", 454 - "blockAccountTitle": "Send user to the shadow realm", 455 "muteAccountLabel": "Reason (optional)", 456 "unmuteAccountTitle": "Unmute account", 457 "unmuteAccountDescription": "Woots and replies by this user will be shown again.", ··· 560 "bookmarkPost": "Bookmark woot", 561 "unbookmarkPost": "Unbookmark woot", 562 "likePost": "Like woot", 563 - "dislikePost": "Remove woot", 564 "reportPost": "Report woot", 565 "editPost": "Edit woot", 566 "deletePost": "Delete woot", ··· 569 "unsilenceReplyNotifications": "Unsilence woot", 570 "muteUser": "Mute user", 571 "unmuteUser": "Unmute user", 572 - "blockUser": "Send user to the shadow realm", 573 - "unblockUser": "Retrieve user from the shadow realm", 574 "reportUser": "Report user", 575 - "bitePost": "Bite post", 576 "biteUser": "Bite user" 577 }, 578 "post-header": { ··· 628 }, 629 "typing": { 630 "instructions": "Type the following phrase:", 631 - "verifyLabel": "Verify..." 632 } 633 } 634 - }
··· 25 "showPassword": "Show password", 26 "hidePassword": "Hide password", 27 "loginButton": "Log in", 28 "checkEmail": "Please check your email", 29 "checkEmailLine1": "We have sent you an email. Please do check your email and the spam folder", 30 "checkEmailLine2": "If you have any problem, write to the instance admin, whose email can be found <a href=\"/about\">here</a>", ··· 54 "writeWoot": "Woot", 55 "notifications": "Notifications", 56 "explore": "Explore", 57 + "exploreWafrn": "Explore WAFRN", 58 + "exploreFediverse": "WAFRN & friends", 59 "unansweredAsks": "Unanswered Asks", 60 "privateMessages": "Private messages", 61 "search": "Search", ··· 63 "myBlog": "My blog", 64 "about": "About", 65 "privacy": "Privacy", 66 + "source": "Source", 67 "patreon": "Patreon", 68 "kofi": "Ko-fi", 69 "inbox": "Inbox", ··· 75 "faq": "FAQ", 76 "more": "More", 77 "silencedPosts": "Silenced Woots", 78 + "bookmarkedPosts": "Bookmarked woots", 79 "settings": { 80 "title": "Settings", 81 "follows": "Manage followers", ··· 202 "notRecommendedOptions": "Not recommended options", 203 "disableNSFWFilter": "Disable NSFW images filter", 204 "automaticallyExpandAllPosts": "Automatically expand all woots", 205 + "displayMentionsOfBlockedUsersFromOtherUsers": "Do not hide woots containing mentions to blocked users", 206 "hideNoDescriptionMedia": "Automatically CW media with no alt text" 207 }, 208 "privacy": { ··· 367 "hideProfileNotLoggedIn": "Hide profile to users who are not logged in", 368 "hideProfileNotLoggedInDescription": "Only applies to your profile in this wafrn, not to your woots nor other fedi instances or bluesky.", 369 "hideFollows": "Hide my follows and followers", 370 + "displayMentionsOfBlockedUsersFromOtherUsers": "Show woots containing mentions to blocked users", 371 "defaultPostEditorPrivacy": "Default Woot Editor Privacy", 372 "postEditorPrivacyOptions": { 373 "public": "Public", ··· 376 "unlisted": "Unlisted" 377 }, 378 "disableRewootsExploreLocal": "Disable rewoots in Explore local feed", 379 + "disableRewootsDashboard": "Disable rewoots in Dashboard", 380 + "disableReplies": "Disable replies in feeds", 381 "mutedWords": "Muted words", 382 "mutedWordsDescription": "One word per line. Woots with these phrases will be placed behind a CW.", 383 "superMutedWords": "Blocked words", ··· 450 "silenceReplyDescription": "Silence notifications from likes, rewoots, reacts, and replies of this woot.", 451 "muteAccountTitle": "Mute account", 452 "muteAccountDescription": "Hide woots and replies by this user. They will still be able to interact with your woots.", 453 + "blockAccountTitle": "Block account", 454 "muteAccountLabel": "Reason (optional)", 455 "unmuteAccountTitle": "Unmute account", 456 "unmuteAccountDescription": "Woots and replies by this user will be shown again.", ··· 559 "bookmarkPost": "Bookmark woot", 560 "unbookmarkPost": "Unbookmark woot", 561 "likePost": "Like woot", 562 + "dislikePost": "Remove like", 563 "reportPost": "Report woot", 564 "editPost": "Edit woot", 565 "deletePost": "Delete woot", ··· 568 "unsilenceReplyNotifications": "Unsilence woot", 569 "muteUser": "Mute user", 570 "unmuteUser": "Unmute user", 571 + "blockUser": "Block user", 572 + "unblockUser": "Unblock user", 573 "reportUser": "Report user", 574 + "bitePost": "Bite woot", 575 "biteUser": "Bite user" 576 }, 577 "post-header": { ··· 627 }, 628 "typing": { 629 "instructions": "Type the following phrase:", 630 + "verifyLabel": "Here..." 631 } 632 } 633 + }
+260 -9
packages/frontend/src/assets/i18n/pl.json
··· 1 { 2 "login": { 3 "welcomeBack": "Witaj z powrotem!", 4 "exploreWithoutAccount": "Kliknij tutaj, by przejrzeć Gofra bez konta!", ··· 12 "forgottenPassword": "Zapomniałoś hasła? Kliknij tutaj!", 13 "loginButton": "Zaloguj się" 14 }, 15 "menu": { 16 "dashboard": "Oś czasu", 17 "dashboardHover": "Zobacz oś czasu", 18 - "writeWoot": "Stwórz woota", 19 "notifications": "Powiadomienia", 20 "explore": "Eksploruj", 21 - "exploreWafrn": "Eksploruj Gofra", 22 "exploreFediverse": "Eksploruj Fediwersum", 23 "unansweredAsks": "Pytania bez odpowiedzi", 24 "privateMessages": "Wiadomości prywatne", ··· 30 "patreon": "Patreon", 31 "kofi": "Ko-fi", 32 "logout": "Wyloguj się", 33 "settings": { 34 "title": "Ustawienia", 35 "follows": "Zarządzaj obserwującymi", 36 - "enableBluesky": "Włącz mostek z Bluesky", 37 "editProfile": "Edytuj profil", 38 "themeEditor": "Edytor motywu", 39 "mutedUsers": "Wyciszeni użytkownicy", 40 "mutedPosts": "Wyciszone posty", 41 - "bookmarkedPosts": "Bookmarked posts [TRANSLATION PENDING]", 42 "myBlockedUsers": "Zablokowani użytkownicy", 43 "myBlockedServers": "Zablokowane serwery", 44 "importFollows": "Importuj obserwujących", 45 - "superSecretMenu": "Seekretne Menu" 46 }, 47 "admin": { 48 "title": "Administracja", ··· 55 "awaitingAproval": "Oczekujący na akceptację" 56 } 57 }, 58 "editor": { 59 "inReplyTo": "W odpowiedzi do:", 60 "quoteButton": "Zacytuj woota", ··· 80 "uploadMediaTooltip": "Dodaj media", 81 "contentWarningTooltip": "Ostrzeżenie o treściach wrażliwych" 82 }, 83 "profile": { 84 "security": { 85 "header": "Bezpieczeństwo", ··· 97 "installInstructions": "Jeśli nie masz aplikacji uwierzytelniającej, zalecamy użycie <a href=\"https://getaegis.app/\" target=\"_blank\">Aegis</a> (Android) lub <a href=\"https://2fas.com/\" target=\"_blank\">2FAS</a> (iOS/Android)", 98 "imageAltSecret": "Sekretny kod do uwierzytelnienia to '{{ secret }}'", 99 "secretFallback": "Jeśli nie możesz zeskanować kodu QR, użyj sekretnego kodu '{{ secret }}' w swojej aplikacji", 100 - "confirmDeleteMessage": "Czy napewno chcesz usunąć ten token? Uwaga: Usunięcie wszystkich tokenów wyłączy MFA na twoim koncie", 101 "deleteSuccess": "Token usunięty", 102 "verifySuccess": "Gratulacje, MFA zostało włączone na twoim koncie", 103 "verifyFailed": "Weryfikacja nieudana. Sprawdź swój token, a jeśli dalej masz problem, skontaktuj się z administracją", 104 "errorMessageGeneric": "Wystąpił błąd. Spróbuj ponownie, lub poproś administrację o pomoc", 105 "noMfa": "MFA nie jest włączone na twoim koncie", 106 - "mfaList": "Ustawiłoś poniższe tokeny:", 107 "type": { 108 "totpLabel": "Token" 109 } ··· 116 "misc": "Inne" 117 } 118 }, 119 "ask-dialog-content": { 120 - "askAnonymously": "Anonimowe pytanie", 121 "askFormLabel": "Pytanie", 122 "askButtonText": "Zapytaj" 123 } 124 - }
··· 1 { 2 + "common": { 3 + "commaSeparation": "rozdzielone przecinkami", 4 + "characters": "znaków", 5 + "default": "Domyślne", 6 + "commit": "commit" 7 + }, 8 "login": { 9 "welcomeBack": "Witaj z powrotem!", 10 "exploreWithoutAccount": "Kliknij tutaj, by przejrzeć Gofra bez konta!", ··· 18 "forgottenPassword": "Zapomniałoś hasła? Kliknij tutaj!", 19 "loginButton": "Zaloguj się" 20 }, 21 + "register": { 22 + "title": "Zarejestruj się teraz!", 23 + "welcome": "Witaj w Wafrn!", 24 + "manualReviewInfo": "Administracja zewalułuje twój profil po rejestracji. Może to potrwać kilka godzin", 25 + "registerButton": "Zarejestruj się" 26 + }, 27 + "notifications": { 28 + "title": "Powiadomienia", 29 + "loadingMore": "Ładowanie więcej powiadomień", 30 + "refresh": "Odśwież" 31 + }, 32 + "asks": { 33 + "title": "Zapytania", 34 + "noAsks": "Nie ma więcej zapytań", 35 + "nobodyAsked": "Nikt nie pytał..." 36 + }, 37 "menu": { 38 "dashboard": "Oś czasu", 39 "dashboardHover": "Zobacz oś czasu", 40 + "writeWoot": "Stwórz Woota", 41 "notifications": "Powiadomienia", 42 "explore": "Eksploruj", 43 + "exploreWafrn": "Eksploruj Wafrn", 44 "exploreFediverse": "Eksploruj Fediwersum", 45 "unansweredAsks": "Pytania bez odpowiedzi", 46 "privateMessages": "Wiadomości prywatne", ··· 52 "patreon": "Patreon", 53 "kofi": "Ko-fi", 54 "logout": "Wyloguj się", 55 + "bookmarkedPosts": "Zapisane wooty", 56 "settings": { 57 "title": "Ustawienia", 58 "follows": "Zarządzaj obserwującymi", 59 + "enableBluesky": "Włącz połączenie z Bluesky", 60 "editProfile": "Edytuj profil", 61 "themeEditor": "Edytor motywu", 62 "mutedUsers": "Wyciszeni użytkownicy", 63 "mutedPosts": "Wyciszone posty", 64 + "bookmarkedPosts": "Zapisane wooty", 65 "myBlockedUsers": "Zablokowani użytkownicy", 66 "myBlockedServers": "Zablokowane serwery", 67 "importFollows": "Importuj obserwujących", 68 + "superSecretMenu": "Sekretne menu" 69 }, 70 "admin": { 71 "title": "Administracja", ··· 78 "awaitingAproval": "Oczekujący na akceptację" 79 } 80 }, 81 + "theme-editor": { 82 + "title": "Edytor Motywów", 83 + "templateNote": "Możesz zobaczyć przykłady i kilka wskazówek w", 84 + "themeTextLabel": "Edytuj własny motyw CSS", 85 + "themeTextPlaceholder": "Wpisz tutaj własny motyw CSS...", 86 + "themeGuideName": "Samouczku Motywów", 87 + "updateButton": "Zaktualizuj Motyw" 88 + }, 89 "editor": { 90 "inReplyTo": "W odpowiedzi do:", 91 "quoteButton": "Zacytuj woota", ··· 111 "uploadMediaTooltip": "Dodaj media", 112 "contentWarningTooltip": "Ostrzeżenie o treściach wrażliwych" 113 }, 114 + "blog": { 115 + "tabWoots": "Wooty", 116 + "tabMedia": "Multimedia" 117 + }, 118 "profile": { 119 "security": { 120 "header": "Bezpieczeństwo", ··· 132 "installInstructions": "Jeśli nie masz aplikacji uwierzytelniającej, zalecamy użycie <a href=\"https://getaegis.app/\" target=\"_blank\">Aegis</a> (Android) lub <a href=\"https://2fas.com/\" target=\"_blank\">2FAS</a> (iOS/Android)", 133 "imageAltSecret": "Sekretny kod do uwierzytelnienia to '{{ secret }}'", 134 "secretFallback": "Jeśli nie możesz zeskanować kodu QR, użyj sekretnego kodu '{{ secret }}' w swojej aplikacji", 135 + "confirmDeleteMessage": "Czy na pewno chcesz usunąć ten token? Uwaga: Usunięcie wszystkich tokenów wyłączy MFA na twoim koncie", 136 "deleteSuccess": "Token usunięty", 137 "verifySuccess": "Gratulacje, MFA zostało włączone na twoim koncie", 138 "verifyFailed": "Weryfikacja nieudana. Sprawdź swój token, a jeśli dalej masz problem, skontaktuj się z administracją", 139 "errorMessageGeneric": "Wystąpił błąd. Spróbuj ponownie, lub poproś administrację o pomoc", 140 "noMfa": "MFA nie jest włączone na twoim koncie", 141 + "mfaList": "Ustawione zostały poniższe tokeny:", 142 "type": { 143 "totpLabel": "Token" 144 } ··· 151 "misc": "Inne" 152 } 153 }, 154 + "settings": { 155 + "sidebar": { 156 + "profile": "Profil", 157 + "account": "Konto", 158 + "appearance": "Wygląd", 159 + "behavior": "Zachowanie", 160 + "privacy": "Prywatność", 161 + "mutesAndBlocks": "Wyciszenia i Blokady", 162 + "miscellaneous": "Inne" 163 + }, 164 + "header": { 165 + "fediAttachments": "Atrybuty w Fediwersum", 166 + "emailAndPassword": "Email i hasło", 167 + "integrations": "Integracje", 168 + "migration": "Migracja konta", 169 + "deleteAccount": "Usuwanie Konta", 170 + "appearance": "Wygląd", 171 + "userInterface": "Interfejs", 172 + "classicOptions": "Opcje klasyczne", 173 + "animationsAndSounds": "Animacje i dźwięki", 174 + "dashboardBehavior": "Zachowanie osi czasu", 175 + "cwBehavior": "Zachowanie treści oznaczonych jako nieprzyzwoite", 176 + "profilePrivacy": "Prywatność profilu", 177 + "editor": "Edytor", 178 + "followers": "Obserwujący", 179 + "mutedBlockedWords": "Zablokowane i wyciszone słowa", 180 + "blockBehavior": "Zachowanie blokowania", 181 + "mutedBlockedUsers": "Zablokowani i wyciszeni użytkownicy", 182 + "fun": "Frajda" 183 + }, 184 + "fediPropertyKey": "Nazwa atrybutu", 185 + "fediPropertyValue": "Wartość atrybutu", 186 + "addFediAttachment": "Dodaj atrybut", 187 + "changeAvatar": "Zmień awatar", 188 + "changeHeaderImage": "Zmień baner", 189 + "name": "Nazwa", 190 + "description": "Opis", 191 + "changePasswordTitle": "Rozwiń aby zmienić hasło", 192 + "changePassword": "Zmień hasło", 193 + "changePasswordDescription": "Link do zmiany hasła został wysłany na twój adres email.", 194 + "changePasswordMessage": "Link do zmiany hasła został wysłany na twój adres email.", 195 + "disableEmailNotifications": "Wyłącz powiadomienia email.", 196 + "rssOptions": "Włącz RSS i microfront dla twojego bloga", 197 + "rssOptionsOptions": { 198 + "none": "Brak", 199 + "articles": "Artykuły (opcja w budowie)", 200 + "all": "Wszystko" 201 + }, 202 + "alsoKnownAs": "[FIXME] Znany też jako", 203 + "requestAccountDeletion": "Rozwiń aby usunąć konto", 204 + "alsoKnownAsDescription": "Migracja z innego konta (nie pisz @)", 205 + "theme": "Motyw", 206 + "themeDescription": "Motywy zmieniają kolory, niektóre wpływają na ciemny i jasny tryb.", 207 + "lightDarkMode": "Tryb Jasny/Ciemny ", 208 + "lightDarkModeDescription": "Tryb Jasny/Ciemny. Niektóre motywy nie działają z tymi trybami", 209 + "additionalStyleModes": "Dodatkowa stylizacja", 210 + "additionalStyleModesDescription": "Dodatkowe opcje dla układu oraz klasycznego interfejsu.", 211 + "useOtherUserCustomThemes": "Używaj motywów innych blogów", 212 + "useOtherUserCustomThemesDescription": "Używaj motywów CSS które zostały zdefiniowane w innych blogach. Własne motywy nie są moderowane i mogą zawierać elementy migoczące", 213 + "askToUseOtherUserCustomThemes": "Pytaj czy używać motywów innych blogów", 214 + "askToUseOtherUserCustomThemesDescription": "Otrzymasz pytanie jeśli wejdziesz na bloga z własnym motywem.", 215 + "uiLanguage": "Język interfejsu użytkownika", 216 + "uiLanguageDescription": "Wafrn jest tłumaczony przez wolontariuszy. Sprawdź <a href=\"https://codeberg.org/wafrn/wafrn\">kod źródłowy</a> w jaki sposób możesz pomóc!", 217 + "forceClassicLogo": "Wymuś klasyczne logo", 218 + "forceClassicVideoPlayer": "Wymuś klasyczny odtwarzacz wideo", 219 + "forceClassicAudioPlayer": "Wymuś klasyczny odtwarzacz dźwięków", 220 + "forceClassicMediaView": "Wymuś klasyczny odtwarzacz obrazów", 221 + "disableConfetti": "Wyłącz konfetti gdy klikasz niektóre przyciski", 222 + "disableConfettiDescription": "Domyślnie konfetti wybucha podczas polubień, reakcji, wootowania, edycji i podbić.", 223 + "enableConfettiReceivingLike": "Włącz konfetti gdy otrzymasz polubienie (PvP)", 224 + "disableSounds": "Wyłącz dźwięk gdy klikasz niektóre przyciski", 225 + "disableSoundsDescription": "Domyślnie dźwięki odtwarzają się podczas polubień, reakcji, wootowania, edycji i podbić.", 226 + "defaultDashboard": "Domyślna strona główna", 227 + "defaultDashboardOptions": { 228 + "dashboard": "Oś czasu", 229 + "exploreLocal": "Eksploruj Wafrn" 230 + }, 231 + "automaticallyExpandPosts": "Automatycznie rozwiń wooty", 232 + "expandQuotes": "Nie zwijaj cytatów", 233 + "atprotoLinkDestination": "Przekierowanie linków Atproto", 234 + "atprotoLinkDestinationDescription": "Strona gdzie zostaną wyświetlone wooty Atproto. Domyślnie jest 'bsky.app'.", 235 + "disableCW": "Wyłącz ostrzeżenia przed treściami nieprzyzwoitymi", 236 + "disableNSFWFilter": "Wyłącz filtr multimediów oznaczonych jako nieprzyzwoite", 237 + "hideNoDescriptionMedia": "Ukryj za filtrem multimedia bez tekstu alternatywnego", 238 + "disableForceAltText": "Zezwól na przesyłanie multimediów bez tekstu alternatywnego", 239 + "disableForceAltTextDescription": "Włącz tę opcję jeśli nie masz RiGCz-u", 240 + "manuallyAcceptsFollows": "Ręcznie akceptuj obserwujących", 241 + "enableAsks": "Włącz zapytania", 242 + "enableAnonymousAsks": "Włącz anonimowe zapytania", 243 + "hideProfileNotLoggedIn": "Ukryj twój profil dla niezalogowanych", 244 + "hideProfileNotLoggedInDescription": "Dotyczy tylko tego Wafrna. Niezalogowani mogą zobaczyć twoje wooty. Nie dotyczy to innych instancji oraz blueskaja", 245 + "hideFollows": "Ukryj obserwujących i obserwowanych ", 246 + "displayMentionsOfBlockedUsersFromOtherUsers": "Pokaż wooty ze wzmiankami zablokowanych użytkowników", 247 + "defaultPostEditorPrivacy": "Domyślna wartość prywatności w edytorze", 248 + "postEditorPrivacyOptions": { 249 + "public": "Publiczny", 250 + "followersOnly": "Tylko obserwujący", 251 + "instanceOnly": "Tylko ta instancja", 252 + "unlisted": "Niepubliczne" 253 + } 254 + }, 255 + "dialog": { 256 + "confirm": "Potwierdź", 257 + "cancel": "Anuluj" 258 + }, 259 "ask-dialog-content": { 260 + "askAnonymously": "Zadaj pytanie anonimowo", 261 "askFormLabel": "Pytanie", 262 "askButtonText": "Zapytaj" 263 + }, 264 + "messages": { 265 + "deleteRewootSuccess": "Podbicie zostało usunięte.", 266 + "likePostSuccess": "Woot został polubiony.", 267 + "unlikePostSuccess": "Polubienie zostało usunięte.", 268 + "bookmarkPostSuccess": "Woot został zapisany.", 269 + "unbookmarkPostSuccess": "Woot został usunięty z zapisanych.", 270 + "rewootPostSuccess": "Woot został podbity.", 271 + "followMessageSuccess": "Obserwujesz tego użytkownika.", 272 + "cancelFollowMessageSuccess": "Anulowano prośbę o obserwację użytkownika.", 273 + "unfollowMessageSuccess": "Nie obserwujesz już tego użytkownika.", 274 + "copyLocalLinkSuccess": "Skopiowano link do schowka.", 275 + "copyRemoteLinkSuccess": "Skopiowano link zewnętrzny do schowka.", 276 + "silencePostSuccess": "Wyciszono powiadomienia dla tego woota.", 277 + "unsilencePostSuccess": "Będziesz otrzymywać powiadomienia dla tego woota.", 278 + "blockUserSuccess": "Zablokowano tego użytkownika.", 279 + "unblockUserSuccess": "Odblokowano tego użytkownika.", 280 + "muteUserSuccess": "Wyciszono tego użytkownika.", 281 + "unmuteUserSuccess": "Odciszono użytkownika.", 282 + "blockServerSuccess": "Zablokowano server.", 283 + "reportUserSuccess": "Stworzono raport dla tego użytkownika.", 284 + "reportPostSuccess": "Stworzono raport dla tego woota.", 285 + "genericError": "Coś poszło nie tak! Sprawdź czy masz połączenie internetowe. ", 286 + "notRewootableError": "Nie udało się podbić woota, podbicia wyłączone przez autora.", 287 + "bitePostSuccess": "Woot został ugryziony!", 288 + "biteUserSuccess": "Użytkownik został ugryziony!" 289 + }, 290 + "keyboard-shortcuts": { 291 + "keyboardShortcuts": "Skróty Klawiszowe", 292 + "groupNavigation": "Nawigacja", 293 + "scrollDown": "Przewiń w dół", 294 + "scrollUp": "Przewiń w górę", 295 + "scrollDownPage": "Przewiń w dół o stronę", 296 + "scrollUpPage": "Przewiń w górę o stronę", 297 + "nextPost": "Następny woot", 298 + "previousPost": "Poprzedni woot", 299 + "groupPostAction": "Akcje Wootów", 300 + "likePost": "Polub woot", 301 + "rewootPost": "Podbij woot", 302 + "replyPost": "Odpowiedz na woot", 303 + "quotePost": "Zacytuj woota", 304 + "groupMisc": "Inne", 305 + "openEditor": "Otwórz edytor", 306 + "viewKeyboardShortcuts": "Zobacz skróty klawiszowe", 307 + "unbound": "nieprzypisany", 308 + "additionalOptions": "Dodatkowe Opcje", 309 + "smoothScroll": "Płynne przewijanie" 310 + }, 311 + "post-actions": { 312 + "shareUrl": "Skopjuj link", 313 + "shareExternalUrl": "Skopjuj link zewnętzrzny", 314 + "viewOnAtproto": "Zobacz na Atproto", 315 + "forceRefederate": "Wymuś ponowną federację", 316 + "viewOriginalPost": "Zobacz orginał", 317 + "replyPost": "Odpowiedz na woota", 318 + "rewootPost": "Podbij woota", 319 + "deleteRewootPost": "Cofnij Podbicie", 320 + "quotePost": "Cytuj woota", 321 + "bookmarkPost": "Zapisz woota", 322 + "unbookmarkPost": "Usuń z zapisanych", 323 + "likePost": "Polub woota", 324 + "dislikePost": "Cofnij polubienie", 325 + "reportPost": "Zgłoś woota", 326 + "editPost": "Edytuj woota", 327 + "deletePost": "Usuń woota", 328 + "silenceInteractions": "Wycisz powiadomienia", 329 + "silenceReplyNotifications": "Wycisz odpowiedzi", 330 + "unsilenceReplyNotifications": "Odcisz odpowiedzi", 331 + "muteUser": "Wycisz użytkownika", 332 + "unmuteUser": "Odcisz użytkownika", 333 + "blockUser": "Zablokuj użytkownika", 334 + "unblockUser": "Odblokuj użytkownika", 335 + "reportUser": "Zgłoś użytkownika", 336 + "bitePost": "Ugryź woota", 337 + "biteUser": "Ugryź użytkownika" 338 + }, 339 + "post-header": { 340 + "follow": "Obserwuj", 341 + "awaitingApproval": "Oczekiwanie na akceptację", 342 + "viewPost": "Wyświetl woot" 343 + }, 344 + "post-fragment": { 345 + "mutedWords": "Woot zawiera zablokowane słowa:", 346 + "mediaContentWarning": "Woot zawiera ostrzerzenie:", 347 + "mediaNsfw": "Multimedia oznaczone są jako nieprzyzwoite", 348 + "replyingTo": "Wzmianki: " 349 + }, 350 + "media": { 351 + "noAltText": "Brak tekstu alternatywnego", 352 + "sensitiveContent": "Te multimedia zawierają niestosowne treści", 353 + "manuallyHidden": "Multimedia zostały ręcznie ukryte" 354 + }, 355 + "page-not-found": { 356 + "title": "404: Nie znaleziono strony", 357 + "subtitle": "Niczego tutaj nie ma.", 358 + "couldNotFind": "nie prowadzi do poprawnej strony", 359 + "returnHome": "Wróć do strony głównej" 360 + }, 361 + "games": { 362 + "common": { 363 + "moves": "Ruchów" 364 + }, 365 + "fifteen": { 366 + "title": "Piętnastka", 367 + "newBoard": "Nowa Plansza", 368 + "resetBoard": "Zresetuj Planszę" 369 + }, 370 + "typing": { 371 + "instructions": "Wpisz podaną frazę:", 372 + "verifyLabel": "Tutaj..." 373 + } 374 } 375 + }
+1 -1
packages/frontend/src/assets/i18n/ru.json
··· 572 "couldNotFind": "не ведёт к странице", 573 "returnHome": "Вернуться на главную" 574 } 575 - }
··· 572 "couldNotFind": "не ведёт к странице", 573 "returnHome": "Вернуться на главную" 574 } 575 + }