mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Merge branch 'main' into cal/fix-content-rows-issues
This commit is contained in:
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
SQLX_OFFLINE: true
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -16,10 +16,10 @@ on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
required: true
|
||||
type: string
|
||||
description: 'The environment to deploy to (staging-preview or production-preview)'
|
||||
preview:
|
||||
required: false
|
||||
type: boolean
|
||||
description: 'Whether this is a preview deployment'
|
||||
head_sha:
|
||||
required: false
|
||||
type: string
|
||||
@@ -28,9 +28,13 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
description: 'The branch name to deploy (defaults to github.ref_name)'
|
||||
branch_alias:
|
||||
required: false
|
||||
type: string
|
||||
description: 'The persistent preview alias for the branch'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment || 'push' }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.preview && 'preview' || 'push' }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
@@ -49,23 +53,31 @@ jobs:
|
||||
- name: Configure environment
|
||||
id: meta
|
||||
env:
|
||||
ENV_INPUT: ${{ inputs.environment }}
|
||||
PREVIEW_INPUT: ${{ inputs.preview }}
|
||||
REF: ${{ github.ref }}
|
||||
DEPLOY_SHA: ${{ inputs.head_sha || github.sha }}
|
||||
BRANCH_ALIAS: ${{ inputs.branch_alias }}
|
||||
run: |
|
||||
echo "cmd=deploy" >> $GITHUB_OUTPUT
|
||||
|
||||
SHA_SHORT="${DEPLOY_SHA::8}"
|
||||
|
||||
if [ "$ENV_INPUT" = "staging-preview" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
echo "url=https://git-$SHA_SHORT-frontend-staging.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true" >> $GITHUB_OUTPUT
|
||||
|
||||
elif [ "$ENV_INPUT" = "production-preview" ]; then
|
||||
if [ "$PREVIEW_INPUT" = "true" ]; then
|
||||
URL="https://git-$SHA_SHORT-frontend.modrinth.workers.dev"
|
||||
STAGING_URL="https://git-$SHA_SHORT-staging-frontend.modrinth.workers.dev"
|
||||
echo "env=production" >> $GITHUB_OUTPUT
|
||||
echo "url=https://git-$SHA_SHORT-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true" >> $GITHUB_OUTPUT
|
||||
echo "url=$URL" >> $GITHUB_OUTPUT
|
||||
echo "staging_url=$STAGING_URL" >> $GITHUB_OUTPUT
|
||||
echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true --var CF_PAGES_URL:$URL" >> $GITHUB_OUTPUT
|
||||
|
||||
STAGING_VARS=$(jq -r '.env.staging.vars | to_entries[] | "--var \(.key):\(.value)"' ./apps/frontend/wrangler.jsonc | tr '\n' ' ')
|
||||
echo "staging_cmd=versions upload --preview-alias git-$SHA_SHORT-staging ${STAGING_VARS}--var PREVIEW:true --var CF_PAGES_URL:$STAGING_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -n "$BRANCH_ALIAS" ]; then
|
||||
echo "branch_alias_url=https://$BRANCH_ALIAS-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "staging_branch_alias_url=https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "staging_branch_alias_cmd=versions upload --preview-alias $BRANCH_ALIAS-staging ${STAGING_VARS}--var PREVIEW:true --var CF_PAGES_URL:https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [ "$REF" = "refs/heads/main" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
@@ -120,7 +132,7 @@ jobs:
|
||||
CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }}
|
||||
CF_PAGES_URL: ${{ steps.meta.outputs.url }}
|
||||
BUILD_ENV: ${{ steps.meta.outputs.env }}
|
||||
PREVIEW: ${{ inputs.environment != '' && 'true' || 'false' }}
|
||||
PREVIEW: ${{ inputs.preview && 'true' || 'false' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create Sentry release and upload sourcemaps
|
||||
@@ -146,6 +158,39 @@ jobs:
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.cmd }}
|
||||
|
||||
- name: Deploy staging Cloudflare Worker preview
|
||||
if: ${{ inputs.preview }}
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.staging_cmd }}
|
||||
|
||||
- name: Deploy persistent Cloudflare Worker preview
|
||||
if: ${{ inputs.preview && inputs.branch_alias != '' }}
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias ${{ inputs.branch_alias }} --var PREVIEW:true --var CF_PAGES_URL:${{ steps.meta.outputs.branch_alias_url }}
|
||||
|
||||
- name: Deploy persistent staging Cloudflare Worker preview
|
||||
if: ${{ inputs.preview && inputs.branch_alias != '' }}
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.staging_branch_alias_cmd }}
|
||||
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
run: |
|
||||
@@ -155,14 +200,22 @@ jobs:
|
||||
--data '{"hosts": ["modrinth.com", "www.modrinth.com", "staging.modrinth.com"]}' \
|
||||
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache
|
||||
|
||||
- name: Write deployment URL to file
|
||||
if: ${{ inputs.environment != '' }}
|
||||
- name: Write deployment URLs to files
|
||||
if: ${{ inputs.preview }}
|
||||
run: |
|
||||
echo "${{ steps.meta.outputs.url }}" > deployment-url-${{ inputs.environment }}.txt
|
||||
echo "${{ steps.meta.outputs.url }}" > deployment-url-production.txt
|
||||
echo "${{ steps.meta.outputs.staging_url }}" > deployment-url-staging.txt
|
||||
|
||||
- name: Upload deployment URL
|
||||
if: ${{ inputs.environment != '' }}
|
||||
- name: Upload production deployment URL
|
||||
if: ${{ inputs.preview }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-${{ inputs.environment }}
|
||||
path: deployment-url-${{ inputs.environment }}.txt
|
||||
name: deployment-url-production
|
||||
path: deployment-url-production.txt
|
||||
|
||||
- name: Upload staging deployment URL
|
||||
if: ${{ inputs.preview }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-staging
|
||||
path: deployment-url-staging.txt
|
||||
|
||||
@@ -12,23 +12,33 @@ on:
|
||||
- '.github/workflows/frontend-preview.yml'
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branch-alias: ${{ steps.meta.outputs.branch_alias }}
|
||||
steps:
|
||||
- name: Configure branch alias
|
||||
id: meta
|
||||
run: echo "branch_alias=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
needs: metadata
|
||||
uses: ./.github/workflows/frontend-deploy.yml
|
||||
secrets: inherit
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.environment }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-deploy
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
environment: [staging-preview, production-preview]
|
||||
with:
|
||||
environment: ${{ matrix.environment }}
|
||||
preview: true
|
||||
head_sha: ${{ github.event.pull_request.head.sha }}
|
||||
head_ref: ${{ github.event.pull_request.head.ref }}
|
||||
branch_alias: ${{ needs.metadata.outputs.branch-alias }}
|
||||
|
||||
deploy-storybook:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
needs: metadata
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-storybook
|
||||
@@ -86,6 +96,18 @@ jobs:
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }}
|
||||
|
||||
- name: Deploy persistent Storybook preview
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./packages/ui
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias ${{ needs.metadata.outputs.branch-alias }}
|
||||
|
||||
comment:
|
||||
if: |
|
||||
!cancelled()
|
||||
@@ -93,7 +115,7 @@ jobs:
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
&& needs.deploy.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy, deploy-storybook]
|
||||
needs: [metadata, deploy, deploy-storybook]
|
||||
steps:
|
||||
- name: Download deployment URLs
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -105,25 +127,29 @@ jobs:
|
||||
id: urls
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BRANCH_ALIAS: ${{ needs.metadata.outputs.branch-alias }}
|
||||
STORYBOOK_RESULT: ${{ needs.deploy-storybook.result }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt)
|
||||
PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt)
|
||||
STAGING_SHA_URL=$(cat deployment-url-staging.txt)
|
||||
PRODUCTION_SHA_URL=$(cat deployment-url-production.txt)
|
||||
STAGING_BRANCH_URL="https://$BRANCH_ALIAS-staging-frontend.modrinth.workers.dev"
|
||||
PRODUCTION_BRANCH_URL="https://$BRANCH_ALIAS-frontend.modrinth.workers.dev"
|
||||
|
||||
if [ "$STORYBOOK_RESULT" = "success" ]; then
|
||||
STORYBOOK_PREVIEW_URL="https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev"
|
||||
STORYBOOK_SHA_URL="https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev"
|
||||
STORYBOOK_BRANCH_URL="https://$BRANCH_ALIAS-storybook.modrinth.workers.dev"
|
||||
else
|
||||
STORYBOOK_PREVIEW_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))"
|
||||
STORYBOOK_SHA_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))"
|
||||
STORYBOOK_BRANCH_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))"
|
||||
fi
|
||||
|
||||
echo "Production preview URL: $PRODUCTION_PREVIEW_URL"
|
||||
echo "Staging preview URL: $STAGING_PREVIEW_URL"
|
||||
echo "Storybook preview URL: $STORYBOOK_PREVIEW_URL"
|
||||
|
||||
echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "storybook-preview-url=$STORYBOOK_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "staging-sha-url=$STAGING_SHA_URL" >> $GITHUB_OUTPUT
|
||||
echo "production-sha-url=$PRODUCTION_SHA_URL" >> $GITHUB_OUTPUT
|
||||
echo "storybook-sha-url=$STORYBOOK_SHA_URL" >> $GITHUB_OUTPUT
|
||||
echo "staging-branch-url=$STAGING_BRANCH_URL" >> $GITHUB_OUTPUT
|
||||
echo "production-branch-url=$PRODUCTION_BRANCH_URL" >> $GITHUB_OUTPUT
|
||||
echo "storybook-branch-url=$STORYBOOK_BRANCH_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find comment
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -146,9 +172,19 @@ jobs:
|
||||
|
||||
Last deployed commit is [${{ github.event.pull_request.head.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }})
|
||||
|
||||
### Persistent
|
||||
|
||||
| Environment | URL |
|
||||
|-------------|-----|
|
||||
| staging | ${{ steps.urls.outputs.staging-preview-url }} |
|
||||
| production | ${{ steps.urls.outputs.production-preview-url }} |
|
||||
| storybook | ${{ steps.urls.outputs.storybook-preview-url }} |
|
||||
| staging | ${{ steps.urls.outputs.staging-branch-url }} |
|
||||
| production | ${{ steps.urls.outputs.production-branch-url }} |
|
||||
| storybook | ${{ steps.urls.outputs.storybook-branch-url }} |
|
||||
|
||||
### Latest commit
|
||||
|
||||
| Environment | URL |
|
||||
|-------------|-----|
|
||||
| staging | ${{ steps.urls.outputs.staging-sha-url }} |
|
||||
| production | ${{ steps.urls.outputs.production-sha-url }} |
|
||||
| storybook | ${{ steps.urls.outputs.storybook-sha-url }} |
|
||||
edit-mode: replace
|
||||
|
||||
@@ -82,6 +82,13 @@ jobs:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
CARGO_INCREMENTAL: 0
|
||||
# Redis config for tests
|
||||
REDIS_TOPOLOGY: cluster
|
||||
REDIS_CONNECTION_TYPE: multiplexed
|
||||
REDIS_URL: redis://127.0.0.1:7000,redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003,redis://127.0.0.1:7004,redis://127.0.0.1:7005
|
||||
# Avoid stack overflows in tests
|
||||
RUST_MIN_STACK: 16777216
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
@@ -185,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Start services
|
||||
if: steps.check-labrinth.outputs.needs_services == 'true'
|
||||
run: docker compose up --wait
|
||||
run: docker compose --profile clustered-redis up --wait
|
||||
|
||||
- name: Setup labrinth environment and database
|
||||
if: steps.check-labrinth.outputs.needs_services == 'true'
|
||||
|
||||
Generated
+71
-43
@@ -244,7 +244,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.6.5",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -458,6 +458,12 @@ version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "arcstr"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d"
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
version = "0.3.4"
|
||||
@@ -2478,6 +2484,12 @@ dependencies = [
|
||||
"spin 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc16"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -2801,7 +2813,7 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.3"
|
||||
source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde"
|
||||
source = "git+https://github.com/modrinth/deadpool?rev=c7209b086829572f368f1194aba54bdea8dc540b#c7209b086829572f368f1194aba54bdea8dc540b"
|
||||
dependencies = [
|
||||
"deadpool-runtime",
|
||||
"num_cpus",
|
||||
@@ -2811,8 +2823,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-redis"
|
||||
version = "0.22.1"
|
||||
source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/modrinth/deadpool?rev=c7209b086829572f368f1194aba54bdea8dc540b#c7209b086829572f368f1194aba54bdea8dc540b"
|
||||
dependencies = [
|
||||
"deadpool",
|
||||
"redis",
|
||||
@@ -2822,7 +2834,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.1.5"
|
||||
source = "git+https://github.com/modrinth/deadpool?rev=db5fb00b036ecc8fe5f18853c559b745ffe47bde#db5fb00b036ecc8fe5f18853c559b745ffe47bde"
|
||||
source = "git+https://github.com/modrinth/deadpool?rev=c7209b086829572f368f1194aba54bdea8dc540b#c7209b086829572f368f1194aba54bdea8dc540b"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
@@ -4794,7 +4806,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.5.10",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -5135,17 +5147,6 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.2"
|
||||
@@ -5470,7 +5471,6 @@ dependencies = [
|
||||
"color-thief",
|
||||
"const_format",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"derive_more 2.1.1",
|
||||
"dotenv-build",
|
||||
"dotenvy",
|
||||
@@ -5490,7 +5490,6 @@ dependencies = [
|
||||
"json-patch 4.1.0",
|
||||
"labrinth",
|
||||
"lettre",
|
||||
"lz4_flex",
|
||||
"meilisearch-sdk",
|
||||
"modrinth-content-management",
|
||||
"modrinth-util",
|
||||
@@ -5540,6 +5539,7 @@ dependencies = [
|
||||
"webauthn-rs-proto",
|
||||
"webp",
|
||||
"woothee",
|
||||
"xredis",
|
||||
"yaserde",
|
||||
"zip 6.0.0",
|
||||
"zxcvbn",
|
||||
@@ -5605,7 +5605,7 @@ dependencies = [
|
||||
"quoted_printable",
|
||||
"rustls 0.23.32",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.6.5",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"url",
|
||||
@@ -5643,9 +5643,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
@@ -6059,14 +6059,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8014,7 +8014,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.32",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -8051,7 +8051,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
@@ -8345,26 +8345,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.32.7"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44"
|
||||
checksum = "b0b9503711b03773e43b31668c7b5bd279ee7cd9b7d18cff7c23a42cc1d08e5a"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"arcstr",
|
||||
"async-lock",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"crc16",
|
||||
"futures-util",
|
||||
"itoa",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"r2d2",
|
||||
"rand 0.10.1",
|
||||
"ryu",
|
||||
"sha1_smol",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.6.5",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9817,12 +9823,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11272,30 +11278,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
version = "1.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.6.5",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11499,7 +11502,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.6.5",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -13429,6 +13432,31 @@ version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
|
||||
|
||||
[[package]]
|
||||
name = "xredis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ariadne",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"futures",
|
||||
"lz4_flex",
|
||||
"prometheus",
|
||||
"redis",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
version = "0.8.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985eec839aaf2a1270af8f4ebcf63cf9401cfd90f0902f97c28d9f104ffbde72"
|
||||
|
||||
[[package]]
|
||||
name = "yaserde"
|
||||
version = "0.12.0"
|
||||
|
||||
+4
-2
@@ -15,6 +15,7 @@ members = [
|
||||
"packages/modrinth-util",
|
||||
"packages/neverbounce",
|
||||
"packages/path-util",
|
||||
"packages/xredis",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -72,7 +73,7 @@ daedalus = { path = "packages/daedalus" }
|
||||
darling = { version = "0.23" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.2"
|
||||
deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "db5fb00b036ecc8fe5f18853c559b745ffe47bde", version = "0.22.1" }
|
||||
deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "c7209b086829572f368f1194aba54bdea8dc540b", version = "0.23.0" }
|
||||
derive_more = "2.1.1"
|
||||
directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
@@ -154,7 +155,7 @@ quote = { version = "1.0" }
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
rdkafka = { version = "0.36.2", features = ["cmake-build"] }
|
||||
redis = "0.32.7"
|
||||
redis = "1.4.1"
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.12.24", default-features = false }
|
||||
rgb = "0.8.52"
|
||||
@@ -241,6 +242,7 @@ windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
|
||||
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
xredis = { path = "packages/xredis" }
|
||||
yaserde = "0.12.0"
|
||||
zbus = "5.11.0"
|
||||
zip = { version = "6.0.0", default-features = false, features = [
|
||||
|
||||
@@ -364,11 +364,7 @@ function getFeatureFlagOverrides() {
|
||||
|
||||
function getDomain() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// @ts-ignore
|
||||
if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
|
||||
// @ts-ignore
|
||||
return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL
|
||||
} else if (process.env.HEROKU_APP_NAME) {
|
||||
if (process.env.HEROKU_APP_NAME) {
|
||||
return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
|
||||
} else if (process.env.VERCEL_URL) {
|
||||
return `https://${process.env.VERCEL_URL}`
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import { FolderSearchIcon, StarIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
@@ -94,6 +95,7 @@ const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
|
||||
const clearModalRef = useTemplateRef<InstanceType<typeof ConfirmModal>>('clearModalRef')
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const rows = ref<ScanRow[]>([])
|
||||
@@ -217,6 +219,10 @@ async function fetchAllScans() {
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirmClearGroups() {
|
||||
clearModalRef.value?.show()
|
||||
}
|
||||
|
||||
async function clearAllGroups() {
|
||||
if (isBusy.value) {
|
||||
return
|
||||
@@ -270,6 +276,14 @@ defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModal
|
||||
ref="clearModalRef"
|
||||
title="Clear all permission groups?"
|
||||
description="This will clear **all** groups for this project. This action cannot be undone."
|
||||
proceed-label="Clear"
|
||||
@proceed="clearAllGroups"
|
||||
/>
|
||||
|
||||
<NewModal
|
||||
ref="modalRef"
|
||||
width="60vw"
|
||||
@@ -288,11 +302,11 @@ defineExpose({ show, hide })
|
||||
}}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled circular>
|
||||
<ButtonStyled circular color="red" color-fill="none">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.clearAllGroups)"
|
||||
:disabled="isBusy || rows.length === 0"
|
||||
@click="clearAllGroups"
|
||||
@click="showConfirmClearGroups"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -2315,24 +2315,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Vytvořeno {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "License {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Zveřejněno {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Odesláno {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detaily"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Aktualizováno {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Vytvořit server"
|
||||
},
|
||||
@@ -2360,9 +2342,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Popisek"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licencováno"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} nepodporuje {gameVersion} pro {platform}"
|
||||
},
|
||||
@@ -2393,15 +2372,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galerie"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Text licence se nepodařilo načíst."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Načítání textu licence..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licence"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Váš projekt je soukromý, což znamená, že k němu máte přístup pouze vy a lidé, které pozvete."
|
||||
},
|
||||
|
||||
@@ -1724,18 +1724,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Oprettet {date}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Offentliggjort {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detaljer"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Opdateret {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Lav en server"
|
||||
},
|
||||
|
||||
@@ -3218,24 +3218,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Erstellt {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Lienziert unter {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Veröffentlicht {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Eingereicht {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Details"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Aktualisiert {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Erstelle einen Server"
|
||||
},
|
||||
@@ -3266,9 +3248,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Beschreibung"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Lizenziert unter"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Diese Spielversion ist mit dem Basisprojekt nicht kompatibel."
|
||||
},
|
||||
@@ -3422,15 +3401,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galerie"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Lizenztext konnte nicht abgerufen werden."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Lade Lizenztext..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lizenz"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Dein Projekt ist privat, was bedeutet, dass nur du und von dir eingeladene Personen darauf zugreifen können."
|
||||
},
|
||||
|
||||
@@ -3218,24 +3218,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Erstellt {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Lienziert unter {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Veröffentlicht {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Eingereicht {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Details"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Aktualisiert {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Erstelle einen Server"
|
||||
},
|
||||
@@ -3266,9 +3248,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Beschreibung"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Lizenziert unter"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Diese Spielversion ist mit dem Basisprojekt nicht kompatibel."
|
||||
},
|
||||
@@ -3422,15 +3401,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galerie"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Lizenztext konnte nicht abgerufen werden."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Lade Lizenztext..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lizenz"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Dein Projekt ist privat, was bedeutet, dass nur du und von dir eingeladene Personen darauf zugreifen können."
|
||||
},
|
||||
|
||||
@@ -3521,24 +3521,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Created {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licensed {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Published {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Submitted {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Details"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Updated {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Create a server"
|
||||
},
|
||||
@@ -3575,9 +3557,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Description"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licensed"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "This game version is incompatible with the base project."
|
||||
},
|
||||
@@ -3734,15 +3713,6 @@
|
||||
"project.install-context.back-to-discover": {
|
||||
"message": "Back to discover"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "License text could not be retrieved."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Loading license text..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "License"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Your project is private, meaning it can only be accessed by you and people you invite."
|
||||
},
|
||||
|
||||
@@ -3218,24 +3218,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Creado {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Con licencia {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publicado {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Enviado {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalles"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Actualizado {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Crear un servidor"
|
||||
},
|
||||
@@ -3266,9 +3248,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Descripción"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Con licencia"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Esta versión del juego no es compatible con el proyecto base."
|
||||
},
|
||||
@@ -3422,15 +3401,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galería"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "No se pudo recuperar el texto de la licencia."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Cargando el texto de la licencia..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licencia"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Tu proyecto es privado, lo que significa que solo pueden acceder tú y las personas a las que invites."
|
||||
},
|
||||
|
||||
@@ -3041,24 +3041,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Sombreador"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Creado el {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Con licencia {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publicado el {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Enviado el {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalles"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Actualizado el {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Crear un servidor"
|
||||
},
|
||||
@@ -3089,9 +3071,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Descripción"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Con licencia"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} no está soportado para la versión {gameVersion} de {platform}"
|
||||
},
|
||||
@@ -3143,15 +3122,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galería"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "No se pudo recuperar el texto de la licencia."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Cargando texto de la licencia..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licencia"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Tu proyecto es privado, lo que significa que solo tú, o las personas que invites, pueden acceder."
|
||||
},
|
||||
|
||||
@@ -971,21 +971,12 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Yksityiskohdat"
|
||||
},
|
||||
"project.description.title": {
|
||||
"message": "Kuvaus"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Lisenssoitu"
|
||||
},
|
||||
"project.gallery.title": {
|
||||
"message": "Galleria"
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lisenssi"
|
||||
},
|
||||
"project.moderation.title": {
|
||||
"message": "Moderaatio"
|
||||
},
|
||||
|
||||
@@ -2360,24 +2360,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Linikha {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Linisensiya {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Inilathala {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Isinumite {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalye"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "In-update {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Lumikha ng server"
|
||||
},
|
||||
@@ -2408,9 +2390,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Paglalarawan"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Lisensiyang"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "Hindi masuporta ng {title} ang {gameVersion} para sa {platform}"
|
||||
},
|
||||
@@ -2462,15 +2441,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeriya"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Hindi makuha ang teksto ng lisensiya."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Nag-load sa teksto ng lisensiya..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lisensiya"
|
||||
},
|
||||
"project.moderation.title": {
|
||||
"message": "Moderation"
|
||||
},
|
||||
|
||||
@@ -3224,24 +3224,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Créé le {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Sous licence {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publié {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Soumis le {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Détails"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Mis à jour {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Créer un serveur"
|
||||
},
|
||||
@@ -3272,9 +3254,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Description"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "License"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Cette version du jeu est incompatible avec le projet de base."
|
||||
},
|
||||
@@ -3428,15 +3407,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galerie"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Le texte de la license n'a pas pu être obtenu."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Chargement du texte de la license..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "License"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Votre projet est privé, il n'est accessible que par vous et les personnes que vous invitez."
|
||||
},
|
||||
|
||||
@@ -1985,24 +1985,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "שיידר"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "נוצר ב-{date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "ברישיון {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "פורסם {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "נשלח ב-{date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "פרטים"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "עודכן ב-{date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "צור שרת"
|
||||
},
|
||||
@@ -2033,9 +2015,6 @@
|
||||
"project.description.title": {
|
||||
"message": "תיאור"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "ברישיון"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} אינו תומך ב-{gameVersion} עבור {platform}"
|
||||
},
|
||||
@@ -2087,15 +2066,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "גלריה"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "לא ניתן היה לשלוף את טקסט הרישיון."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "טוען את טקסט הרישיון..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "רישיון"
|
||||
},
|
||||
"project.moderation.title": {
|
||||
"message": "פיקוח ובקרה"
|
||||
},
|
||||
|
||||
@@ -3116,24 +3116,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Létrehozva {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licesz: {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Közzététel: {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Benyújtva {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Részletek"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Frissítve {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Hozz létre egy szervert"
|
||||
},
|
||||
@@ -3164,9 +3146,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Leírás"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licenszelt"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Ez a játékverzió nem kompatibilis az alap projekttel."
|
||||
},
|
||||
@@ -3317,15 +3296,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Képek"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "A licensz szövege nem tölthető le."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Licensz szövegének betöltése..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licensz"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "A projekted privát, vagyis csak te és az általad meghívott személyek férhetnek hozzá."
|
||||
},
|
||||
|
||||
@@ -2366,24 +2366,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Efek Gambar"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Dibuat {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Berlisensi {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Diterbitkan {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Diserahkan {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Perincian"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Diperbarui {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Buat server"
|
||||
},
|
||||
@@ -2414,9 +2396,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Keterangan"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Berlisensi"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} tidak mendukung {gameVersion} untuk {platform}"
|
||||
},
|
||||
@@ -2468,15 +2447,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeri"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Teks lisensi tidak dapat diperoleh."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Memuat teks lisensi..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lisensi"
|
||||
},
|
||||
"project.moderation.title": {
|
||||
"message": "Moderasi"
|
||||
},
|
||||
|
||||
@@ -3209,24 +3209,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Creato {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licenza {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Pubblicato {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Inserito {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Dettagli"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Aggiornato {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Crea un server"
|
||||
},
|
||||
@@ -3257,9 +3239,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Descrizione"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licenza"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Questo progetto non è compatibile con questa versione del gioco."
|
||||
},
|
||||
@@ -3413,15 +3392,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galleria"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Impossibile ottenere testo licenza."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Caricamento testo licenza..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licenza"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Il progetto è privato: solo tu e chiunque inviti potranno accederci."
|
||||
},
|
||||
|
||||
@@ -2741,24 +2741,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "シェーダー"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "{date}に作成"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "ライセンス: {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "{date} に公開"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "提出時刻: {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "詳細"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "{date} に更新"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "サーバーを作成"
|
||||
},
|
||||
@@ -2789,9 +2771,6 @@
|
||||
"project.description.title": {
|
||||
"message": "説明"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "ライセンス"
|
||||
},
|
||||
"project.download.download": {
|
||||
"message": "ダウンロード"
|
||||
},
|
||||
@@ -2846,15 +2825,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "ギャラリー"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "ライセンステキストを取得できませんでした。"
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "ライセンステキストを読み込み中…"
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "ライセンス"
|
||||
},
|
||||
"project.moderation.admonition.approved.header": {
|
||||
"message": "プロジェクトが承認されました"
|
||||
},
|
||||
|
||||
@@ -3218,24 +3218,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "셰이더"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "{date} 에 생성됨"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "{license} 라이선스"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "{date} 게시"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "{date} 에 제출됨"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "세부 사항"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "{date} 업데이트"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "서버 만들기"
|
||||
},
|
||||
@@ -3266,9 +3248,6 @@
|
||||
"project.description.title": {
|
||||
"message": "설명"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "라이선스"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "이 게임 버전은 기본 프로젝트와 호환되지 않습니다."
|
||||
},
|
||||
@@ -3422,15 +3401,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "갤러리"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "라이선스 텍스트를 불러올 수 없습니다."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "라이선스 텍스트를 불러오는 중..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "라이선스"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "이 프로젝트는 비공개로 설정되어 있어, 본인과 본인이 초대한 사람만 접근할 수 있습니다."
|
||||
},
|
||||
|
||||
@@ -2897,24 +2897,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Pembayang"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Dicipta {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Berlesen {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Diterbitkan {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Diserahkan {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Butiran"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Dikemas kini {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Cipta pelayan"
|
||||
},
|
||||
@@ -2945,9 +2927,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Keterangan"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Berlesen"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} tidak menyokong {gameVersion} untuk {platform}"
|
||||
},
|
||||
@@ -2999,15 +2978,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeri"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Teks lesen tidak dapat diambil."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Sedang memuat teks lesen..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lesen"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Projek anda adalah peribadi, yang bermakna ia hanya boleh diakses oleh anda dan orang yang anda jemput."
|
||||
},
|
||||
|
||||
@@ -3215,24 +3215,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Gemaakt op {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Gelicentieerd met {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Gepubliceerd op {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Ingediend op {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Details"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Bijgewerkt op {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Creëer een server"
|
||||
},
|
||||
@@ -3263,9 +3245,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Beschrijving"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Gelicentieerd"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Deze spelversie is niet compatibel met het basisproject."
|
||||
},
|
||||
@@ -3410,15 +3389,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Gallerij"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Licentie inhoud kon niet worden verkregen."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Licentietekst aan het laden..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licentie"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.unlisted": {
|
||||
"message": "Jouw project is verborgen. Dit betekend dat het alleen beschikbaar is via een directe link en niet vindbaar is op Modrinth."
|
||||
},
|
||||
|
||||
@@ -2825,24 +2825,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Oppretta {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Lisensiert {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publisert {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Sendt inn {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detaljer"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Oppdatert {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Lag en server"
|
||||
},
|
||||
@@ -2873,9 +2855,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Beskrivelse"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Lisensert"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} støtter ikke {gameVersion} for {platform}"
|
||||
},
|
||||
@@ -2927,15 +2906,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galleri"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Lisenstekst kunne ikke bli henta."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Laster inn linsenstekst..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lisens"
|
||||
},
|
||||
"project.moderation.admonition.approved.header": {
|
||||
"message": "Prosjekt godkjent"
|
||||
},
|
||||
|
||||
@@ -3209,24 +3209,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Utworzony {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Pod licencją {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Opublikowano {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Przesłano {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Szczegóły"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Zaktualizowany {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Utwórz serwer"
|
||||
},
|
||||
@@ -3257,9 +3239,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Opis"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licencja"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Ta wersja gry jest niekompatybilna z bazowym projektem."
|
||||
},
|
||||
@@ -3413,15 +3392,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeria"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Nie można pobrać tekstu licencji."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Ładowanie tekstu licencji..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licencja"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Twój projekt jest prywatny, co oznacza, że jest widoczny tylko dla ciebie i tych, których zaprosisz."
|
||||
},
|
||||
|
||||
@@ -3224,24 +3224,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Sombreador"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Criado {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licenciado como {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publicado {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Enviado {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalhes"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Atualizado {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Criar um servidor"
|
||||
},
|
||||
@@ -3272,9 +3254,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Descrição"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licença"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Esta versão do jogo é incompatível com o projeto base."
|
||||
},
|
||||
@@ -3428,15 +3407,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeria"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Não foi possível obter o texto da licença."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Carregando texto da licença..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licença"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Seu projeto é privado, o que significa que só você e as pessoas que você convidar podem acessá-lo."
|
||||
},
|
||||
|
||||
@@ -2708,24 +2708,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Criado {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licenciado {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publicado {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Enviado {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalhes"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Atualizado {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Criar um servidor"
|
||||
},
|
||||
@@ -2756,9 +2738,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Descrição"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licenciado"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} não suporta {gameVersion} para {platform}"
|
||||
},
|
||||
@@ -2810,15 +2789,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeria"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Não foi possível obter o texto da licença."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "A carregar texto da licença..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licença"
|
||||
},
|
||||
"project.moderation.title": {
|
||||
"message": "Moderação"
|
||||
},
|
||||
|
||||
@@ -1367,24 +1367,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Creat pe {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licențiat cu {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Publicat pe {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Trimis pe {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalii"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Actualizat pe {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Creează un server"
|
||||
},
|
||||
@@ -1412,9 +1394,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Descriere"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licență"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} nu este compatibil cu {gameVersion} pentru {platform}"
|
||||
},
|
||||
@@ -1463,15 +1442,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galerie"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Textul licenței nu a putut fi preluat."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Se încarcă textul licenței..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licență"
|
||||
},
|
||||
"project.moderation.title": {
|
||||
"message": "Moderare"
|
||||
},
|
||||
|
||||
@@ -3215,24 +3215,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Шейдер"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Создан {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Лицензия {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Размещён {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Отправлен {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Сведения"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Обновлён {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Создать сервер"
|
||||
},
|
||||
@@ -3263,9 +3245,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Описание"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Лицензия"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Эта версия игры несовместима с зависимым контентом."
|
||||
},
|
||||
@@ -3419,15 +3398,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Галерея"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Не удалось получить текст лицензии."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Загрузка текста лицензии..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Лицензия"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Ваш проект приватный, доступ к нему есть только у вас и приглашённых вами людей."
|
||||
},
|
||||
|
||||
@@ -1343,24 +1343,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Kreirano {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licencirano {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Objavljeno {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Poslato {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detalji"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Ažurirano {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Kreiraj server"
|
||||
},
|
||||
|
||||
@@ -2882,24 +2882,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Skapad {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Licensierat {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Skapades {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Inskickad {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detaljer"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Uppdaterad {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Skapa en server"
|
||||
},
|
||||
@@ -2930,9 +2912,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Beskrivning"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Licensierad"
|
||||
},
|
||||
"project.download.dependency-download-file": {
|
||||
"message": "Ladda ner {filename}"
|
||||
},
|
||||
@@ -3005,15 +2984,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galleri"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Licenstext kunde inte hämtas."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Laddar licenstext..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Licens"
|
||||
},
|
||||
"project.moderation.admonition.under-review.header": {
|
||||
"message": "Projekt under granskning"
|
||||
},
|
||||
|
||||
@@ -3218,24 +3218,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "{date} oluşturuldu"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "{license} lisanslı"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "{date} yayınlandı"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "{date} sunuldu"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Detaylar"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "{date} güncellendi"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Sunucu oluştur"
|
||||
},
|
||||
@@ -3266,9 +3248,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Açıklama"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Lisans:"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Bu oyun sürümü projenin bu sürümü ile uygun değil."
|
||||
},
|
||||
@@ -3422,15 +3401,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Galeri"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Lisans yazısı getirilemedi."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Lisans yazısı yükleniyor..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Lisans"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Projeniz özeldir, yani yalnızca siz ve davet ettiğiniz kişiler tarafından erişilebilir."
|
||||
},
|
||||
|
||||
@@ -3221,24 +3221,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Шейдер"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Створено {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Ліцензія «{license}»"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Опубліковано {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Подано на розгляд {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Деталі"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Оновлено {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Створити сервер"
|
||||
},
|
||||
@@ -3269,9 +3251,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Опис"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Ліцензія:"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "Ця версія гри несумісна з базовим проєктом."
|
||||
},
|
||||
@@ -3425,15 +3404,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Галерея"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Неможливо отримати текст ліцензії."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Завантаження тексту ліцензії…"
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Ліцензія"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Ваш проєкт є приватним, тобто доступ до нього маєте лише ви та запрошені вами люди."
|
||||
},
|
||||
|
||||
@@ -3011,24 +3011,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "Shader"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "Tạo vào {date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "Giấy phép: {license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "Xuất bản vào {date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "Gửi vào {date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "Chi tiết"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "Cập nhật vào {date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Tạo máy chủ"
|
||||
},
|
||||
@@ -3059,9 +3041,6 @@
|
||||
"project.description.title": {
|
||||
"message": "Mô tả"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "Giấy phép"
|
||||
},
|
||||
"project.download.game-version-unsupported-tooltip": {
|
||||
"message": "{title} không hỗ trợ {gameVersion} cho {platform}"
|
||||
},
|
||||
@@ -3113,15 +3092,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "Thư viện"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "Không thể tải nội dung giấy phép."
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "Đang tải nội dung giấy phép..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "Giấy phép"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "Dự án của bạn đang để chế độ riêng tư, nghĩa là chỉ có bạn và những người khác được mời có thể truy cập nó."
|
||||
},
|
||||
|
||||
@@ -3218,24 +3218,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "光影包"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "{date}创建"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "许可证:{license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "{date}发布"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "{date}提交"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "详细信息"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "{date}更新"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "创建服务器"
|
||||
},
|
||||
@@ -3266,9 +3248,6 @@
|
||||
"project.description.title": {
|
||||
"message": "描述"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "许可证"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "此游戏版本与基础项目不兼容。"
|
||||
},
|
||||
@@ -3422,15 +3401,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "图库"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "获取许可证文本内容时出错。"
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "正在加载许可证文本……"
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "许可证"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "你的项目是私人的,这意味着它只能由你本人或受你邀请的人访问。"
|
||||
},
|
||||
|
||||
@@ -3224,24 +3224,6 @@
|
||||
"project-type.shader.singular": {
|
||||
"message": "光影包"
|
||||
},
|
||||
"project.about.details.created": {
|
||||
"message": "建立時間:{date}"
|
||||
},
|
||||
"project.about.details.licensed": {
|
||||
"message": "授權條款:{license}"
|
||||
},
|
||||
"project.about.details.published": {
|
||||
"message": "發布時間:{date}"
|
||||
},
|
||||
"project.about.details.submitted": {
|
||||
"message": "提交時間:{date}"
|
||||
},
|
||||
"project.about.details.title": {
|
||||
"message": "詳細資訊"
|
||||
},
|
||||
"project.about.details.updated": {
|
||||
"message": "更新時間:{date}"
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "建立伺服器"
|
||||
},
|
||||
@@ -3272,9 +3254,6 @@
|
||||
"project.description.title": {
|
||||
"message": "描述"
|
||||
},
|
||||
"project.details.licensed": {
|
||||
"message": "授權條款"
|
||||
},
|
||||
"project.download.base-game-version-incompatible-tooltip": {
|
||||
"message": "這個遊戲版本與基底專案不相容。"
|
||||
},
|
||||
@@ -3428,15 +3407,6 @@
|
||||
"project.gallery.title": {
|
||||
"message": "圖庫"
|
||||
},
|
||||
"project.license.error": {
|
||||
"message": "取得授權條款文字內容時發生錯誤。"
|
||||
},
|
||||
"project.license.loading": {
|
||||
"message": "正在載入授權條款文字..."
|
||||
},
|
||||
"project.license.title": {
|
||||
"message": "授權條款"
|
||||
},
|
||||
"project.moderation.admonition.approved.body.private": {
|
||||
"message": "你的專案目前設為私人,這代表只有你和你邀請的人才能存取。"
|
||||
},
|
||||
|
||||
@@ -48,25 +48,6 @@
|
||||
</template>
|
||||
|
||||
<div v-else>
|
||||
<NewModal
|
||||
ref="modalLicense"
|
||||
:header="project.license.name ? project.license.name : formatMessage(messages.licenseTitle)"
|
||||
>
|
||||
<template #title>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" no-shadow />
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ project.license.name ? project.license.name : formatMessage(messages.licenseTitle) }}
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
renderString(licenseText).isEmpty
|
||||
? formatMessage(messages.loadingLicenseText)
|
||||
: renderString(licenseText)
|
||||
"
|
||||
/>
|
||||
</NewModal>
|
||||
<OpenInAppModal ref="openInAppModal" />
|
||||
<div
|
||||
class="over-the-top-download-animation"
|
||||
@@ -472,116 +453,12 @@
|
||||
:user-link="(username) => `/user/${username}`"
|
||||
class="card flex-card"
|
||||
/>
|
||||
<!-- TODO: Finish license modal and enable -->
|
||||
<ProjectSidebarDetails
|
||||
v-if="false"
|
||||
:project="project"
|
||||
:has-versions="versions.length > 0"
|
||||
:link-target="$external()"
|
||||
:show-followers="isServerProject"
|
||||
class="card flex-card"
|
||||
/>
|
||||
<div class="card flex-card">
|
||||
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
||||
|
||||
<div class="details-list">
|
||||
<div v-if="projectV3Loaded && !isServerProject" class="details-list__item">
|
||||
<BookTextIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(messages.licensedLabel) }}
|
||||
<a
|
||||
v-if="project.license.url"
|
||||
class="text-link hover:underline"
|
||||
:href="project.license.url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
{{ licenseIdDisplay }}
|
||||
<ExternalIcon aria-hidden="true" class="external-icon ml-1 mt-[-1px] inline" />
|
||||
</a>
|
||||
<span
|
||||
v-else-if="
|
||||
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
|
||||
!project.license.id.includes('LicenseRef')
|
||||
"
|
||||
class="text-link hover:underline"
|
||||
@click="(event) => getLicenseData(event)"
|
||||
>
|
||||
{{ licenseIdDisplay }}
|
||||
</span>
|
||||
<span v-else>{{ licenseIdDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isServerProject" class="details-list__item">
|
||||
<HeartIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{
|
||||
capitalizeString(
|
||||
formatMessage(commonMessages.projectFollowers, {
|
||||
count: project.followers,
|
||||
}),
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="project.approved"
|
||||
v-tooltip="formatDateTime(project.approved)"
|
||||
class="details-list__item"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{
|
||||
capitalizeString(
|
||||
formatMessage(detailsMessages.published, {
|
||||
date: publishedDate,
|
||||
}),
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else v-tooltip="formatDateTime(project.published)" class="details-list__item">
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{
|
||||
capitalizeString(formatMessage(detailsMessages.created, { date: createdDate }))
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="project.status === 'processing' && project.queued"
|
||||
v-tooltip="formatDateTime(project.queued)"
|
||||
class="details-list__item"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{
|
||||
capitalizeString(
|
||||
formatMessage(detailsMessages.submitted, {
|
||||
date: submittedDate,
|
||||
}),
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="versions.length > 0 && project.updated"
|
||||
v-tooltip="formatDateTime(project.updated)"
|
||||
class="details-list__item"
|
||||
>
|
||||
<VersionIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{
|
||||
capitalizeString(formatMessage(detailsMessages.updated, { date: updatedDate }))
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="normal-page__content">
|
||||
@@ -612,13 +489,10 @@
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BookTextIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
ChevronRightIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
FolderSearchIcon,
|
||||
HeartIcon,
|
||||
ListIcon,
|
||||
@@ -629,7 +503,6 @@ import {
|
||||
ScanEyeIcon,
|
||||
ServerPlusIcon,
|
||||
SettingsIcon,
|
||||
VersionIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
@@ -643,7 +516,6 @@ import {
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NavTabs,
|
||||
NewModal,
|
||||
OpenInAppModal,
|
||||
PROJECT_DEP_MARKER_QUERY,
|
||||
ProjectBackgroundGradient,
|
||||
@@ -659,13 +531,11 @@ import {
|
||||
SelectedProjectsFloatingBar,
|
||||
TeleportOverflowMenu,
|
||||
useDebugLogger,
|
||||
useFormatDateTime,
|
||||
useFormatPrice,
|
||||
useRelativeTime,
|
||||
useStickyObserver,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { capitalizeString, formatProjectType, renderString } from '@modrinth/utils'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
@@ -733,10 +603,6 @@ const flags = useFeatureFlags()
|
||||
const cosmetics = useCosmetics()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
const formatPrice = useFormatPrice()
|
||||
|
||||
const debug = useDebugLogger('DownloadModal')
|
||||
@@ -773,35 +639,6 @@ function handlePlayServerProject() {
|
||||
})
|
||||
}
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const detailsMessages = defineMessages({
|
||||
title: {
|
||||
id: 'project.about.details.title',
|
||||
defaultMessage: 'Details',
|
||||
},
|
||||
licensed: {
|
||||
id: 'project.about.details.licensed',
|
||||
defaultMessage: 'Licensed {license}',
|
||||
},
|
||||
created: {
|
||||
id: 'project.about.details.created',
|
||||
defaultMessage: 'Created {date}',
|
||||
},
|
||||
submitted: {
|
||||
id: 'project.about.details.submitted',
|
||||
defaultMessage: 'Submitted {date}',
|
||||
},
|
||||
published: {
|
||||
id: 'project.about.details.published',
|
||||
defaultMessage: 'Published {date}',
|
||||
},
|
||||
updated: {
|
||||
id: 'project.about.details.updated',
|
||||
defaultMessage: 'Updated {date}',
|
||||
},
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
archivedMessage: {
|
||||
id: 'project.status.archived.message',
|
||||
@@ -870,22 +707,6 @@ const messages = defineMessages({
|
||||
id: 'project.gallery.title',
|
||||
defaultMessage: 'Gallery',
|
||||
},
|
||||
licenseErrorMessage: {
|
||||
id: 'project.license.error',
|
||||
defaultMessage: 'License text could not be retrieved.',
|
||||
},
|
||||
licenseTitle: {
|
||||
id: 'project.license.title',
|
||||
defaultMessage: 'License',
|
||||
},
|
||||
licensedLabel: {
|
||||
id: 'project.details.licensed',
|
||||
defaultMessage: 'Licensed',
|
||||
},
|
||||
loadingLicenseText: {
|
||||
id: 'project.license.loading',
|
||||
defaultMessage: 'Loading license text...',
|
||||
},
|
||||
moderationTab: {
|
||||
id: 'project.moderation.title',
|
||||
defaultMessage: 'Moderation',
|
||||
@@ -956,45 +777,7 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const modalLicense = ref(null)
|
||||
const modalCollection = useTemplateRef('modal_collection')
|
||||
const licenseText = ref('')
|
||||
|
||||
const createdDate = computed(() =>
|
||||
project.value.published ? formatRelativeTime(project.value.published) : 'unknown',
|
||||
)
|
||||
const submittedDate = computed(() =>
|
||||
project.value.queued ? formatRelativeTime(project.value.queued) : 'unknown',
|
||||
)
|
||||
const publishedDate = computed(() =>
|
||||
project.value.approved ? formatRelativeTime(project.value.approved) : 'unknown',
|
||||
)
|
||||
const updatedDate = computed(() =>
|
||||
project.value.updated ? formatRelativeTime(project.value.updated) : 'unknown',
|
||||
)
|
||||
|
||||
const licenseIdDisplay = computed(() => {
|
||||
const id = project.value.license.id
|
||||
|
||||
if (id === 'LicenseRef-All-Rights-Reserved') {
|
||||
return 'ARR'
|
||||
} else if (id.includes('LicenseRef')) {
|
||||
return id.replaceAll('LicenseRef-', '').replaceAll('-', ' ')
|
||||
} else {
|
||||
return id
|
||||
}
|
||||
})
|
||||
|
||||
async function getLicenseData(event) {
|
||||
modalLicense.value.show(event)
|
||||
|
||||
try {
|
||||
const text = await client.labrinth.tags_v2.getLicenseText(project.value.license.id)
|
||||
licenseText.value = text.body || formatMessage(messages.licenseErrorMessage)
|
||||
} catch {
|
||||
licenseText.value = formatMessage(messages.licenseErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const collections = computed(() =>
|
||||
user.value && user.value.collections ? user.value.collections : [],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const mod = 'cloudflare:workers'
|
||||
const { env } = await import(/* @vite-ignore */ mod)
|
||||
const cfEnv = env as any
|
||||
const config = useRuntimeConfig(event)
|
||||
|
||||
if (cfEnv.CF_PAGES_URL) config.public.siteUrl = cfEnv.CF_PAGES_URL
|
||||
if (cfEnv.BROWSER_BASE_URL) config.public.apiBaseUrl = cfEnv.BROWSER_BASE_URL
|
||||
if (cfEnv.BASE_URL) config.apiBaseUrl = cfEnv.BASE_URL
|
||||
if (cfEnv.PYRO_BASE_URL) {
|
||||
config.public.pyroBaseUrl = cfEnv.PYRO_BASE_URL
|
||||
config.pyroBaseUrl = cfEnv.PYRO_BASE_URL
|
||||
}
|
||||
if (cfEnv.STRIPE_PUBLISHABLE_KEY)
|
||||
config.public.stripePublishableKey = cfEnv.STRIPE_PUBLISHABLE_KEY
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
})
|
||||
@@ -10,6 +10,7 @@ CDN_URL=file:///tmp/modrinth
|
||||
LABRINTH_ADMIN_KEY=feedbeef
|
||||
LABRINTH_MEDAL_KEY=
|
||||
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
|
||||
LABRINTH_SUBSCRIPTIONS_KEY=
|
||||
RATE_LIMIT_IGNORE_KEY=feedbeef
|
||||
|
||||
DATABASE_URL=postgresql://labrinth:labrinth@labrinth-postgres/labrinth
|
||||
@@ -29,9 +30,14 @@ TYPESENSE_URL=http://localhost:8108
|
||||
TYPESENSE_API_KEY=modrinth
|
||||
TYPESENSE_INDEX_PREFIX=labrinth
|
||||
|
||||
REDIS_MODE=standalone
|
||||
REDIS_CONNECTION_TYPE=pooled
|
||||
# Cache fill coordination is local to each process
|
||||
REDIS_CACHE_LOCKING_STRATEGY=local
|
||||
REDIS_URL=redis://labrinth-redis
|
||||
REDIS_MIN_CONNECTIONS=0
|
||||
REDIS_MAX_CONNECTIONS=10000
|
||||
REDIS_BLOCKING_MAX_CONNECTIONS=8
|
||||
|
||||
KAFKA_BOOTSTRAP_SERVERS=redpanda:9092
|
||||
KAFKA_CLIENT_ID=labrinth
|
||||
|
||||
@@ -10,6 +10,7 @@ CDN_URL=file:///tmp/modrinth
|
||||
LABRINTH_ADMIN_KEY=feedbeef
|
||||
LABRINTH_MEDAL_KEY=
|
||||
LABRINTH_EXTERNAL_NOTIFICATION_KEY=beeffeed
|
||||
LABRINTH_SUBSCRIPTIONS_KEY=
|
||||
RATE_LIMIT_IGNORE_KEY=feedbeef
|
||||
|
||||
DATABASE_URL=postgresql://labrinth:labrinth@localhost/labrinth
|
||||
@@ -47,9 +48,19 @@ TYPESENSE_URL=http://localhost:8108
|
||||
TYPESENSE_API_KEY=modrinth
|
||||
TYPESENSE_INDEX_PREFIX=labrinth
|
||||
|
||||
REDIS_MODE=standalone
|
||||
REDIS_CONNECTION_TYPE=pooled
|
||||
# Cache fill coordination is local to each process
|
||||
REDIS_CACHE_LOCKING_STRATEGY=local
|
||||
REDIS_URL=redis://localhost
|
||||
REDIS_MIN_CONNECTIONS=0
|
||||
REDIS_MAX_CONNECTIONS=10000
|
||||
REDIS_BLOCKING_MAX_CONNECTIONS=8
|
||||
|
||||
# For a clustered Redis setup (`clustered-redis` Docker Compose profile)
|
||||
# REDIS_MODE=cluster
|
||||
# REDIS_CONNECTION_TYPE=multiplexed
|
||||
# REDIS_URL=redis://127.0.0.1:7000,redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003,redis://127.0.0.1:7004,redis://127.0.0.1:7005
|
||||
|
||||
KAFKA_BOOTSTRAP_SERVERS=localhost:19092
|
||||
KAFKA_CLIENT_ID=labrinth
|
||||
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET email_verified = TRUE WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cb626a36deffd73e67de2dc4789bd875675779a173fb2cd41ba365c1acca96f9"
|
||||
}
|
||||
@@ -41,7 +41,6 @@ color-eyre = { workspace = true }
|
||||
color-thief = { workspace = true }
|
||||
const_format = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
deadpool-redis.workspace = true
|
||||
derive_more = { workspace = true, features = ["deref", "deref_mut"] }
|
||||
dotenvy = { workspace = true }
|
||||
either = { workspace = true }
|
||||
@@ -73,7 +72,6 @@ image = { workspace = true, features = [
|
||||
itertools = { workspace = true }
|
||||
json-patch = { workspace = true }
|
||||
lettre = { workspace = true }
|
||||
lz4_flex = { workspace = true }
|
||||
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
||||
modrinth-content-management = { workspace = true }
|
||||
modrinth-util = { workspace = true, features = ["decimal", "sentry", "utoipa"] }
|
||||
@@ -88,7 +86,13 @@ quick-xml = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rdkafka = { workspace = true }
|
||||
redis = { workspace = true, features = ["ahash", "r2d2", "tokio-comp"] }
|
||||
redis = { workspace = true, features = [
|
||||
"ahash",
|
||||
"cluster",
|
||||
"cluster-async",
|
||||
"r2d2",
|
||||
"tokio-comp"
|
||||
] }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true, features = [
|
||||
"http2",
|
||||
@@ -141,12 +145,14 @@ webauthn-rs = { workspace = true, features = [
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
webp = { workspace = true }
|
||||
woothee = { workspace = true }
|
||||
xredis = { workspace = true }
|
||||
yaserde = { workspace = true, features = ["derive"] }
|
||||
zip = { workspace = true }
|
||||
zxcvbn = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
labrinth = { path = ".", features = ["test"] }
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
|
||||
[build-dependencies]
|
||||
chrono = { workspace = true }
|
||||
@@ -162,7 +168,7 @@ tikv-jemallocator = { workspace = true, features = [
|
||||
] }
|
||||
|
||||
[features]
|
||||
test = []
|
||||
test = ["xredis/test"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::database;
|
||||
use crate::database::models::project_item::ProjectQueryResult;
|
||||
use crate::database::models::version_item::VersionQueryResult;
|
||||
use crate::database::models::{DBCollection, DBOrganization, DBTeamMember};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{DBProject, DBVersion, models};
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
use crate::models::ids::FileId;
|
||||
@@ -14,6 +13,7 @@ use crate::queue::file_scan::get_files_missing_attribution;
|
||||
use crate::routes::ApiError;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub fn require_verified_email(user: &User) -> Result<(), ApiError> {
|
||||
if !user.email_verified.unwrap_or(false) {
|
||||
|
||||
@@ -59,6 +59,12 @@ pub enum AuthenticationError {
|
||||
Url,
|
||||
}
|
||||
|
||||
impl From<xredis::Error> for AuthenticationError {
|
||||
fn from(error: xredis::Error) -> Self {
|
||||
Self::Database(error.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for AuthenticationError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::database::models::{
|
||||
DBOAuthClientAuthorizationId, generate_oauth_access_token_id,
|
||||
generate_oauth_client_authorization_id,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models;
|
||||
use crate::models::ids::OAuthClientId;
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -25,6 +24,7 @@ use rand::distributions::Alphanumeric;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use self::errors::{OAuthError, OAuthErrorType};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::AuthProvider;
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::database::models::{DBUser, user_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::User;
|
||||
@@ -10,6 +9,7 @@ use crate::routes::internal::session::get_session_metadata;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::http::header::{AUTHORIZATION, HeaderValue};
|
||||
use chrono::Utc;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub async fn get_maybe_user_from_headers<'a, E>(
|
||||
req: &HttpRequest,
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::database;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::ids::DBUserId;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::queue::analytics::cache::cache_analytics;
|
||||
@@ -20,6 +19,7 @@ use actix_web::web;
|
||||
use clap::ValueEnum;
|
||||
use eyre::WrapErr;
|
||||
use tracing::info;
|
||||
use xredis::RedisPool;
|
||||
|
||||
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[clap(rename_all = "kebab_case")]
|
||||
@@ -386,11 +386,11 @@ mod version_updater {
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::legacy_loader_fields::MinecraftGameVersion;
|
||||
use crate::database::redis::RedisPool;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
use xredis::RedisPool;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct InputFormat<'a> {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use sqlx::types::Json;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
models::{DBAnalyticsEventId, DatabaseError},
|
||||
redis::RedisPool,
|
||||
},
|
||||
database::models::{DBAnalyticsEventId, DatabaseError},
|
||||
models::v3::analytics_event::AnalyticsEventMeta,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events:v1";
|
||||
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events:v3";
|
||||
const ANALYTICS_EVENTS_ALL_KEY: &str = "all";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -86,14 +84,11 @@ impl DBAnalyticsEvent {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<DBAnalyticsEvent>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY);
|
||||
|
||||
if let Some(events) = redis
|
||||
.get_deserialized(
|
||||
ANALYTICS_EVENTS_NAMESPACE,
|
||||
ANALYTICS_EVENTS_ALL_KEY,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(events) = redis.get_deserialized(&key).await? {
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
@@ -118,23 +113,17 @@ impl DBAnalyticsEvent {
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
ANALYTICS_EVENTS_NAMESPACE,
|
||||
ANALYTICS_EVENTS_ALL_KEY,
|
||||
&events,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &events, None).await?;
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub async fn clear_cache(redis: &RedisPool) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.delete(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY)
|
||||
.await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY);
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::DatabaseError;
|
||||
use super::ids::*;
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TAGS_NAMESPACE: &str = "tags:v1";
|
||||
const TAGS_NAMESPACE: &str = "tags:v3";
|
||||
|
||||
pub struct ProjectType {
|
||||
pub id: ProjectTypeId,
|
||||
@@ -95,9 +95,10 @@ impl Category {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "category");
|
||||
|
||||
let res: Option<Vec<Category>> =
|
||||
redis.get_deserialized(TAGS_NAMESPACE, "category").await?;
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -124,10 +125,9 @@ impl Category {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "category");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "category", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -163,10 +163,10 @@ impl LinkPlatform {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "link_platform");
|
||||
|
||||
let res: Option<Vec<LinkPlatform>> = redis
|
||||
.get_deserialized(TAGS_NAMESPACE, "link_platform")
|
||||
.await?;
|
||||
let res: Option<Vec<LinkPlatform>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -188,10 +188,9 @@ impl LinkPlatform {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "link_platform");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "link_platform", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -227,10 +226,9 @@ impl ReportType {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "report_type");
|
||||
|
||||
let res: Option<Vec<String>> = redis
|
||||
.get_deserialized(TAGS_NAMESPACE, "report_type")
|
||||
.await?;
|
||||
let res: Option<Vec<String>> = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -248,10 +246,9 @@ impl ReportType {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "report_type");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "report_type", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -287,10 +284,9 @@ impl ProjectType {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "project_type");
|
||||
|
||||
let res: Option<Vec<String>> = redis
|
||||
.get_deserialized(TAGS_NAMESPACE, "project_type")
|
||||
.await?;
|
||||
let res: Option<Vec<String>> = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -308,10 +304,9 @@ impl ProjectType {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TAGS_NAMESPACE, "project_type");
|
||||
|
||||
redis
|
||||
.set_serialized(TAGS_NAMESPACE, "project_type", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use super::ids::*;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::models::collections::CollectionStatus;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const COLLECTIONS_NAMESPACE: &str = "collections:v1";
|
||||
const COLLECTIONS_NAMESPACE: &str = "collections:v3";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CollectionBuilder {
|
||||
@@ -204,7 +204,7 @@ impl DBCollection {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(collections)
|
||||
Ok::<_, DatabaseError>(collections)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -217,8 +217,9 @@ impl DBCollection {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(COLLECTIONS_NAMESPACE, id.0);
|
||||
|
||||
redis.delete(COLLECTIONS_NAMESPACE, id.0).await?;
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::auth::oauth::uris::OAuthRedirectUris;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::{auth::AuthProvider, routes::internal::flows::TempUser};
|
||||
use chrono::Duration;
|
||||
@@ -12,8 +11,9 @@ use rand_chacha::rand_core::SeedableRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const FLOWS_NAMESPACE: &str = "flows:v1";
|
||||
const FLOWS_NAMESPACE: &str = "flows:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum DBFlow {
|
||||
@@ -75,14 +75,10 @@ impl DBFlow {
|
||||
state: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(FLOWS_NAMESPACE, state);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
FLOWS_NAMESPACE,
|
||||
&state,
|
||||
&self,
|
||||
Some(expires.num_seconds()),
|
||||
)
|
||||
.set_serialized(&key, &self, Some(expires.num_seconds()))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -107,8 +103,9 @@ impl DBFlow {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<DBFlow>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(FLOWS_NAMESPACE, id);
|
||||
|
||||
redis.get_deserialized(FLOWS_NAMESPACE, id).await
|
||||
redis.get_deserialized(&key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets the flow and removes it from the cache, but only removes if the flow was present and the predicate returned true
|
||||
@@ -132,8 +129,9 @@ impl DBFlow {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<()>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(FLOWS_NAMESPACE, id);
|
||||
|
||||
redis.delete(FLOWS_NAMESPACE, id).await?;
|
||||
redis.delete(&key).await?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::{database::models::DatabaseError, models::images::ImageContext};
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const IMAGES_NAMESPACE: &str = "images:v1";
|
||||
const IMAGES_NAMESPACE: &str = "images:v3";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DBImage {
|
||||
@@ -217,7 +217,7 @@ impl DBImage {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(images)
|
||||
Ok::<_, DatabaseError>(images)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -229,8 +229,9 @@ impl DBImage {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(IMAGES_NAMESPACE, id.0);
|
||||
|
||||
redis.delete(IMAGES_NAMESPACE, id.0).await?;
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{
|
||||
DatabaseError, LoaderFieldEnumValueId,
|
||||
@@ -220,11 +220,11 @@ impl<'a> MinecraftGameVersionBuilder<'a> {
|
||||
.await?;
|
||||
|
||||
let mut conn = redis.connect().await?;
|
||||
conn.delete(
|
||||
let key = conn.key().entity(
|
||||
crate::database::models::loader_fields::LOADER_FIELD_ENUM_VALUES_NAMESPACE,
|
||||
game_versions_enum.id.0,
|
||||
)
|
||||
.await?;
|
||||
);
|
||||
conn.delete(&key).await?;
|
||||
|
||||
Ok(LoaderFieldEnumValueId(result.id))
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@ use std::hash::Hasher;
|
||||
use super::DatabaseError;
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::redis::RedisPool;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const GAMES_LIST_NAMESPACE: &str = "games:v1";
|
||||
const LOADER_ID: &str = "loader_id:v1";
|
||||
const LOADERS_LIST_NAMESPACE: &str = "loaders:v1";
|
||||
const LOADER_FIELDS_NAMESPACE: &str = "loader_fields:v1";
|
||||
const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all:v1";
|
||||
const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums:v1";
|
||||
const GAMES_LIST_NAMESPACE: &str = "games:v3";
|
||||
const LOADER_ID: &str = "loader_id:v3";
|
||||
const LOADERS_LIST_NAMESPACE: &str = "loaders:v3";
|
||||
const LOADER_FIELDS_NAMESPACE: &str = "loader_fields:v3";
|
||||
const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all:v3";
|
||||
const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums:v3";
|
||||
pub const LOADER_FIELD_ENUM_VALUES_NAMESPACE: &str =
|
||||
"loader_field_enum_values:v1";
|
||||
"loader_field_enum_values:v3";
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Game {
|
||||
@@ -54,9 +54,9 @@ impl Game {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_games: Option<Vec<Game>> = redis
|
||||
.get_deserialized(GAMES_LIST_NAMESPACE, "games")
|
||||
.await?;
|
||||
let key = redis.key().metadata(GAMES_LIST_NAMESPACE, "games");
|
||||
let cached_games: Option<Vec<Game>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_games) = cached_games {
|
||||
return Ok(cached_games);
|
||||
}
|
||||
@@ -79,10 +79,9 @@ impl Game {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(GAMES_LIST_NAMESPACE, "games");
|
||||
|
||||
redis
|
||||
.set_serialized(GAMES_LIST_NAMESPACE, "games", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -109,8 +108,8 @@ impl Loader {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_id: Option<i32> =
|
||||
redis.get_deserialized(LOADER_ID, name).await?;
|
||||
let key = redis.key().metadata(LOADER_ID, name);
|
||||
let cached_id: Option<i32> = redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_id) = cached_id {
|
||||
return Ok(Some(LoaderId(cached_id)));
|
||||
}
|
||||
@@ -129,9 +128,8 @@ impl Loader {
|
||||
|
||||
if let Some(result) = result {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.set_serialized(LOADER_ID, name, &result.0, None)
|
||||
.await?;
|
||||
let key = redis.key().metadata(LOADER_ID, name);
|
||||
redis.set_serialized(&key, &result.0, None).await?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
@@ -146,9 +144,9 @@ impl Loader {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_loaders: Option<Vec<Loader>> = redis
|
||||
.get_deserialized(LOADERS_LIST_NAMESPACE, "all")
|
||||
.await?;
|
||||
let key = redis.key().metadata(LOADERS_LIST_NAMESPACE, "all");
|
||||
let cached_loaders: Option<Vec<Loader>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_loaders) = cached_loaders {
|
||||
return Ok(cached_loaders);
|
||||
}
|
||||
@@ -187,10 +185,9 @@ impl Loader {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(LOADERS_LIST_NAMESPACE, "all");
|
||||
|
||||
redis
|
||||
.set_serialized(LOADERS_LIST_NAMESPACE, "all", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -441,7 +438,7 @@ impl LoaderField {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
Ok::<_, DatabaseError>(result)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -460,10 +457,10 @@ impl LoaderField {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(LOADER_FIELDS_NAMESPACE_ALL, "");
|
||||
|
||||
let cached_fields: Option<Vec<LoaderField>> = redis
|
||||
.get_deserialized(LOADER_FIELDS_NAMESPACE_ALL, "")
|
||||
.await?;
|
||||
let cached_fields: Option<Vec<LoaderField>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(cached_fields) = cached_fields {
|
||||
return Ok(cached_fields);
|
||||
@@ -494,10 +491,9 @@ impl LoaderField {
|
||||
.collect();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(LOADER_FIELDS_NAMESPACE_ALL, "");
|
||||
|
||||
redis
|
||||
.set_serialized(LOADER_FIELDS_NAMESPACE_ALL, "", &result, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -513,10 +509,11 @@ impl LoaderFieldEnum {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name);
|
||||
|
||||
let cached_enum = redis
|
||||
.get_deserialized(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name)
|
||||
.await?;
|
||||
let cached_enum = redis.get_deserialized(&key).await?;
|
||||
if let Some(cached_enum) = cached_enum {
|
||||
return Ok(cached_enum);
|
||||
}
|
||||
@@ -541,15 +538,11 @@ impl LoaderFieldEnum {
|
||||
});
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.metadata(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
LOADER_FIELD_ENUMS_ID_NAMESPACE,
|
||||
enum_name,
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &result, None).await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -652,7 +645,7 @@ impl LoaderFieldEnumValue {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(values)
|
||||
Ok::<_, DatabaseError>(values)
|
||||
},
|
||||
).await?;
|
||||
|
||||
|
||||
@@ -72,25 +72,12 @@ pub enum DatabaseError {
|
||||
RandomId,
|
||||
#[error("Error while interacting with the cache: {0}")]
|
||||
CacheError(#[from] redis::RedisError),
|
||||
#[error("Redis Pool Error: {0}")]
|
||||
RedisPool(#[from] deadpool_redis::PoolError),
|
||||
#[error("Error while serializing with the cache: {0}")]
|
||||
SerdeCacheError(#[from] serde_json::Error),
|
||||
#[error("error while encoding or decoding the cache: {0}")]
|
||||
PostcardCacheError(#[from] postcard::Error),
|
||||
#[error(transparent)]
|
||||
Redis(#[from] xredis::Error),
|
||||
#[error("Schema error: {0}")]
|
||||
SchemaError(String),
|
||||
#[error(
|
||||
"Timeout waiting on Redis cache lock ({locks_released}/{locks_waiting} released, spent {time_spent_pool_wait_ms}ms/{time_spent_total_ms}ms waiting on connections from pool)"
|
||||
)]
|
||||
CacheTimeout {
|
||||
locks_released: usize,
|
||||
locks_waiting: usize,
|
||||
time_spent_pool_wait_ms: u64,
|
||||
time_spent_total_ms: u64,
|
||||
},
|
||||
#[error(
|
||||
"Timeout waiting on local cache lock ({released}/{total} released)"
|
||||
)]
|
||||
LocalCacheTimeout { released: usize, total: usize },
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ use std::collections::HashMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::redis::RedisPool;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{DBOrganizationId, DBUserId, DatabaseError};
|
||||
|
||||
const MODERATION_NOTES_USERS_NAMESPACE: &str = "moderation_notes_users:v1";
|
||||
const MODERATION_NOTES_USERS_NAMESPACE: &str = "moderation_notes_users:v3";
|
||||
const MODERATION_NOTES_ORGANIZATIONS_NAMESPACE: &str =
|
||||
"moderation_notes_organizations:v1";
|
||||
"moderation_notes_organizations:v3";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DBModerationNote {
|
||||
@@ -32,19 +32,15 @@ impl DBModerationNote {
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let ids = user_ids
|
||||
.iter()
|
||||
.map(|id| id.0.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cached = {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_many_deserialized::<Self>(
|
||||
MODERATION_NOTES_USERS_NAMESPACE,
|
||||
&ids,
|
||||
)
|
||||
.await?
|
||||
let keys = user_ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
redis.key().entity(MODERATION_NOTES_USERS_NAMESPACE, id.0)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.get_many_deserialized::<Self>(&keys).await?
|
||||
};
|
||||
|
||||
let mut notes = HashMap::new();
|
||||
@@ -86,14 +82,10 @@ impl DBModerationNote {
|
||||
};
|
||||
|
||||
if let Some(user_id) = note.user_id {
|
||||
redis
|
||||
.set_serialized(
|
||||
MODERATION_NOTES_USERS_NAMESPACE,
|
||||
user_id.0,
|
||||
¬e,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let key = redis
|
||||
.key()
|
||||
.entity(MODERATION_NOTES_USERS_NAMESPACE, user_id.0);
|
||||
redis.set_serialized(&key, ¬e, None).await?;
|
||||
notes.insert(user_id, note);
|
||||
}
|
||||
}
|
||||
@@ -122,19 +114,17 @@ impl DBModerationNote {
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let ids = organization_ids
|
||||
.iter()
|
||||
.map(|id| id.0.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cached = {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_many_deserialized::<Self>(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
&ids,
|
||||
)
|
||||
.await?
|
||||
let keys = organization_ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
redis
|
||||
.key()
|
||||
.entity(MODERATION_NOTES_ORGANIZATIONS_NAMESPACE, id.0)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.get_many_deserialized::<Self>(&keys).await?
|
||||
};
|
||||
|
||||
let mut notes = HashMap::new();
|
||||
@@ -176,14 +166,11 @@ impl DBModerationNote {
|
||||
};
|
||||
|
||||
if let Some(organization_id) = note.organization_id {
|
||||
redis
|
||||
.set_serialized(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
¬e,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let key = redis.key().entity(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
);
|
||||
redis.set_serialized(&key, ¬e, None).await?;
|
||||
notes.insert(organization_id, note);
|
||||
}
|
||||
}
|
||||
@@ -289,9 +276,10 @@ impl DBModerationNote {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.delete(MODERATION_NOTES_USERS_NAMESPACE, user_id.0)
|
||||
.await
|
||||
let key = redis
|
||||
.key()
|
||||
.entity(MODERATION_NOTES_USERS_NAMESPACE, user_id.0);
|
||||
redis.delete(&key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn clear_organization_cache(
|
||||
@@ -299,8 +287,10 @@ impl DBModerationNote {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.delete(MODERATION_NOTES_ORGANIZATIONS_NAMESPACE, organization_id.0)
|
||||
.await
|
||||
let key = redis.key().entity(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
);
|
||||
redis.delete(&key).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::{models::DatabaseError, redis::RedisPool};
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::models::notifications::{
|
||||
NotificationBody, NotificationChannel, NotificationDeliveryStatus,
|
||||
NotificationType,
|
||||
@@ -8,8 +8,9 @@ use crate::models::notifications::{
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications:v1";
|
||||
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications:v3";
|
||||
|
||||
pub struct NotificationBuilder {
|
||||
pub body: NotificationBody,
|
||||
@@ -433,13 +434,11 @@ impl DBNotification {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key =
|
||||
redis.key().entity(USER_NOTIFICATIONS_NAMESPACE, user_id.0);
|
||||
|
||||
let cached_notifications: Option<Vec<DBNotification>> = redis
|
||||
.get_deserialized(
|
||||
USER_NOTIFICATIONS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let cached_notifications: Option<Vec<DBNotification>> =
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(notifications) = cached_notifications {
|
||||
return Ok(notifications);
|
||||
@@ -491,15 +490,9 @@ impl DBNotification {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(USER_NOTIFICATIONS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
USER_NOTIFICATIONS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_notifications,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_notifications, None).await?;
|
||||
|
||||
Ok(db_notifications)
|
||||
}
|
||||
@@ -638,12 +631,12 @@ impl DBNotification {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let keys = user_ids
|
||||
.into_iter()
|
||||
.map(|id| redis.key().entity(USER_NOTIFICATIONS_NAMESPACE, id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.delete_many(user_ids.into_iter().map(|id| {
|
||||
(USER_NOTIFICATIONS_NAMESPACE, Some(id.0.to_string()))
|
||||
}))
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::{NotificationChannel, NotificationType};
|
||||
use crate::routes::ApiError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates:v1";
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates:v3";
|
||||
const TEMPLATES_HTML_DATA_NAMESPACE: &str =
|
||||
"notifications_templates_html_data:v1";
|
||||
"notifications_templates_html_data:v3";
|
||||
const TEMPLATES_DYNAMIC_HTML_NAMESPACE: &str =
|
||||
"notifications_templates_dynamic_html:v1";
|
||||
"notifications_templates_dynamic_html:v3";
|
||||
|
||||
const HTML_DATA_CACHE_EXPIRY: i64 = 60 * 15; // 15 minutes
|
||||
const TEMPLATES_CACHE_EXPIRY: i64 = 60 * 30; // 30 minutes
|
||||
@@ -55,10 +55,10 @@ impl NotificationTemplate {
|
||||
) -> Result<Vec<NotificationTemplate>, DatabaseError> {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key =
|
||||
redis.key().metadata(TEMPLATES_NAMESPACE, channel.as_str());
|
||||
|
||||
let maybe_cached_templates = redis
|
||||
.get_deserialized(TEMPLATES_NAMESPACE, channel.as_str())
|
||||
.await?;
|
||||
let maybe_cached_templates = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(cached) = maybe_cached_templates {
|
||||
return Ok(cached);
|
||||
@@ -78,14 +78,10 @@ impl NotificationTemplate {
|
||||
let templates = results.into_iter().map(Into::into).collect();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TEMPLATES_NAMESPACE, channel.as_str());
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
TEMPLATES_NAMESPACE,
|
||||
channel.as_str(),
|
||||
&templates,
|
||||
Some(TEMPLATES_CACHE_EXPIRY),
|
||||
)
|
||||
.set_serialized(&key, &templates, Some(TEMPLATES_CACHE_EXPIRY))
|
||||
.await?;
|
||||
|
||||
Ok(templates)
|
||||
@@ -96,12 +92,8 @@ impl NotificationTemplate {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<String>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_deserialized(
|
||||
TEMPLATES_HTML_DATA_NAMESPACE,
|
||||
&self.id.to_string(),
|
||||
)
|
||||
.await
|
||||
let key = redis.key().metadata(TEMPLATES_HTML_DATA_NAMESPACE, self.id);
|
||||
redis.get_deserialized(&key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn set_cached_html_data(
|
||||
@@ -110,14 +102,11 @@ impl NotificationTemplate {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(TEMPLATES_HTML_DATA_NAMESPACE, self.id);
|
||||
redis
|
||||
.set_serialized(
|
||||
TEMPLATES_HTML_DATA_NAMESPACE,
|
||||
&self.id.to_string(),
|
||||
&data,
|
||||
Some(HTML_DATA_CACHE_EXPIRY),
|
||||
)
|
||||
.set_serialized(&key, &data, Some(HTML_DATA_CACHE_EXPIRY))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +124,11 @@ where
|
||||
}
|
||||
|
||||
let mut redis_conn = redis.connect().await?;
|
||||
if let Some(body) = redis_conn
|
||||
.get_deserialized::<HtmlBody>(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key)
|
||||
.await?
|
||||
let redis_key = redis_conn
|
||||
.key()
|
||||
.metadata(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key);
|
||||
if let Some(body) =
|
||||
redis_conn.get_deserialized::<HtmlBody>(&redis_key).await?
|
||||
{
|
||||
return Ok(body.html);
|
||||
}
|
||||
@@ -146,14 +137,12 @@ where
|
||||
|
||||
let cached = HtmlBody { html: get().await? };
|
||||
let mut redis_conn = redis.connect().await?;
|
||||
let redis_key = redis_conn
|
||||
.key()
|
||||
.metadata(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key);
|
||||
|
||||
redis_conn
|
||||
.set_serialized(
|
||||
TEMPLATES_DYNAMIC_HTML_NAMESPACE,
|
||||
key,
|
||||
&cached,
|
||||
Some(HTML_DATA_CACHE_EXPIRY),
|
||||
)
|
||||
.set_serialized(&redis_key, &cached, Some(HTML_DATA_CACHE_EXPIRY))
|
||||
.await?;
|
||||
|
||||
Ok(cached.html)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::NotificationType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const NOTIFICATION_TYPES_NAMESPACE: &str = "notification_types:v1";
|
||||
const NOTIFICATION_TYPES_NAMESPACE: &str = "notification_types:v3";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct NotificationTypeItem {
|
||||
@@ -41,10 +41,9 @@ impl NotificationTypeItem {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(NOTIFICATION_TYPES_NAMESPACE, "all");
|
||||
|
||||
let cached_types = redis
|
||||
.get_deserialized(NOTIFICATION_TYPES_NAMESPACE, "all")
|
||||
.await?;
|
||||
let cached_types = redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(types) = cached_types {
|
||||
return Ok(types);
|
||||
@@ -61,10 +60,9 @@ impl NotificationTypeItem {
|
||||
let types = results.into_iter().map(Into::into).collect();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(NOTIFICATION_TYPES_NAMESPACE, "all");
|
||||
|
||||
redis
|
||||
.set_serialized(NOTIFICATION_TYPES_NAMESPACE, "all", &types, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &types, None).await?;
|
||||
|
||||
Ok(types)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::redis::RedisPool;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::{DBTeamMember, ids::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ORGANIZATIONS_NAMESPACE: &str = "organizations:v1";
|
||||
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles:v1";
|
||||
const ORGANIZATIONS_NAMESPACE: &str = "organizations:v3";
|
||||
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
/// An organization of users who together control one or more projects and organizations.
|
||||
@@ -159,7 +159,9 @@ impl DBOrganization {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(organizations)
|
||||
Ok::<_, crate::database::models::DatabaseError>(
|
||||
organizations,
|
||||
)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -256,16 +258,17 @@ impl DBOrganization {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), super::DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.delete_many([
|
||||
(ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())),
|
||||
(
|
||||
let mut keys = vec![redis.key().entity(ORGANIZATIONS_NAMESPACE, id.0)];
|
||||
if let Some(slug) = slug {
|
||||
keys.push(
|
||||
redis.key().entity(
|
||||
ORGANIZATIONS_TITLES_NAMESPACE,
|
||||
slug.map(|x| x.to_lowercase()),
|
||||
slug.to_lowercase(),
|
||||
),
|
||||
])
|
||||
.await?;
|
||||
);
|
||||
}
|
||||
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -10,10 +9,11 @@ use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const PATS_NAMESPACE: &str = "pats:v1";
|
||||
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens:v1";
|
||||
const PATS_USERS_NAMESPACE: &str = "pats_users:v1";
|
||||
const PATS_NAMESPACE: &str = "pats:v3";
|
||||
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens:v3";
|
||||
const PATS_USERS_NAMESPACE: &str = "pats_users:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBPersonalAccessToken {
|
||||
@@ -141,7 +141,7 @@ impl DBPersonalAccessToken {
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
Ok(pats)
|
||||
Ok::<_, DatabaseError>(pats)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -159,13 +159,9 @@ impl DBPersonalAccessToken {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PATS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
let res = redis
|
||||
.get_deserialized::<Vec<i64>>(
|
||||
PATS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let res = redis.get_deserialized::<Vec<i64>>(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res.into_iter().map(DBPatId).collect());
|
||||
@@ -187,10 +183,9 @@ impl DBPersonalAccessToken {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PATS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(PATS_USERS_NAMESPACE, user_id.0, &db_pats, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_pats, None).await?;
|
||||
Ok(db_pats)
|
||||
}
|
||||
|
||||
@@ -204,20 +199,23 @@ impl DBPersonalAccessToken {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
redis
|
||||
.delete_many(clear_pats.into_iter().flat_map(
|
||||
|(id, token, user_id)| {
|
||||
[
|
||||
(PATS_NAMESPACE, id.map(|i| i.0.to_string())),
|
||||
(PATS_TOKENS_NAMESPACE, token),
|
||||
(
|
||||
PATS_USERS_NAMESPACE,
|
||||
user_id.map(|i| i.0.to_string()),
|
||||
),
|
||||
]
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
let keys = clear_pats
|
||||
.into_iter()
|
||||
.flat_map(|(id, token, user_id)| {
|
||||
[
|
||||
id.map(|id| redis.key().entity(PATS_NAMESPACE, id.0)),
|
||||
token.map(|token| {
|
||||
redis.key().entity(PATS_TOKENS_NAMESPACE, token)
|
||||
}),
|
||||
user_id.map(|user_id| {
|
||||
redis.key().entity(PATS_USERS_NAMESPACE, user_id.0)
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.delete_many(&keys).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::database::models::{
|
||||
DBProductId, DBProductPriceId, DatabaseError, product_item,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::billing::{Price, ProductMetadata};
|
||||
use dashmap::DashMap;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const PRODUCTS_NAMESPACE: &str = "products:v1";
|
||||
const PRODUCTS_NAMESPACE: &str = "products:v3";
|
||||
|
||||
pub struct DBProduct {
|
||||
pub id: DBProductId,
|
||||
@@ -152,9 +152,10 @@ impl QueryProductWithPrices {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(PRODUCTS_NAMESPACE, "all");
|
||||
|
||||
let res: Option<Vec<QueryProductWithPrices>> =
|
||||
redis.get_deserialized(PRODUCTS_NAMESPACE, "all").await?;
|
||||
redis.get_deserialized(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -193,10 +194,9 @@ impl QueryProductWithPrices {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().metadata(PRODUCTS_NAMESPACE, "all");
|
||||
|
||||
redis
|
||||
.set_serialized(PRODUCTS_NAMESPACE, "all", &products, None)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &products, None).await?;
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use super::loader_fields::{
|
||||
};
|
||||
use super::{DBUser, ids::*};
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::exp;
|
||||
@@ -22,10 +21,11 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects:v1";
|
||||
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs:v1";
|
||||
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies:v1";
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects:v3";
|
||||
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs:v3";
|
||||
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies:v3";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LinkUrl {
|
||||
@@ -942,7 +942,7 @@ impl DBProject {
|
||||
})
|
||||
?;
|
||||
|
||||
Ok(projects)
|
||||
Ok::<_, DatabaseError>(projects)
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -974,13 +974,10 @@ impl DBProject {
|
||||
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PROJECTS_DEPENDENCIES_NAMESPACE, id.0);
|
||||
|
||||
let dependencies = redis
|
||||
.get_deserialized::<Dependencies>(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
&id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let dependencies =
|
||||
redis.get_deserialized::<Dependencies>(&key).await?;
|
||||
if let Some(dependencies) = dependencies {
|
||||
return Ok(dependencies);
|
||||
}
|
||||
@@ -1012,15 +1009,9 @@ impl DBProject {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(PROJECTS_DEPENDENCIES_NAMESPACE, id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
id.0,
|
||||
&dependencies,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &dependencies, None).await?;
|
||||
Ok(dependencies)
|
||||
}
|
||||
|
||||
@@ -1031,21 +1022,21 @@ impl DBProject {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let mut keys = vec![redis.key().entity(PROJECTS_NAMESPACE, id.0)];
|
||||
if let Some(slug) = slug {
|
||||
keys.push(
|
||||
redis
|
||||
.key()
|
||||
.entity(PROJECTS_SLUGS_NAMESPACE, slug.to_lowercase()),
|
||||
);
|
||||
}
|
||||
if clear_dependencies.unwrap_or(false) {
|
||||
keys.push(
|
||||
redis.key().entity(PROJECTS_DEPENDENCIES_NAMESPACE, id.0),
|
||||
);
|
||||
}
|
||||
|
||||
redis
|
||||
.delete_many([
|
||||
(PROJECTS_NAMESPACE, Some(id.0.to_string())),
|
||||
(PROJECTS_SLUGS_NAMESPACE, slug.map(|x| x.to_lowercase())),
|
||||
(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
if clear_dependencies.unwrap_or(false) {
|
||||
Some(id.0.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
])
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
@@ -9,10 +8,11 @@ use futures_util::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const SESSIONS_NAMESPACE: &str = "sessions:v1";
|
||||
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids:v1";
|
||||
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users:v1";
|
||||
const SESSIONS_NAMESPACE: &str = "sessions:v3";
|
||||
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids:v3";
|
||||
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users:v3";
|
||||
|
||||
pub struct SessionBuilder {
|
||||
pub session: String,
|
||||
@@ -208,7 +208,7 @@ impl DBSession {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(db_sessions)
|
||||
Ok::<_, DatabaseError>(db_sessions)
|
||||
}).await?;
|
||||
|
||||
Ok(val)
|
||||
@@ -224,13 +224,9 @@ impl DBSession {
|
||||
{
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(SESSIONS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
let res = redis
|
||||
.get_deserialized::<Vec<i64>>(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let res = redis.get_deserialized::<Vec<i64>>(&key).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res.into_iter().map(DBSessionId).collect());
|
||||
@@ -253,15 +249,9 @@ impl DBSession {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(SESSIONS_USERS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_sessions,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_sessions, None).await?;
|
||||
|
||||
Ok(db_sessions)
|
||||
}
|
||||
@@ -280,20 +270,23 @@ impl DBSession {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
redis
|
||||
.delete_many(clear_sessions.into_iter().flat_map(
|
||||
|(id, session, user_id)| {
|
||||
[
|
||||
(SESSIONS_NAMESPACE, id.map(|i| i.0.to_string())),
|
||||
(SESSIONS_IDS_NAMESPACE, session),
|
||||
(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
user_id.map(|i| i.0.to_string()),
|
||||
),
|
||||
]
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
let keys = clear_sessions
|
||||
.into_iter()
|
||||
.flat_map(|(id, session, user_id)| {
|
||||
[
|
||||
id.map(|id| redis.key().entity(SESSIONS_NAMESPACE, id.0)),
|
||||
session.map(|session| {
|
||||
redis.key().entity(SESSIONS_IDS_NAMESPACE, session)
|
||||
}),
|
||||
user_id.map(|user_id| {
|
||||
redis.key().entity(SESSIONS_USERS_NAMESPACE, user_id.0)
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{DBOrganization, DBProject, ids::*};
|
||||
use crate::{
|
||||
database::{PgTransaction, redis::RedisPool},
|
||||
database::PgTransaction,
|
||||
models::teams::{OrganizationPermissions, ProjectPermissions},
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
@@ -8,8 +8,9 @@ use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const TEAMS_NAMESPACE: &str = "teams:v1";
|
||||
const TEAMS_NAMESPACE: &str = "teams:v3";
|
||||
|
||||
pub struct TeamBuilder {
|
||||
pub members: Vec<TeamMemberBuilder>,
|
||||
@@ -253,7 +254,7 @@ impl DBTeamMember {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(teams)
|
||||
Ok::<_, crate::database::models::DatabaseError>(teams)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -265,7 +266,8 @@ impl DBTeamMember {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), super::DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis.delete(TEAMS_NAMESPACE, id.0).await?;
|
||||
let key = redis.key().entity(TEAMS_NAMESPACE, id.0);
|
||||
redis.delete(&key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ use super::{DBCollectionId, DBReportId, DBThreadId};
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::database::models::{DBOrganizationId, DatabaseError};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgTransaction, models};
|
||||
use crate::models::billing::ChargeStatus;
|
||||
use crate::models::users::Badges;
|
||||
@@ -15,10 +14,11 @@ use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
use xredis::RedisPool;
|
||||
|
||||
const USERS_NAMESPACE: &str = "users:v1";
|
||||
const USER_USERNAMES_NAMESPACE: &str = "users_usernames:v1";
|
||||
const USERS_PROJECTS_NAMESPACE: &str = "users_projects:v1";
|
||||
const USERS_NAMESPACE: &str = "users:v3";
|
||||
const USER_USERNAMES_NAMESPACE: &str = "users_usernames:v3";
|
||||
const USERS_PROJECTS_NAMESPACE: &str = "users_projects:v3";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBUser {
|
||||
@@ -273,7 +273,7 @@ impl DBUser {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
Ok::<_, DatabaseError>(users)
|
||||
}).await?;
|
||||
Ok(val)
|
||||
}
|
||||
@@ -389,13 +389,10 @@ impl DBUser {
|
||||
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(USERS_PROJECTS_NAMESPACE, user_id.0);
|
||||
|
||||
let cached_projects = redis
|
||||
.get_deserialized::<Vec<DBProjectId>>(
|
||||
USERS_PROJECTS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let cached_projects =
|
||||
redis.get_deserialized::<Vec<DBProjectId>>(&key).await?;
|
||||
|
||||
if let Some(projects) = cached_projects {
|
||||
return Ok(projects);
|
||||
@@ -417,15 +414,9 @@ impl DBUser {
|
||||
.await?;
|
||||
|
||||
let mut redis = redis.connect().await?;
|
||||
let key = redis.key().entity(USERS_PROJECTS_NAMESPACE, user_id.0);
|
||||
|
||||
redis
|
||||
.set_serialized(
|
||||
USERS_PROJECTS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_projects,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
redis.set_serialized(&key, &db_projects, None).await?;
|
||||
|
||||
Ok(db_projects)
|
||||
}
|
||||
@@ -556,18 +547,24 @@ impl DBUser {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.delete_many(user_ids.iter().flat_map(|(id, username)| {
|
||||
let keys = user_ids
|
||||
.iter()
|
||||
.flat_map(|(id, username)| {
|
||||
[
|
||||
(USERS_NAMESPACE, Some(id.0.to_string())),
|
||||
(
|
||||
USER_USERNAMES_NAMESPACE,
|
||||
username.clone().map(|i| i.to_lowercase()),
|
||||
),
|
||||
Some(redis.key().entity(USERS_NAMESPACE, id.0)),
|
||||
username.as_ref().map(|username| {
|
||||
redis.key().entity(
|
||||
USER_USERNAMES_NAMESPACE,
|
||||
username.to_lowercase(),
|
||||
)
|
||||
}),
|
||||
]
|
||||
}))
|
||||
.await?;
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -576,14 +573,12 @@ impl DBUser {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let keys = user_ids
|
||||
.iter()
|
||||
.map(|id| redis.key().entity(USERS_PROJECTS_NAMESPACE, id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.delete_many(
|
||||
user_ids.iter().map(|id| {
|
||||
(USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::models::{
|
||||
DBProductPriceId, DBUserId, DBUserSubscriptionId, DatabaseError,
|
||||
};
|
||||
@@ -131,7 +130,7 @@ impl DBUserSubscription {
|
||||
|
||||
pub async fn upsert(
|
||||
&self,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -156,7 +155,7 @@ impl DBUserSubscription {
|
||||
self.status.as_str(),
|
||||
serde_json::to_value(&self.metadata)?,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -5,9 +5,9 @@ use crate::database::PgTransaction;
|
||||
use crate::database::models::loader_fields::{
|
||||
QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::exp;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::models::projects::{FileType, VersionStatus};
|
||||
use crate::queue::file_scan::scan_file;
|
||||
@@ -19,11 +19,10 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::iter;
|
||||
use tracing::error;
|
||||
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions:v1";
|
||||
const VERSION_FILES_NAMESPACE: &str = "versions_files:v1";
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions:v3";
|
||||
const VERSION_FILES_NAMESPACE: &str = "versions_files:v3";
|
||||
|
||||
pub async fn cleanup_unused_attribution_files_and_groups(
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
@@ -948,7 +947,7 @@ impl DBVersion {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
Ok::<_, DatabaseError>(res)
|
||||
},
|
||||
).await?;
|
||||
|
||||
@@ -1039,7 +1038,7 @@ impl DBVersion {
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(files)
|
||||
Ok::<_, DatabaseError>(files)
|
||||
}
|
||||
).await?;
|
||||
|
||||
@@ -1051,25 +1050,18 @@ impl DBVersion {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let mut keys =
|
||||
vec![redis.key().entity(VERSIONS_NAMESPACE, version.inner.id.0)];
|
||||
keys.extend(version.files.iter().flat_map(|file| {
|
||||
file.hashes.iter().map(|(algorithm, hash)| {
|
||||
redis.key().entity(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
format!("{algorithm}_{hash}"),
|
||||
)
|
||||
})
|
||||
}));
|
||||
|
||||
redis
|
||||
.delete_many(
|
||||
iter::once((
|
||||
VERSIONS_NAMESPACE,
|
||||
Some(version.inner.id.0.to_string()),
|
||||
))
|
||||
.chain(version.files.iter().flat_map(
|
||||
|file| {
|
||||
file.hashes.iter().map(|(algo, hash)| {
|
||||
(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
Some(format!("{algo}_{hash}")),
|
||||
)
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1078,14 +1070,12 @@ impl DBVersion {
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
let keys = version_ids
|
||||
.iter()
|
||||
.map(|id| redis.key().entity(VERSIONS_NAMESPACE, id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
redis
|
||||
.delete_many(
|
||||
version_ids
|
||||
.iter()
|
||||
.map(|id| (VERSIONS_NAMESPACE, Some(id.0.to_string()))),
|
||||
)
|
||||
.await?;
|
||||
redis.delete_many(&keys).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::env::ENV;
|
||||
|
||||
struct RedisConfig {
|
||||
inner: xredis::RedisConfig,
|
||||
cache_settings: xredis::CacheSettings,
|
||||
}
|
||||
|
||||
impl RedisConfig {
|
||||
fn from_env() -> Result<Self, xredis::RedisConfigError> {
|
||||
let inner = xredis::RedisConfig::new(
|
||||
ENV.REDIS_TOPOLOGY,
|
||||
ENV.REDIS_CONNECTION_TYPE,
|
||||
&ENV.REDIS_URL,
|
||||
ENV.REDIS_WAIT_TIMEOUT_MS,
|
||||
(
|
||||
ENV.REDIS_MAX_CONNECTIONS as usize,
|
||||
ENV.REDIS_MIN_CONNECTIONS,
|
||||
),
|
||||
(
|
||||
ENV.REDIS_CLUSTER_MAX_CONNECTIONS as usize,
|
||||
ENV.REDIS_CLUSTER_MIN_CONNECTIONS,
|
||||
),
|
||||
(ENV.REDIS_BLOCKING_MAX_CONNECTIONS as usize, 0),
|
||||
ENV.REDIS_CACHE_LOCKING_STRATEGY,
|
||||
ENV.REDIS_READ_REPLICA_STRATEGY,
|
||||
)?;
|
||||
let cache_settings = xredis::CacheSettings {
|
||||
default_expiry: ENV.REDIS_DEFAULT_EXPIRY,
|
||||
actual_expiry: ENV.REDIS_ACTUAL_EXPIRY,
|
||||
version_default_expiry: ENV.REDIS_VERSION_DEFAULT_EXPIRY,
|
||||
version_actual_expiry: ENV.REDIS_VERSION_ACTUAL_EXPIRY,
|
||||
encoding_format: ENV.REDIS_ENCODING_FORMAT,
|
||||
compression_algorithm: ENV.REDIS_COMPRESSION_ALGORITHM,
|
||||
compression_level: ENV.REDIS_COMPRESSION_LEVEL,
|
||||
compression_threshold_bytes: ENV.REDIS_COMPRESSION_THRESHOLD_BYTES,
|
||||
compression_min_savings_ratio: ENV
|
||||
.REDIS_COMPRESSION_MIN_SAVINGS_RATIO,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
cache_settings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_env(
|
||||
meta_namespace: impl Into<Arc<str>>,
|
||||
) -> xredis::RedisPool {
|
||||
let config = RedisConfig::from_env().expect("invalid Redis configuration");
|
||||
xredis::RedisPool::new(meta_namespace, config.inner, config.cache_settings)
|
||||
.await
|
||||
.expect("failed to initialize Redis connections")
|
||||
}
|
||||
@@ -1,957 +0,0 @@
|
||||
use crate::env::ENV;
|
||||
|
||||
use super::models::DatabaseError;
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use deadpool_redis::{Config, Runtime};
|
||||
use futures::TryStreamExt;
|
||||
use futures::future::Either;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use prometheus::{IntGauge, Registry};
|
||||
use redis::ToRedisArgs;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
use util::{cmd, redis_pipe};
|
||||
|
||||
pub mod util;
|
||||
|
||||
// Bound how many commands we send in a single Redis pipeline. The multiplexed
|
||||
// connection's BytesMut write buffer keeps its peak capacity for the life of
|
||||
// the connection, so larger pipelines cause higher steady-state RSS.
|
||||
const PIPELINE_CHUNK_SIZE: usize = 25;
|
||||
// Bound how many keys we send in a single MGET. Each MGET response must fit
|
||||
// into the connection's read buffer, which also retains its peak capacity. At
|
||||
// ~1 MB per cached value, 32 keys caps any single response at ~32 MB.
|
||||
const MGET_CHUNK_SIZE: usize = 32;
|
||||
// How long a pooled Redis connection lives before being recycled, regardless
|
||||
// of activity. Forced recycling is the only way to release the per-connection
|
||||
// BytesMut peak capacity that builds up under steady load.
|
||||
const REDIS_MAX_CONN_AGE: Duration = Duration::from_secs(120);
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Codec {
|
||||
Raw = 0,
|
||||
Lz4 = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodingFormat {
|
||||
Json,
|
||||
Postcard,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid redis codec")]
|
||||
pub struct InvalidCodec;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid redis encoding format")]
|
||||
pub struct InvalidEncodingFormat;
|
||||
|
||||
impl TryFrom<u8> for Codec {
|
||||
type Error = InvalidCodec;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Raw),
|
||||
1 => Ok(Self::Lz4),
|
||||
_ => Err(InvalidCodec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Codec {
|
||||
type Err = InvalidCodec;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"lz4" => Ok(Self::Lz4),
|
||||
_ => Err(InvalidCodec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EncodingFormat {
|
||||
type Err = InvalidEncodingFormat;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"json" => Ok(Self::Json),
|
||||
"postcard" => Ok(Self::Postcard),
|
||||
_ => Err(InvalidEncodingFormat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_value<T: Serialize>(value: &T) -> Result<Vec<u8>, DatabaseError> {
|
||||
let mut value = match ENV.REDIS_ENCODING_FORMAT {
|
||||
EncodingFormat::Json => serde_json::to_vec(value)?,
|
||||
EncodingFormat::Postcard => postcard::to_allocvec(value)?,
|
||||
};
|
||||
|
||||
if ENV.REDIS_COMPRESSION_LEVEL > 0
|
||||
&& ENV.REDIS_COMPRESSION_ALGORITHM == Codec::Lz4
|
||||
&& value.len() >= ENV.REDIS_COMPRESSION_THRESHOLD_BYTES
|
||||
{
|
||||
let compressed = lz4_flex::block::compress_prepend_size(&value);
|
||||
let savings_ratio = value.len().saturating_sub(compressed.len()) as f64
|
||||
/ value.len().max(1) as f64
|
||||
* 100.0;
|
||||
|
||||
if savings_ratio >= ENV.REDIS_COMPRESSION_MIN_SAVINGS_RATIO {
|
||||
let mut encoded = Vec::with_capacity(compressed.len() + 1);
|
||||
encoded.push(Codec::Lz4 as u8);
|
||||
encoded.extend(compressed);
|
||||
return Ok(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
let mut encoded = Vec::with_capacity(value.len() + 1);
|
||||
encoded.push(Codec::Raw as u8);
|
||||
encoded.append(&mut value);
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn decode_value<T>(value: &[u8]) -> Option<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
let (codec, value) = value.split_first()?;
|
||||
let value = match Codec::try_from(*codec).ok()? {
|
||||
Codec::Raw => Cow::Borrowed(value),
|
||||
Codec::Lz4 => {
|
||||
Cow::Owned(lz4_flex::block::decompress_size_prepended(value).ok()?)
|
||||
}
|
||||
};
|
||||
|
||||
match ENV.REDIS_ENCODING_FORMAT {
|
||||
EncodingFormat::Json => serde_json::from_slice(&value).ok(),
|
||||
EncodingFormat::Postcard => postcard::from_bytes(&value).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_expiries(namespace: &str) -> (i64, i64) {
|
||||
// Namespaces may embed a version suffix like `:v1`, so split it out.
|
||||
match namespace.split_once(':').map(|t| t.0).unwrap_or(namespace) {
|
||||
"versions" | "versions_files" => (
|
||||
ENV.REDIS_VERSION_DEFAULT_EXPIRY,
|
||||
ENV.REDIS_VERSION_ACTUAL_EXPIRY,
|
||||
),
|
||||
_ => (ENV.REDIS_DEFAULT_EXPIRY, ENV.REDIS_ACTUAL_EXPIRY),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RedisPool {
|
||||
pub url: String,
|
||||
pub pool: deadpool_redis::Pool,
|
||||
cache_list: Arc<DashMap<String, util::CacheSubscriber>>,
|
||||
meta_namespace: Arc<str>,
|
||||
}
|
||||
|
||||
pub struct RedisConnection {
|
||||
pub connection: deadpool_redis::Connection,
|
||||
meta_namespace: Arc<str>,
|
||||
}
|
||||
|
||||
impl RedisPool {
|
||||
// initiate a new redis pool
|
||||
// testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests)
|
||||
// PANICS: production pool will panic if redis url is not set
|
||||
pub fn new(meta_namespace: impl Into<Arc<str>>) -> Self {
|
||||
let wait_timeout = Duration::from_millis(ENV.REDIS_WAIT_TIMEOUT_MS);
|
||||
|
||||
let url = &ENV.REDIS_URL;
|
||||
let pool = Config::from_url(url.clone())
|
||||
.builder()
|
||||
.expect("Error building Redis pool")
|
||||
.max_size(ENV.REDIS_MAX_CONNECTIONS as usize)
|
||||
.wait_timeout(Some(wait_timeout))
|
||||
.runtime(Runtime::Tokio1)
|
||||
.build()
|
||||
.expect("Redis connection failed");
|
||||
|
||||
let pool = RedisPool {
|
||||
url: url.clone(),
|
||||
pool,
|
||||
cache_list: Arc::new(DashMap::with_capacity(2048)),
|
||||
meta_namespace: meta_namespace.into(),
|
||||
};
|
||||
|
||||
let redis_min_connections = ENV.REDIS_MIN_CONNECTIONS;
|
||||
let spawn_min_connections = (0..redis_min_connections)
|
||||
.map(|_| {
|
||||
let pool = pool.clone();
|
||||
tokio::spawn(async move { pool.pool.get().await })
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
tokio::spawn({
|
||||
let pool = pool.clone();
|
||||
async move {
|
||||
// collect the connections into a buffer while we're spawning them,
|
||||
// to make sure that we're not `get`ing any connections we previously took
|
||||
let _connections =
|
||||
spawn_min_connections.try_collect::<Vec<_>>().await;
|
||||
info!(
|
||||
pool_status = ?pool.pool.status(),
|
||||
"Finished getting {redis_min_connections} initial Redis connections"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let interval = Duration::from_secs(30);
|
||||
let max_idle = Duration::from_secs(5 * 60); // 5 minutes
|
||||
let pool_ref = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(interval).await;
|
||||
pool_ref.pool.retain(|_, metrics| {
|
||||
// Drop connections that have been idle too long, OR that
|
||||
// are older than REDIS_MAX_CONN_AGE regardless of use.
|
||||
// The age-based recycle is what releases the per-connection
|
||||
// BytesMut peak capacity under steady traffic.
|
||||
metrics.last_used() < max_idle
|
||||
&& metrics.created.elapsed() < REDIS_MAX_CONN_AGE
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub async fn register_and_set_metrics(
|
||||
&self,
|
||||
registry: &Registry,
|
||||
) -> Result<(), prometheus::Error> {
|
||||
let redis_max_size = IntGauge::new(
|
||||
"labrinth_redis_pool_max_size",
|
||||
"Maximum size of Redis pool",
|
||||
)?;
|
||||
let redis_size = IntGauge::new(
|
||||
"labrinth_redis_pool_size",
|
||||
"Current size of Redis pool",
|
||||
)?;
|
||||
let redis_available = IntGauge::new(
|
||||
"labrinth_redis_pool_available",
|
||||
"Available connections in Redis pool",
|
||||
)?;
|
||||
let redis_waiting = IntGauge::new(
|
||||
"labrinth_redis_pool_waiting",
|
||||
"Number of futures waiting for a Redis connection",
|
||||
)?;
|
||||
|
||||
registry.register(Box::new(redis_max_size.clone()))?;
|
||||
registry.register(Box::new(redis_size.clone()))?;
|
||||
registry.register(Box::new(redis_available.clone()))?;
|
||||
registry.register(Box::new(redis_waiting.clone()))?;
|
||||
|
||||
let redis_pool_ref = self.pool.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let status = redis_pool_ref.status();
|
||||
redis_max_size.set(status.max_size as i64);
|
||||
redis_size.set(status.size as i64);
|
||||
redis_available.set(status.available as i64);
|
||||
redis_waiting.set(status.waiting as i64);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn connect(&self) -> Result<RedisConnection, DatabaseError> {
|
||||
Ok(RedisConnection {
|
||||
connection: self.pool.get().await?,
|
||||
meta_namespace: self.meta_namespace.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys<F, Fut, T, K>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
keys: &[K],
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<K>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ Debug,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw(namespace, keys, closure)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.1)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys_raw<F, Fut, T, K>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
keys: &[K],
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<K>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ Debug,
|
||||
{
|
||||
self.get_cached_keys_raw_with_slug(
|
||||
namespace,
|
||||
None,
|
||||
false,
|
||||
keys,
|
||||
|ids| async move {
|
||||
Ok(closure(ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, val)| (key, (None::<String>, val)))
|
||||
.collect())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys_with_slug<F, Fut, T, I, K, S>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
slug_namespace: &str,
|
||||
case_sensitive: bool,
|
||||
keys: &[I],
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<I>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
I: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize,
|
||||
S: Display + Clone + DeserializeOwned + Serialize + Debug,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw_with_slug(
|
||||
namespace,
|
||||
Some(slug_namespace),
|
||||
case_sensitive,
|
||||
keys,
|
||||
closure,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.1)
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, closure))]
|
||||
pub async fn get_cached_keys_raw_with_slug<F, Fut, T, I, K, S>(
|
||||
&self,
|
||||
namespace: &str,
|
||||
slug_namespace: Option<&str>,
|
||||
case_sensitive: bool,
|
||||
keys: &[I],
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<I>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
I: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize,
|
||||
S: Display + Clone + DeserializeOwned + Serialize + Debug,
|
||||
{
|
||||
let ids = keys
|
||||
.iter()
|
||||
.map(|x| (x.to_string(), x.clone()))
|
||||
.collect::<DashMap<String, I>>();
|
||||
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let get_cached_values = |ids: DashMap<String, I>| {
|
||||
async move {
|
||||
let slug_ids = if let Some(slug_namespace) = slug_namespace {
|
||||
async {
|
||||
let mut connection = self.pool.get().await?;
|
||||
|
||||
let args = ids
|
||||
.iter()
|
||||
.map(|x| {
|
||||
format!(
|
||||
"{}_{slug_namespace}:{}",
|
||||
self.meta_namespace,
|
||||
if case_sensitive {
|
||||
x.value().to_string()
|
||||
} else {
|
||||
x.value().to_string().to_lowercase()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut v = Vec::new();
|
||||
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||
let part = cmd("MGET")
|
||||
.arg(chunk)
|
||||
.query_async::<Vec<Option<String>>>(
|
||||
&mut connection,
|
||||
)
|
||||
.await?;
|
||||
v.extend(part.into_iter().flatten());
|
||||
}
|
||||
Ok::<_, DatabaseError>(v)
|
||||
}
|
||||
.instrument(info_span!("get slug ids"))
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut connection = self.pool.get().await?;
|
||||
let args = ids
|
||||
.iter()
|
||||
.map(|x| x.value().to_string())
|
||||
.chain(ids.iter().filter_map(|x| {
|
||||
parse_base62(&x.value().to_string())
|
||||
.ok()
|
||||
.map(|x| x.to_string())
|
||||
}))
|
||||
.chain(slug_ids)
|
||||
.map(|x| format!("{}_{namespace}:{x}", self.meta_namespace))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut cached_values = HashMap::new();
|
||||
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||
let part = cmd("MGET")
|
||||
.arg(chunk)
|
||||
.query_async::<Vec<Option<Vec<u8>>>>(&mut connection)
|
||||
.await?;
|
||||
cached_values.extend(part.into_iter().filter_map(|x| {
|
||||
x.and_then(|val| {
|
||||
decode_value::<RedisValue<T, K, S>>(&val)
|
||||
})
|
||||
.map(|val| (val.key.clone(), val))
|
||||
}));
|
||||
}
|
||||
|
||||
Ok::<_, DatabaseError>((cached_values, ids))
|
||||
}
|
||||
.instrument(info_span!("get cached values"))
|
||||
};
|
||||
|
||||
let (default_expiry, actual_expiry) = cache_expiries(namespace);
|
||||
let current_time = Utc::now();
|
||||
let mut expired_values = HashMap::new();
|
||||
|
||||
let (cached_values_raw, ids) = get_cached_values(ids).await?;
|
||||
let mut cached_values = cached_values_raw
|
||||
.into_iter()
|
||||
.filter_map(|(key, val)| {
|
||||
if Utc.timestamp_opt(val.iat + actual_expiry, 0).unwrap()
|
||||
< current_time
|
||||
{
|
||||
expired_values.insert(val.key.to_string(), val);
|
||||
|
||||
None
|
||||
} else {
|
||||
let key_str = val.key.to_string();
|
||||
ids.remove(&key_str);
|
||||
|
||||
if let Ok(value) = key_str.parse::<u64>() {
|
||||
let base62 = to_base62(value);
|
||||
ids.remove(&base62);
|
||||
}
|
||||
|
||||
if let Some(ref alias) = val.alias {
|
||||
ids.remove(&alias.to_string());
|
||||
}
|
||||
|
||||
Some((key, val))
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let subscribe_ids = DashMap::new();
|
||||
let mut cache_writers = HashMap::new();
|
||||
|
||||
if !ids.is_empty() {
|
||||
let fetch_ids =
|
||||
ids.iter().map(|x| x.key().clone()).collect::<Vec<_>>();
|
||||
|
||||
fetch_ids.into_iter().for_each(|key| {
|
||||
let ns_key_value = if case_sensitive {
|
||||
key.to_lowercase()
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
let namespaced_key = format!(
|
||||
"{}_{namespace}:{ns_key_value}",
|
||||
self.meta_namespace,
|
||||
);
|
||||
let either = self.acquire_lock(namespaced_key);
|
||||
|
||||
match either {
|
||||
Either::Left(sentinel) => {
|
||||
cache_writers.insert(key, sentinel);
|
||||
}
|
||||
|
||||
Either::Right(subscriber) => {
|
||||
if let Some((key, raw_key)) = ids.remove(&key) {
|
||||
if let Some(val) = expired_values.remove(&key) {
|
||||
if let Some(ref alias) = val.alias {
|
||||
ids.remove(&alias.to_string());
|
||||
}
|
||||
|
||||
if let Ok(value) =
|
||||
val.key.to_string().parse::<u64>()
|
||||
{
|
||||
let base62 = to_base62(value);
|
||||
ids.remove(&base62);
|
||||
}
|
||||
|
||||
cached_values.insert(val.key.clone(), val);
|
||||
} else {
|
||||
subscribe_ids.insert(raw_key, subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut fetch_tasks = Vec::new();
|
||||
|
||||
if !ids.is_empty() {
|
||||
fetch_tasks.push(Either::Left(async {
|
||||
let fetch_ids =
|
||||
ids.iter().map(|x| x.value().clone()).collect::<Vec<_>>();
|
||||
|
||||
let vals = closure(fetch_ids).await?;
|
||||
let mut return_values = HashMap::new();
|
||||
|
||||
let mut pipe = redis_pipe();
|
||||
let mut pipe_cmds: usize = 0;
|
||||
let mut connection = self.pool.get().await?;
|
||||
// Doesn't need to be atomic
|
||||
|
||||
if !vals.is_empty() {
|
||||
for (key, (slug, value)) in vals {
|
||||
let value = RedisValue {
|
||||
key: key.clone(),
|
||||
iat: Utc::now().timestamp(),
|
||||
val: value,
|
||||
alias: slug.clone(),
|
||||
};
|
||||
|
||||
pipe.set_ex(
|
||||
format!(
|
||||
"{}_{namespace}:{key}",
|
||||
self.meta_namespace
|
||||
),
|
||||
encode_value(&value)?,
|
||||
default_expiry as u64,
|
||||
);
|
||||
pipe_cmds += 1;
|
||||
|
||||
if let Some(slug) = slug {
|
||||
ids.remove(&slug.to_string());
|
||||
|
||||
if let Some(slug_namespace) = slug_namespace {
|
||||
let actual_slug = if case_sensitive {
|
||||
slug.to_string()
|
||||
} else {
|
||||
slug.to_string().to_lowercase()
|
||||
};
|
||||
|
||||
pipe.set_ex(
|
||||
format!(
|
||||
"{}_{slug_namespace}:{}",
|
||||
self.meta_namespace, actual_slug
|
||||
),
|
||||
key.to_string(),
|
||||
default_expiry as u64,
|
||||
);
|
||||
pipe_cmds += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let key_str = key.to_string();
|
||||
ids.remove(&key_str);
|
||||
|
||||
if let Ok(value) = key_str.parse::<u64>() {
|
||||
let base62 = to_base62(value);
|
||||
ids.remove(&base62);
|
||||
}
|
||||
|
||||
return_values.insert(key, value);
|
||||
|
||||
if pipe_cmds >= PIPELINE_CHUNK_SIZE {
|
||||
pipe.query_async::<()>(&mut connection).await?;
|
||||
pipe = redis_pipe();
|
||||
pipe_cmds = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pipe_cmds > 0 {
|
||||
pipe.query_async::<()>(&mut connection).await?;
|
||||
}
|
||||
|
||||
drop(cache_writers);
|
||||
|
||||
Result::<_, DatabaseError>::Ok(return_values)
|
||||
}));
|
||||
}
|
||||
|
||||
if !subscribe_ids.is_empty() {
|
||||
fetch_tasks.push(Either::Right(async move {
|
||||
let mut futures = FuturesUnordered::new();
|
||||
let len = subscribe_ids.len();
|
||||
|
||||
for (key, subscriber) in subscribe_ids {
|
||||
futures.push(async move {
|
||||
(
|
||||
key,
|
||||
subscriber
|
||||
.wait_timeout(Duration::from_secs(5))
|
||||
.await,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let fetch_ids = DashMap::with_capacity(len);
|
||||
while let Some((key, result)) = futures.next().await {
|
||||
result?;
|
||||
fetch_ids.insert(key.to_string(), key);
|
||||
}
|
||||
|
||||
let (return_values, _) = get_cached_values(fetch_ids).await?;
|
||||
Ok(return_values)
|
||||
}));
|
||||
}
|
||||
|
||||
if !fetch_tasks.is_empty() {
|
||||
for map in futures::future::try_join_all(fetch_tasks).await? {
|
||||
for (key, value) in map {
|
||||
cached_values.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cached_values.into_iter().map(|x| (x.0, x.1.val)).collect())
|
||||
}
|
||||
|
||||
/// Acquire or create a cache lock onto the given key.
|
||||
fn acquire_lock(
|
||||
&self,
|
||||
key: String,
|
||||
) -> Either<LockSentinel<'_>, util::CacheSubscriber> {
|
||||
let mut out_writer = None;
|
||||
let subscriber =
|
||||
self.cache_list.entry(key.clone()).or_insert_with(|| {
|
||||
let (writer, subscriber) = util::cache();
|
||||
out_writer = Some(writer);
|
||||
subscriber
|
||||
});
|
||||
|
||||
match out_writer {
|
||||
Some(writer) => Either::Left(LockSentinel {
|
||||
pool: self,
|
||||
key,
|
||||
writer,
|
||||
}),
|
||||
None => Either::Right(subscriber.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LockSentinel<'a> {
|
||||
pool: &'a RedisPool,
|
||||
key: String,
|
||||
writer: util::CacheWriter,
|
||||
}
|
||||
|
||||
impl<'a> Drop for LockSentinel<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.writer.write();
|
||||
self.pool.cache_list.remove(&self.key);
|
||||
}
|
||||
}
|
||||
|
||||
impl RedisConnection {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn set<D>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
data: D,
|
||||
expiry: Option<i64>,
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
D: ToRedisArgs + Send + Sync + Debug,
|
||||
{
|
||||
let mut cmd = cmd("SET");
|
||||
cmd.arg(format!("{}_{}:{}", self.meta_namespace, namespace, id))
|
||||
.arg(data)
|
||||
.arg("EX")
|
||||
.arg(expiry.unwrap_or(ENV.REDIS_DEFAULT_EXPIRY));
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, id, data))]
|
||||
pub async fn set_serialized<Id, D>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: Id,
|
||||
data: D,
|
||||
expiry: Option<i64>,
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
Id: Display,
|
||||
D: serde::Serialize,
|
||||
{
|
||||
self.set(namespace, &id.to_string(), encode_value(&data)?, expiry)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
) -> Result<Option<String>, DatabaseError> {
|
||||
let mut cmd = cmd("GET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
let res = redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_many(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<Vec<u8>>>, DatabaseError> {
|
||||
let mut cmd = cmd("MGET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
ids.iter()
|
||||
.map(|x| format!("{}_{}:{}", self.meta_namespace, namespace, x))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
);
|
||||
let res = redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_deserialized<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
) -> Result<Option<R>, DatabaseError>
|
||||
where
|
||||
R: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
let mut cmd = cmd("GET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
let value: Option<Vec<u8>> =
|
||||
redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(value.and_then(|value| decode_value(&value)))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_many_deserialized<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<R>>, DatabaseError>
|
||||
where
|
||||
R: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
Ok(self
|
||||
.get_many(namespace, ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|value| value.and_then(|value| decode_value::<R>(&value)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, id))]
|
||||
pub async fn delete<T1>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: T1,
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
T1: Display,
|
||||
{
|
||||
let mut cmd = cmd("DEL");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, iter))]
|
||||
pub async fn delete_many(
|
||||
&mut self,
|
||||
iter: impl IntoIterator<Item = (&str, Option<String>)>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut cmd = cmd("DEL");
|
||||
let mut any = false;
|
||||
for (namespace, id) in iter {
|
||||
if let Some(id) = id {
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
[format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if any {
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, value))]
|
||||
pub async fn lpush(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: impl ToRedisArgs + Send + Sync + Debug,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
|
||||
cmd("LPUSH")
|
||||
.arg(key)
|
||||
.arg(value)
|
||||
.query_async::<()>(&mut self.connection)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn brpop(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
timeout: Option<f64>,
|
||||
) -> Result<Option<[Vec<u8>; 2]>, DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
|
||||
// a timeout of 0 is infinite
|
||||
let timeout = timeout.unwrap_or(0.0);
|
||||
let values = cmd("BRPOP")
|
||||
.arg(key)
|
||||
.arg(timeout)
|
||||
.query_async(&mut self.connection)
|
||||
.await?;
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn incr(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
) -> Result<Option<u64>, DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{id}", self.meta_namespace);
|
||||
let value = cmd("INCR")
|
||||
.arg(key)
|
||||
.query_async(&mut self.connection)
|
||||
.await?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RedisValue<T, K, S> {
|
||||
key: K,
|
||||
alias: Option<S>,
|
||||
iat: i64,
|
||||
val: T,
|
||||
}
|
||||
|
||||
impl<T, K, S> RedisValue<T, K, S> {
|
||||
pub fn value(&self) -> &T {
|
||||
&self.val
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redis_args(cmd: &mut util::InstrumentedCmd, args: &[String]) {
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn redis_execute<T>(
|
||||
cmd: &mut util::InstrumentedCmd,
|
||||
redis: &mut deadpool_redis::Connection,
|
||||
) -> Result<T, deadpool_redis::PoolError>
|
||||
where
|
||||
T: redis::FromRedisValue,
|
||||
{
|
||||
let res = cmd.query_async::<T>(redis).await?;
|
||||
Ok(res)
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use redis::{FromRedisValue, RedisResult, ToRedisArgs};
|
||||
use tokio::sync::Notify;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::{Instrument, info_span};
|
||||
|
||||
use crate::database::models::DatabaseError;
|
||||
|
||||
pub fn redis_pipe() -> InstrumentedPipeline {
|
||||
InstrumentedPipeline {
|
||||
inner: redis::pipe(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deref, DerefMut)]
|
||||
pub struct InstrumentedPipeline {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
inner: redis::Pipeline,
|
||||
}
|
||||
|
||||
impl InstrumentedPipeline {
|
||||
pub fn atomic(&mut self) -> &mut Self {
|
||||
self.inner.atomic();
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn query_async<T: FromRedisValue>(
|
||||
&self,
|
||||
con: &mut impl redis::aio::ConnectionLike,
|
||||
) -> RedisResult<T> {
|
||||
self.inner
|
||||
.query_async(con)
|
||||
.instrument(info_span!("pipeline.query_async"))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd(name: &str) -> InstrumentedCmd {
|
||||
InstrumentedCmd {
|
||||
inner: redis::cmd(name),
|
||||
name: name.to_string(),
|
||||
args: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InstrumentedCmd {
|
||||
inner: redis::Cmd,
|
||||
name: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl InstrumentedCmd {
|
||||
#[inline]
|
||||
pub fn arg<T: ToRedisArgs + Debug>(&mut self, arg: T) -> &mut Self {
|
||||
self.args.push(format!("{arg:?}"));
|
||||
self.inner.arg(arg);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn query_async<T: FromRedisValue>(
|
||||
&self,
|
||||
con: &mut impl redis::aio::ConnectionLike,
|
||||
) -> RedisResult<T> {
|
||||
let span = info_span!(
|
||||
"cmd.query_async",
|
||||
// <https://opentelemetry.io/docs/specs/semconv/db/redis/>
|
||||
db.system.name = "redis",
|
||||
db.operation.name = self.name,
|
||||
db.query.text = format!("{} {}", self.name, self.args.join(" ")),
|
||||
);
|
||||
self.inner.query_async(con).instrument(span).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache() -> (CacheWriter, CacheSubscriber) {
|
||||
let shared = Arc::new(Shared::new());
|
||||
(
|
||||
CacheWriter {
|
||||
shared: shared.clone(),
|
||||
},
|
||||
CacheSubscriber { shared },
|
||||
)
|
||||
}
|
||||
|
||||
pub struct CacheWriter {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl CacheWriter {
|
||||
pub fn write(&self) {
|
||||
self.shared.make_ready();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CacheSubscriber {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
|
||||
impl CacheSubscriber {
|
||||
pub async fn wait_timeout(
|
||||
self,
|
||||
duration: Duration,
|
||||
) -> Result<(), DatabaseError> {
|
||||
timeout(duration, self.shared.wait()).await.map_err(|_| {
|
||||
DatabaseError::LocalCacheTimeout {
|
||||
released: 0,
|
||||
total: 1,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
ready: AtomicBool,
|
||||
// With this implementation's intrusive linked lists, the waiters are stored inline in the future
|
||||
// so there's no heap allocation per waiter.
|
||||
wakers: Notify,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ready: AtomicBool::new(false),
|
||||
wakers: Notify::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_ready(&self) {
|
||||
self.ready.store(true, Ordering::Release);
|
||||
self.wakers.notify_waiters();
|
||||
}
|
||||
|
||||
async fn wait(&self) {
|
||||
let ready = self.ready.load(Ordering::Acquire);
|
||||
|
||||
if ready {
|
||||
return;
|
||||
}
|
||||
|
||||
let notification = self.wakers.notified();
|
||||
// Don't need to call `enable` as we use notify_waiters
|
||||
|
||||
// Prevent race where the writer set the ready bit and notified waiters between the load and registering the waiter
|
||||
let ready = self.ready.load(Ordering::SeqCst);
|
||||
|
||||
if ready {
|
||||
return;
|
||||
}
|
||||
|
||||
notification.await;
|
||||
}
|
||||
}
|
||||
+48
-13
@@ -5,6 +5,11 @@ use eyre::{Context, eyre};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use xredis::{
|
||||
CacheLockingStrategy, ReadReplicaStrategy, RedisConnectionType,
|
||||
RedisTopology,
|
||||
};
|
||||
|
||||
macro_rules! vars {
|
||||
(
|
||||
$(
|
||||
@@ -127,9 +132,52 @@ vars! {
|
||||
LABRINTH_ADMIN_KEY: String = "";
|
||||
LABRINTH_MEDAL_KEY: String = "";
|
||||
LABRINTH_EXTERNAL_NOTIFICATION_KEY: String = "";
|
||||
LABRINTH_SUBSCRIPTIONS_KEY: String = "";
|
||||
RATE_LIMIT_IGNORE_KEY: String = "";
|
||||
DATABASE_URL: String = "postgresql://labrinth:labrinth@localhost/labrinth";
|
||||
|
||||
// Redis
|
||||
REDIS_TOPOLOGY: RedisTopology = RedisTopology::Standalone;
|
||||
REDIS_CONNECTION_TYPE: RedisConnectionType = RedisConnectionType::Pooled;
|
||||
REDIS_CACHE_LOCKING_STRATEGY: CacheLockingStrategy = CacheLockingStrategy::Local;
|
||||
// URL(s) for Redis. Use comma-separated values for multiple URLs in Cluster topology.
|
||||
REDIS_URL: String = "redis://localhost";
|
||||
|
||||
// Configures the waiting timeout for Redis connection *pools*.
|
||||
// This doesn't affect the bulk of Redis work in Multiplexed connection type.
|
||||
REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64;
|
||||
|
||||
// Minimum and maximum number of connections when Redis is in Standalone topology.
|
||||
REDIS_MAX_CONNECTIONS: u32 = 2048u32;
|
||||
REDIS_MIN_CONNECTIONS: usize = 0usize;
|
||||
|
||||
REDIS_DEFAULT_EXPIRY: i64 = 60 * 60 * 12;
|
||||
REDIS_ACTUAL_EXPIRY: i64 = 60 * 30;
|
||||
REDIS_VERSION_DEFAULT_EXPIRY: i64 = 60 * 60 * 12;
|
||||
REDIS_VERSION_ACTUAL_EXPIRY: i64 = 60 * 30;
|
||||
|
||||
// Minimum and maximum number of connections when Redis is in Cluster topology, Pooled connection type.
|
||||
REDIS_CLUSTER_MAX_CONNECTIONS: u32 = 16u32;
|
||||
REDIS_CLUSTER_MIN_CONNECTIONS: usize = 0usize;
|
||||
|
||||
// The maximum number of connections of the Redis blocking pool. There's a blocking pool regardless of topology
|
||||
// and main connection type.
|
||||
REDIS_BLOCKING_MAX_CONNECTIONS: u32 = 256u32;
|
||||
|
||||
// The encoding format used for Redis cache values.
|
||||
REDIS_ENCODING_FORMAT: xredis::EncodingFormat = xredis::EncodingFormat::Json;
|
||||
// The level of LZ4 compression used for Redis cache values. A value of 0 disables compression (supports 1-12)
|
||||
REDIS_COMPRESSION_LEVEL: i32 = 0i32;
|
||||
// The compression algorithm used for Redis cache values. Currently only LZ4 is supported.
|
||||
REDIS_COMPRESSION_ALGORITHM: xredis::Codec = xredis::Codec::Lz4;
|
||||
// The minimum number of bytes required to trigger compression for Redis cache values.
|
||||
REDIS_COMPRESSION_THRESHOLD_BYTES: usize = 1024usize;
|
||||
// The minimum savings ratio required to trigger compression for Redis cache values. If the savings ratio is lower than this,
|
||||
// the compressed payload is discarded and the original payload is stored as-is.
|
||||
REDIS_COMPRESSION_MIN_SAVINGS_RATIO: f64 = 12.5f64;
|
||||
|
||||
REDIS_READ_REPLICA_STRATEGY: ReadReplicaStrategy = ReadReplicaStrategy::Primary;
|
||||
|
||||
KAFKA_BOOTSTRAP_SERVERS: StringCsv = StringCsv(vec!["localhost:19092".into()]);
|
||||
KAFKA_CLIENT_ID: String = "labrinth";
|
||||
BIND_ADDR: String = "";
|
||||
@@ -291,19 +339,6 @@ vars! {
|
||||
READONLY_DATABASE_MIN_CONNECTIONS: u32 = 0u32;
|
||||
READONLY_DATABASE_MAX_CONNECTIONS: u32 = 1u32;
|
||||
|
||||
REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64;
|
||||
REDIS_MAX_CONNECTIONS: u32 = 10000u32;
|
||||
REDIS_MIN_CONNECTIONS: usize = 0usize;
|
||||
REDIS_DEFAULT_EXPIRY: i64 = 60 * 60 * 12;
|
||||
REDIS_ACTUAL_EXPIRY: i64 = 60 * 30;
|
||||
REDIS_VERSION_DEFAULT_EXPIRY: i64 = 60 * 60 * 12;
|
||||
REDIS_VERSION_ACTUAL_EXPIRY: i64 = 60 * 30;
|
||||
REDIS_ENCODING_FORMAT: crate::database::redis::EncodingFormat = crate::database::redis::EncodingFormat::Json;
|
||||
REDIS_COMPRESSION_LEVEL: i32 = 0i32;
|
||||
REDIS_COMPRESSION_ALGORITHM: crate::database::redis::Codec = crate::database::redis::Codec::Lz4;
|
||||
REDIS_COMPRESSION_THRESHOLD_BYTES: usize = 1024usize;
|
||||
REDIS_COMPRESSION_MIN_SAVINGS_RATIO: f64 = 12.5f64;
|
||||
|
||||
SEARCH_OPERATION_TIMEOUT: u64 = 300000u64;
|
||||
|
||||
SMTP_REPLY_TO_NAME: String = "";
|
||||
|
||||
@@ -4,12 +4,12 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::web;
|
||||
use database::redis::RedisPool;
|
||||
use queue::{
|
||||
analytics::AnalyticsQueue, email::EmailQueue, payouts::PayoutsQueue,
|
||||
session::AuthQueue, socket::ActiveSockets,
|
||||
};
|
||||
use tracing::{debug, info, warn};
|
||||
use xredis::RedisPool;
|
||||
|
||||
extern crate clickhouse as clickhouse_crate;
|
||||
use clickhouse_crate::Client;
|
||||
@@ -26,7 +26,7 @@ use crate::util::archon::ArchonClient;
|
||||
use crate::util::http::HttpClient;
|
||||
use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters};
|
||||
use crate::util::tiltify::TiltifyClient;
|
||||
use sync::friends::handle_pubsub;
|
||||
use sync::friends::{FRIENDS_CHANNEL_NAME, handle_pubsub};
|
||||
use url::Url;
|
||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||
|
||||
@@ -290,11 +290,10 @@ pub fn app_setup(
|
||||
|
||||
{
|
||||
let pool = pool.clone();
|
||||
let redis_client = redis::Client::open(redis_pool.url.clone()).unwrap();
|
||||
let pubsub_messages = redis_pool.subscribe(FRIENDS_CHANNEL_NAME);
|
||||
let sockets = active_sockets.clone();
|
||||
actix_rt::spawn(async move {
|
||||
let pubsub = redis_client.get_async_pubsub().await.unwrap();
|
||||
handle_pubsub(pubsub, pool, sockets).await;
|
||||
handle_pubsub(pubsub_messages, pool, sockets).await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use clap::Parser;
|
||||
|
||||
use labrinth::background_task::BackgroundTask;
|
||||
use labrinth::database::redis::RedisPool;
|
||||
use labrinth::database::redis;
|
||||
use labrinth::env::ENV;
|
||||
use labrinth::file_hosting::{FileHost, FileHostKind, S3BucketConfig, S3Host};
|
||||
use labrinth::queue::email::EmailQueue;
|
||||
@@ -110,7 +110,7 @@ async fn app() -> std::io::Result<()> {
|
||||
.expect("Database connection failed");
|
||||
|
||||
// Redis connector
|
||||
let redis_pool = RedisPool::new("");
|
||||
let redis_pool = redis::from_env("").await;
|
||||
|
||||
let storage_backend = ENV.STORAGE_BACKEND;
|
||||
let file_host: Arc<dyn FileHost> = match storage_backend {
|
||||
|
||||
@@ -2,12 +2,10 @@ use eyre::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use validator::Validate;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
models::{DBProjectId, DBVersionId},
|
||||
redis::RedisPool,
|
||||
},
|
||||
database::models::{DBProjectId, DBVersionId},
|
||||
models::{
|
||||
exp::{
|
||||
component::{
|
||||
@@ -257,14 +255,14 @@ pub async fn fetch_query_context(
|
||||
{
|
||||
HashMap::new()
|
||||
} else {
|
||||
let ping_keys = minecraft_java_server_pings
|
||||
.iter()
|
||||
.map(|project_id| {
|
||||
redis.key().entity(server_ping::REDIS_NAMESPACE, project_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis
|
||||
.get_many_deserialized::<minecraft::JavaServerPing>(
|
||||
server_ping::REDIS_NAMESPACE,
|
||||
&minecraft_java_server_pings
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.get_many_deserialized::<minecraft::JavaServerPing>(&ping_keys)
|
||||
.await?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -280,14 +278,14 @@ pub async fn fetch_query_context(
|
||||
let minecraft_server_analytics = if minecraft_server_analytics.is_empty() {
|
||||
HashMap::new()
|
||||
} else {
|
||||
let analytics_keys = minecraft_server_analytics
|
||||
.iter()
|
||||
.map(|project_id| {
|
||||
redis.key().entity(MINECRAFT_SERVER_ANALYTICS, project_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
redis
|
||||
.get_many_deserialized::<MinecraftServerAnalytics>(
|
||||
MINECRAFT_SERVER_ANALYTICS,
|
||||
&minecraft_server_analytics
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.get_many_deserialized::<MinecraftServerAnalytics>(&analytics_keys)
|
||||
.await?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::collections::HashMap;
|
||||
|
||||
use super::super::ids::OrganizationId;
|
||||
use crate::database::models::{DatabaseError, version_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::{ProjectId, TeamId, ThreadId, VersionId};
|
||||
use crate::models::projects::{
|
||||
Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus,
|
||||
@@ -16,6 +15,7 @@ use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
use xredis::RedisPool;
|
||||
|
||||
/// A project returned from the API
|
||||
#[derive(Serialize, Deserialize, Clone, utoipa::ToSchema)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use const_format::formatcp;
|
||||
use eyre::{Result, eyre};
|
||||
@@ -7,13 +8,11 @@ use sqlx::PgPool;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
database::{DBProject, redis::RedisPool},
|
||||
models::ids::ProjectId,
|
||||
routes::analytics::MINECRAFT_SERVER_PLAYS,
|
||||
util::error::Context,
|
||||
database::DBProject, models::ids::ProjectId,
|
||||
routes::analytics::MINECRAFT_SERVER_PLAYS, util::error::Context,
|
||||
};
|
||||
|
||||
pub const MINECRAFT_SERVER_ANALYTICS: &str = "minecraft_server_analytics:v1";
|
||||
pub const MINECRAFT_SERVER_ANALYTICS: &str = "minecraft_server_analytics:v3";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MinecraftServerAnalytics {
|
||||
@@ -116,13 +115,9 @@ pub async fn cache_analytics(
|
||||
};
|
||||
|
||||
debug!("Caching analytics for {project_id}: {analytics:?}");
|
||||
let key = redis.key().entity(MINECRAFT_SERVER_ANALYTICS, project_id);
|
||||
redis
|
||||
.set_serialized(
|
||||
MINECRAFT_SERVER_ANALYTICS,
|
||||
project_id.to_string(),
|
||||
analytics,
|
||||
None,
|
||||
)
|
||||
.set_serialized(&key, analytics, None)
|
||||
.await
|
||||
.wrap_err_with(|| {
|
||||
eyre!("failed to set analytics for project '{project_id}'")
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::analytics::{
|
||||
AffiliateCodeClick, Download, MinecraftServerPlay, PageView, Playtime,
|
||||
};
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::analytics::MINECRAFT_SERVER_PLAYS;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use redis::cmd;
|
||||
use std::collections::HashMap;
|
||||
use tracing::trace;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub mod cache;
|
||||
|
||||
const DOWNLOADS_NAMESPACE: &str = "downloads:v1";
|
||||
const VIEWS_NAMESPACE: &str = "views:v1";
|
||||
const MINECRAFT_SERVER_PLAYS_NAMESPACE: &str = "minecraft_server_plays:v1";
|
||||
const DOWNLOADS_NAMESPACE: &str = "downloads:v3";
|
||||
const VIEWS_NAMESPACE: &str = "views:v3";
|
||||
const MINECRAFT_SERVER_PLAYS_NAMESPACE: &str = "minecraft_server_plays:v3";
|
||||
const MINECRAFT_SERVER_PLAYS_EXPIRY: u64 = 86_400; // 24 hours
|
||||
const MINECRAFT_SERVER_PLAYS_LIMIT: u32 = 5;
|
||||
|
||||
@@ -133,29 +131,22 @@ impl AnalyticsQueue {
|
||||
raw_plays.insert(index, play);
|
||||
}
|
||||
|
||||
let mut redis =
|
||||
redis.pool.get().await.map_err(DatabaseError::RedisPool)?;
|
||||
let redis_keys = plays_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let logical_key = format!("{}-{}", key.0, key.1);
|
||||
redis.key().with_slot(
|
||||
MINECRAFT_SERVER_PLAYS_NAMESPACE,
|
||||
&logical_key,
|
||||
&logical_key,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut redis_connection = redis.connect().await?;
|
||||
|
||||
let results = cmd("MGET")
|
||||
.arg(
|
||||
plays_keys
|
||||
.iter()
|
||||
.map(|x| {
|
||||
format!(
|
||||
"{}:{}-{}",
|
||||
MINECRAFT_SERVER_PLAYS_NAMESPACE, x.0, x.1
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.query_async::<Vec<Option<u32>>>(&mut redis)
|
||||
.await
|
||||
.map_err(DatabaseError::CacheError)?;
|
||||
|
||||
let mut pipe = redis::pipe();
|
||||
let results =
|
||||
redis_connection.get_many_typed::<u32>(&redis_keys).await?;
|
||||
for (idx, count) in results.into_iter().enumerate() {
|
||||
let key = &plays_keys[idx];
|
||||
|
||||
let new_count = if let Some(count) = count {
|
||||
if count >= MINECRAFT_SERVER_PLAYS_LIMIT {
|
||||
raw_plays.remove(&idx);
|
||||
@@ -166,18 +157,15 @@ impl AnalyticsQueue {
|
||||
1
|
||||
};
|
||||
|
||||
pipe.atomic().set_ex(
|
||||
format!(
|
||||
"{}:{}-{}",
|
||||
MINECRAFT_SERVER_PLAYS_NAMESPACE, key.0, key.1
|
||||
),
|
||||
new_count,
|
||||
MINECRAFT_SERVER_PLAYS_EXPIRY,
|
||||
);
|
||||
let key = &redis_keys[idx];
|
||||
redis_connection
|
||||
.set(
|
||||
key,
|
||||
new_count,
|
||||
Some(MINECRAFT_SERVER_PLAYS_EXPIRY as i64),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
pipe.query_async::<()>(&mut *redis)
|
||||
.await
|
||||
.map_err(DatabaseError::CacheError)?;
|
||||
|
||||
let mut plays = client
|
||||
.insert::<MinecraftServerPlay>(MINECRAFT_SERVER_PLAYS)
|
||||
@@ -199,24 +187,22 @@ impl AnalyticsQueue {
|
||||
raw_views.push((views, true));
|
||||
}
|
||||
|
||||
let mut redis =
|
||||
redis.pool.get().await.map_err(DatabaseError::RedisPool)?;
|
||||
let redis_keys = views_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let logical_key = format!("{}-{}", key.0, key.1);
|
||||
redis.key().with_slot(
|
||||
VIEWS_NAMESPACE,
|
||||
&logical_key,
|
||||
&logical_key,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut redis_connection = redis.connect().await?;
|
||||
|
||||
let results = cmd("MGET")
|
||||
.arg(
|
||||
views_keys
|
||||
.iter()
|
||||
.map(|x| format!("{}:{}-{}", VIEWS_NAMESPACE, x.0, x.1))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.query_async::<Vec<Option<u32>>>(&mut redis)
|
||||
.await
|
||||
.map_err(DatabaseError::CacheError)?;
|
||||
|
||||
let mut pipe = redis::pipe();
|
||||
let results =
|
||||
redis_connection.get_many_typed::<u32>(&redis_keys).await?;
|
||||
for (idx, count) in results.into_iter().enumerate() {
|
||||
let key = &views_keys[idx];
|
||||
|
||||
let new_count =
|
||||
if let Some((views, monetized)) = raw_views.get_mut(idx) {
|
||||
if let Some(count) = count {
|
||||
@@ -237,15 +223,11 @@ impl AnalyticsQueue {
|
||||
1
|
||||
};
|
||||
|
||||
pipe.atomic().set_ex(
|
||||
format!("{}:{}-{}", VIEWS_NAMESPACE, key.0, key.1),
|
||||
new_count,
|
||||
6 * 60 * 60,
|
||||
);
|
||||
let key = &redis_keys[idx];
|
||||
redis_connection
|
||||
.set(key, new_count, Some(6 * 60 * 60))
|
||||
.await?;
|
||||
}
|
||||
pipe.query_async::<()>(&mut *redis)
|
||||
.await
|
||||
.map_err(DatabaseError::CacheError)?;
|
||||
|
||||
let mut views = client.insert::<PageView>("views").await?;
|
||||
|
||||
@@ -274,26 +256,22 @@ impl AnalyticsQueue {
|
||||
raw_downloads.insert(index, download);
|
||||
}
|
||||
|
||||
let mut redis =
|
||||
redis.pool.get().await.map_err(DatabaseError::RedisPool)?;
|
||||
let redis_keys = downloads_keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
let logical_key = format!("{}-{}", key.0, key.1);
|
||||
redis.key().with_slot(
|
||||
DOWNLOADS_NAMESPACE,
|
||||
&logical_key,
|
||||
&logical_key,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut redis_connection = redis.connect().await?;
|
||||
|
||||
let results = cmd("MGET")
|
||||
.arg(
|
||||
downloads_keys
|
||||
.iter()
|
||||
.map(|x| {
|
||||
format!("{}:{}-{}", DOWNLOADS_NAMESPACE, x.0, x.1)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.query_async::<Vec<Option<u32>>>(&mut redis)
|
||||
.await
|
||||
.map_err(DatabaseError::CacheError)?;
|
||||
|
||||
let mut pipe = redis::pipe();
|
||||
let results =
|
||||
redis_connection.get_many_typed::<u32>(&redis_keys).await?;
|
||||
for (idx, count) in results.into_iter().enumerate() {
|
||||
let key = &downloads_keys[idx];
|
||||
|
||||
let new_count = if let Some(count) = count {
|
||||
if count > 5 {
|
||||
raw_downloads.remove(&idx);
|
||||
@@ -305,15 +283,11 @@ impl AnalyticsQueue {
|
||||
1
|
||||
};
|
||||
|
||||
pipe.atomic().set_ex(
|
||||
format!("{}:{}-{}", DOWNLOADS_NAMESPACE, key.0, key.1),
|
||||
new_count,
|
||||
6 * 60 * 60,
|
||||
);
|
||||
let key = &redis_keys[idx];
|
||||
redis_connection
|
||||
.set(key, new_count, Some(6 * 60 * 60))
|
||||
.await?;
|
||||
}
|
||||
pipe.query_async::<()>(&mut *redis)
|
||||
.await
|
||||
.map_err(DatabaseError::CacheError)?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut downloads = client.insert::<Download>("downloads").await?;
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::database::models::{DatabaseError, ids::*};
|
||||
use crate::database::models::{
|
||||
product_item, user_subscription_item, users_redeemals,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::billing::{
|
||||
@@ -35,6 +34,7 @@ use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
use stripe::{self, Currency};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use xredis::RedisPool;
|
||||
|
||||
/// Updates charges which need to have their tax amount updated. This is done within a timer to avoid reaching
|
||||
/// Anrok API limits.
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::database::models::notification_item::DBNotification;
|
||||
use crate::database::models::notifications_deliveries_item::DBNotificationDelivery;
|
||||
use crate::database::models::notifications_template_item::NotificationTemplate;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::notifications::{NotificationBody, NotificationType};
|
||||
@@ -23,6 +22,7 @@ use thiserror::Error;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::{error, info, instrument, warn};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const EMAIL_RETRY_DELAY_SECONDS: i64 = 10;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::database::models::notifications_template_item::{
|
||||
use crate::database::models::{
|
||||
DBOrganization, DBProject, DBUser, DatabaseError,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::v3::notifications::NotificationBody;
|
||||
use crate::routes::ApiError;
|
||||
@@ -20,6 +19,7 @@ use sqlx::query;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, warn};
|
||||
use xredis::RedisPool;
|
||||
|
||||
const USER_NAME: &str = "user.name";
|
||||
const USER_EMAIL: &str = "user.email";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::sync::Arc;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use chrono::Utc;
|
||||
use eyre::{Result, eyre};
|
||||
@@ -17,7 +18,7 @@ use crate::database::models::ids::{
|
||||
};
|
||||
use crate::database::models::moderation_external_item::ExternalLicense;
|
||||
use crate::database::models::{DBFileId, DBUserId, DBVersion};
|
||||
use crate::database::{PgPool, PgTransaction, redis::RedisPool};
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::error::ApiError;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::payouts_values_notifications;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::payouts::{
|
||||
@@ -31,6 +30,7 @@ use sqlx::postgres::PgQueryResult;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{error, info, warn};
|
||||
use xredis::RedisPool;
|
||||
|
||||
mod affiliate;
|
||||
pub mod flow;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::database::DBProject;
|
||||
use crate::database::models::DBProjectId;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::exp;
|
||||
use crate::models::ids::ProjectId;
|
||||
@@ -18,6 +17,7 @@ use std::time::{Duration, Instant};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{Instrument, info, info_span, trace, warn};
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub struct ServerPingQueue {
|
||||
pub db: PgPool,
|
||||
@@ -26,9 +26,9 @@ pub struct ServerPingQueue {
|
||||
pub incremental_search_queue: IncrementalSearchQueue,
|
||||
}
|
||||
|
||||
pub const REDIS_NAMESPACE: &str = "minecraft_java_server_ping:v1";
|
||||
pub const REDIS_NAMESPACE: &str = "minecraft_java_server_ping:v3";
|
||||
pub const REDIS_FAILURE_NAMESPACE: &str =
|
||||
"minecraft_java_server_ping_failures:v1";
|
||||
"minecraft_java_server_ping_failures:v3";
|
||||
pub const CLICKHOUSE_TABLE: &str = "minecraft_java_server_pings";
|
||||
|
||||
impl ServerPingQueue {
|
||||
@@ -111,6 +111,12 @@ impl ServerPingQueue {
|
||||
|
||||
for (project_id, ping) in &pings {
|
||||
let data = ping.data.as_ref();
|
||||
let ping_key =
|
||||
self.redis.key().entity(REDIS_NAMESPACE, project_id);
|
||||
let failure_key = self
|
||||
.redis
|
||||
.key()
|
||||
.entity(REDIS_FAILURE_NAMESPACE, project_id);
|
||||
|
||||
let row = ServerPingRecord {
|
||||
recorded: ping.when.timestamp_nanos_opt().unwrap()
|
||||
@@ -134,13 +140,13 @@ impl ServerPingQueue {
|
||||
// ping succeeded; immediately update its online status in redis
|
||||
|
||||
redis
|
||||
.set_serialized(REDIS_NAMESPACE, project_id, ping, None)
|
||||
.set_serialized(&ping_key, ping, None)
|
||||
.await
|
||||
.wrap_err("failed to set redis key")?;
|
||||
updated_project = true;
|
||||
|
||||
redis
|
||||
.delete(REDIS_FAILURE_NAMESPACE, project_id)
|
||||
.delete(&failure_key)
|
||||
.await
|
||||
.wrap_err("failed to delete failure count")?;
|
||||
} else {
|
||||
@@ -148,7 +154,7 @@ impl ServerPingQueue {
|
||||
// otherwise, just add to the fail counter
|
||||
|
||||
let failure_count = redis
|
||||
.incr(REDIS_FAILURE_NAMESPACE, &project_id.to_string())
|
||||
.incr(&failure_key)
|
||||
.await
|
||||
.wrap_err("failed to increment failure count")?;
|
||||
|
||||
@@ -156,12 +162,7 @@ impl ServerPingQueue {
|
||||
&& count >= ENV.SERVER_PING_MAX_FAIL_COUNT
|
||||
{
|
||||
redis
|
||||
.set_serialized(
|
||||
REDIS_NAMESPACE,
|
||||
project_id,
|
||||
ping,
|
||||
None,
|
||||
)
|
||||
.set_serialized(&ping_key, ping, None)
|
||||
.await
|
||||
.wrap_err(
|
||||
"failed to set failed ping record in redis",
|
||||
@@ -246,14 +247,17 @@ impl ServerPingQueue {
|
||||
// and if we do miss an entry that we shouldn't, we just ping it again
|
||||
let all_project_ids = all_server_projects
|
||||
.iter()
|
||||
.map(|row| ProjectId::from(DBProjectId(row.id)).to_string())
|
||||
.map(|row| ProjectId::from(DBProjectId(row.id)))
|
||||
.collect::<Vec<_>>();
|
||||
let ping_keys = all_project_ids
|
||||
.iter()
|
||||
.map(|project_id| {
|
||||
self.redis.key().entity(REDIS_NAMESPACE, project_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_server_last_pings = redis
|
||||
.get_many_deserialized::<exp::minecraft::JavaServerPing>(
|
||||
REDIS_NAMESPACE,
|
||||
&all_project_ids,
|
||||
)
|
||||
.get_many_deserialized::<exp::minecraft::JavaServerPing>(&ping_keys)
|
||||
.await
|
||||
.wrap_err("failed to fetch server project last pings")?;
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ use crate::database::models::session_item::DBSession;
|
||||
use crate::database::models::{
|
||||
DBOAuthAccessTokenId, DBPatId, DBSessionId, DBUserId, DatabaseError,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::routes::internal::session::SessionMetadata;
|
||||
use chrono::Utc;
|
||||
use itertools::Itertools;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::sync::Mutex;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub struct AuthQueue {
|
||||
session_queue: Mutex<HashMap<DBSessionId, SessionMetadata>>,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::DBProject;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::models::analytics::{MinecraftServerPlay, PageView, Playtime};
|
||||
use crate::models::ids::ProjectId;
|
||||
@@ -22,6 +21,7 @@ use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub const FILTERED_HEADERS: &[&str] = &[
|
||||
"authorization",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::analytics::{Download, DownloadReason};
|
||||
use crate::models::ids::{ProjectId, VersionId};
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -22,6 +21,7 @@ use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use std::{collections::HashMap, net::Ipv4Addr, sync::Arc};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::env::ENV;
|
||||
use crate::{
|
||||
auth::get_user_from_headers,
|
||||
database::{
|
||||
models::{DBAffiliateCode, DBAffiliateCodeId, DBUser, DBUserId},
|
||||
redis::RedisPool,
|
||||
},
|
||||
database::models::{DBAffiliateCode, DBAffiliateCodeId, DBUser, DBUserId},
|
||||
models::{
|
||||
analytics::AffiliateCodeClick, ids::AffiliateCodeId, pats::Scopes,
|
||||
users::Badges, v3::affiliate_code::AffiliateCode,
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::database::models::{
|
||||
generate_attribution_group_id,
|
||||
},
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::ids::{FileId, ProjectId, VersionId};
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -27,6 +26,7 @@ use crate::queue::moderation::ApprovalType;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::Context;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(list)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use self::payments::*;
|
||||
use self::update_subscriptions::*;
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::ids::DBUserSubscriptionId;
|
||||
@@ -10,7 +11,6 @@ use crate::database::models::{
|
||||
DBAffiliateCodeId, charge_item, generate_charge_id, product_item,
|
||||
user_subscription_item,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::database::{PgPool, PgTransaction};
|
||||
use crate::env::ENV;
|
||||
use crate::models::billing::{
|
||||
@@ -41,6 +41,7 @@ use stripe::{
|
||||
Webhook,
|
||||
};
|
||||
use tracing::warn;
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(
|
||||
@@ -56,6 +57,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
.service(charges)
|
||||
.service(credit)
|
||||
.service(active_servers)
|
||||
.service(update_many)
|
||||
.service(initiate_payment)
|
||||
.service(stripe_webhook)
|
||||
.service(refund_charge)
|
||||
@@ -63,7 +65,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
/// List products.
|
||||
/// List products.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -113,7 +115,7 @@ struct UserSubscriptionWithNextChargeTaxAmount {
|
||||
pub next_charge_tax_amount: Option<i64>,
|
||||
}
|
||||
|
||||
/// List subscriptions.
|
||||
/// List subscriptions.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -187,7 +189,7 @@ pub struct ChargeRefund {
|
||||
pub unprovision: Option<bool>,
|
||||
}
|
||||
|
||||
/// Refund a charge.
|
||||
/// Refund a charge.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -457,7 +459,7 @@ pub async fn refund_charge(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
/// Reprocess tax for a charge.
|
||||
/// Reprocess tax for a charge.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -646,7 +648,7 @@ pub struct SubscriptionEditQuery {
|
||||
pub dry: Option<bool>,
|
||||
}
|
||||
|
||||
/// Update a subscription.
|
||||
/// Update a subscription.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1146,7 +1148,7 @@ pub async fn edit_subscription(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current customer.
|
||||
/// Get the current customer.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1190,7 +1192,7 @@ pub struct ChargesQuery {
|
||||
pub user_id: Option<ariadne::ids::UserId>,
|
||||
}
|
||||
|
||||
/// List payments.
|
||||
/// List payments.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1256,7 +1258,7 @@ pub async fn charges(
|
||||
))
|
||||
}
|
||||
|
||||
/// Start a payment method flow.
|
||||
/// Start a payment method flow.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1315,7 +1317,7 @@ pub struct EditPaymentMethod {
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
/// Update a payment method.
|
||||
/// Update a payment method.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1385,7 +1387,7 @@ pub async fn edit_payment_method(
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a payment method.
|
||||
/// Remove a payment method.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1474,7 +1476,7 @@ pub async fn remove_payment_method(
|
||||
}
|
||||
}
|
||||
|
||||
/// List payment methods.
|
||||
/// List payment methods.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1537,7 +1539,7 @@ struct ActiveServerResponse {
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
/// List active servers.
|
||||
/// List active servers.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1651,7 +1653,7 @@ pub struct PaymentRequest {
|
||||
pub metadata: Option<PaymentRequestMetadata>,
|
||||
}
|
||||
|
||||
/// Initiate a payment.
|
||||
/// Initiate a payment.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1721,7 +1723,7 @@ pub async fn initiate_payment(
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a Stripe webhook.
|
||||
/// Receive a Stripe webhook.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -1967,7 +1969,7 @@ pub async fn stripe_webhook(
|
||||
|
||||
if charge_status != ChargeStatus::Failed {
|
||||
subscription
|
||||
.upsert(transaction)
|
||||
.upsert(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -2009,7 +2011,7 @@ pub async fn stripe_webhook(
|
||||
};
|
||||
|
||||
if charge_status != ChargeStatus::Failed {
|
||||
charge.upsert(transaction).await?;
|
||||
charge.upsert(&mut *transaction).await?;
|
||||
}
|
||||
|
||||
(charge, price, product, subscription, new_region)
|
||||
@@ -2644,7 +2646,7 @@ pub enum CreditTarget {
|
||||
},
|
||||
}
|
||||
|
||||
/// Credit subscriptions.
|
||||
/// Credit subscriptions.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
@@ -2773,3 +2775,4 @@ pub async fn credit(
|
||||
}
|
||||
|
||||
pub mod payments;
|
||||
pub mod update_subscriptions;
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::database::models::{
|
||||
generate_charge_id, generate_user_subscription_id, product_item,
|
||||
products_tax_identifier_item, user_subscription_item,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::ids::*;
|
||||
use crate::models::v3::billing::SubscriptionStatus;
|
||||
use crate::models::v3::users::User;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::anrok;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::database::PgPool;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::{post, web};
|
||||
use ariadne::ids::UserId as Id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::user_subscription_item::DBUserSubscription;
|
||||
use crate::models::billing::SubscriptionMetadata;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::Context;
|
||||
use crate::util::guards::subscriptions_key_guard;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateManySubscriptions {
|
||||
pub subscriptions: Vec<SubscriptionUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SubscriptionUpdate {
|
||||
pub target: SubscriptionTarget,
|
||||
pub update_region: Option<String>,
|
||||
pub ignore_if_missing: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, ToSchema,
|
||||
)]
|
||||
pub enum SubscriptionTarget {
|
||||
Pyro { user_id: Id, server_id: Uuid },
|
||||
}
|
||||
|
||||
/// Update multiple managed subscriptions.
|
||||
#[utoipa::path(
|
||||
context_path = "/billing",
|
||||
tag = "billing",
|
||||
request_body = UpdateManySubscriptions,
|
||||
responses((status = OK))
|
||||
)]
|
||||
#[post(
|
||||
"/subscriptions/manager/update_many",
|
||||
guard = "subscriptions_key_guard"
|
||||
)]
|
||||
pub async fn update_many(
|
||||
pool: web::Data<PgPool>,
|
||||
body: web::Json<UpdateManySubscriptions>,
|
||||
) -> Result<(), ApiError> {
|
||||
let UpdateManySubscriptions { subscriptions } = body.into_inner();
|
||||
|
||||
let mut txn = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to begin transaction")?;
|
||||
|
||||
// Only supports hosting subscriptions now, so gather the server IDs and
|
||||
// fetch the subscriptions from them
|
||||
let server_ids = subscriptions
|
||||
.iter()
|
||||
.map(|update| match update.target {
|
||||
SubscriptionTarget::Pyro { server_id, .. } => server_id.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let found_subscriptions =
|
||||
DBUserSubscription::get_many_by_server_ids(&server_ids, &mut txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch subscriptions to update")?;
|
||||
|
||||
let mut subscriptions_by_target =
|
||||
HashMap::<SubscriptionTarget, DBUserSubscription>::new();
|
||||
|
||||
// Creates a map of subscription "key" -> it's DB representation.
|
||||
for subscription in found_subscriptions {
|
||||
let Some(SubscriptionMetadata::Pyro { id, .. }) =
|
||||
subscription.metadata.as_ref()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Ok(server_id) = Uuid::parse_str(id) else {
|
||||
continue;
|
||||
};
|
||||
let target = SubscriptionTarget::Pyro {
|
||||
user_id: subscription.user_id.into(),
|
||||
server_id,
|
||||
};
|
||||
|
||||
subscriptions_by_target.insert(target, subscription);
|
||||
}
|
||||
|
||||
for update in subscriptions {
|
||||
let Some(subscription) =
|
||||
subscriptions_by_target.get_mut(&update.target)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Update the subscription region
|
||||
if let Some(SubscriptionMetadata::Pyro { region, .. }) =
|
||||
subscription.metadata.as_mut()
|
||||
&& let Some(new_region) = update.update_region.clone()
|
||||
{
|
||||
*region = Some(new_region);
|
||||
}
|
||||
|
||||
subscription.upsert(&mut txn).await?;
|
||||
}
|
||||
|
||||
txn.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use sha2::Sha256;
|
||||
use std::collections::HashSet;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
@@ -18,7 +19,6 @@ use crate::{
|
||||
DBCampaignDonationId, DBUser, DBUserId,
|
||||
generate_campaign_donation_id,
|
||||
},
|
||||
redis::RedisPool,
|
||||
},
|
||||
env::ENV,
|
||||
models::payouts::TremendousForexResponse,
|
||||
@@ -68,7 +68,7 @@ pub struct CampaignInfo {
|
||||
cached_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
const CAMPAIGN_INFO_CACHE_NAMESPACE: &str = "campaign_info:v1";
|
||||
const CAMPAIGN_INFO_CACHE_NAMESPACE: &str = "campaign_info:v3";
|
||||
const CAMPAIGN_INFO_CACHE_STALE_SECONDS: i64 = 15 * 60;
|
||||
const CAMPAIGN_INFO_CACHE_TTL_SECONDS: i64 = 24 * 60 * 60;
|
||||
|
||||
@@ -143,7 +143,7 @@ impl CampaignDonation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a Tiltify webhook.
|
||||
/// Receive a Tiltify webhook.
|
||||
#[utoipa::path(
|
||||
context_path = "/campaign",
|
||||
tag = "campaigns",
|
||||
@@ -307,7 +307,7 @@ fn verify_tiltify_webhook_signature(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get Pride campaign data.
|
||||
/// Get Pride campaign data.
|
||||
#[utoipa::path(
|
||||
context_path = "/campaign",
|
||||
tag = "campaigns",
|
||||
@@ -324,12 +324,12 @@ pub async fn pride_26(
|
||||
.connect()
|
||||
.await
|
||||
.wrap_internal_err("connecting to redis")?;
|
||||
let cache_key = redis
|
||||
.key()
|
||||
.entity(CAMPAIGN_INFO_CACHE_NAMESPACE, campaign_id);
|
||||
|
||||
let cached = redis_connection
|
||||
.get_deserialized::<CampaignInfo>(
|
||||
CAMPAIGN_INFO_CACHE_NAMESPACE,
|
||||
campaign_id,
|
||||
)
|
||||
.get_deserialized::<CampaignInfo>(&cache_key)
|
||||
.await
|
||||
.wrap_internal_err("getting cached campaign info")?;
|
||||
|
||||
@@ -383,8 +383,7 @@ pub async fn pride_26(
|
||||
|
||||
redis_connection
|
||||
.set_serialized(
|
||||
CAMPAIGN_INFO_CACHE_NAMESPACE,
|
||||
campaign_id,
|
||||
&cache_key,
|
||||
&campaign_info,
|
||||
Some(CAMPAIGN_INFO_CACHE_TTL_SECONDS),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{collections::HashMap, fmt::Write, time::Instant};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use crate::env::ENV;
|
||||
use crate::{database::PgPool, util::http::HttpClient};
|
||||
@@ -11,16 +12,13 @@ use tracing::info;
|
||||
|
||||
use crate::{
|
||||
auth::check_is_moderator_from_headers,
|
||||
database::{
|
||||
models::{
|
||||
DBFileId, DBProjectId, DelphiReportId, DelphiReportIssueDetailsId,
|
||||
DelphiReportIssueId,
|
||||
delphi_report_item::{
|
||||
DBDelphiReport, DBDelphiReportIssue, DelphiSeverity,
|
||||
DelphiStatus, ReportIssueDetail,
|
||||
},
|
||||
database::models::{
|
||||
DBFileId, DBProjectId, DelphiReportId, DelphiReportIssueDetailsId,
|
||||
DelphiReportIssueId,
|
||||
delphi_report_item::{
|
||||
DBDelphiReport, DBDelphiReportIssue, DelphiSeverity, DelphiStatus,
|
||||
ReportIssueDetail,
|
||||
},
|
||||
redis::RedisPool,
|
||||
},
|
||||
models::{
|
||||
ids::{ProjectId, VersionId},
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::database::models::ids::{DBNotificationId, DBUserId};
|
||||
use crate::database::models::notification_item::DBNotification;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::notifications::NotificationDeliveryStatus;
|
||||
use crate::models::users::Role;
|
||||
use crate::models::v3::notifications::{Notification, NotificationBody};
|
||||
@@ -27,6 +26,7 @@ use ariadne::ids::UserId;
|
||||
use eyre::eyre;
|
||||
use lettre::message::Mailbox;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(create)
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::database::models::flow_item::DBFlow;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::session_item::DBSession;
|
||||
use crate::database::models::{DBPasskey, DBPasskeyId, DBUser, DBUserId};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::error::ApiError as ApiErrorResponse;
|
||||
@@ -59,6 +58,7 @@ use webauthn_rs::prelude::{
|
||||
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
|
||||
Webauthn, WebauthnError,
|
||||
};
|
||||
use xredis::RedisPool;
|
||||
use zxcvbn::Score;
|
||||
|
||||
/// Sourced from <https://github.com/disposable-email-domains/disposable-email-domains>.
|
||||
@@ -2213,15 +2213,15 @@ async fn validate_2fa_code(
|
||||
)
|
||||
.map_err(|_| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
const TOTP_NAMESPACE: &str = "used_totp:v1";
|
||||
const TOTP_NAMESPACE: &str = "used_totp:v3";
|
||||
let mut conn = redis.connect().await?;
|
||||
let logical_key = format!("{}-{}", input, user_id.0);
|
||||
let key = redis
|
||||
.key()
|
||||
.with_slot(TOTP_NAMESPACE, &logical_key, &logical_key);
|
||||
|
||||
// Check if TOTP has already been used
|
||||
if conn
|
||||
.get(TOTP_NAMESPACE, &format!("{}-{}", input, user_id.0))
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
if conn.get(&key).await?.is_some() {
|
||||
return Err(AuthenticationError::InvalidCredentials);
|
||||
}
|
||||
|
||||
@@ -2229,13 +2229,7 @@ async fn validate_2fa_code(
|
||||
.check_current(input.as_str())
|
||||
.map_err(|_| AuthenticationError::InvalidCredentials)?
|
||||
{
|
||||
conn.set(
|
||||
TOTP_NAMESPACE,
|
||||
&format!("{}-{}", input, user_id.0),
|
||||
"",
|
||||
Some(60),
|
||||
)
|
||||
.await?;
|
||||
conn.set(&key, "", Some(60)).await?;
|
||||
|
||||
Ok(true)
|
||||
} else if allow_backup {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{HttpRequest, HttpResponse, post, web};
|
||||
use xredis::RedisPool;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(web::scope("/gdpr").service(export));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user