forked from OpenWF/SpaceNinjaServer
		
	Initial commit
This commit is contained in:
		
						commit
						0b6f9bb026
					
				
							
								
								
									
										19
									
								
								.coderabbit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.coderabbit.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
 | 
				
			||||||
 | 
					language: "en-US"
 | 
				
			||||||
 | 
					early_access: false
 | 
				
			||||||
 | 
					reviews:
 | 
				
			||||||
 | 
					    profile: "chill"
 | 
				
			||||||
 | 
					    request_changes_workflow: false
 | 
				
			||||||
 | 
					    changed_files_summary: false
 | 
				
			||||||
 | 
					    high_level_summary: false
 | 
				
			||||||
 | 
					    poem: false
 | 
				
			||||||
 | 
					    review_status: true
 | 
				
			||||||
 | 
					    commit_status: false
 | 
				
			||||||
 | 
					    collapse_walkthrough: false
 | 
				
			||||||
 | 
					    sequence_diagrams: false
 | 
				
			||||||
 | 
					    related_prs: false
 | 
				
			||||||
 | 
					    auto_review:
 | 
				
			||||||
 | 
					        enabled: true
 | 
				
			||||||
 | 
					        drafts: false
 | 
				
			||||||
 | 
					chat:
 | 
				
			||||||
 | 
					    auto_reply: true
 | 
				
			||||||
							
								
								
									
										8
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					**/.dockerignore
 | 
				
			||||||
 | 
					**/.git
 | 
				
			||||||
 | 
					Dockerfile*
 | 
				
			||||||
 | 
					.*
 | 
				
			||||||
 | 
					docker-data/
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					static/data/
 | 
				
			||||||
 | 
					logs/
 | 
				
			||||||
							
								
								
									
										47
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								.eslintrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "plugins": ["@typescript-eslint", "prettier", "import"],
 | 
				
			||||||
 | 
					    "extends": [
 | 
				
			||||||
 | 
					        "eslint:recommended",
 | 
				
			||||||
 | 
					        "plugin:@typescript-eslint/recommended",
 | 
				
			||||||
 | 
					        "plugin:@typescript-eslint/recommended-requiring-type-checking",
 | 
				
			||||||
 | 
					        "plugin:import/recommended",
 | 
				
			||||||
 | 
					        "plugin:import/typescript"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "env": {
 | 
				
			||||||
 | 
					        "browser": true,
 | 
				
			||||||
 | 
					        "es6": true,
 | 
				
			||||||
 | 
					        "node": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "rules": {
 | 
				
			||||||
 | 
					        "@typescript-eslint/consistent-type-imports": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/explicit-function-return-type": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/restrict-template-expressions": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/restrict-plus-operands": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-unsafe-member-access": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-unsafe-argument": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-unsafe-call": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-unsafe-assignment": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-explicit-any": "off",
 | 
				
			||||||
 | 
					        "no-loss-of-precision": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-unnecessary-condition": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/no-base-to-string": "off",
 | 
				
			||||||
 | 
					        "no-case-declarations": "error",
 | 
				
			||||||
 | 
					        "prettier/prettier": "error",
 | 
				
			||||||
 | 
					        "no-mixed-spaces-and-tabs": "error",
 | 
				
			||||||
 | 
					        "@typescript-eslint/require-await": "error",
 | 
				
			||||||
 | 
					        "import/no-named-as-default-member": "off",
 | 
				
			||||||
 | 
					        "import/no-cycle": "warn"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "parser": "@typescript-eslint/parser",
 | 
				
			||||||
 | 
					    "parserOptions": {
 | 
				
			||||||
 | 
					        "project": "./tsconfig.json"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "settings": {
 | 
				
			||||||
 | 
					        "import/extensions": [ ".ts" ],
 | 
				
			||||||
 | 
					        "import/resolver": {
 | 
				
			||||||
 | 
					            "typescript": true,
 | 
				
			||||||
 | 
					            "node": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					# Auto detect text files and perform LF normalization
 | 
				
			||||||
 | 
					* text=auto eol=lf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static/webui/libs/ linguist-vendored
 | 
				
			||||||
							
								
								
									
										6
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					version: 2
 | 
				
			||||||
 | 
					updates:
 | 
				
			||||||
 | 
					    - package-ecosystem: "npm"
 | 
				
			||||||
 | 
					      directory: "/"
 | 
				
			||||||
 | 
					      schedule:
 | 
				
			||||||
 | 
					          interval: "weekly"
 | 
				
			||||||
							
								
								
									
										29
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					name: Build
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					    push:
 | 
				
			||||||
 | 
					        branches: ["main"]
 | 
				
			||||||
 | 
					    pull_request: {}
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					        runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					        steps:
 | 
				
			||||||
 | 
					            - name: Checkout
 | 
				
			||||||
 | 
					              uses: actions/checkout@v4.1.2
 | 
				
			||||||
 | 
					            - name: Setup Node.js environment
 | 
				
			||||||
 | 
					              uses: actions/setup-node@v4.0.2
 | 
				
			||||||
 | 
					              with:
 | 
				
			||||||
 | 
					                  node-version: ">=20.18.1"
 | 
				
			||||||
 | 
					            - run: npm ci
 | 
				
			||||||
 | 
					            - run: cp config-vanilla.json config.json
 | 
				
			||||||
 | 
					            - run: npm run verify
 | 
				
			||||||
 | 
					            - run: npm run lint:ci
 | 
				
			||||||
 | 
					            - run: npm run prettier
 | 
				
			||||||
 | 
					            - run: npm run update-translations
 | 
				
			||||||
 | 
					            - name: Fail if there are uncommitted changes
 | 
				
			||||||
 | 
					              run: |
 | 
				
			||||||
 | 
					                  if [[ -n "$(git status --porcelain)" ]]; then
 | 
				
			||||||
 | 
					                    echo "Uncommitted changes detected:"
 | 
				
			||||||
 | 
					                    git status
 | 
				
			||||||
 | 
					                    git --no-pager diff
 | 
				
			||||||
 | 
					                    exit 1
 | 
				
			||||||
 | 
					                  fi
 | 
				
			||||||
							
								
								
									
										44
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					name: Build Docker image
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					    push:
 | 
				
			||||||
 | 
					        branches:
 | 
				
			||||||
 | 
					            - main
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					    docker-amd64:
 | 
				
			||||||
 | 
					        if: github.repository == 'OpenWF/SpaceNinjaServer'
 | 
				
			||||||
 | 
					        runs-on: amd64
 | 
				
			||||||
 | 
					        steps:
 | 
				
			||||||
 | 
					            - name: Set up Docker buildx
 | 
				
			||||||
 | 
					              uses: docker/setup-buildx-action@v3
 | 
				
			||||||
 | 
					            - name: Log in to container registry
 | 
				
			||||||
 | 
					              uses: docker/login-action@v3
 | 
				
			||||||
 | 
					              with:
 | 
				
			||||||
 | 
					                  username: openwf
 | 
				
			||||||
 | 
					                  password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
				
			||||||
 | 
					            - name: Build and push
 | 
				
			||||||
 | 
					              uses: docker/build-push-action@v6
 | 
				
			||||||
 | 
					              with:
 | 
				
			||||||
 | 
					                  platforms: linux/amd64
 | 
				
			||||||
 | 
					                  push: true
 | 
				
			||||||
 | 
					                  tags: |
 | 
				
			||||||
 | 
					                      openwf/spaceninjaserver:latest
 | 
				
			||||||
 | 
					                      openwf/spaceninjaserver:${{ github.sha }}
 | 
				
			||||||
 | 
					    docker-arm64:
 | 
				
			||||||
 | 
					        if: github.repository == 'OpenWF/SpaceNinjaServer'
 | 
				
			||||||
 | 
					        runs-on: arm64
 | 
				
			||||||
 | 
					        steps:
 | 
				
			||||||
 | 
					            - name: Set up Docker buildx
 | 
				
			||||||
 | 
					              uses: docker/setup-buildx-action@v3
 | 
				
			||||||
 | 
					            - name: Log in to container registry
 | 
				
			||||||
 | 
					              uses: docker/login-action@v3
 | 
				
			||||||
 | 
					              with:
 | 
				
			||||||
 | 
					                  username: openwf
 | 
				
			||||||
 | 
					                  password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
				
			||||||
 | 
					            - name: Build and push
 | 
				
			||||||
 | 
					              uses: docker/build-push-action@v6
 | 
				
			||||||
 | 
					              with:
 | 
				
			||||||
 | 
					                  platforms: linux/arm64
 | 
				
			||||||
 | 
					                  push: true
 | 
				
			||||||
 | 
					                  tags: |
 | 
				
			||||||
 | 
					                      openwf/spaceninjaserver:latest-arm64
 | 
				
			||||||
 | 
					                      openwf/spaceninjaserver:${{ github.sha }}-arm64
 | 
				
			||||||
							
								
								
									
										21
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					/node_modules
 | 
				
			||||||
 | 
					/build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/.env
 | 
				
			||||||
 | 
					/config.json
 | 
				
			||||||
 | 
					/static/data/**
 | 
				
			||||||
 | 
					!/static/data/.gitkeep
 | 
				
			||||||
 | 
					yarn.lock
 | 
				
			||||||
 | 
					/tmp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# JetBrains/webstorm configs
 | 
				
			||||||
 | 
					.idea/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# logs
 | 
				
			||||||
 | 
					/logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# MongoDB VSCode extension playground scripts
 | 
				
			||||||
 | 
					/database_scripts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Default Docker directory
 | 
				
			||||||
 | 
					/docker-data
 | 
				
			||||||
							
								
								
									
										5
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					src/routes/api.ts
 | 
				
			||||||
 | 
					static/webui/libs/
 | 
				
			||||||
 | 
					*.html
 | 
				
			||||||
 | 
					*.md
 | 
				
			||||||
 | 
					config-vanilla.json
 | 
				
			||||||
							
								
								
									
										27
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "tabWidth": 4,
 | 
				
			||||||
 | 
					    "useTabs": false,
 | 
				
			||||||
 | 
					    "endOfLine": "auto",
 | 
				
			||||||
 | 
					    "trailingComma": "none",
 | 
				
			||||||
 | 
					    "htmlWhitespaceSensitivity": "css",
 | 
				
			||||||
 | 
					    "insertPragma": false,
 | 
				
			||||||
 | 
					    "jsxSingleQuote": false,
 | 
				
			||||||
 | 
					    "printWidth": 120,
 | 
				
			||||||
 | 
					    "proseWrap": "never",
 | 
				
			||||||
 | 
					    "quoteProps": "as-needed",
 | 
				
			||||||
 | 
					    "requirePragma": false,
 | 
				
			||||||
 | 
					    "semi": true,
 | 
				
			||||||
 | 
					    "singleQuote": false,
 | 
				
			||||||
 | 
					    "vueIndentScriptAndStyle": true,
 | 
				
			||||||
 | 
					    "arrowParens": "avoid",
 | 
				
			||||||
 | 
					    "bracketSpacing": true,
 | 
				
			||||||
 | 
					    "overrides": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "files": "*.json",
 | 
				
			||||||
 | 
					            "options": {
 | 
				
			||||||
 | 
					                "tabWidth": 2,
 | 
				
			||||||
 | 
					                "printWidth": 200
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "recommendations": ["dbaeumer.vscode-eslint"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					// Use IntelliSense to learn about possible attributes.
 | 
				
			||||||
 | 
					// Hover to view descriptions of existing attributes.
 | 
				
			||||||
 | 
					// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "version": "0.2.0",
 | 
				
			||||||
 | 
					  "configurations": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "type": "node",
 | 
				
			||||||
 | 
					      "request": "launch",
 | 
				
			||||||
 | 
					      "name": "Debug and Watch",
 | 
				
			||||||
 | 
					      "args": ["${workspaceFolder}/scripts/dev.js"],
 | 
				
			||||||
 | 
					      "console": "integratedTerminal"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//can use "console": "internalConsole" for VS Code's Debug Console. For that, forceConsole in logger.ts is needed to be true
 | 
				
			||||||
 | 
					//"internalConsoleOptions": "openOnSessionStart" can be useful then
 | 
				
			||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "typescript.preferences.preferTypeOnlyAutoImports": true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								AGENTS.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								AGENTS.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					## In General
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Prerequisites
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Use `npm i` or `npm ci` to install all dependencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Use `npm run verify` to verify that your changes pass TypeScript's checks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Formatting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Use `npm run prettier` to ensure your formatting matches the expected format. Failing to do so will cause CI failure.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## WebUI Specific
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT produce non-English strings; we want them to be translated by humans who can understand the full context.
 | 
				
			||||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					FROM node:24-alpine3.21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN apk add --no-cache bash jq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY . /app
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN npm i --omit=dev --omit=optional
 | 
				
			||||||
 | 
					RUN date '+%d %B %Y' > BUILD_DATE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENTRYPOINT ["/app/docker-entrypoint.sh"]
 | 
				
			||||||
							
								
								
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,674 @@
 | 
				
			|||||||
 | 
					GNU GENERAL PUBLIC LICENSE
 | 
				
			||||||
 | 
					                       Version 3, 29 June 2007
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 | 
				
			||||||
 | 
					 Everyone is permitted to copy and distribute verbatim copies
 | 
				
			||||||
 | 
					 of this license document, but changing it is not allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Preamble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The GNU General Public License is a free, copyleft license for
 | 
				
			||||||
 | 
					software and other kinds of works.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The licenses for most software and other practical works are designed
 | 
				
			||||||
 | 
					to take away your freedom to share and change the works.  By contrast,
 | 
				
			||||||
 | 
					the GNU General Public License is intended to guarantee your freedom to
 | 
				
			||||||
 | 
					share and change all versions of a program--to make sure it remains free
 | 
				
			||||||
 | 
					software for all its users.  We, the Free Software Foundation, use the
 | 
				
			||||||
 | 
					GNU General Public License for most of our software; it applies also to
 | 
				
			||||||
 | 
					any other work released this way by its authors.  You can apply it to
 | 
				
			||||||
 | 
					your programs, too.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  When we speak of free software, we are referring to freedom, not
 | 
				
			||||||
 | 
					price.  Our General Public Licenses are designed to make sure that you
 | 
				
			||||||
 | 
					have the freedom to distribute copies of free software (and charge for
 | 
				
			||||||
 | 
					them if you wish), that you receive source code or can get it if you
 | 
				
			||||||
 | 
					want it, that you can change the software or use pieces of it in new
 | 
				
			||||||
 | 
					free programs, and that you know you can do these things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  To protect your rights, we need to prevent others from denying you
 | 
				
			||||||
 | 
					these rights or asking you to surrender the rights.  Therefore, you have
 | 
				
			||||||
 | 
					certain responsibilities if you distribute copies of the software, or if
 | 
				
			||||||
 | 
					you modify it: responsibilities to respect the freedom of others.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  For example, if you distribute copies of such a program, whether
 | 
				
			||||||
 | 
					gratis or for a fee, you must pass on to the recipients the same
 | 
				
			||||||
 | 
					freedoms that you received.  You must make sure that they, too, receive
 | 
				
			||||||
 | 
					or can get the source code.  And you must show them these terms so they
 | 
				
			||||||
 | 
					know their rights.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Developers that use the GNU GPL protect your rights with two steps:
 | 
				
			||||||
 | 
					(1) assert copyright on the software, and (2) offer you this License
 | 
				
			||||||
 | 
					giving you legal permission to copy, distribute and/or modify it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  For the developers' and authors' protection, the GPL clearly explains
 | 
				
			||||||
 | 
					that there is no warranty for this free software.  For both users' and
 | 
				
			||||||
 | 
					authors' sake, the GPL requires that modified versions be marked as
 | 
				
			||||||
 | 
					changed, so that their problems will not be attributed erroneously to
 | 
				
			||||||
 | 
					authors of previous versions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Some devices are designed to deny users access to install or run
 | 
				
			||||||
 | 
					modified versions of the software inside them, although the manufacturer
 | 
				
			||||||
 | 
					can do so.  This is fundamentally incompatible with the aim of
 | 
				
			||||||
 | 
					protecting users' freedom to change the software.  The systematic
 | 
				
			||||||
 | 
					pattern of such abuse occurs in the area of products for individuals to
 | 
				
			||||||
 | 
					use, which is precisely where it is most unacceptable.  Therefore, we
 | 
				
			||||||
 | 
					have designed this version of the GPL to prohibit the practice for those
 | 
				
			||||||
 | 
					products.  If such problems arise substantially in other domains, we
 | 
				
			||||||
 | 
					stand ready to extend this provision to those domains in future versions
 | 
				
			||||||
 | 
					of the GPL, as needed to protect the freedom of users.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Finally, every program is threatened constantly by software patents.
 | 
				
			||||||
 | 
					States should not allow patents to restrict development and use of
 | 
				
			||||||
 | 
					software on general-purpose computers, but in those that do, we wish to
 | 
				
			||||||
 | 
					avoid the special danger that patents applied to a free program could
 | 
				
			||||||
 | 
					make it effectively proprietary.  To prevent this, the GPL assures that
 | 
				
			||||||
 | 
					patents cannot be used to render the program non-free.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The precise terms and conditions for copying, distribution and
 | 
				
			||||||
 | 
					modification follow.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                       TERMS AND CONDITIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  0. Definitions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "This License" refers to version 3 of the GNU General Public License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "Copyright" also means copyright-like laws that apply to other kinds of
 | 
				
			||||||
 | 
					works, such as semiconductor masks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "The Program" refers to any copyrightable work licensed under this
 | 
				
			||||||
 | 
					License.  Each licensee is addressed as "you".  "Licensees" and
 | 
				
			||||||
 | 
					"recipients" may be individuals or organizations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  To "modify" a work means to copy from or adapt all or part of the work
 | 
				
			||||||
 | 
					in a fashion requiring copyright permission, other than the making of an
 | 
				
			||||||
 | 
					exact copy.  The resulting work is called a "modified version" of the
 | 
				
			||||||
 | 
					earlier work or a work "based on" the earlier work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A "covered work" means either the unmodified Program or a work based
 | 
				
			||||||
 | 
					on the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  To "propagate" a work means to do anything with it that, without
 | 
				
			||||||
 | 
					permission, would make you directly or secondarily liable for
 | 
				
			||||||
 | 
					infringement under applicable copyright law, except executing it on a
 | 
				
			||||||
 | 
					computer or modifying a private copy.  Propagation includes copying,
 | 
				
			||||||
 | 
					distribution (with or without modification), making available to the
 | 
				
			||||||
 | 
					public, and in some countries other activities as well.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  To "convey" a work means any kind of propagation that enables other
 | 
				
			||||||
 | 
					parties to make or receive copies.  Mere interaction with a user through
 | 
				
			||||||
 | 
					a computer network, with no transfer of a copy, is not conveying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  An interactive user interface displays "Appropriate Legal Notices"
 | 
				
			||||||
 | 
					to the extent that it includes a convenient and prominently visible
 | 
				
			||||||
 | 
					feature that (1) displays an appropriate copyright notice, and (2)
 | 
				
			||||||
 | 
					tells the user that there is no warranty for the work (except to the
 | 
				
			||||||
 | 
					extent that warranties are provided), that licensees may convey the
 | 
				
			||||||
 | 
					work under this License, and how to view a copy of this License.  If
 | 
				
			||||||
 | 
					the interface presents a list of user commands or options, such as a
 | 
				
			||||||
 | 
					menu, a prominent item in the list meets this criterion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  1. Source Code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The "source code" for a work means the preferred form of the work
 | 
				
			||||||
 | 
					for making modifications to it.  "Object code" means any non-source
 | 
				
			||||||
 | 
					form of a work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A "Standard Interface" means an interface that either is an official
 | 
				
			||||||
 | 
					standard defined by a recognized standards body, or, in the case of
 | 
				
			||||||
 | 
					interfaces specified for a particular programming language, one that
 | 
				
			||||||
 | 
					is widely used among developers working in that language.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The "System Libraries" of an executable work include anything, other
 | 
				
			||||||
 | 
					than the work as a whole, that (a) is included in the normal form of
 | 
				
			||||||
 | 
					packaging a Major Component, but which is not part of that Major
 | 
				
			||||||
 | 
					Component, and (b) serves only to enable use of the work with that
 | 
				
			||||||
 | 
					Major Component, or to implement a Standard Interface for which an
 | 
				
			||||||
 | 
					implementation is available to the public in source code form.  A
 | 
				
			||||||
 | 
					"Major Component", in this context, means a major essential component
 | 
				
			||||||
 | 
					(kernel, window system, and so on) of the specific operating system
 | 
				
			||||||
 | 
					(if any) on which the executable work runs, or a compiler used to
 | 
				
			||||||
 | 
					produce the work, or an object code interpreter used to run it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The "Corresponding Source" for a work in object code form means all
 | 
				
			||||||
 | 
					the source code needed to generate, install, and (for an executable
 | 
				
			||||||
 | 
					work) run the object code and to modify the work, including scripts to
 | 
				
			||||||
 | 
					control those activities.  However, it does not include the work's
 | 
				
			||||||
 | 
					System Libraries, or general-purpose tools or generally available free
 | 
				
			||||||
 | 
					programs which are used unmodified in performing those activities but
 | 
				
			||||||
 | 
					which are not part of the work.  For example, Corresponding Source
 | 
				
			||||||
 | 
					includes interface definition files associated with source files for
 | 
				
			||||||
 | 
					the work, and the source code for shared libraries and dynamically
 | 
				
			||||||
 | 
					linked subprograms that the work is specifically designed to require,
 | 
				
			||||||
 | 
					such as by intimate data communication or control flow between those
 | 
				
			||||||
 | 
					subprograms and other parts of the work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The Corresponding Source need not include anything that users
 | 
				
			||||||
 | 
					can regenerate automatically from other parts of the Corresponding
 | 
				
			||||||
 | 
					Source.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The Corresponding Source for a work in source code form is that
 | 
				
			||||||
 | 
					same work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  2. Basic Permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  All rights granted under this License are granted for the term of
 | 
				
			||||||
 | 
					copyright on the Program, and are irrevocable provided the stated
 | 
				
			||||||
 | 
					conditions are met.  This License explicitly affirms your unlimited
 | 
				
			||||||
 | 
					permission to run the unmodified Program.  The output from running a
 | 
				
			||||||
 | 
					covered work is covered by this License only if the output, given its
 | 
				
			||||||
 | 
					content, constitutes a covered work.  This License acknowledges your
 | 
				
			||||||
 | 
					rights of fair use or other equivalent, as provided by copyright law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may make, run and propagate covered works that you do not
 | 
				
			||||||
 | 
					convey, without conditions so long as your license otherwise remains
 | 
				
			||||||
 | 
					in force.  You may convey covered works to others for the sole purpose
 | 
				
			||||||
 | 
					of having them make modifications exclusively for you, or provide you
 | 
				
			||||||
 | 
					with facilities for running those works, provided that you comply with
 | 
				
			||||||
 | 
					the terms of this License in conveying all material for which you do
 | 
				
			||||||
 | 
					not control copyright.  Those thus making or running the covered works
 | 
				
			||||||
 | 
					for you must do so exclusively on your behalf, under your direction
 | 
				
			||||||
 | 
					and control, on terms that prohibit them from making any copies of
 | 
				
			||||||
 | 
					your copyrighted material outside their relationship with you.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Conveying under any other circumstances is permitted solely under
 | 
				
			||||||
 | 
					the conditions stated below.  Sublicensing is not allowed; section 10
 | 
				
			||||||
 | 
					makes it unnecessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  No covered work shall be deemed part of an effective technological
 | 
				
			||||||
 | 
					measure under any applicable law fulfilling obligations under article
 | 
				
			||||||
 | 
					11 of the WIPO copyright treaty adopted on 20 December 1996, or
 | 
				
			||||||
 | 
					similar laws prohibiting or restricting circumvention of such
 | 
				
			||||||
 | 
					measures.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  When you convey a covered work, you waive any legal power to forbid
 | 
				
			||||||
 | 
					circumvention of technological measures to the extent such circumvention
 | 
				
			||||||
 | 
					is effected by exercising rights under this License with respect to
 | 
				
			||||||
 | 
					the covered work, and you disclaim any intention to limit operation or
 | 
				
			||||||
 | 
					modification of the work as a means of enforcing, against the work's
 | 
				
			||||||
 | 
					users, your or third parties' legal rights to forbid circumvention of
 | 
				
			||||||
 | 
					technological measures.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  4. Conveying Verbatim Copies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may convey verbatim copies of the Program's source code as you
 | 
				
			||||||
 | 
					receive it, in any medium, provided that you conspicuously and
 | 
				
			||||||
 | 
					appropriately publish on each copy an appropriate copyright notice;
 | 
				
			||||||
 | 
					keep intact all notices stating that this License and any
 | 
				
			||||||
 | 
					non-permissive terms added in accord with section 7 apply to the code;
 | 
				
			||||||
 | 
					keep intact all notices of the absence of any warranty; and give all
 | 
				
			||||||
 | 
					recipients a copy of this License along with the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may charge any price or no price for each copy that you convey,
 | 
				
			||||||
 | 
					and you may offer support or warranty protection for a fee.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  5. Conveying Modified Source Versions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may convey a work based on the Program, or the modifications to
 | 
				
			||||||
 | 
					produce it from the Program, in the form of source code under the
 | 
				
			||||||
 | 
					terms of section 4, provided that you also meet all of these conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a) The work must carry prominent notices stating that you modified
 | 
				
			||||||
 | 
					    it, and giving a relevant date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    b) The work must carry prominent notices stating that it is
 | 
				
			||||||
 | 
					    released under this License and any conditions added under section
 | 
				
			||||||
 | 
					    7.  This requirement modifies the requirement in section 4 to
 | 
				
			||||||
 | 
					    "keep intact all notices".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    c) You must license the entire work, as a whole, under this
 | 
				
			||||||
 | 
					    License to anyone who comes into possession of a copy.  This
 | 
				
			||||||
 | 
					    License will therefore apply, along with any applicable section 7
 | 
				
			||||||
 | 
					    additional terms, to the whole of the work, and all its parts,
 | 
				
			||||||
 | 
					    regardless of how they are packaged.  This License gives no
 | 
				
			||||||
 | 
					    permission to license the work in any other way, but it does not
 | 
				
			||||||
 | 
					    invalidate such permission if you have separately received it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    d) If the work has interactive user interfaces, each must display
 | 
				
			||||||
 | 
					    Appropriate Legal Notices; however, if the Program has interactive
 | 
				
			||||||
 | 
					    interfaces that do not display Appropriate Legal Notices, your
 | 
				
			||||||
 | 
					    work need not make them do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A compilation of a covered work with other separate and independent
 | 
				
			||||||
 | 
					works, which are not by their nature extensions of the covered work,
 | 
				
			||||||
 | 
					and which are not combined with it such as to form a larger program,
 | 
				
			||||||
 | 
					in or on a volume of a storage or distribution medium, is called an
 | 
				
			||||||
 | 
					"aggregate" if the compilation and its resulting copyright are not
 | 
				
			||||||
 | 
					used to limit the access or legal rights of the compilation's users
 | 
				
			||||||
 | 
					beyond what the individual works permit.  Inclusion of a covered work
 | 
				
			||||||
 | 
					in an aggregate does not cause this License to apply to the other
 | 
				
			||||||
 | 
					parts of the aggregate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  6. Conveying Non-Source Forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may convey a covered work in object code form under the terms
 | 
				
			||||||
 | 
					of sections 4 and 5, provided that you also convey the
 | 
				
			||||||
 | 
					machine-readable Corresponding Source under the terms of this License,
 | 
				
			||||||
 | 
					in one of these ways:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a) Convey the object code in, or embodied in, a physical product
 | 
				
			||||||
 | 
					    (including a physical distribution medium), accompanied by the
 | 
				
			||||||
 | 
					    Corresponding Source fixed on a durable physical medium
 | 
				
			||||||
 | 
					    customarily used for software interchange.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    b) Convey the object code in, or embodied in, a physical product
 | 
				
			||||||
 | 
					    (including a physical distribution medium), accompanied by a
 | 
				
			||||||
 | 
					    written offer, valid for at least three years and valid for as
 | 
				
			||||||
 | 
					    long as you offer spare parts or customer support for that product
 | 
				
			||||||
 | 
					    model, to give anyone who possesses the object code either (1) a
 | 
				
			||||||
 | 
					    copy of the Corresponding Source for all the software in the
 | 
				
			||||||
 | 
					    product that is covered by this License, on a durable physical
 | 
				
			||||||
 | 
					    medium customarily used for software interchange, for a price no
 | 
				
			||||||
 | 
					    more than your reasonable cost of physically performing this
 | 
				
			||||||
 | 
					    conveying of source, or (2) access to copy the
 | 
				
			||||||
 | 
					    Corresponding Source from a network server at no charge.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    c) Convey individual copies of the object code with a copy of the
 | 
				
			||||||
 | 
					    written offer to provide the Corresponding Source.  This
 | 
				
			||||||
 | 
					    alternative is allowed only occasionally and noncommercially, and
 | 
				
			||||||
 | 
					    only if you received the object code with such an offer, in accord
 | 
				
			||||||
 | 
					    with subsection 6b.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    d) Convey the object code by offering access from a designated
 | 
				
			||||||
 | 
					    place (gratis or for a charge), and offer equivalent access to the
 | 
				
			||||||
 | 
					    Corresponding Source in the same way through the same place at no
 | 
				
			||||||
 | 
					    further charge.  You need not require recipients to copy the
 | 
				
			||||||
 | 
					    Corresponding Source along with the object code.  If the place to
 | 
				
			||||||
 | 
					    copy the object code is a network server, the Corresponding Source
 | 
				
			||||||
 | 
					    may be on a different server (operated by you or a third party)
 | 
				
			||||||
 | 
					    that supports equivalent copying facilities, provided you maintain
 | 
				
			||||||
 | 
					    clear directions next to the object code saying where to find the
 | 
				
			||||||
 | 
					    Corresponding Source.  Regardless of what server hosts the
 | 
				
			||||||
 | 
					    Corresponding Source, you remain obligated to ensure that it is
 | 
				
			||||||
 | 
					    available for as long as needed to satisfy these requirements.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    e) Convey the object code using peer-to-peer transmission, provided
 | 
				
			||||||
 | 
					    you inform other peers where the object code and Corresponding
 | 
				
			||||||
 | 
					    Source of the work are being offered to the general public at no
 | 
				
			||||||
 | 
					    charge under subsection 6d.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A separable portion of the object code, whose source code is excluded
 | 
				
			||||||
 | 
					from the Corresponding Source as a System Library, need not be
 | 
				
			||||||
 | 
					included in conveying the object code work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A "User Product" is either (1) a "consumer product", which means any
 | 
				
			||||||
 | 
					tangible personal property which is normally used for personal, family,
 | 
				
			||||||
 | 
					or household purposes, or (2) anything designed or sold for incorporation
 | 
				
			||||||
 | 
					into a dwelling.  In determining whether a product is a consumer product,
 | 
				
			||||||
 | 
					doubtful cases shall be resolved in favor of coverage.  For a particular
 | 
				
			||||||
 | 
					product received by a particular user, "normally used" refers to a
 | 
				
			||||||
 | 
					typical or common use of that class of product, regardless of the status
 | 
				
			||||||
 | 
					of the particular user or of the way in which the particular user
 | 
				
			||||||
 | 
					actually uses, or expects or is expected to use, the product.  A product
 | 
				
			||||||
 | 
					is a consumer product regardless of whether the product has substantial
 | 
				
			||||||
 | 
					commercial, industrial or non-consumer uses, unless such uses represent
 | 
				
			||||||
 | 
					the only significant mode of use of the product.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "Installation Information" for a User Product means any methods,
 | 
				
			||||||
 | 
					procedures, authorization keys, or other information required to install
 | 
				
			||||||
 | 
					and execute modified versions of a covered work in that User Product from
 | 
				
			||||||
 | 
					a modified version of its Corresponding Source.  The information must
 | 
				
			||||||
 | 
					suffice to ensure that the continued functioning of the modified object
 | 
				
			||||||
 | 
					code is in no case prevented or interfered with solely because
 | 
				
			||||||
 | 
					modification has been made.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If you convey an object code work under this section in, or with, or
 | 
				
			||||||
 | 
					specifically for use in, a User Product, and the conveying occurs as
 | 
				
			||||||
 | 
					part of a transaction in which the right of possession and use of the
 | 
				
			||||||
 | 
					User Product is transferred to the recipient in perpetuity or for a
 | 
				
			||||||
 | 
					fixed term (regardless of how the transaction is characterized), the
 | 
				
			||||||
 | 
					Corresponding Source conveyed under this section must be accompanied
 | 
				
			||||||
 | 
					by the Installation Information.  But this requirement does not apply
 | 
				
			||||||
 | 
					if neither you nor any third party retains the ability to install
 | 
				
			||||||
 | 
					modified object code on the User Product (for example, the work has
 | 
				
			||||||
 | 
					been installed in ROM).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The requirement to provide Installation Information does not include a
 | 
				
			||||||
 | 
					requirement to continue to provide support service, warranty, or updates
 | 
				
			||||||
 | 
					for a work that has been modified or installed by the recipient, or for
 | 
				
			||||||
 | 
					the User Product in which it has been modified or installed.  Access to a
 | 
				
			||||||
 | 
					network may be denied when the modification itself materially and
 | 
				
			||||||
 | 
					adversely affects the operation of the network or violates the rules and
 | 
				
			||||||
 | 
					protocols for communication across the network.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Corresponding Source conveyed, and Installation Information provided,
 | 
				
			||||||
 | 
					in accord with this section must be in a format that is publicly
 | 
				
			||||||
 | 
					documented (and with an implementation available to the public in
 | 
				
			||||||
 | 
					source code form), and must require no special password or key for
 | 
				
			||||||
 | 
					unpacking, reading or copying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  7. Additional Terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "Additional permissions" are terms that supplement the terms of this
 | 
				
			||||||
 | 
					License by making exceptions from one or more of its conditions.
 | 
				
			||||||
 | 
					Additional permissions that are applicable to the entire Program shall
 | 
				
			||||||
 | 
					be treated as though they were included in this License, to the extent
 | 
				
			||||||
 | 
					that they are valid under applicable law.  If additional permissions
 | 
				
			||||||
 | 
					apply only to part of the Program, that part may be used separately
 | 
				
			||||||
 | 
					under those permissions, but the entire Program remains governed by
 | 
				
			||||||
 | 
					this License without regard to the additional permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  When you convey a copy of a covered work, you may at your option
 | 
				
			||||||
 | 
					remove any additional permissions from that copy, or from any part of
 | 
				
			||||||
 | 
					it.  (Additional permissions may be written to require their own
 | 
				
			||||||
 | 
					removal in certain cases when you modify the work.)  You may place
 | 
				
			||||||
 | 
					additional permissions on material, added by you to a covered work,
 | 
				
			||||||
 | 
					for which you have or can give appropriate copyright permission.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Notwithstanding any other provision of this License, for material you
 | 
				
			||||||
 | 
					add to a covered work, you may (if authorized by the copyright holders of
 | 
				
			||||||
 | 
					that material) supplement the terms of this License with terms:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a) Disclaiming warranty or limiting liability differently from the
 | 
				
			||||||
 | 
					    terms of sections 15 and 16 of this License; or
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    b) Requiring preservation of specified reasonable legal notices or
 | 
				
			||||||
 | 
					    author attributions in that material or in the Appropriate Legal
 | 
				
			||||||
 | 
					    Notices displayed by works containing it; or
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    c) Prohibiting misrepresentation of the origin of that material, or
 | 
				
			||||||
 | 
					    requiring that modified versions of such material be marked in
 | 
				
			||||||
 | 
					    reasonable ways as different from the original version; or
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    d) Limiting the use for publicity purposes of names of licensors or
 | 
				
			||||||
 | 
					    authors of the material; or
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    e) Declining to grant rights under trademark law for use of some
 | 
				
			||||||
 | 
					    trade names, trademarks, or service marks; or
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    f) Requiring indemnification of licensors and authors of that
 | 
				
			||||||
 | 
					    material by anyone who conveys the material (or modified versions of
 | 
				
			||||||
 | 
					    it) with contractual assumptions of liability to the recipient, for
 | 
				
			||||||
 | 
					    any liability that these contractual assumptions directly impose on
 | 
				
			||||||
 | 
					    those licensors and authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  All other non-permissive additional terms are considered "further
 | 
				
			||||||
 | 
					restrictions" within the meaning of section 10.  If the Program as you
 | 
				
			||||||
 | 
					received it, or any part of it, contains a notice stating that it is
 | 
				
			||||||
 | 
					governed by this License along with a term that is a further
 | 
				
			||||||
 | 
					restriction, you may remove that term.  If a license document contains
 | 
				
			||||||
 | 
					a further restriction but permits relicensing or conveying under this
 | 
				
			||||||
 | 
					License, you may add to a covered work material governed by the terms
 | 
				
			||||||
 | 
					of that license document, provided that the further restriction does
 | 
				
			||||||
 | 
					not survive such relicensing or conveying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If you add terms to a covered work in accord with this section, you
 | 
				
			||||||
 | 
					must place, in the relevant source files, a statement of the
 | 
				
			||||||
 | 
					additional terms that apply to those files, or a notice indicating
 | 
				
			||||||
 | 
					where to find the applicable terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Additional terms, permissive or non-permissive, may be stated in the
 | 
				
			||||||
 | 
					form of a separately written license, or stated as exceptions;
 | 
				
			||||||
 | 
					the above requirements apply either way.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  8. Termination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may not propagate or modify a covered work except as expressly
 | 
				
			||||||
 | 
					provided under this License.  Any attempt otherwise to propagate or
 | 
				
			||||||
 | 
					modify it is void, and will automatically terminate your rights under
 | 
				
			||||||
 | 
					this License (including any patent licenses granted under the third
 | 
				
			||||||
 | 
					paragraph of section 11).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  However, if you cease all violation of this License, then your
 | 
				
			||||||
 | 
					license from a particular copyright holder is reinstated (a)
 | 
				
			||||||
 | 
					provisionally, unless and until the copyright holder explicitly and
 | 
				
			||||||
 | 
					finally terminates your license, and (b) permanently, if the copyright
 | 
				
			||||||
 | 
					holder fails to notify you of the violation by some reasonable means
 | 
				
			||||||
 | 
					prior to 60 days after the cessation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Moreover, your license from a particular copyright holder is
 | 
				
			||||||
 | 
					reinstated permanently if the copyright holder notifies you of the
 | 
				
			||||||
 | 
					violation by some reasonable means, this is the first time you have
 | 
				
			||||||
 | 
					received notice of violation of this License (for any work) from that
 | 
				
			||||||
 | 
					copyright holder, and you cure the violation prior to 30 days after
 | 
				
			||||||
 | 
					your receipt of the notice.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Termination of your rights under this section does not terminate the
 | 
				
			||||||
 | 
					licenses of parties who have received copies or rights from you under
 | 
				
			||||||
 | 
					this License.  If your rights have been terminated and not permanently
 | 
				
			||||||
 | 
					reinstated, you do not qualify to receive new licenses for the same
 | 
				
			||||||
 | 
					material under section 10.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  9. Acceptance Not Required for Having Copies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You are not required to accept this License in order to receive or
 | 
				
			||||||
 | 
					run a copy of the Program.  Ancillary propagation of a covered work
 | 
				
			||||||
 | 
					occurring solely as a consequence of using peer-to-peer transmission
 | 
				
			||||||
 | 
					to receive a copy likewise does not require acceptance.  However,
 | 
				
			||||||
 | 
					nothing other than this License grants you permission to propagate or
 | 
				
			||||||
 | 
					modify any covered work.  These actions infringe copyright if you do
 | 
				
			||||||
 | 
					not accept this License.  Therefore, by modifying or propagating a
 | 
				
			||||||
 | 
					covered work, you indicate your acceptance of this License to do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  10. Automatic Licensing of Downstream Recipients.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Each time you convey a covered work, the recipient automatically
 | 
				
			||||||
 | 
					receives a license from the original licensors, to run, modify and
 | 
				
			||||||
 | 
					propagate that work, subject to this License.  You are not responsible
 | 
				
			||||||
 | 
					for enforcing compliance by third parties with this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  An "entity transaction" is a transaction transferring control of an
 | 
				
			||||||
 | 
					organization, or substantially all assets of one, or subdividing an
 | 
				
			||||||
 | 
					organization, or merging organizations.  If propagation of a covered
 | 
				
			||||||
 | 
					work results from an entity transaction, each party to that
 | 
				
			||||||
 | 
					transaction who receives a copy of the work also receives whatever
 | 
				
			||||||
 | 
					licenses to the work the party's predecessor in interest had or could
 | 
				
			||||||
 | 
					give under the previous paragraph, plus a right to possession of the
 | 
				
			||||||
 | 
					Corresponding Source of the work from the predecessor in interest, if
 | 
				
			||||||
 | 
					the predecessor has it or can get it with reasonable efforts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You may not impose any further restrictions on the exercise of the
 | 
				
			||||||
 | 
					rights granted or affirmed under this License.  For example, you may
 | 
				
			||||||
 | 
					not impose a license fee, royalty, or other charge for exercise of
 | 
				
			||||||
 | 
					rights granted under this License, and you may not initiate litigation
 | 
				
			||||||
 | 
					(including a cross-claim or counterclaim in a lawsuit) alleging that
 | 
				
			||||||
 | 
					any patent claim is infringed by making, using, selling, offering for
 | 
				
			||||||
 | 
					sale, or importing the Program or any portion of it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  11. Patents.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A "contributor" is a copyright holder who authorizes use under this
 | 
				
			||||||
 | 
					License of the Program or a work on which the Program is based.  The
 | 
				
			||||||
 | 
					work thus licensed is called the contributor's "contributor version".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A contributor's "essential patent claims" are all patent claims
 | 
				
			||||||
 | 
					owned or controlled by the contributor, whether already acquired or
 | 
				
			||||||
 | 
					hereafter acquired, that would be infringed by some manner, permitted
 | 
				
			||||||
 | 
					by this License, of making, using, or selling its contributor version,
 | 
				
			||||||
 | 
					but do not include claims that would be infringed only as a
 | 
				
			||||||
 | 
					consequence of further modification of the contributor version.  For
 | 
				
			||||||
 | 
					purposes of this definition, "control" includes the right to grant
 | 
				
			||||||
 | 
					patent sublicenses in a manner consistent with the requirements of
 | 
				
			||||||
 | 
					this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Each contributor grants you a non-exclusive, worldwide, royalty-free
 | 
				
			||||||
 | 
					patent license under the contributor's essential patent claims, to
 | 
				
			||||||
 | 
					make, use, sell, offer for sale, import and otherwise run, modify and
 | 
				
			||||||
 | 
					propagate the contents of its contributor version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  In the following three paragraphs, a "patent license" is any express
 | 
				
			||||||
 | 
					agreement or commitment, however denominated, not to enforce a patent
 | 
				
			||||||
 | 
					(such as an express permission to practice a patent or covenant not to
 | 
				
			||||||
 | 
					sue for patent infringement).  To "grant" such a patent license to a
 | 
				
			||||||
 | 
					party means to make such an agreement or commitment not to enforce a
 | 
				
			||||||
 | 
					patent against the party.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If you convey a covered work, knowingly relying on a patent license,
 | 
				
			||||||
 | 
					and the Corresponding Source of the work is not available for anyone
 | 
				
			||||||
 | 
					to copy, free of charge and under the terms of this License, through a
 | 
				
			||||||
 | 
					publicly available network server or other readily accessible means,
 | 
				
			||||||
 | 
					then you must either (1) cause the Corresponding Source to be so
 | 
				
			||||||
 | 
					available, or (2) arrange to deprive yourself of the benefit of the
 | 
				
			||||||
 | 
					patent license for this particular work, or (3) arrange, in a manner
 | 
				
			||||||
 | 
					consistent with the requirements of this License, to extend the patent
 | 
				
			||||||
 | 
					license to downstream recipients.  "Knowingly relying" means you have
 | 
				
			||||||
 | 
					actual knowledge that, but for the patent license, your conveying the
 | 
				
			||||||
 | 
					covered work in a country, or your recipient's use of the covered work
 | 
				
			||||||
 | 
					in a country, would infringe one or more identifiable patents in that
 | 
				
			||||||
 | 
					country that you have reason to believe are valid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If, pursuant to or in connection with a single transaction or
 | 
				
			||||||
 | 
					arrangement, you convey, or propagate by procuring conveyance of, a
 | 
				
			||||||
 | 
					covered work, and grant a patent license to some of the parties
 | 
				
			||||||
 | 
					receiving the covered work authorizing them to use, propagate, modify
 | 
				
			||||||
 | 
					or convey a specific copy of the covered work, then the patent license
 | 
				
			||||||
 | 
					you grant is automatically extended to all recipients of the covered
 | 
				
			||||||
 | 
					work and works based on it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  A patent license is "discriminatory" if it does not include within
 | 
				
			||||||
 | 
					the scope of its coverage, prohibits the exercise of, or is
 | 
				
			||||||
 | 
					conditioned on the non-exercise of one or more of the rights that are
 | 
				
			||||||
 | 
					specifically granted under this License.  You may not convey a covered
 | 
				
			||||||
 | 
					work if you are a party to an arrangement with a third party that is
 | 
				
			||||||
 | 
					in the business of distributing software, under which you make payment
 | 
				
			||||||
 | 
					to the third party based on the extent of your activity of conveying
 | 
				
			||||||
 | 
					the work, and under which the third party grants, to any of the
 | 
				
			||||||
 | 
					parties who would receive the covered work from you, a discriminatory
 | 
				
			||||||
 | 
					patent license (a) in connection with copies of the covered work
 | 
				
			||||||
 | 
					conveyed by you (or copies made from those copies), or (b) primarily
 | 
				
			||||||
 | 
					for and in connection with specific products or compilations that
 | 
				
			||||||
 | 
					contain the covered work, unless you entered into that arrangement,
 | 
				
			||||||
 | 
					or that patent license was granted, prior to 28 March 2007.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Nothing in this License shall be construed as excluding or limiting
 | 
				
			||||||
 | 
					any implied license or other defenses to infringement that may
 | 
				
			||||||
 | 
					otherwise be available to you under applicable patent law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  12. No Surrender of Others' Freedom.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If conditions are imposed on you (whether by court order, agreement or
 | 
				
			||||||
 | 
					otherwise) that contradict the conditions of this License, they do not
 | 
				
			||||||
 | 
					excuse you from the conditions of this License.  If you cannot convey a
 | 
				
			||||||
 | 
					covered work so as to satisfy simultaneously your obligations under this
 | 
				
			||||||
 | 
					License and any other pertinent obligations, then as a consequence you may
 | 
				
			||||||
 | 
					not convey it at all.  For example, if you agree to terms that obligate you
 | 
				
			||||||
 | 
					to collect a royalty for further conveying from those to whom you convey
 | 
				
			||||||
 | 
					the Program, the only way you could satisfy both those terms and this
 | 
				
			||||||
 | 
					License would be to refrain entirely from conveying the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  13. Use with the GNU Affero General Public License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Notwithstanding any other provision of this License, you have
 | 
				
			||||||
 | 
					permission to link or combine any covered work with a work licensed
 | 
				
			||||||
 | 
					under version 3 of the GNU Affero General Public License into a single
 | 
				
			||||||
 | 
					combined work, and to convey the resulting work.  The terms of this
 | 
				
			||||||
 | 
					License will continue to apply to the part which is the covered work,
 | 
				
			||||||
 | 
					but the special requirements of the GNU Affero General Public License,
 | 
				
			||||||
 | 
					section 13, concerning interaction through a network will apply to the
 | 
				
			||||||
 | 
					combination as such.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  14. Revised Versions of this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The Free Software Foundation may publish revised and/or new versions of
 | 
				
			||||||
 | 
					the GNU General Public License from time to time.  Such new versions will
 | 
				
			||||||
 | 
					be similar in spirit to the present version, but may differ in detail to
 | 
				
			||||||
 | 
					address new problems or concerns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Each version is given a distinguishing version number.  If the
 | 
				
			||||||
 | 
					Program specifies that a certain numbered version of the GNU General
 | 
				
			||||||
 | 
					Public License "or any later version" applies to it, you have the
 | 
				
			||||||
 | 
					option of following the terms and conditions either of that numbered
 | 
				
			||||||
 | 
					version or of any later version published by the Free Software
 | 
				
			||||||
 | 
					Foundation.  If the Program does not specify a version number of the
 | 
				
			||||||
 | 
					GNU General Public License, you may choose any version ever published
 | 
				
			||||||
 | 
					by the Free Software Foundation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If the Program specifies that a proxy can decide which future
 | 
				
			||||||
 | 
					versions of the GNU General Public License can be used, that proxy's
 | 
				
			||||||
 | 
					public statement of acceptance of a version permanently authorizes you
 | 
				
			||||||
 | 
					to choose that version for the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Later license versions may give you additional or different
 | 
				
			||||||
 | 
					permissions.  However, no additional obligations are imposed on any
 | 
				
			||||||
 | 
					author or copyright holder as a result of your choosing to follow a
 | 
				
			||||||
 | 
					later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  15. Disclaimer of Warranty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
 | 
				
			||||||
 | 
					APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
 | 
				
			||||||
 | 
					HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
 | 
				
			||||||
 | 
					OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
 | 
				
			||||||
 | 
					THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 | 
				
			||||||
 | 
					PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
 | 
				
			||||||
 | 
					IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
 | 
				
			||||||
 | 
					ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  16. Limitation of Liability.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 | 
				
			||||||
 | 
					WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
 | 
				
			||||||
 | 
					THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
 | 
				
			||||||
 | 
					GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
 | 
				
			||||||
 | 
					USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
 | 
				
			||||||
 | 
					DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
 | 
				
			||||||
 | 
					PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
 | 
				
			||||||
 | 
					EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
 | 
				
			||||||
 | 
					SUCH DAMAGES.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  17. Interpretation of Sections 15 and 16.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If the disclaimer of warranty and limitation of liability provided
 | 
				
			||||||
 | 
					above cannot be given local legal effect according to their terms,
 | 
				
			||||||
 | 
					reviewing courts shall apply local law that most closely approximates
 | 
				
			||||||
 | 
					an absolute waiver of all civil liability in connection with the
 | 
				
			||||||
 | 
					Program, unless a warranty or assumption of liability accompanies a
 | 
				
			||||||
 | 
					copy of the Program in return for a fee.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                     END OF TERMS AND CONDITIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            How to Apply These Terms to Your New Programs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If you develop a new program, and you want it to be of the greatest
 | 
				
			||||||
 | 
					possible use to the public, the best way to achieve this is to make it
 | 
				
			||||||
 | 
					free software which everyone can redistribute and change under these terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  To do so, attach the following notices to the program.  It is safest
 | 
				
			||||||
 | 
					to attach them to the start of each source file to most effectively
 | 
				
			||||||
 | 
					state the exclusion of warranty; and each file should have at least
 | 
				
			||||||
 | 
					the "copyright" line and a pointer to where the full notice is found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <one line to give the program's name and a brief idea of what it does.>
 | 
				
			||||||
 | 
					    Copyright (C) <year>  <name of author>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This program is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					    it under the terms of the GNU General Public License as published by
 | 
				
			||||||
 | 
					    the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
 | 
					    (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This program is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					    but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					    GNU General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    You should have received a copy of the GNU General Public License
 | 
				
			||||||
 | 
					    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also add information on how to contact you by electronic and paper mail.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  If the program does terminal interaction, make it output a short
 | 
				
			||||||
 | 
					notice like this when it starts in an interactive mode:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <program>  Copyright (C) <year>  <name of author>
 | 
				
			||||||
 | 
					    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
 | 
				
			||||||
 | 
					    This is free software, and you are welcome to redistribute it
 | 
				
			||||||
 | 
					    under certain conditions; type `show c' for details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The hypothetical commands `show w' and `show c' should show the appropriate
 | 
				
			||||||
 | 
					parts of the General Public License.  Of course, your program's commands
 | 
				
			||||||
 | 
					might be different; for a GUI interface, you would use an "about box".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  You should also get your employer (if you work as a programmer) or school,
 | 
				
			||||||
 | 
					if any, to sign a "copyright disclaimer" for the program, if necessary.
 | 
				
			||||||
 | 
					For more information on this, and how to apply and follow the GNU GPL, see
 | 
				
			||||||
 | 
					<https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The GNU General Public License does not permit incorporating your program
 | 
				
			||||||
 | 
					into proprietary programs.  If your program is a subroutine library, you
 | 
				
			||||||
 | 
					may consider it more useful to permit linking proprietary applications with
 | 
				
			||||||
 | 
					the library.  If this is what you want to do, use the GNU Lesser General
 | 
				
			||||||
 | 
					Public License instead of this License.  But first, please read
 | 
				
			||||||
 | 
					<https://www.gnu.org/licenses/why-not-lgpl.html>.
 | 
				
			||||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					# Space Ninja Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Project Status
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
				
			||||||
 | 
					- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
				
			||||||
 | 
					- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
 | 
				
			||||||
 | 
					- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
 | 
				
			||||||
 | 
					- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
 | 
				
			||||||
 | 
					- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission4Syndicate` for Nora's Choice
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission3Syndicate` for Intermission III
 | 
				
			||||||
 | 
					  - `RadioLegion3Syndicate` for Glassmaker
 | 
				
			||||||
 | 
					  - `RadioLegionIntermission2Syndicate` for Intermission II
 | 
				
			||||||
 | 
					  - `RadioLegion2Syndicate` for The Emissary
 | 
				
			||||||
 | 
					  - `RadioLegionIntermissionSyndicate` for Intermission I
 | 
				
			||||||
 | 
					  - `RadioLegionSyndicate` for The Wolf of Saturn Six
 | 
				
			||||||
 | 
					- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
 | 
				
			||||||
 | 
					- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.
 | 
				
			||||||
							
								
								
									
										28
									
								
								UPDATE AND START SERVER.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								UPDATE AND START SERVER.bat
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					@echo off
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo Updating SpaceNinjaServer...
 | 
				
			||||||
 | 
					git fetch --prune
 | 
				
			||||||
 | 
					if %errorlevel% == 0 (
 | 
				
			||||||
 | 
						git stash
 | 
				
			||||||
 | 
						git checkout -f origin/main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if exist static\data\0\ (
 | 
				
			||||||
 | 
							echo Updating stripped assets...
 | 
				
			||||||
 | 
							cd static\data\0\
 | 
				
			||||||
 | 
							git pull
 | 
				
			||||||
 | 
							cd ..\..\..\
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						echo Updating dependencies...
 | 
				
			||||||
 | 
						call npm i --omit=dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						call npm run build
 | 
				
			||||||
 | 
						if %errorlevel% == 0 (
 | 
				
			||||||
 | 
							call npm run start
 | 
				
			||||||
 | 
							echo SpaceNinjaServer seems to have crashed.
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:a
 | 
				
			||||||
 | 
					pause > nul
 | 
				
			||||||
 | 
					goto a
 | 
				
			||||||
							
								
								
									
										24
									
								
								UPDATE AND START SERVER.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								UPDATE AND START SERVER.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Updating SpaceNinjaServer..."
 | 
				
			||||||
 | 
					git fetch --prune
 | 
				
			||||||
 | 
					if [ $? -eq 0 ]; then
 | 
				
			||||||
 | 
					    git stash
 | 
				
			||||||
 | 
					    git checkout -f origin/main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if [ -d "static/data/0/" ]; then
 | 
				
			||||||
 | 
					        echo "Updating stripped assets..."
 | 
				
			||||||
 | 
					        cd static/data/0/
 | 
				
			||||||
 | 
					        git pull
 | 
				
			||||||
 | 
					        cd ../../../
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    echo "Updating dependencies..."
 | 
				
			||||||
 | 
					    npm i --omit=dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    npm run build
 | 
				
			||||||
 | 
					    if [ $? -eq 0 ]; then
 | 
				
			||||||
 | 
					        npm run start
 | 
				
			||||||
 | 
					        echo "SpaceNinjaServer seems to have crashed."
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
							
								
								
									
										77
									
								
								config-vanilla.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								config-vanilla.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
 | 
				
			||||||
 | 
					  "logger": {
 | 
				
			||||||
 | 
					    "files": true,
 | 
				
			||||||
 | 
					    "level": "trace"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "myAddress": "localhost",
 | 
				
			||||||
 | 
					  "httpPort": 80,
 | 
				
			||||||
 | 
					  "httpsPort": 443,
 | 
				
			||||||
 | 
					  "administratorNames": [],
 | 
				
			||||||
 | 
					  "autoCreateAccount": true,
 | 
				
			||||||
 | 
					  "skipTutorial": false,
 | 
				
			||||||
 | 
					  "unlockAllScans": false,
 | 
				
			||||||
 | 
					  "unlockAllShipFeatures": false,
 | 
				
			||||||
 | 
					  "unlockAllShipDecorations": false,
 | 
				
			||||||
 | 
					  "unlockAllFlavourItems": false,
 | 
				
			||||||
 | 
					  "unlockAllSkins": false,
 | 
				
			||||||
 | 
					  "unlockAllCapturaScenes": false,
 | 
				
			||||||
 | 
					  "fullyStockedVendors": false,
 | 
				
			||||||
 | 
					  "skipClanKeyCrafting": false,
 | 
				
			||||||
 | 
					  "noDojoRoomBuildStage": false,
 | 
				
			||||||
 | 
					  "noDojoDecoBuildStage": false,
 | 
				
			||||||
 | 
					  "fastDojoRoomDestruction": false,
 | 
				
			||||||
 | 
					  "noDojoResearchCosts": false,
 | 
				
			||||||
 | 
					  "noDojoResearchTime": false,
 | 
				
			||||||
 | 
					  "fastClanAscension": false,
 | 
				
			||||||
 | 
					  "spoofMasteryRank": -1,
 | 
				
			||||||
 | 
					  "relicRewardItemCountMultiplier": 1,
 | 
				
			||||||
 | 
					  "nightwaveStandingMultiplier": 1,
 | 
				
			||||||
 | 
					  "unfaithfulBugFixes": {
 | 
				
			||||||
 | 
					    "ignore1999LastRegionPlayed": false,
 | 
				
			||||||
 | 
					    "fixXtraCheeseTimer": false,
 | 
				
			||||||
 | 
					    "useAnniversaryTagForOldGoals": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "worldState": {
 | 
				
			||||||
 | 
					    "creditBoost": false,
 | 
				
			||||||
 | 
					    "affinityBoost": false,
 | 
				
			||||||
 | 
					    "resourceBoost": false,
 | 
				
			||||||
 | 
					    "tennoLiveRelay": false,
 | 
				
			||||||
 | 
					    "baroTennoConRelay": false,
 | 
				
			||||||
 | 
					    "baroAlwaysAvailable": false,
 | 
				
			||||||
 | 
					    "baroFullyStocked": false,
 | 
				
			||||||
 | 
					    "varziaFullyStocked": false,
 | 
				
			||||||
 | 
					    "wolfHunt": false,
 | 
				
			||||||
 | 
					    "orphixVenom": false,
 | 
				
			||||||
 | 
					    "longShadow": false,
 | 
				
			||||||
 | 
					    "hallowedFlame": false,
 | 
				
			||||||
 | 
					    "anniversary": null,
 | 
				
			||||||
 | 
					    "hallowedNightmares": false,
 | 
				
			||||||
 | 
					    "hallowedNightmaresRewardsOverride": 0,
 | 
				
			||||||
 | 
					    "proxyRebellion": false,
 | 
				
			||||||
 | 
					    "proxyRebellionRewardsOverride": 0,
 | 
				
			||||||
 | 
					    "galleonOfGhouls": 0,
 | 
				
			||||||
 | 
					    "ghoulEmergenceOverride": null,
 | 
				
			||||||
 | 
					    "plagueStarOverride": null,
 | 
				
			||||||
 | 
					    "starDaysOverride": null,
 | 
				
			||||||
 | 
					    "dogDaysOverride": null,
 | 
				
			||||||
 | 
					    "dogDaysRewardsOverride": null,
 | 
				
			||||||
 | 
					    "bellyOfTheBeast": false,
 | 
				
			||||||
 | 
					    "bellyOfTheBeastProgressOverride": 0,
 | 
				
			||||||
 | 
					    "eightClaw": false,
 | 
				
			||||||
 | 
					    "eightClawProgressOverride": 0,
 | 
				
			||||||
 | 
					    "thermiaFracturesOverride": null,
 | 
				
			||||||
 | 
					    "thermiaFracturesProgressOverride": 0,
 | 
				
			||||||
 | 
					    "eidolonOverride": "",
 | 
				
			||||||
 | 
					    "vallisOverride": "",
 | 
				
			||||||
 | 
					    "duviriOverride": "",
 | 
				
			||||||
 | 
					    "nightwaveOverride": "",
 | 
				
			||||||
 | 
					    "allTheFissures": "",
 | 
				
			||||||
 | 
					    "varziaOverride": "",
 | 
				
			||||||
 | 
					    "circuitGameModes": null,
 | 
				
			||||||
 | 
					    "darvoStockMultiplier": 1
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dev": {
 | 
				
			||||||
 | 
					    "keepVendorsExpired": false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					services:
 | 
				
			||||||
 | 
					    spaceninjaserver:
 | 
				
			||||||
 | 
					        # The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'.
 | 
				
			||||||
 | 
					        image: openwf/spaceninjaserver:latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        volumes:
 | 
				
			||||||
 | 
					            - ./docker-data/conf:/app/conf
 | 
				
			||||||
 | 
					            - ./docker-data/static-data:/app/static/data
 | 
				
			||||||
 | 
					            - ./docker-data/logs:/app/logs
 | 
				
			||||||
 | 
					        ports:
 | 
				
			||||||
 | 
					            - 80:80
 | 
				
			||||||
 | 
					            - 443:443
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
 | 
				
			||||||
 | 
					        #build: .
 | 
				
			||||||
 | 
					        # Works best when using `docker-compose up --force-recreate --build`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        depends_on:
 | 
				
			||||||
 | 
					            - mongodb
 | 
				
			||||||
 | 
					    mongodb:
 | 
				
			||||||
 | 
					        image: docker.io/library/mongo:8.0.0-noble
 | 
				
			||||||
 | 
					        environment:
 | 
				
			||||||
 | 
					            MONGO_INITDB_ROOT_USERNAME: openwfagent
 | 
				
			||||||
 | 
					            MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
 | 
				
			||||||
 | 
					        volumes:
 | 
				
			||||||
 | 
					            - ./docker-data/database:/data/db
 | 
				
			||||||
 | 
					        command: mongod --quiet --logpath /dev/null
 | 
				
			||||||
							
								
								
									
										8
									
								
								docker-entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docker-entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ ! -f conf/config.json ]; then
 | 
				
			||||||
 | 
						jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exec npm run raw -- --configPath conf/config.json
 | 
				
			||||||
							
								
								
									
										5776
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5776
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										65
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "wf-emulator",
 | 
				
			||||||
 | 
					  "version": "0.1.0",
 | 
				
			||||||
 | 
					  "description": "WF Emulator",
 | 
				
			||||||
 | 
					  "main": "index.ts",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "start": "node --enable-source-maps build/src/index.js",
 | 
				
			||||||
 | 
					    "build": "tsgo --sourceMap && ncp static/webui build/static/webui",
 | 
				
			||||||
 | 
					    "build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
 | 
				
			||||||
 | 
					    "build:dev": "tsgo --sourceMap",
 | 
				
			||||||
 | 
					    "build:dev:tsc": "tsc --incremental --sourceMap",
 | 
				
			||||||
 | 
					    "build-and-start": "npm run build && npm run start",
 | 
				
			||||||
 | 
					    "build-and-start:bun": "npm run verify && npm run bun-run",
 | 
				
			||||||
 | 
					    "dev": "node scripts/dev.cjs",
 | 
				
			||||||
 | 
					    "dev:bun": "bun scripts/dev.cjs",
 | 
				
			||||||
 | 
					    "verify": "tsgo --noEmit",
 | 
				
			||||||
 | 
					    "verify:tsc": "tsc --noEmit",
 | 
				
			||||||
 | 
					    "raw": "node --experimental-transform-types src/index.ts",
 | 
				
			||||||
 | 
					    "raw:bun": "bun src/index.ts",
 | 
				
			||||||
 | 
					    "lint": "eslint --ext .ts .",
 | 
				
			||||||
 | 
					    "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
 | 
				
			||||||
 | 
					    "lint:fix": "eslint --fix --ext .ts .",
 | 
				
			||||||
 | 
					    "prettier": "prettier --write .",
 | 
				
			||||||
 | 
					    "update-translations": "cd scripts && node update-translations.cjs",
 | 
				
			||||||
 | 
					    "fix": "npm run update-translations && npm run lint:fix"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "license": "GNU",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "chokidar": "^4.0.3",
 | 
				
			||||||
 | 
					    "crc-32": "^1.2.2",
 | 
				
			||||||
 | 
					    "express": "^5",
 | 
				
			||||||
 | 
					    "json-with-bigint": "^3.4.4",
 | 
				
			||||||
 | 
					    "mongoose": "^8.11.0",
 | 
				
			||||||
 | 
					    "morgan": "^1.10.0",
 | 
				
			||||||
 | 
					    "ncp": "^2.0.0",
 | 
				
			||||||
 | 
					    "undici": "^7.10.0",
 | 
				
			||||||
 | 
					    "warframe-public-export-plus": "^0.5.83",
 | 
				
			||||||
 | 
					    "warframe-riven-info": "^0.1.2",
 | 
				
			||||||
 | 
					    "winston": "^3.17.0",
 | 
				
			||||||
 | 
					    "winston-daily-rotate-file": "^5.0.0",
 | 
				
			||||||
 | 
					    "ws": "^8.18.2"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "optionalDependencies": {
 | 
				
			||||||
 | 
					    "@types/express": "^5",
 | 
				
			||||||
 | 
					    "@types/morgan": "^1.9.9",
 | 
				
			||||||
 | 
					    "@types/websocket": "^1.0.10",
 | 
				
			||||||
 | 
					    "@types/ws": "^8.18.1",
 | 
				
			||||||
 | 
					    "@typescript/native-preview": "^7.0.0-dev.20250625.1",
 | 
				
			||||||
 | 
					    "typescript": "^5.7"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
				
			||||||
 | 
					    "@typescript-eslint/parser": "^8.28.0",
 | 
				
			||||||
 | 
					    "eslint": "^8",
 | 
				
			||||||
 | 
					    "eslint-import-resolver-typescript": "^4.4.4",
 | 
				
			||||||
 | 
					    "eslint-plugin-import": "^2.32.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-prettier": "^5.2.5",
 | 
				
			||||||
 | 
					    "prettier": "^3.5.3",
 | 
				
			||||||
 | 
					    "tree-kill": "^1.2.2"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "engines": {
 | 
				
			||||||
 | 
					    "node": ">=20.18.1"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										58
									
								
								scripts/dev.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								scripts/dev.cjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					/* eslint-disable */
 | 
				
			||||||
 | 
					const { spawn } = require("child_process");
 | 
				
			||||||
 | 
					const chokidar = require("chokidar");
 | 
				
			||||||
 | 
					const kill = require("tree-kill");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let secret = "";
 | 
				
			||||||
 | 
					for (let i = 0; i != 10; ++i) {
 | 
				
			||||||
 | 
					    secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const args = [...process.argv].splice(2);
 | 
				
			||||||
 | 
					args.push("--dev");
 | 
				
			||||||
 | 
					args.push("--secret");
 | 
				
			||||||
 | 
					args.push(secret);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let buildproc, runproc;
 | 
				
			||||||
 | 
					const spawnopts = { stdio: "inherit", shell: true };
 | 
				
			||||||
 | 
					function run(changedFile) {
 | 
				
			||||||
 | 
					    if (changedFile) {
 | 
				
			||||||
 | 
					        console.log(`Change to ${changedFile} detected`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (buildproc) {
 | 
				
			||||||
 | 
					        kill(buildproc.pid);
 | 
				
			||||||
 | 
					        buildproc = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (runproc) {
 | 
				
			||||||
 | 
					        kill(runproc.pid);
 | 
				
			||||||
 | 
					        runproc = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const thisbuildproc = spawn("npm", ["run", process.versions.bun ? "verify" : "build:dev"], spawnopts);
 | 
				
			||||||
 | 
					    const thisbuildstart = Date.now();
 | 
				
			||||||
 | 
					    buildproc = thisbuildproc;
 | 
				
			||||||
 | 
					    buildproc.on("exit", code => {
 | 
				
			||||||
 | 
					        if (buildproc !== thisbuildproc) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        buildproc = undefined;
 | 
				
			||||||
 | 
					        if (code === 0) {
 | 
				
			||||||
 | 
					            console.log(`${process.versions.bun ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
 | 
				
			||||||
 | 
					            runproc = spawn("npm", ["run", process.versions.bun ? "raw:bun" : "start", "--", ...args], spawnopts);
 | 
				
			||||||
 | 
					            runproc.on("exit", () => {
 | 
				
			||||||
 | 
					                runproc = undefined;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					run();
 | 
				
			||||||
 | 
					chokidar.watch("src").on("change", run);
 | 
				
			||||||
 | 
					chokidar.watch("static/fixed_responses").on("change", run);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chokidar.watch("static/webui").on("change", async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        await fetch("http://localhost/custom/webuiFileChangeDetected?secret=" + secret);
 | 
				
			||||||
 | 
					    } catch (e) {}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										47
									
								
								scripts/update-translations.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								scripts/update-translations.cjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
 | 
				
			||||||
 | 
					// Converted via ChatGPT-4o
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* eslint-disable */
 | 
				
			||||||
 | 
					const fs = require("fs");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function extractStrings(content) {
 | 
				
			||||||
 | 
					    const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
 | 
				
			||||||
 | 
					    let matches;
 | 
				
			||||||
 | 
					    const strings = {};
 | 
				
			||||||
 | 
					    while ((matches = regex.exec(content)) !== null) {
 | 
				
			||||||
 | 
					        strings[matches[1]] = matches[2];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return strings;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
 | 
				
			||||||
 | 
					const sourceStrings = extractStrings(source);
 | 
				
			||||||
 | 
					const sourceLines = source.substring(0, source.length - 1).split("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fs.readdirSync("../static/webui/translations").forEach(file => {
 | 
				
			||||||
 | 
					    if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
 | 
				
			||||||
 | 
					        const content = fs.readFileSync(`../static/webui/translations/${file}`, "utf8");
 | 
				
			||||||
 | 
					        const targetStrings = extractStrings(content);
 | 
				
			||||||
 | 
					        const contentLines = content.split("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const fileHandle = fs.openSync(`../static/webui/translations/${file}`, "w");
 | 
				
			||||||
 | 
					        fs.writeSync(fileHandle, contentLines[0] + "\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sourceLines.forEach(line => {
 | 
				
			||||||
 | 
					            const strings = extractStrings(line);
 | 
				
			||||||
 | 
					            if (Object.keys(strings).length > 0) {
 | 
				
			||||||
 | 
					                Object.entries(strings).forEach(([key, value]) => {
 | 
				
			||||||
 | 
					                    if (targetStrings.hasOwnProperty(key) && !targetStrings[key].startsWith("[UNTRANSLATED]")) {
 | 
				
			||||||
 | 
					                        fs.writeSync(fileHandle, `    ${key}: \`${targetStrings[key]}\`,\n`);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        fs.writeSync(fileHandle, `    ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                fs.writeSync(fileHandle, line + "\n");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fs.closeSync(fileHandle);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										51
									
								
								src/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/app.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					import express from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import bodyParser from "body-parser";
 | 
				
			||||||
 | 
					import { unknownEndpointHandler } from "./middleware/middleware.ts";
 | 
				
			||||||
 | 
					import { requestLogger } from "./middleware/morgenMiddleware.ts";
 | 
				
			||||||
 | 
					import { errorHandler } from "./middleware/errorHandler.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { apiRouter } from "./routes/api.ts";
 | 
				
			||||||
 | 
					import { cacheRouter } from "./routes/cache.ts";
 | 
				
			||||||
 | 
					import { customRouter } from "./routes/custom.ts";
 | 
				
			||||||
 | 
					import { dynamicController } from "./routes/dynamic.ts";
 | 
				
			||||||
 | 
					import { payRouter } from "./routes/pay.ts";
 | 
				
			||||||
 | 
					import { statsRouter } from "./routes/stats.ts";
 | 
				
			||||||
 | 
					import { webuiRouter } from "./routes/webui.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const app = express();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use((req, _res, next) => {
 | 
				
			||||||
 | 
					    // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
 | 
				
			||||||
 | 
					    // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
 | 
				
			||||||
 | 
					    if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
 | 
				
			||||||
 | 
					        req.headers["content-encoding"] = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
 | 
				
			||||||
 | 
					    // U17 sets no Content-Type at all, which Express also doesn't like.
 | 
				
			||||||
 | 
					    if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
 | 
				
			||||||
 | 
					        req.headers["content-type"] = "application/octet-stream";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    next();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use(bodyParser.raw());
 | 
				
			||||||
 | 
					app.use(express.json({ limit: "4mb" }));
 | 
				
			||||||
 | 
					app.use(bodyParser.text({ limit: "4mb" }));
 | 
				
			||||||
 | 
					app.use(requestLogger);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use("/api", apiRouter);
 | 
				
			||||||
 | 
					app.use("/", cacheRouter);
 | 
				
			||||||
 | 
					app.use("/custom", customRouter);
 | 
				
			||||||
 | 
					app.use("/dynamic", dynamicController);
 | 
				
			||||||
 | 
					app.use("/:id/dynamic", dynamicController);
 | 
				
			||||||
 | 
					app.use("/pay", payRouter);
 | 
				
			||||||
 | 
					app.use("/stats", statsRouter);
 | 
				
			||||||
 | 
					app.use("/", webuiRouter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.use(unknownEndpointHandler);
 | 
				
			||||||
 | 
					app.use(errorHandler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { app };
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/constants/timeConstants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/constants/timeConstants.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					const millisecondsPerSecond = 1000;
 | 
				
			||||||
 | 
					const secondsPerMinute = 60;
 | 
				
			||||||
 | 
					const minutesPerHour = 60;
 | 
				
			||||||
 | 
					const hoursPerDay = 24;
 | 
				
			||||||
 | 
					const daysPerWeek = 7;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const unixSecond = millisecondsPerSecond;
 | 
				
			||||||
 | 
					const unixMinute = secondsPerMinute * millisecondsPerSecond;
 | 
				
			||||||
 | 
					const unixHour = unixMinute * minutesPerHour;
 | 
				
			||||||
 | 
					const unixDay = hoursPerDay * unixHour;
 | 
				
			||||||
 | 
					const unixWeek = daysPerWeek * unixDay;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const unixTimesInMs = {
 | 
				
			||||||
 | 
					    second: unixSecond,
 | 
				
			||||||
 | 
					    minute: unixMinute,
 | 
				
			||||||
 | 
					    hour: unixHour,
 | 
				
			||||||
 | 
					    day: unixDay,
 | 
				
			||||||
 | 
					    week: unixWeek
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/controllers/api/abandonLibraryDailyTaskController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/controllers/api/abandonLibraryDailyTaskController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const abandonLibraryDailyTaskController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    inventory.LibraryActiveDailyTaskInfo = undefined;
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.status(200).end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/controllers/api/abortDojoComponentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/controllers/api/abortDojoComponentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getDojoClient,
 | 
				
			||||||
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
 | 
					    hasAccessToDojo,
 | 
				
			||||||
 | 
					    hasGuildPermission,
 | 
				
			||||||
 | 
					    removeDojoDeco,
 | 
				
			||||||
 | 
					    removeDojoRoom
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const abortDojoComponentController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        !hasAccessToDojo(inventory) ||
 | 
				
			||||||
 | 
					        !(await hasGuildPermission(
 | 
				
			||||||
 | 
					            guild,
 | 
				
			||||||
 | 
					            accountId,
 | 
				
			||||||
 | 
					            request.DecoId ? GuildPermission.Decorator : GuildPermission.Architect
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (request.DecoId) {
 | 
				
			||||||
 | 
					        removeDojoDeco(guild, request.ComponentId, request.DecoId);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        await removeDojoRoom(guild, request.ComponentId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					    res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAbortDojoComponentRequest {
 | 
				
			||||||
 | 
					    DecoType?: string;
 | 
				
			||||||
 | 
					    ComponentId: string;
 | 
				
			||||||
 | 
					    DecoId?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getDojoClient,
 | 
				
			||||||
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
 | 
					    hasAccessToDojo,
 | 
				
			||||||
 | 
					    hasGuildPermission
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
 | 
				
			||||||
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const componentId = req.query.componentId as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild.DojoComponents.id(componentId)!.DestructionTime = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					    res.json(await getDojoClient(guild, 0, componentId));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/controllers/api/activateRandomModController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/controllers/api/activateRandomModController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    createVeiledRivenFingerprint,
 | 
				
			||||||
 | 
					    createUnveiledRivenFingerprint,
 | 
				
			||||||
 | 
					    rivenRawToRealWeighted
 | 
				
			||||||
 | 
					} from "../../helpers/rivenHelper.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { addMods, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getRandomElement } from "../../services/rngService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportUpgrades } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const activateRandomModController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "RawUpgrades Upgrades instantFinishRivenChallenge");
 | 
				
			||||||
 | 
					    const request = getJSONfromString<IActiveRandomModRequest>(String(req.body));
 | 
				
			||||||
 | 
					    addMods(inventory, [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ItemType: request.ItemType,
 | 
				
			||||||
 | 
					            ItemCount: -1
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					    const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!;
 | 
				
			||||||
 | 
					    const fingerprint = inventory.instantFinishRivenChallenge
 | 
				
			||||||
 | 
					        ? createUnveiledRivenFingerprint(ExportUpgrades[rivenType])
 | 
				
			||||||
 | 
					        : createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
 | 
				
			||||||
 | 
					    const upgradeIndex =
 | 
				
			||||||
 | 
					        inventory.Upgrades.push({
 | 
				
			||||||
 | 
					            ItemType: rivenType,
 | 
				
			||||||
 | 
					            UpgradeFingerprint: JSON.stringify(fingerprint)
 | 
				
			||||||
 | 
					        }) - 1;
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    // For some reason, in this response, the UpgradeFingerprint is simply a nested object and not a string
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        NewMod: {
 | 
				
			||||||
 | 
					            UpgradeFingerprint: fingerprint,
 | 
				
			||||||
 | 
					            ItemType: rivenType,
 | 
				
			||||||
 | 
					            ItemId: toOid(inventory.Upgrades[upgradeIndex]._id)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IActiveRandomModRequest {
 | 
				
			||||||
 | 
					    ItemType: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/controllers/api/addFriendController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/controllers/api/addFriendController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Friendship } from "../../models/friendModel.ts";
 | 
				
			||||||
 | 
					import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addFriendController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<IAddFriendRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const promises: Promise<void>[] = [];
 | 
				
			||||||
 | 
					    const newFriends: IFriendInfo[] = [];
 | 
				
			||||||
 | 
					    if (payload.friend == "all") {
 | 
				
			||||||
 | 
					        const [internalFriendships, externalFriendships] = await Promise.all([
 | 
				
			||||||
 | 
					            Friendship.find({ owner: accountId }, "friend"),
 | 
				
			||||||
 | 
					            Friendship.find({ friend: accountId }, "owner")
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        for (const externalFriendship of externalFriendships) {
 | 
				
			||||||
 | 
					            if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
 | 
				
			||||||
 | 
					                promises.push(
 | 
				
			||||||
 | 
					                    Friendship.insertOne({
 | 
				
			||||||
 | 
					                        owner: accountId,
 | 
				
			||||||
 | 
					                        friend: externalFriendship.owner,
 | 
				
			||||||
 | 
					                        Note: externalFriendship.Note // TOVERIFY: Should the note be copied when accepting a friend request?
 | 
				
			||||||
 | 
					                    }) as unknown as Promise<void>
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                newFriends.push({
 | 
				
			||||||
 | 
					                    _id: toOid(externalFriendship.owner)
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const externalFriendship = await Friendship.findOne({ owner: payload.friend, friend: accountId }, "Note");
 | 
				
			||||||
 | 
					        if (externalFriendship) {
 | 
				
			||||||
 | 
					            promises.push(
 | 
				
			||||||
 | 
					                Friendship.insertOne({
 | 
				
			||||||
 | 
					                    owner: accountId,
 | 
				
			||||||
 | 
					                    friend: payload.friend,
 | 
				
			||||||
 | 
					                    Note: externalFriendship.Note
 | 
				
			||||||
 | 
					                }) as unknown as Promise<void>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            newFriends.push({
 | 
				
			||||||
 | 
					                _id: { $oid: payload.friend }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const newFriend of newFriends) {
 | 
				
			||||||
 | 
					        promises.push(addAccountDataToFriendInfo(newFriend));
 | 
				
			||||||
 | 
					        promises.push(addInventoryDataToFriendInfo(newFriend));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await Promise.all(promises);
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        Friends: newFriends
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAddFriendRequest {
 | 
				
			||||||
 | 
					    friend: string; // oid or "all" in which case all=1 is also a query parameter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/controllers/api/addFriendImageController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/controllers/api/addFriendImageController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addFriendImageController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const json = getJSONfromString<IUpdateGlyphRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Inventory.updateOne(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            accountOwnerId: accountId
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ActiveAvatarImageType: json.AvatarImageType
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({});
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IUpdateGlyphRequest {
 | 
				
			||||||
 | 
					    AvatarImageType: string;
 | 
				
			||||||
 | 
					    AvatarImage: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/controllers/api/addIgnoredUserController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/controllers/api/addIgnoredUserController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Account, Ignore } from "../../models/loginModel.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addIgnoredUserController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const data = getJSONfromString<IAddIgnoredUserRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const ignoreeAccount = await Account.findOne(
 | 
				
			||||||
 | 
					        { DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
 | 
				
			||||||
 | 
					        "_id"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (ignoreeAccount) {
 | 
				
			||||||
 | 
					        await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id });
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            Ignored: {
 | 
				
			||||||
 | 
					                _id: toOid(ignoreeAccount._id),
 | 
				
			||||||
 | 
					                DisplayName: data.playerName
 | 
				
			||||||
 | 
					            } satisfies IFriendInfo
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        res.status(400).end();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAddIgnoredUserRequest {
 | 
				
			||||||
 | 
					    playerName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								src/controllers/api/addPendingFriendController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/controllers/api/addPendingFriendController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Friendship } from "../../models/friendModel.ts";
 | 
				
			||||||
 | 
					import { Account } from "../../models/loginModel.ts";
 | 
				
			||||||
 | 
					import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "../../services/friendService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addPendingFriendController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<IAddPendingFriendRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const account = await Account.findOne({ DisplayName: payload.friend });
 | 
				
			||||||
 | 
					    if (!account) {
 | 
				
			||||||
 | 
					        res.status(400).end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(account._id.toString(), "Settings");
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        inventory.Settings?.FriendInvRestriction == "GIFT_MODE_NONE" ||
 | 
				
			||||||
 | 
					        (inventory.Settings?.FriendInvRestriction == "GIFT_MODE_FRIENDS" &&
 | 
				
			||||||
 | 
					            !(await areFriendsOfFriends(account._id, accountId)))
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        res.status(400).send("Friend Invite Restriction");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Friendship.insertOne({
 | 
				
			||||||
 | 
					        owner: accountId,
 | 
				
			||||||
 | 
					        friend: account._id,
 | 
				
			||||||
 | 
					        Note: payload.message
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const friendInfo: IFriendInfo = {
 | 
				
			||||||
 | 
					        _id: toOid(account._id),
 | 
				
			||||||
 | 
					        DisplayName: account.DisplayName,
 | 
				
			||||||
 | 
					        LastLogin: toMongoDate(account.LastLogin),
 | 
				
			||||||
 | 
					        Note: payload.message
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    await addInventoryDataToFriendInfo(friendInfo);
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        Friend: friendInfo
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAddPendingFriendRequest {
 | 
				
			||||||
 | 
					    friend: string;
 | 
				
			||||||
 | 
					    message: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										117
									
								
								src/controllers/api/addToAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/controllers/api/addToAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString, regexEscape } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { createMessage } from "../../services/inboxService.ts";
 | 
				
			||||||
 | 
					import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportFlavour } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addToAllianceController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    // Check requester is a warlord in their guild
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
 | 
				
			||||||
 | 
					    if (guildMember.rank > 1) {
 | 
				
			||||||
 | 
					        res.status(400).json({ Error: 104 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check guild has invite permissions in the alliance
 | 
				
			||||||
 | 
					    const allianceMember = (await AllianceMember.findOne({
 | 
				
			||||||
 | 
					        allianceId: req.query.allianceId,
 | 
				
			||||||
 | 
					        guildId: guildMember.guildId
 | 
				
			||||||
 | 
					    }))!;
 | 
				
			||||||
 | 
					    if (!(allianceMember.Permissions & GuildPermission.Recruiter)) {
 | 
				
			||||||
 | 
					        res.status(400).json({ Error: 104 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Find clan to invite
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<IAddToAllianceRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const guilds = await Guild.find(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Name:
 | 
				
			||||||
 | 
					                payload.clanName.indexOf("#") == -1
 | 
				
			||||||
 | 
					                    ? new RegExp("^" + regexEscape(payload.clanName) + "#...$")
 | 
				
			||||||
 | 
					                    : payload.clanName
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "Name"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (guilds.length == 0) {
 | 
				
			||||||
 | 
					        res.status(400).json({ Error: 101 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (guilds.length > 1) {
 | 
				
			||||||
 | 
					        const choices: IGuildChoice[] = [];
 | 
				
			||||||
 | 
					        for (const guild of guilds) {
 | 
				
			||||||
 | 
					            choices.push({
 | 
				
			||||||
 | 
					                OriginalPlatform: 0,
 | 
				
			||||||
 | 
					                Name: guild.Name
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        res.json(choices);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add clan as a pending alliance member
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        await AllianceMember.insertOne({
 | 
				
			||||||
 | 
					            allianceId: req.query.allianceId,
 | 
				
			||||||
 | 
					            guildId: guilds[0]._id,
 | 
				
			||||||
 | 
					            Pending: true,
 | 
				
			||||||
 | 
					            Permissions: 0
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        logger.debug(`alliance invite failed due to ${String(e)}`);
 | 
				
			||||||
 | 
					        res.status(400).json({ Error: 102 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send inbox message to founding warlord
 | 
				
			||||||
 | 
					    // TOVERIFY: Should other warlords get this as well?
 | 
				
			||||||
 | 
					    // TOVERIFY: Who/what should the sender be?
 | 
				
			||||||
 | 
					    // TOVERIFY: Should this message be highPriority?
 | 
				
			||||||
 | 
					    const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
 | 
				
			||||||
 | 
					    const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
 | 
				
			||||||
 | 
					    const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
 | 
				
			||||||
 | 
					    const alliance = (await Alliance.findById(req.query.allianceId as string, "Name"))!;
 | 
				
			||||||
 | 
					    await createMessage(invitedClanOwnerMember.accountId, [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            sndr: getSuffixedName(account),
 | 
				
			||||||
 | 
					            msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body",
 | 
				
			||||||
 | 
					            arg: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Key: "THEIR_CLAN",
 | 
				
			||||||
 | 
					                    Tag: senderGuild.Name
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Key: "CLAN",
 | 
				
			||||||
 | 
					                    Tag: guilds[0].Name
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Key: "ALLIANCE",
 | 
				
			||||||
 | 
					                    Tag: alliance.Name
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
 | 
				
			||||||
 | 
					            icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
				
			||||||
 | 
					            contextInfo: alliance._id.toString(),
 | 
				
			||||||
 | 
					            highPriority: true,
 | 
				
			||||||
 | 
					            acceptAction: "ALLIANCE_INVITE",
 | 
				
			||||||
 | 
					            declineAction: "ALLIANCE_INVITE",
 | 
				
			||||||
 | 
					            hasAccountAction: true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAddToAllianceRequest {
 | 
				
			||||||
 | 
					    clanName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IGuildChoice {
 | 
				
			||||||
 | 
					    OriginalPlatform: number;
 | 
				
			||||||
 | 
					    Name: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								src/controllers/api/addToGuildController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/controllers/api/addToGuildController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { Account } from "../../models/loginModel.ts";
 | 
				
			||||||
 | 
					import { addInventoryDataToFriendInfo, areFriends } from "../../services/friendService.ts";
 | 
				
			||||||
 | 
					import { hasGuildPermission } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { createMessage } from "../../services/inboxService.ts";
 | 
				
			||||||
 | 
					import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IOid } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import type { IGuildMemberClient } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportFlavour } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addToGuildController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const payload = JSON.parse(String(req.body)) as IAddToGuildRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ("UserName" in payload) {
 | 
				
			||||||
 | 
					        // Clan recruiter sending an invite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const account = await Account.findOne({ DisplayName: payload.UserName });
 | 
				
			||||||
 | 
					        if (!account) {
 | 
				
			||||||
 | 
					            res.status(400).json("Username does not exist");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const senderAccount = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					        const inventory = await getInventory(account._id.toString(), "Settings");
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" ||
 | 
				
			||||||
 | 
					            (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" &&
 | 
				
			||||||
 | 
					                !(await areFriends(account._id, senderAccount._id)))
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            res.status(400).json("Invite restricted");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
 | 
				
			||||||
 | 
					        if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
 | 
				
			||||||
 | 
					            res.status(400).json("Invalid permission");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await GuildMember.insertOne({
 | 
				
			||||||
 | 
					                accountId: account._id,
 | 
				
			||||||
 | 
					                guildId: payload.GuildId.$oid,
 | 
				
			||||||
 | 
					                status: 2 // outgoing invite
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            logger.debug(`guild invite failed due to ${String(e)}`);
 | 
				
			||||||
 | 
					            res.status(400).json("User already invited to clan");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType");
 | 
				
			||||||
 | 
					        await createMessage(account._id, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                sndr: getSuffixedName(senderAccount),
 | 
				
			||||||
 | 
					                msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body",
 | 
				
			||||||
 | 
					                arg: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Key: "clan",
 | 
				
			||||||
 | 
					                        Tag: guild.Name
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
 | 
				
			||||||
 | 
					                icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
				
			||||||
 | 
					                contextInfo: payload.GuildId.$oid,
 | 
				
			||||||
 | 
					                highPriority: true,
 | 
				
			||||||
 | 
					                acceptAction: "GUILD_INVITE",
 | 
				
			||||||
 | 
					                declineAction: "GUILD_INVITE",
 | 
				
			||||||
 | 
					                hasAccountAction: true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const member: IGuildMemberClient = {
 | 
				
			||||||
 | 
					            _id: { $oid: account._id.toString() },
 | 
				
			||||||
 | 
					            DisplayName: account.DisplayName,
 | 
				
			||||||
 | 
					            LastLogin: toMongoDate(account.LastLogin),
 | 
				
			||||||
 | 
					            Rank: 7,
 | 
				
			||||||
 | 
					            Status: 2
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        await addInventoryDataToFriendInfo(member);
 | 
				
			||||||
 | 
					        res.json({ NewMember: member });
 | 
				
			||||||
 | 
					    } else if ("RequestMsg" in payload) {
 | 
				
			||||||
 | 
					        // Player applying to join a clan
 | 
				
			||||||
 | 
					        const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await GuildMember.insertOne({
 | 
				
			||||||
 | 
					                accountId,
 | 
				
			||||||
 | 
					                guildId: payload.GuildId.$oid,
 | 
				
			||||||
 | 
					                status: 1, // incoming invite
 | 
				
			||||||
 | 
					                RequestMsg: payload.RequestMsg,
 | 
				
			||||||
 | 
					                RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable.
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            logger.debug(`guild invite failed due to ${String(e)}`);
 | 
				
			||||||
 | 
					            res.status(400).send("Already requested");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        res.end();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.error(`data provided to ${req.path}: ${String(req.body)}`);
 | 
				
			||||||
 | 
					        res.status(400).end();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAddToGuildRequest {
 | 
				
			||||||
 | 
					    UserName?: string;
 | 
				
			||||||
 | 
					    GuildId: IOid;
 | 
				
			||||||
 | 
					    RequestMsg?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/controllers/api/adoptPetController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/controllers/api/adoptPetController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const adoptPetController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "KubrowPets");
 | 
				
			||||||
 | 
					    const data = getJSONfromString<IAdoptPetRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const details = inventory.KubrowPets.id(data.petId)!.Details!;
 | 
				
			||||||
 | 
					    details.Name = data.name;
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        petId: data.petId,
 | 
				
			||||||
 | 
					        newName: data.name
 | 
				
			||||||
 | 
					    } satisfies IAdoptPetResponse);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAdoptPetRequest {
 | 
				
			||||||
 | 
					    petId: string;
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IAdoptPetResponse {
 | 
				
			||||||
 | 
					    petId: string;
 | 
				
			||||||
 | 
					    newName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/controllers/api/apartmentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/controllers/api/apartmentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const apartmentController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const personalRooms = await getPersonalRooms(accountId, "Apartment");
 | 
				
			||||||
 | 
					    const response: IApartmentResponse = {};
 | 
				
			||||||
 | 
					    if (req.query.backdrop !== undefined) {
 | 
				
			||||||
 | 
					        response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (req.query.soundscape !== undefined) {
 | 
				
			||||||
 | 
					        response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await personalRooms.save();
 | 
				
			||||||
 | 
					    res.json(response);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IApartmentResponse {
 | 
				
			||||||
 | 
					    NewBackdropItem?: string;
 | 
				
			||||||
 | 
					    NewSoundscapeItem?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/controllers/api/arcaneCommonController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/controllers/api/arcaneCommonController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getInventory, addMods } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IOid } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const arcaneCommonController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const json = getJSONfromString<IArcaneCommonRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const upgrade = inventory.Upgrades.id(json.arcane.ItemId.$oid);
 | 
				
			||||||
 | 
					    if (json.newRank == -1) {
 | 
				
			||||||
 | 
					        // Break down request?
 | 
				
			||||||
 | 
					        if (!upgrade || !upgrade.UpgradeFingerprint) {
 | 
				
			||||||
 | 
					            throw new Error(`Failed to find upgrade with OID ${json.arcane.ItemId.$oid}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Remove Upgrade
 | 
				
			||||||
 | 
					        inventory.Upgrades.pull({ _id: json.arcane.ItemId.$oid });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add RawUpgrades
 | 
				
			||||||
 | 
					        const numRawUpgradesToGive = arcaneLevelCounts[(JSON.parse(upgrade.UpgradeFingerprint) as { lvl: number }).lvl];
 | 
				
			||||||
 | 
					        addMods(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: json.arcane.ItemType,
 | 
				
			||||||
 | 
					                ItemCount: numRawUpgradesToGive
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.json({ upgradeId: json.arcane.ItemId.$oid, numConsumed: numRawUpgradesToGive });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // Upgrade request?
 | 
				
			||||||
 | 
					        let numConsumed = arcaneLevelCounts[json.newRank];
 | 
				
			||||||
 | 
					        let upgradeId = json.arcane.ItemId.$oid;
 | 
				
			||||||
 | 
					        if (upgrade) {
 | 
				
			||||||
 | 
					            // Have an existing Upgrade item?
 | 
				
			||||||
 | 
					            if (upgrade.UpgradeFingerprint) {
 | 
				
			||||||
 | 
					                const existingLevel = (JSON.parse(upgrade.UpgradeFingerprint) as { lvl: number }).lvl;
 | 
				
			||||||
 | 
					                numConsumed -= arcaneLevelCounts[existingLevel];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            upgrade.UpgradeFingerprint = JSON.stringify({ lvl: json.newRank });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const newLength = inventory.Upgrades.push({
 | 
				
			||||||
 | 
					                ItemType: json.arcane.ItemType,
 | 
				
			||||||
 | 
					                UpgradeFingerprint: JSON.stringify({ lvl: json.newRank })
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            upgradeId = inventory.Upgrades[newLength - 1]._id.toString();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Remove RawUpgrades
 | 
				
			||||||
 | 
					        addMods(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: json.arcane.ItemType,
 | 
				
			||||||
 | 
					                ItemCount: numConsumed * -1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.json({ newLevel: json.newRank, numConsumed, upgradeId });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const arcaneLevelCounts = [0, 3, 6, 10, 15, 21];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IArcaneCommonRequest {
 | 
				
			||||||
 | 
					    arcane: {
 | 
				
			||||||
 | 
					        ItemType: string;
 | 
				
			||||||
 | 
					        ItemId: IOid;
 | 
				
			||||||
 | 
					        FromSKU: boolean;
 | 
				
			||||||
 | 
					        UpgradeFingerprint: string;
 | 
				
			||||||
 | 
					        PendingRerollFingerprint: string;
 | 
				
			||||||
 | 
					        ItemCount: number;
 | 
				
			||||||
 | 
					        LastAdded: IOid;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    newRank: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										51
									
								
								src/controllers/api/archonFusionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/controllers/api/archonFusionController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { colorToShard, combineColors, shardToColor } from "../../helpers/shardHelper.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const archonFusionController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IArchonFusionRequest;
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    request.Consumed.forEach(x => {
 | 
				
			||||||
 | 
					        x.ItemCount *= -1;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    addMiscItems(inventory, request.Consumed);
 | 
				
			||||||
 | 
					    const newArchons: IMiscItem[] = [];
 | 
				
			||||||
 | 
					    switch (request.FusionType) {
 | 
				
			||||||
 | 
					        case "AFT_ASCENT":
 | 
				
			||||||
 | 
					            newArchons.push({
 | 
				
			||||||
 | 
					                ItemType: request.Consumed[0].ItemType + "Mythic",
 | 
				
			||||||
 | 
					                ItemCount: 1
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "AFT_COALESCENT":
 | 
				
			||||||
 | 
					            newArchons.push({
 | 
				
			||||||
 | 
					                ItemType:
 | 
				
			||||||
 | 
					                    colorToShard[
 | 
				
			||||||
 | 
					                        combineColors(
 | 
				
			||||||
 | 
					                            shardToColor[request.Consumed[0].ItemType],
 | 
				
			||||||
 | 
					                            shardToColor[request.Consumed[1].ItemType]
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                ItemCount: 1
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					            throw new Error(`unknown archon fusion type: ${request.FusionType}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    addMiscItems(inventory, newArchons);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        NewArchons: newArchons
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IArchonFusionRequest {
 | 
				
			||||||
 | 
					    Consumed: IMiscItem[];
 | 
				
			||||||
 | 
					    FusionType: string;
 | 
				
			||||||
 | 
					    StatResultType: "SRT_NEW_STAT"; // ???
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										169
									
								
								src/controllers/api/artifactTransmutationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/controllers/api/artifactTransmutationController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,169 @@
 | 
				
			|||||||
 | 
					import { fromOid, toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "../../helpers/rivenHelper.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, addMods, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "../../services/rngService.ts";
 | 
				
			||||||
 | 
					import type { IUpgradeFromClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import type { TRarity } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { ExportBoosterPacks, ExportUpgrades } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const artifactTransmutationController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const payload = JSON.parse(String(req.body)) as IArtifactTransmutationRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inventory.RegularCredits -= payload.Cost;
 | 
				
			||||||
 | 
					    inventory.FusionPoints -= payload.FusionPointCost;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (payload.RivenTransmute) {
 | 
				
			||||||
 | 
					        addMiscItems(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientSecretItem",
 | 
				
			||||||
 | 
					                ItemCount: -1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        payload.Consumed.forEach(upgrade => {
 | 
				
			||||||
 | 
					            inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const rawRivenType = getRandomRawRivenType();
 | 
				
			||||||
 | 
					        const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
 | 
				
			||||||
 | 
					        const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const upgradeIndex =
 | 
				
			||||||
 | 
					            inventory.Upgrades.push({
 | 
				
			||||||
 | 
					                ItemType: rivenType,
 | 
				
			||||||
 | 
					                UpgradeFingerprint: JSON.stringify(fingerprint)
 | 
				
			||||||
 | 
					            }) - 1;
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            NewMods: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ItemId: toOid(inventory.Upgrades[upgradeIndex]._id),
 | 
				
			||||||
 | 
					                    ItemType: rivenType,
 | 
				
			||||||
 | 
					                    UpgradeFingerprint: fingerprint
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const counts: Record<TRarity, number> = {
 | 
				
			||||||
 | 
					            COMMON: 0,
 | 
				
			||||||
 | 
					            UNCOMMON: 0,
 | 
				
			||||||
 | 
					            RARE: 0,
 | 
				
			||||||
 | 
					            LEGENDARY: 0
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let forcedPolarity: string | undefined;
 | 
				
			||||||
 | 
					        payload.Consumed.forEach(upgrade => {
 | 
				
			||||||
 | 
					            const meta = ExportUpgrades[upgrade.ItemType];
 | 
				
			||||||
 | 
					            counts[meta.rarity] += upgrade.ItemCount;
 | 
				
			||||||
 | 
					            if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
 | 
				
			||||||
 | 
					                inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                addMods(inventory, [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        ItemType: upgrade.ItemType,
 | 
				
			||||||
 | 
					                        ItemCount: upgrade.ItemCount * -1
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
 | 
				
			||||||
 | 
					                forcedPolarity = "AP_ATTACK";
 | 
				
			||||||
 | 
					            } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
 | 
				
			||||||
 | 
					                forcedPolarity = "AP_DEFENSE";
 | 
				
			||||||
 | 
					            } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/TacticTransmuteCore") {
 | 
				
			||||||
 | 
					                forcedPolarity = "AP_TACTIC";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let newModType: string | undefined;
 | 
				
			||||||
 | 
					        for (const specialModSet of specialModSets) {
 | 
				
			||||||
 | 
					            if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
 | 
				
			||||||
 | 
					                newModType = getRandomElement(specialModSet);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!newModType) {
 | 
				
			||||||
 | 
					            // Based on the table on https://wiki.warframe.com/w/Transmutation
 | 
				
			||||||
 | 
					            const weights: Record<TRarity, number> = {
 | 
				
			||||||
 | 
					                COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
 | 
				
			||||||
 | 
					                UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
 | 
				
			||||||
 | 
					                RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
 | 
				
			||||||
 | 
					                LEGENDARY: 0
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const options: { uniqueName: string; rarity: TRarity }[] = [];
 | 
				
			||||||
 | 
					            Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
 | 
				
			||||||
 | 
					                if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
 | 
				
			||||||
 | 
					                    options.push({ uniqueName, rarity: upgrade.rarity });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            newModType = getRandomWeightedReward(options, weights)!.uniqueName;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        addMods(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: newModType,
 | 
				
			||||||
 | 
					                ItemCount: 1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            NewMods: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ItemType: newModType,
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getRandomRawRivenType = (): string => {
 | 
				
			||||||
 | 
					    const pack = ExportBoosterPacks["/Lotus/Types/BoosterPacks/CalendarRivenPack"];
 | 
				
			||||||
 | 
					    return getRandomWeightedRewardUc(pack.components, pack.rarityWeightsPerRoll[0])!.Item;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IArtifactTransmutationRequest {
 | 
				
			||||||
 | 
					    Upgrade: IUpgradeFromClient;
 | 
				
			||||||
 | 
					    LevelDiff: number;
 | 
				
			||||||
 | 
					    Consumed: IUpgradeFromClient[];
 | 
				
			||||||
 | 
					    Cost: number;
 | 
				
			||||||
 | 
					    FusionPointCost: number;
 | 
				
			||||||
 | 
					    RivenTransmute?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const specialModSets: string[][] = [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
 | 
				
			||||||
 | 
					        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
							
								
								
									
										68
									
								
								src/controllers/api/artifactsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/controllers/api/artifactsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { addMods, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const artifactsController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const artifactsData = getJSONfromString<IArtifactsRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { Upgrade, LevelDiff, Cost, FusionPointCost } = artifactsData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const { Upgrades } = inventory;
 | 
				
			||||||
 | 
					    const { ItemType, UpgradeFingerprint, ItemId } = Upgrade;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}';
 | 
				
			||||||
 | 
					    const parsedUpgradeFingerprint = JSON.parse(safeUpgradeFingerprint) as { lvl: number };
 | 
				
			||||||
 | 
					    parsedUpgradeFingerprint.lvl += LevelDiff;
 | 
				
			||||||
 | 
					    const stringifiedUpgradeFingerprint = JSON.stringify(parsedUpgradeFingerprint);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let itemIndex = Upgrades.findIndex(upgrade => upgrade._id.equals(ItemId.$oid));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (itemIndex !== -1) {
 | 
				
			||||||
 | 
					        Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        itemIndex =
 | 
				
			||||||
 | 
					            Upgrades.push({
 | 
				
			||||||
 | 
					                UpgradeFingerprint: stringifiedUpgradeFingerprint,
 | 
				
			||||||
 | 
					                ItemType
 | 
				
			||||||
 | 
					            }) - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        addMods(inventory, [{ ItemType, ItemCount: -1 }]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!inventory.infiniteCredits) {
 | 
				
			||||||
 | 
					        inventory.RegularCredits -= Cost;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!inventory.infiniteEndo) {
 | 
				
			||||||
 | 
					        inventory.FusionPoints -= FusionPointCost;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (artifactsData.LegendaryFusion) {
 | 
				
			||||||
 | 
					        addMods(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: "/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser",
 | 
				
			||||||
 | 
					                ItemCount: -1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const changedInventory = await inventory.save();
 | 
				
			||||||
 | 
					    const itemId = changedInventory.toJSON<IInventoryClient>().Upgrades[itemIndex].ItemId.$oid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!itemId) {
 | 
				
			||||||
 | 
					        throw new Error("Item Id not found in upgradeMod");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send(itemId);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IArtifactsRequest {
 | 
				
			||||||
 | 
					    Upgrade: IUpgradeClient;
 | 
				
			||||||
 | 
					    LevelDiff: number;
 | 
				
			||||||
 | 
					    Cost: number;
 | 
				
			||||||
 | 
					    FusionPointCost: number;
 | 
				
			||||||
 | 
					    LegendaryFusion?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/controllers/api/cancelGuildAdvertisementController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/controllers/api/cancelGuildAdvertisementController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { GuildAd } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId");
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) {
 | 
				
			||||||
 | 
					        res.status(400).end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await GuildAd.deleteOne({ GuildId: guild._id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										99
									
								
								src/controllers/api/changeDojoRootController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/controllers/api/changeDojoRootController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getDojoClient,
 | 
				
			||||||
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
 | 
					    hasAccessToDojo,
 | 
				
			||||||
 | 
					    hasGuildPermission
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { IDojoComponentDatabase } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const changeDojoRootController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
 | 
				
			||||||
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"}
 | 
				
			||||||
 | 
					    if (req.body) {
 | 
				
			||||||
 | 
					        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
				
			||||||
 | 
					        throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const idToNode: Record<string, INode> = {};
 | 
				
			||||||
 | 
					    guild.DojoComponents.forEach(x => {
 | 
				
			||||||
 | 
					        idToNode[x._id.toString()] = {
 | 
				
			||||||
 | 
					            component: x,
 | 
				
			||||||
 | 
					            parent: undefined,
 | 
				
			||||||
 | 
					            children: []
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let oldRoot: INode | undefined;
 | 
				
			||||||
 | 
					    guild.DojoComponents.forEach(x => {
 | 
				
			||||||
 | 
					        const node = idToNode[x._id.toString()];
 | 
				
			||||||
 | 
					        if (x.pi) {
 | 
				
			||||||
 | 
					            idToNode[x.pi.toString()].children.push(node);
 | 
				
			||||||
 | 
					            node.parent = idToNode[x.pi.toString()];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            oldRoot = node;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    logger.debug("Old tree:\n" + treeToString(oldRoot!));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newRoot = idToNode[req.query.newRoot as string];
 | 
				
			||||||
 | 
					    recursivelyTurnParentsIntoChildren(newRoot);
 | 
				
			||||||
 | 
					    newRoot.component.pi = undefined;
 | 
				
			||||||
 | 
					    newRoot.component.op = undefined;
 | 
				
			||||||
 | 
					    newRoot.component.pp = undefined;
 | 
				
			||||||
 | 
					    newRoot.parent = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Set/update SortId in top-to-bottom order
 | 
				
			||||||
 | 
					    const stack: INode[] = [newRoot];
 | 
				
			||||||
 | 
					    while (stack.length != 0) {
 | 
				
			||||||
 | 
					        const top = stack.shift()!;
 | 
				
			||||||
 | 
					        top.component.SortId = new Types.ObjectId();
 | 
				
			||||||
 | 
					        top.children.forEach(x => stack.push(x));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.debug("New tree:\n" + treeToString(newRoot));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(await getDojoClient(guild, 0));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface INode {
 | 
				
			||||||
 | 
					    component: IDojoComponentDatabase;
 | 
				
			||||||
 | 
					    parent: INode | undefined;
 | 
				
			||||||
 | 
					    children: INode[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const treeToString = (root: INode, depth: number = 0): string => {
 | 
				
			||||||
 | 
					    let str = " ".repeat(depth * 4) + root.component.pf + " (" + root.component._id.toString() + ")\n";
 | 
				
			||||||
 | 
					    root.children.forEach(x => {
 | 
				
			||||||
 | 
					        str += treeToString(x, depth + 1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return str;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const recursivelyTurnParentsIntoChildren = (node: INode): void => {
 | 
				
			||||||
 | 
					    if (node.parent!.parent) {
 | 
				
			||||||
 | 
					        recursivelyTurnParentsIntoChildren(node.parent!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    node.parent!.component.pi = node.component._id;
 | 
				
			||||||
 | 
					    node.parent!.component.op = node.component.pp;
 | 
				
			||||||
 | 
					    node.parent!.component.pp = node.component.op;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    node.parent!.parent = node;
 | 
				
			||||||
 | 
					    node.parent!.children.splice(node.parent!.children.indexOf(node), 1);
 | 
				
			||||||
 | 
					    node.children.push(node.parent!);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/controllers/api/changeGuildRankController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/controllers/api/changeGuildRankController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					import { GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getGuildForRequest, hasGuildPermissionEx } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const changeGuildRankController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const member = (await GuildMember.findOne({
 | 
				
			||||||
 | 
					        accountId: accountId,
 | 
				
			||||||
 | 
					        guildId: req.query.guildId as string
 | 
				
			||||||
 | 
					    }))!;
 | 
				
			||||||
 | 
					    const newRank: number = parseInt(req.query.rankChange as string);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequest(req);
 | 
				
			||||||
 | 
					    if (newRank < member.rank || !hasGuildPermissionEx(guild, member, GuildPermission.Promoter)) {
 | 
				
			||||||
 | 
					        res.status(400).json("Invalid permission");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const target = (await GuildMember.findOne({
 | 
				
			||||||
 | 
					        guildId: req.query.guildId as string,
 | 
				
			||||||
 | 
					        accountId: req.query.targetId as string
 | 
				
			||||||
 | 
					    }))!;
 | 
				
			||||||
 | 
					    target.rank = parseInt(req.query.rankChange as string);
 | 
				
			||||||
 | 
					    await target.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newRank == 0) {
 | 
				
			||||||
 | 
					        // If we just promoted someone else to Founding Warlord, we need to demote ourselves to Warlord.
 | 
				
			||||||
 | 
					        member.rank = 1;
 | 
				
			||||||
 | 
					        await member.save();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        _id: req.query.targetId as string,
 | 
				
			||||||
 | 
					        Rank: newRank
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/controllers/api/checkDailyMissionBonusController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/controllers/api/checkDailyMissionBonusController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const checkDailyMissionBonusController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const today = Math.trunc(Date.now() / 86400000) * 86400;
 | 
				
			||||||
 | 
					    if (account.DailyFirstWinDate != today) {
 | 
				
			||||||
 | 
					        res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n");
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										307
									
								
								src/controllers/api/claimCompletedRecipeController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								src/controllers/api/claimCompletedRecipeController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,307 @@
 | 
				
			|||||||
 | 
					//this is a controller for the claimCompletedRecipe route
 | 
				
			||||||
 | 
					//it will claim a recipe for the user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import { getRecipe } from "../../services/itemDataService.ts";
 | 
				
			||||||
 | 
					import type { IOidWithLegacySupport } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type { TAccountDocument } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getInventory,
 | 
				
			||||||
 | 
					    updateCurrency,
 | 
				
			||||||
 | 
					    addItem,
 | 
				
			||||||
 | 
					    addRecipes,
 | 
				
			||||||
 | 
					    occupySlot,
 | 
				
			||||||
 | 
					    combineInventoryChanges,
 | 
				
			||||||
 | 
					    addKubrowPetPrint,
 | 
				
			||||||
 | 
					    addPowerSuit,
 | 
				
			||||||
 | 
					    addEquipment
 | 
				
			||||||
 | 
					} from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
				
			||||||
 | 
					import type { IRecipe } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
 | 
				
			||||||
 | 
					import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IClaimCompletedRecipeRequest {
 | 
				
			||||||
 | 
					    RecipeIds: IOidWithLegacySupport[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IClaimCompletedRecipeResponse {
 | 
				
			||||||
 | 
					    InventoryChanges: IInventoryChanges;
 | 
				
			||||||
 | 
					    BrandedSuits?: IOidWithLegacySupport[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(account._id.toString());
 | 
				
			||||||
 | 
					    const resp: IClaimCompletedRecipeResponse = {
 | 
				
			||||||
 | 
					        InventoryChanges: {}
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
 | 
				
			||||||
 | 
					        const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
 | 
				
			||||||
 | 
					        if (!pendingRecipe) {
 | 
				
			||||||
 | 
					            throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        //check recipe is indeed ready to be completed
 | 
				
			||||||
 | 
					        // if (pendingRecipe.CompletionDate > new Date()) {
 | 
				
			||||||
 | 
					        //     throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
 | 
				
			||||||
 | 
					        // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inventory.PendingRecipes.pull(pendingRecipe._id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const recipe = getRecipe(pendingRecipe.ItemType);
 | 
				
			||||||
 | 
					        if (!recipe) {
 | 
				
			||||||
 | 
					            throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (req.query.cancel) {
 | 
				
			||||||
 | 
					            const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					            await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json(resp);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const claimCompletedRecipe = async (
 | 
				
			||||||
 | 
					    account: TAccountDocument,
 | 
				
			||||||
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
 | 
					    recipe: IRecipe,
 | 
				
			||||||
 | 
					    pendingRecipe: IPendingRecipeDatabase,
 | 
				
			||||||
 | 
					    resp: IClaimCompletedRecipeResponse,
 | 
				
			||||||
 | 
					    rush: any
 | 
				
			||||||
 | 
					): Promise<void> => {
 | 
				
			||||||
 | 
					    logger.debug("Claiming Recipe", { recipe, pendingRecipe });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
 | 
				
			||||||
 | 
					        inventory.PendingSpectreLoadouts ??= [];
 | 
				
			||||||
 | 
					        inventory.SpectreLoadouts ??= [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
 | 
				
			||||||
 | 
					        if (pendingLoadoutIndex != -1) {
 | 
				
			||||||
 | 
					            const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
 | 
				
			||||||
 | 
					            if (loadoutIndex != -1) {
 | 
				
			||||||
 | 
					                inventory.SpectreLoadouts.splice(loadoutIndex, 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            logger.debug(
 | 
				
			||||||
 | 
					                "moving spectre loadout from pending to active",
 | 
				
			||||||
 | 
					                inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
 | 
				
			||||||
 | 
					            inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
 | 
				
			||||||
 | 
					        inventory.BrandedSuits!.splice(
 | 
				
			||||||
 | 
					            inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
 | 
				
			||||||
 | 
					            1
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        resp.BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (recipe.consumeOnUse) {
 | 
				
			||||||
 | 
					        addRecipes(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: pendingRecipe.ItemType,
 | 
				
			||||||
 | 
					                ItemCount: -1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (rush) {
 | 
				
			||||||
 | 
					        const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
 | 
				
			||||||
 | 
					        const start = end - recipe.buildTime;
 | 
				
			||||||
 | 
					        const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
 | 
				
			||||||
 | 
					        const progress = secondsElapsed / recipe.buildTime;
 | 
				
			||||||
 | 
					        logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
 | 
				
			||||||
 | 
					        const cost =
 | 
				
			||||||
 | 
					            progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
 | 
				
			||||||
 | 
					        combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
 | 
				
			||||||
 | 
					        const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
				
			||||||
 | 
					        if (pet.Details!.HatchDate!.getTime() > Date.now()) {
 | 
				
			||||||
 | 
					            pet.Details!.HatchDate = new Date();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let canSetActive = true;
 | 
				
			||||||
 | 
					        for (const pet of inventory.KubrowPets) {
 | 
				
			||||||
 | 
					            if (pet.Details!.Status == Status.StatusAvailable) {
 | 
				
			||||||
 | 
					                canSetActive = false;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
 | 
				
			||||||
 | 
					    } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
 | 
				
			||||||
 | 
					        const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
				
			||||||
 | 
					        addKubrowPetPrint(inventory, pet, resp.InventoryChanges);
 | 
				
			||||||
 | 
					    } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
				
			||||||
 | 
					        if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
 | 
				
			||||||
 | 
					            // Quite the special case here...
 | 
				
			||||||
 | 
					            // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
 | 
				
			||||||
 | 
					            // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const umbraModA = (
 | 
				
			||||||
 | 
					                await addItem(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
 | 
				
			||||||
 | 
					                    1,
 | 
				
			||||||
 | 
					                    false,
 | 
				
			||||||
 | 
					                    undefined,
 | 
				
			||||||
 | 
					                    `{"lvl":5}`
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ).Upgrades![0];
 | 
				
			||||||
 | 
					            const umbraModB = (
 | 
				
			||||||
 | 
					                await addItem(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
 | 
				
			||||||
 | 
					                    1,
 | 
				
			||||||
 | 
					                    false,
 | 
				
			||||||
 | 
					                    undefined,
 | 
				
			||||||
 | 
					                    `{"lvl":5}`
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ).Upgrades![0];
 | 
				
			||||||
 | 
					            const umbraModC = (
 | 
				
			||||||
 | 
					                await addItem(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
 | 
				
			||||||
 | 
					                    1,
 | 
				
			||||||
 | 
					                    false,
 | 
				
			||||||
 | 
					                    undefined,
 | 
				
			||||||
 | 
					                    `{"lvl":5}`
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ).Upgrades![0];
 | 
				
			||||||
 | 
					            const sacrificeModA = (
 | 
				
			||||||
 | 
					                await addItem(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
 | 
				
			||||||
 | 
					                    1,
 | 
				
			||||||
 | 
					                    false,
 | 
				
			||||||
 | 
					                    undefined,
 | 
				
			||||||
 | 
					                    `{"lvl":5}`
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ).Upgrades![0];
 | 
				
			||||||
 | 
					            const sacrificeModB = (
 | 
				
			||||||
 | 
					                await addItem(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
 | 
				
			||||||
 | 
					                    1,
 | 
				
			||||||
 | 
					                    false,
 | 
				
			||||||
 | 
					                    undefined,
 | 
				
			||||||
 | 
					                    `{"lvl":5}`
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ).Upgrades![0];
 | 
				
			||||||
 | 
					            resp.InventoryChanges.Upgrades ??= [];
 | 
				
			||||||
 | 
					            resp.InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await addPowerSuit(
 | 
				
			||||||
 | 
					                inventory,
 | 
				
			||||||
 | 
					                "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Configs: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            Upgrades: [
 | 
				
			||||||
 | 
					                                "",
 | 
				
			||||||
 | 
					                                "",
 | 
				
			||||||
 | 
					                                "",
 | 
				
			||||||
 | 
					                                "",
 | 
				
			||||||
 | 
					                                "",
 | 
				
			||||||
 | 
					                                umbraModA.ItemId.$oid,
 | 
				
			||||||
 | 
					                                umbraModB.ItemId.$oid,
 | 
				
			||||||
 | 
					                                umbraModC.ItemId.$oid
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    XP: 900_000,
 | 
				
			||||||
 | 
					                    Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                resp.InventoryChanges
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            inventory.XPInfo.push({
 | 
				
			||||||
 | 
					                ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
				
			||||||
 | 
					                XP: 900_000
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            addEquipment(
 | 
				
			||||||
 | 
					                inventory,
 | 
				
			||||||
 | 
					                "Melee",
 | 
				
			||||||
 | 
					                "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Configs: [
 | 
				
			||||||
 | 
					                        { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    XP: 450_000,
 | 
				
			||||||
 | 
					                    Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                resp.InventoryChanges
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            inventory.XPInfo.push({
 | 
				
			||||||
 | 
					                ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
				
			||||||
 | 
					                XP: 450_000
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            combineInventoryChanges(
 | 
				
			||||||
 | 
					                resp.InventoryChanges,
 | 
				
			||||||
 | 
					                await addItem(
 | 
				
			||||||
 | 
					                    inventory,
 | 
				
			||||||
 | 
					                    recipe.resultType,
 | 
				
			||||||
 | 
					                    recipe.num,
 | 
				
			||||||
 | 
					                    false,
 | 
				
			||||||
 | 
					                    undefined,
 | 
				
			||||||
 | 
					                    pendingRecipe.TargetFingerprint
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        inventory.claimingBlueprintRefundsIngredients &&
 | 
				
			||||||
 | 
					        recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        await refundRecipeIngredients(inventory, resp.InventoryChanges, recipe, pendingRecipe);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const refundRecipeIngredients = async (
 | 
				
			||||||
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
 | 
					    inventoryChanges: IInventoryChanges,
 | 
				
			||||||
 | 
					    recipe: IRecipe,
 | 
				
			||||||
 | 
					    pendingRecipe: IPendingRecipeDatabase
 | 
				
			||||||
 | 
					): Promise<void> => {
 | 
				
			||||||
 | 
					    updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const equipmentIngredients = new Set();
 | 
				
			||||||
 | 
					    for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
 | 
				
			||||||
 | 
					        if (pendingRecipe[category]) {
 | 
				
			||||||
 | 
					            pendingRecipe[category].forEach(item => {
 | 
				
			||||||
 | 
					                const index = inventory[category].push(item) - 1;
 | 
				
			||||||
 | 
					                inventoryChanges[category] ??= [];
 | 
				
			||||||
 | 
					                inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
 | 
				
			||||||
 | 
					                equipmentIngredients.add(item.ItemType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                occupySlot(inventory, InventorySlot.WEAPONS, false);
 | 
				
			||||||
 | 
					                inventoryChanges.WeaponBin ??= { Slots: 0 };
 | 
				
			||||||
 | 
					                inventoryChanges.WeaponBin.Slots -= 1;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const ingredient of recipe.ingredients) {
 | 
				
			||||||
 | 
					        if (!equipmentIngredients.has(ingredient.ItemType)) {
 | 
				
			||||||
 | 
					            combineInventoryChanges(
 | 
				
			||||||
 | 
					                inventoryChanges,
 | 
				
			||||||
 | 
					                await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { combineInventoryChanges, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportChallenges } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const claimJunctionChallengeRewardController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const data = getJSONfromString<IClaimJunctionChallengeRewardRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const challengeProgress = inventory.ChallengeProgress.find(x => x.Name == data.Challenge)!;
 | 
				
			||||||
 | 
					    if (challengeProgress.ReceivedJunctionReward) {
 | 
				
			||||||
 | 
					        throw new Error(`attempt to double-claim junction reward`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    challengeProgress.ReceivedJunctionReward = true;
 | 
				
			||||||
 | 
					    inventory.ClaimedJunctionChallengeRewards ??= [];
 | 
				
			||||||
 | 
					    inventory.ClaimedJunctionChallengeRewards.push(data.Challenge);
 | 
				
			||||||
 | 
					    const challengeMeta = Object.entries(ExportChallenges).find(arr => arr[0].endsWith("/" + data.Challenge))![1];
 | 
				
			||||||
 | 
					    const inventoryChanges = {};
 | 
				
			||||||
 | 
					    for (const reward of challengeMeta.countedRewards!) {
 | 
				
			||||||
 | 
					        combineInventoryChanges(
 | 
				
			||||||
 | 
					            inventoryChanges,
 | 
				
			||||||
 | 
					            (await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount)).InventoryChanges
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        inventoryChanges: inventoryChanges // Yeah, it's "inventoryChanges" in the response here.
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IClaimJunctionChallengeRewardRequest {
 | 
				
			||||||
 | 
					    Challenge: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/controllers/api/claimLibraryDailyTaskRewardController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/controllers/api/claimLibraryDailyTaskRewardController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rewardQuantity = inventory.LibraryActiveDailyTaskInfo!.RewardQuantity;
 | 
				
			||||||
 | 
					    const rewardStanding = inventory.LibraryActiveDailyTaskInfo!.RewardStanding;
 | 
				
			||||||
 | 
					    inventory.LibraryActiveDailyTaskInfo = undefined;
 | 
				
			||||||
 | 
					    inventory.LibraryAvailableDailyTaskInfo = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let syndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate");
 | 
				
			||||||
 | 
					    if (!syndicate) {
 | 
				
			||||||
 | 
					        syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: "LibrarySyndicate", Standing: 0 }) - 1];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    syndicate.Standing += rewardStanding;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    addFusionPoints(inventory, 80 * rewardQuantity);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        RewardItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle",
 | 
				
			||||||
 | 
					        RewardQuantity: rewardQuantity,
 | 
				
			||||||
 | 
					        StandingAwarded: rewardStanding,
 | 
				
			||||||
 | 
					        InventoryChanges: {
 | 
				
			||||||
 | 
					            FusionPoints: 80 * rewardQuantity
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/controllers/api/clearDialogueHistoryController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/controllers/api/clearDialogueHistoryController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clearDialogueHistoryController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
 | 
				
			||||||
 | 
					    if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
 | 
				
			||||||
 | 
					        inventory.DialogueHistory.Resets ??= 0;
 | 
				
			||||||
 | 
					        inventory.DialogueHistory.Resets += 1;
 | 
				
			||||||
 | 
					        for (const dialogueName of request.Dialogues) {
 | 
				
			||||||
 | 
					            const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
 | 
				
			||||||
 | 
					            if (index != -1) {
 | 
				
			||||||
 | 
					                inventory.DialogueHistory.Dialogues.splice(index, 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IClearDialogueRequest {
 | 
				
			||||||
 | 
					    Dialogues: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/controllers/api/clearNewEpisodeRewardController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/controllers/api/clearNewEpisodeRewardController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// example req.body: {"NewEpisodeReward":true,"crossPlaySetting":"ENABLED"}
 | 
				
			||||||
 | 
					export const clearNewEpisodeRewardController: RequestHandler = (_req, res) => {
 | 
				
			||||||
 | 
					    res.status(200).end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/controllers/api/completeCalendarEventController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/controllers/api/completeCalendarEventController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { checkCalendarAutoAdvance, getCalendarProgress, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
				
			||||||
 | 
					import { getWorldState } from "../../services/worldStateService.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER
 | 
				
			||||||
 | 
					export const completeCalendarEventController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const calendarProgress = getCalendarProgress(inventory);
 | 
				
			||||||
 | 
					    const currentSeason = getWorldState().KnownCalendarSeasons[0];
 | 
				
			||||||
 | 
					    let inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					    const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
 | 
				
			||||||
 | 
					    const day = currentSeason.Days[dayIndex];
 | 
				
			||||||
 | 
					    if (day.events.length != 0) {
 | 
				
			||||||
 | 
					        if (day.events[0].type == "CET_CHALLENGE") {
 | 
				
			||||||
 | 
					            throw new Error(`completeCalendarEvent should not be used for challenges`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
 | 
				
			||||||
 | 
					        if (selection.type == "CET_REWARD") {
 | 
				
			||||||
 | 
					            inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
 | 
				
			||||||
 | 
					        } else if (selection.type == "CET_UPGRADE") {
 | 
				
			||||||
 | 
					            calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
 | 
				
			||||||
 | 
					        } else if (selection.type != "CET_PLOT") {
 | 
				
			||||||
 | 
					            throw new Error(`unexpected selection type: ${selection.type}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
 | 
				
			||||||
 | 
					    checkCalendarAutoAdvance(inventory, currentSeason);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges,
 | 
				
			||||||
 | 
					        CalendarProgress: inventory.CalendarProgress
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/controllers/api/completeRandomModChallengeController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/controllers/api/completeRandomModChallengeController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type { IVeiledRivenFingerprint } from "../../helpers/rivenHelper.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const request = getJSONfromString<ICompleteRandomModChallengeRequest>(String(req.body));
 | 
				
			||||||
 | 
					    let inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove 20 plat or riven cipher
 | 
				
			||||||
 | 
					    if ((req.query.p as string) == "1") {
 | 
				
			||||||
 | 
					        inventoryChanges = { ...updateCurrency(inventory, 20, true) };
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const miscItemChanges: IMiscItem[] = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: "/Lotus/Types/Items/MiscItems/RivenIdentifier",
 | 
				
			||||||
 | 
					                ItemCount: -1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					        inventoryChanges.MiscItems = miscItemChanges;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Complete the riven challenge
 | 
				
			||||||
 | 
					    const upgrade = inventory.Upgrades.id(request.ItemId)!;
 | 
				
			||||||
 | 
					    const fp = JSON.parse(upgrade.UpgradeFingerprint!) as IVeiledRivenFingerprint;
 | 
				
			||||||
 | 
					    fp.challenge.Progress = fp.challenge.Required;
 | 
				
			||||||
 | 
					    upgrade.UpgradeFingerprint = JSON.stringify(fp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges,
 | 
				
			||||||
 | 
					        Fingerprint: upgrade.UpgradeFingerprint
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICompleteRandomModChallengeRequest {
 | 
				
			||||||
 | 
					    ItemId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/controllers/api/confirmAllianceInvitationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/controllers/api/confirmAllianceInvitationController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAllianceClient } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const confirmAllianceInvitationController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    // Check requester is a warlord in their guild
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
 | 
				
			||||||
 | 
					    if (guildMember.rank > 1) {
 | 
				
			||||||
 | 
					        res.status(400).json({ Error: 104 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const allianceMember = await AllianceMember.findOne({
 | 
				
			||||||
 | 
					        allianceId: req.query.allianceId,
 | 
				
			||||||
 | 
					        guildId: guildMember.guildId
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (!allianceMember || !allianceMember.Pending) {
 | 
				
			||||||
 | 
					        res.status(400);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    allianceMember.Pending = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const guild = (await Guild.findById(guildMember.guildId))!;
 | 
				
			||||||
 | 
					    guild.AllianceId = allianceMember.allianceId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all([allianceMember.save(), guild.save()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Give client the new alliance data which uses "AllianceId" instead of "_id" in this response
 | 
				
			||||||
 | 
					    const alliance = (await Alliance.findById(allianceMember.allianceId))!;
 | 
				
			||||||
 | 
					    const { _id, ...rest } = await getAllianceClient(alliance, guild);
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        AllianceId: _id,
 | 
				
			||||||
 | 
					        ...rest
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										121
									
								
								src/controllers/api/confirmGuildInvitationController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/controllers/api/confirmGuildInvitationController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { Account } from "../../models/loginModel.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    deleteGuild,
 | 
				
			||||||
 | 
					    getGuildClient,
 | 
				
			||||||
 | 
					    giveClanKey,
 | 
				
			||||||
 | 
					    hasGuildPermission,
 | 
				
			||||||
 | 
					    removeDojoKeyItems
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GET request: A player accepting an invite they got in their inbox.
 | 
				
			||||||
 | 
					export const confirmGuildInvitationGetController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const invitedGuildMember = await GuildMember.findOne({
 | 
				
			||||||
 | 
					        accountId: account._id,
 | 
				
			||||||
 | 
					        guildId: req.query.clanId as string
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (invitedGuildMember && invitedGuildMember.status == 2) {
 | 
				
			||||||
 | 
					        let inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // If this account is already in a guild, we need to do cleanup first.
 | 
				
			||||||
 | 
					        const guildMember = await GuildMember.findOneAndDelete({ accountId: account._id, status: 0 });
 | 
				
			||||||
 | 
					        if (guildMember) {
 | 
				
			||||||
 | 
					            const inventory = await getInventory(account._id.toString(), "LevelKeys Recipes");
 | 
				
			||||||
 | 
					            inventoryChanges = removeDojoKeyItems(inventory);
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (guildMember.rank == 0) {
 | 
				
			||||||
 | 
					                await deleteGuild(guildMember.guildId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates.
 | 
				
			||||||
 | 
					        invitedGuildMember.status = 0;
 | 
				
			||||||
 | 
					        await invitedGuildMember.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Remove pending applications for this account
 | 
				
			||||||
 | 
					        await GuildMember.deleteMany({ accountId: account._id, status: 1 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update inventory of new member
 | 
				
			||||||
 | 
					        const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
				
			||||||
 | 
					        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
				
			||||||
 | 
					        giveClanKey(inventory, inventoryChanges);
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const guild = (await Guild.findById(req.query.clanId as string))!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add join to clan log
 | 
				
			||||||
 | 
					        guild.RosterActivity ??= [];
 | 
				
			||||||
 | 
					        guild.RosterActivity.push({
 | 
				
			||||||
 | 
					            dateTime: new Date(),
 | 
				
			||||||
 | 
					            entryType: 6,
 | 
				
			||||||
 | 
					            details: getSuffixedName(account)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await guild.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            ...(await getGuildClient(guild, account)),
 | 
				
			||||||
 | 
					            InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        res.end();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// POST request: Clan representative accepting invite(s).
 | 
				
			||||||
 | 
					export const confirmGuildInvitationPostController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!;
 | 
				
			||||||
 | 
					    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) {
 | 
				
			||||||
 | 
					        res.status(400).json("Invalid permission");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<{ userId: string }>(String(req.body));
 | 
				
			||||||
 | 
					    const filter: { accountId?: string; status: number } = { status: 1 };
 | 
				
			||||||
 | 
					    if (payload.userId != "all") {
 | 
				
			||||||
 | 
					        filter.accountId = payload.userId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const guildMembers = await GuildMember.find(filter);
 | 
				
			||||||
 | 
					    const newMembers: string[] = [];
 | 
				
			||||||
 | 
					    for (const guildMember of guildMembers) {
 | 
				
			||||||
 | 
					        guildMember.status = 0;
 | 
				
			||||||
 | 
					        guildMember.RequestMsg = undefined;
 | 
				
			||||||
 | 
					        guildMember.RequestExpiry = undefined;
 | 
				
			||||||
 | 
					        await guildMember.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Remove other pending applications for this account
 | 
				
			||||||
 | 
					        await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Update inventory of new member
 | 
				
			||||||
 | 
					        const inventory = await getInventory(
 | 
				
			||||||
 | 
					            guildMember.accountId.toString(),
 | 
				
			||||||
 | 
					            "GuildId LevelKeys Recipes skipClanKeyCrafting"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
				
			||||||
 | 
					        giveClanKey(inventory);
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add join to clan log
 | 
				
			||||||
 | 
					        const account = (await Account.findOne({ _id: guildMember.accountId }))!;
 | 
				
			||||||
 | 
					        guild.RosterActivity ??= [];
 | 
				
			||||||
 | 
					        guild.RosterActivity.push({
 | 
				
			||||||
 | 
					            dateTime: new Date(),
 | 
				
			||||||
 | 
					            entryType: 6,
 | 
				
			||||||
 | 
					            details: getSuffixedName(account)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        newMembers.push(account._id.toString());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        NewMembers: newMembers
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										67
									
								
								src/controllers/api/contributeGuildClassController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/controllers/api/contributeGuildClassController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { checkClanAscensionHasRequiredContributors } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const contributeGuildClassController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<IContributeGuildClassRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const guild = (await Guild.findById(payload.GuildId))!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // First contributor initiates ceremony and locks the pending class.
 | 
				
			||||||
 | 
					    if (!guild.CeremonyContributors) {
 | 
				
			||||||
 | 
					        guild.CeremonyContributors = [];
 | 
				
			||||||
 | 
					        guild.CeremonyClass = guildXpToClass(guild.XP);
 | 
				
			||||||
 | 
					        guild.CeremonyEndo = 0;
 | 
				
			||||||
 | 
					        for (let i = guild.Class; i != guild.CeremonyClass; ++i) {
 | 
				
			||||||
 | 
					            guild.CeremonyEndo += (i + 1) * 1000;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        guild.ClassChanges ??= [];
 | 
				
			||||||
 | 
					        guild.ClassChanges.push({
 | 
				
			||||||
 | 
					            dateTime: new Date(),
 | 
				
			||||||
 | 
					            entryType: 13,
 | 
				
			||||||
 | 
					            details: guild.CeremonyClass
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild.CeremonyContributors.push(new Types.ObjectId(accountId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await checkClanAscensionHasRequiredContributors(guild);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Either way, endo is given to the contributor.
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "FusionPoints");
 | 
				
			||||||
 | 
					    addFusionPoints(inventory, guild.CeremonyEndo!);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        NumContributors: guild.CeremonyContributors.length,
 | 
				
			||||||
 | 
					        FusionPointReward: guild.CeremonyEndo,
 | 
				
			||||||
 | 
					        Class: guild.Class,
 | 
				
			||||||
 | 
					        CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IContributeGuildClassRequest {
 | 
				
			||||||
 | 
					    GuildId: string;
 | 
				
			||||||
 | 
					    RequiredContributors: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const guildXpToClass = (xp: number): number => {
 | 
				
			||||||
 | 
					    const cummXp = [
 | 
				
			||||||
 | 
					        0, 11000, 34000, 69000, 114000, 168000, 231000, 302000, 381000, 68000, 563000, 665000, 774000, 891000
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    let highest = 0;
 | 
				
			||||||
 | 
					    for (let i = 0; i != cummXp.length; ++i) {
 | 
				
			||||||
 | 
					        if (xp < cummXp[i]) {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        highest = i;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return highest;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										170
									
								
								src/controllers/api/contributeToDojoComponentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/controllers/api/contributeToDojoComponentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,170 @@
 | 
				
			|||||||
 | 
					import type { TGuildDatabaseDocument } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    addGuildMemberMiscItemContribution,
 | 
				
			||||||
 | 
					    getDojoClient,
 | 
				
			||||||
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
 | 
					    hasAccessToDojo,
 | 
				
			||||||
 | 
					    processDojoBuildMaterialsGathered,
 | 
				
			||||||
 | 
					    scaleRequiredCount,
 | 
				
			||||||
 | 
					    setDojoRoomLogFunded
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IDojoContributable, IGuildMemberDatabase } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import type { IDojoBuild } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IContributeToDojoComponentRequest {
 | 
				
			||||||
 | 
					    ComponentId: string;
 | 
				
			||||||
 | 
					    DecoId?: string;
 | 
				
			||||||
 | 
					    DecoType?: string;
 | 
				
			||||||
 | 
					    IngredientContributions: IMiscItem[];
 | 
				
			||||||
 | 
					    RegularCredits: number;
 | 
				
			||||||
 | 
					    VaultIngredientContributions: IMiscItem[];
 | 
				
			||||||
 | 
					    VaultCredits: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const contributeToDojoComponentController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    // Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in.
 | 
				
			||||||
 | 
					    if (!hasAccessToDojo(inventory)) {
 | 
				
			||||||
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne(
 | 
				
			||||||
 | 
					        { accountId, guildId: guild._id },
 | 
				
			||||||
 | 
					        "RegularCreditsContributed MiscItemsContributed"
 | 
				
			||||||
 | 
					    ))!;
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest;
 | 
				
			||||||
 | 
					    const component = guild.DojoComponents.id(request.ComponentId)!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					    if (!component.CompletionTime) {
 | 
				
			||||||
 | 
					        // Room is in "Collecting Materials" state
 | 
				
			||||||
 | 
					        if (request.DecoId) {
 | 
				
			||||||
 | 
					            throw new Error("attempt to contribute to a deco in an unfinished room?!");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
 | 
				
			||||||
 | 
					        processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, component);
 | 
				
			||||||
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
				
			||||||
 | 
					        if (component.CompletionTime) {
 | 
				
			||||||
 | 
					            setDojoRoomLogFunded(guild, component);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // Room is past "Collecting Materials"
 | 
				
			||||||
 | 
					        if (request.DecoId) {
 | 
				
			||||||
 | 
					            const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
 | 
				
			||||||
 | 
					            const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
 | 
				
			||||||
 | 
					            processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, deco);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        ...(await getDojoClient(guild, 0, component._id)),
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const processContribution = (
 | 
				
			||||||
 | 
					    guild: TGuildDatabaseDocument,
 | 
				
			||||||
 | 
					    guildMember: IGuildMemberDatabase,
 | 
				
			||||||
 | 
					    request: IContributeToDojoComponentRequest,
 | 
				
			||||||
 | 
					    inventory: TInventoryDatabaseDocument,
 | 
				
			||||||
 | 
					    inventoryChanges: IInventoryChanges,
 | 
				
			||||||
 | 
					    meta: IDojoBuild,
 | 
				
			||||||
 | 
					    component: IDojoContributable
 | 
				
			||||||
 | 
					): void => {
 | 
				
			||||||
 | 
					    component.RegularCredits ??= 0;
 | 
				
			||||||
 | 
					    if (request.RegularCredits) {
 | 
				
			||||||
 | 
					        component.RegularCredits += request.RegularCredits;
 | 
				
			||||||
 | 
					        inventoryChanges.RegularCredits = -request.RegularCredits;
 | 
				
			||||||
 | 
					        updateCurrency(inventory, request.RegularCredits, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        guildMember.RegularCreditsContributed ??= 0;
 | 
				
			||||||
 | 
					        guildMember.RegularCreditsContributed += request.RegularCredits;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (request.VaultCredits) {
 | 
				
			||||||
 | 
					        component.RegularCredits += request.VaultCredits;
 | 
				
			||||||
 | 
					        guild.VaultRegularCredits! -= request.VaultCredits;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) {
 | 
				
			||||||
 | 
					        guild.VaultRegularCredits ??= 0;
 | 
				
			||||||
 | 
					        guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price);
 | 
				
			||||||
 | 
					        component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    component.MiscItems ??= [];
 | 
				
			||||||
 | 
					    if (request.VaultIngredientContributions.length) {
 | 
				
			||||||
 | 
					        for (const ingredientContribution of request.VaultIngredientContributions) {
 | 
				
			||||||
 | 
					            const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType);
 | 
				
			||||||
 | 
					            if (componentMiscItem) {
 | 
				
			||||||
 | 
					                const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    componentMiscItem.ItemCount + ingredientContribution.ItemCount >
 | 
				
			||||||
 | 
					                    scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    ingredientContribution.ItemCount =
 | 
				
			||||||
 | 
					                        scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                componentMiscItem.ItemCount += ingredientContribution.ItemCount;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                component.MiscItems.push(ingredientContribution);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == ingredientContribution.ItemType)!;
 | 
				
			||||||
 | 
					            vaultMiscItem.ItemCount -= ingredientContribution.ItemCount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (request.IngredientContributions.length) {
 | 
				
			||||||
 | 
					        const miscItemChanges: IMiscItem[] = [];
 | 
				
			||||||
 | 
					        for (const ingredientContribution of request.IngredientContributions) {
 | 
				
			||||||
 | 
					            const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType);
 | 
				
			||||||
 | 
					            if (componentMiscItem) {
 | 
				
			||||||
 | 
					                const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    componentMiscItem.ItemCount + ingredientContribution.ItemCount >
 | 
				
			||||||
 | 
					                    scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    ingredientContribution.ItemCount =
 | 
				
			||||||
 | 
					                        scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                componentMiscItem.ItemCount += ingredientContribution.ItemCount;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                component.MiscItems.push(ingredientContribution);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            miscItemChanges.push({
 | 
				
			||||||
 | 
					                ItemType: ingredientContribution.ItemType,
 | 
				
			||||||
 | 
					                ItemCount: ingredientContribution.ItemCount * -1
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					        inventoryChanges.MiscItems = miscItemChanges;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
 | 
				
			||||||
 | 
					        let fullyFunded = true;
 | 
				
			||||||
 | 
					        for (const ingredient of meta.ingredients) {
 | 
				
			||||||
 | 
					            const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType);
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                !componentMiscItem ||
 | 
				
			||||||
 | 
					                componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                fullyFunded = false;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (fullyFunded) {
 | 
				
			||||||
 | 
					            component.CompletionTime = new Date(Date.now() + meta.time * 1000);
 | 
				
			||||||
 | 
					            processDojoBuildMaterialsGathered(guild, meta);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										109
									
								
								src/controllers/api/contributeToVaultController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/controllers/api/contributeToVaultController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					import type { TGuildDatabaseDocument, TGuildMemberDatabaseDocument } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { Alliance, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    addGuildMemberMiscItemContribution,
 | 
				
			||||||
 | 
					    addGuildMemberShipDecoContribution,
 | 
				
			||||||
 | 
					    addVaultFusionTreasures,
 | 
				
			||||||
 | 
					    addVaultMiscItems,
 | 
				
			||||||
 | 
					    addVaultShipDecos,
 | 
				
			||||||
 | 
					    getGuildForRequestEx
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    addFusionTreasures,
 | 
				
			||||||
 | 
					    addMiscItems,
 | 
				
			||||||
 | 
					    addShipDecorations,
 | 
				
			||||||
 | 
					    getInventory,
 | 
				
			||||||
 | 
					    updateCurrency
 | 
				
			||||||
 | 
					} from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { ITypeCount } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import type { IFusionTreasure, IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const contributeToVaultController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures");
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IContributeToVaultRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (request.Alliance) {
 | 
				
			||||||
 | 
					        const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					        const alliance = (await Alliance.findById(guild.AllianceId!))!;
 | 
				
			||||||
 | 
					        alliance.VaultRegularCredits ??= 0;
 | 
				
			||||||
 | 
					        alliance.VaultRegularCredits += request.RegularCredits;
 | 
				
			||||||
 | 
					        if (request.FromVault) {
 | 
				
			||||||
 | 
					            guild.VaultRegularCredits! -= request.RegularCredits;
 | 
				
			||||||
 | 
					            await Promise.all([guild.save(), alliance.save()]);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            updateCurrency(inventory, request.RegularCredits, false);
 | 
				
			||||||
 | 
					            await Promise.all([inventory.save(), alliance.save()]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        res.end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let guild: TGuildDatabaseDocument;
 | 
				
			||||||
 | 
					    let guildMember: TGuildMemberDatabaseDocument | undefined;
 | 
				
			||||||
 | 
					    if (request.GuildVault) {
 | 
				
			||||||
 | 
					        guild = (await Guild.findById(request.GuildVault))!;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					        guildMember = (await GuildMember.findOne(
 | 
				
			||||||
 | 
					            { accountId, guildId: guild._id },
 | 
				
			||||||
 | 
					            "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed"
 | 
				
			||||||
 | 
					        ))!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (request.RegularCredits) {
 | 
				
			||||||
 | 
					        updateCurrency(inventory, request.RegularCredits, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        guild.VaultRegularCredits ??= 0;
 | 
				
			||||||
 | 
					        guild.VaultRegularCredits += request.RegularCredits;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (guildMember) {
 | 
				
			||||||
 | 
					            guildMember.RegularCreditsContributed ??= 0;
 | 
				
			||||||
 | 
					            guildMember.RegularCreditsContributed += request.RegularCredits;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (request.MiscItems.length) {
 | 
				
			||||||
 | 
					        addVaultMiscItems(guild, request.MiscItems);
 | 
				
			||||||
 | 
					        for (const item of request.MiscItems) {
 | 
				
			||||||
 | 
					            if (guildMember) {
 | 
				
			||||||
 | 
					                addGuildMemberMiscItemContribution(guildMember, item);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (request.ShipDecorations.length) {
 | 
				
			||||||
 | 
					        addVaultShipDecos(guild, request.ShipDecorations);
 | 
				
			||||||
 | 
					        for (const item of request.ShipDecorations) {
 | 
				
			||||||
 | 
					            if (guildMember) {
 | 
				
			||||||
 | 
					                addGuildMemberShipDecoContribution(guildMember, item);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (request.FusionTreasures.length) {
 | 
				
			||||||
 | 
					        addVaultFusionTreasures(guild, request.FusionTreasures);
 | 
				
			||||||
 | 
					        for (const item of request.FusionTreasures) {
 | 
				
			||||||
 | 
					            addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const promises: Promise<unknown>[] = [guild.save(), inventory.save()];
 | 
				
			||||||
 | 
					    if (guildMember) {
 | 
				
			||||||
 | 
					        promises.push(guildMember.save());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await Promise.all(promises);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IContributeToVaultRequest {
 | 
				
			||||||
 | 
					    RegularCredits: number;
 | 
				
			||||||
 | 
					    MiscItems: IMiscItem[];
 | 
				
			||||||
 | 
					    ShipDecorations: ITypeCount[];
 | 
				
			||||||
 | 
					    FusionTreasures: IFusionTreasure[];
 | 
				
			||||||
 | 
					    Alliance?: boolean;
 | 
				
			||||||
 | 
					    FromVault?: boolean;
 | 
				
			||||||
 | 
					    GuildVault?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								src/controllers/api/createAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/controllers/api/createAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAllianceClient } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createAllianceController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId");
 | 
				
			||||||
 | 
					    const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!;
 | 
				
			||||||
 | 
					    if (guild.AllianceId) {
 | 
				
			||||||
 | 
					        res.status(400).send("Guild is already in an alliance").end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!;
 | 
				
			||||||
 | 
					    if (guildMember.rank > 1) {
 | 
				
			||||||
 | 
					        res.status(400).send("Invalid permission").end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const data = getJSONfromString<ICreateAllianceRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const alliance = new Alliance({ Name: data.allianceName });
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        await alliance.save();
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        res.status(400).send("Alliance name already in use").end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    guild.AllianceId = alliance._id;
 | 
				
			||||||
 | 
					    await Promise.all([
 | 
				
			||||||
 | 
					        guild.save(),
 | 
				
			||||||
 | 
					        AllianceMember.insertOne({
 | 
				
			||||||
 | 
					            allianceId: alliance._id,
 | 
				
			||||||
 | 
					            guildId: guild._id,
 | 
				
			||||||
 | 
					            Pending: false,
 | 
				
			||||||
 | 
					            Permissions:
 | 
				
			||||||
 | 
					                GuildPermission.Ruler |
 | 
				
			||||||
 | 
					                GuildPermission.Promoter |
 | 
				
			||||||
 | 
					                GuildPermission.Recruiter |
 | 
				
			||||||
 | 
					                GuildPermission.Treasurer |
 | 
				
			||||||
 | 
					                GuildPermission.ChatModerator
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					    res.json(await getAllianceClient(alliance, guild));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICreateAllianceRequest {
 | 
				
			||||||
 | 
					    allianceName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/controllers/api/createGuildController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/controllers/api/createGuildController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { createUniqueClanName, getGuildClient, giveClanKey } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createGuildController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove pending applications for this account
 | 
				
			||||||
 | 
					    await GuildMember.deleteMany({ accountId: account._id, status: 1 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create guild on database
 | 
				
			||||||
 | 
					    const guild = new Guild({
 | 
				
			||||||
 | 
					        Name: await createUniqueClanName(payload.guildName)
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create guild member on database
 | 
				
			||||||
 | 
					    await GuildMember.insertOne({
 | 
				
			||||||
 | 
					        accountId: account._id,
 | 
				
			||||||
 | 
					        guildId: guild._id,
 | 
				
			||||||
 | 
					        status: 0,
 | 
				
			||||||
 | 
					        rank: 0
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes skipClanKeyCrafting");
 | 
				
			||||||
 | 
					    inventory.GuildId = guild._id;
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					    giveClanKey(inventory, inventoryChanges);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        ...(await getGuildClient(guild, account)),
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICreateGuildRequest {
 | 
				
			||||||
 | 
					    guildName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/controllers/api/creditsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/controllers/api/creditsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const creditsController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const inventory = (
 | 
				
			||||||
 | 
					        await Promise.all([
 | 
				
			||||||
 | 
					            getAccountIdForRequest(req),
 | 
				
			||||||
 | 
					            getInventory(
 | 
				
			||||||
 | 
					                req.query.accountId as string,
 | 
				
			||||||
 | 
					                "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits infiniteCredits infinitePlatinum"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					    )[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = {
 | 
				
			||||||
 | 
					        RegularCredits: inventory.RegularCredits,
 | 
				
			||||||
 | 
					        TradesRemaining: inventory.TradesRemaining,
 | 
				
			||||||
 | 
					        PremiumCreditsFree: inventory.PremiumCreditsFree,
 | 
				
			||||||
 | 
					        PremiumCredits: inventory.PremiumCredits
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (inventory.infiniteCredits) {
 | 
				
			||||||
 | 
					        response.RegularCredits = 999999999;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (inventory.infinitePlatinum) {
 | 
				
			||||||
 | 
					        response.PremiumCreditsFree = 0;
 | 
				
			||||||
 | 
					        response.PremiumCredits = 999999999;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(response);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										54
									
								
								src/controllers/api/crewMembersController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/controllers/api/crewMembersController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const crewMembersController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "CrewMembers NemesisHistory");
 | 
				
			||||||
 | 
					    const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
 | 
				
			||||||
 | 
					    if (data.crewMember.SecondInCommand) {
 | 
				
			||||||
 | 
					        clearOnCall(inventory);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
 | 
				
			||||||
 | 
					        const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
 | 
				
			||||||
 | 
					        convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
 | 
				
			||||||
 | 
					        dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
 | 
				
			||||||
 | 
					        dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
 | 
				
			||||||
 | 
					        dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
 | 
				
			||||||
 | 
					        dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
 | 
				
			||||||
 | 
					        dbCrewMember.Configs = data.crewMember.Configs;
 | 
				
			||||||
 | 
					        dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        crewMemberId: data.crewMember.ItemId.$oid,
 | 
				
			||||||
 | 
					        NemesisFingerprint: data.crewMember.NemesisFingerprint
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICrewMembersRequest {
 | 
				
			||||||
 | 
					    crewMember: ICrewMemberClient;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
 | 
				
			||||||
 | 
					    for (const cm of inventory.CrewMembers) {
 | 
				
			||||||
 | 
					        if (cm.SecondInCommand) {
 | 
				
			||||||
 | 
					            cm.SecondInCommand = false;
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (inventory.NemesisHistory) {
 | 
				
			||||||
 | 
					        for (const cm of inventory.NemesisHistory) {
 | 
				
			||||||
 | 
					            if (cm.SecondInCommand) {
 | 
				
			||||||
 | 
					                cm.SecondInCommand = false;
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										107
									
								
								src/controllers/api/crewShipFusionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/controllers/api/crewShipFusionController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IOid } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import type { ICrewShipComponentFingerprint } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const crewShipFusionController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
 | 
				
			||||||
 | 
					    const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
 | 
				
			||||||
 | 
					    const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
 | 
				
			||||||
 | 
					    const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
 | 
				
			||||||
 | 
					    const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
 | 
				
			||||||
 | 
					    const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
 | 
				
			||||||
 | 
					    const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Charge partial repair cost if fusing with an identified but unrepaired part
 | 
				
			||||||
 | 
					    if (payload.SourceRecipe) {
 | 
				
			||||||
 | 
					        const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
 | 
				
			||||||
 | 
					        updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
 | 
				
			||||||
 | 
					        const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
 | 
				
			||||||
 | 
					        addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					        inventoryChanges.MiscItems = miscItemChanges;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove inferior item
 | 
				
			||||||
 | 
					    if (payload.SourceRecipe) {
 | 
				
			||||||
 | 
					        inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
 | 
				
			||||||
 | 
					        inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
 | 
				
			||||||
 | 
					        inventory[category].pull({ _id: inferiorId.$oid });
 | 
				
			||||||
 | 
					        inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
 | 
				
			||||||
 | 
					        freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
				
			||||||
 | 
					        inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Upgrade superior item
 | 
				
			||||||
 | 
					    const superiorItem = tierA < tierB ? itemB : itemA;
 | 
				
			||||||
 | 
					    const inferiorItem = tierA < tierB ? itemA : itemB;
 | 
				
			||||||
 | 
					    const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
 | 
				
			||||||
 | 
					        superiorItem.UpgradeFingerprint!
 | 
				
			||||||
 | 
					    ) as ICrewShipComponentFingerprint;
 | 
				
			||||||
 | 
					    const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
 | 
				
			||||||
 | 
					        ? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
 | 
				
			||||||
 | 
					        : { compat: "", buffs: [] };
 | 
				
			||||||
 | 
					    if (isWeapon) {
 | 
				
			||||||
 | 
					        for (let i = 0; i != fingerprint.buffs.length; ++i) {
 | 
				
			||||||
 | 
					            const buffA = fingerprint.buffs[i];
 | 
				
			||||||
 | 
					            const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
 | 
				
			||||||
 | 
					            const fvalA = buffA.Value / 0x3fffffff;
 | 
				
			||||||
 | 
					            const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
 | 
				
			||||||
 | 
					            const percA = 0.3 + fvalA * (0.6 - 0.3);
 | 
				
			||||||
 | 
					            const percB = 0.3 + fvalB * (0.6 - 0.3);
 | 
				
			||||||
 | 
					            const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
 | 
				
			||||||
 | 
					            const newFval = (newPerc - 0.3) / (0.6 - 0.3);
 | 
				
			||||||
 | 
					            buffA.Value = Math.trunc(newFval * 0x3fffffff);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
 | 
				
			||||||
 | 
					        const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
 | 
				
			||||||
 | 
					        for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
 | 
				
			||||||
 | 
					            const buffA = fingerprint.buffs[i];
 | 
				
			||||||
 | 
					            const buffB = inferiorFingerprint.buffs[i];
 | 
				
			||||||
 | 
					            const fvalA = buffA.Value / 0x3fffffff;
 | 
				
			||||||
 | 
					            const fvalB = buffB.Value / 0x3fffffff;
 | 
				
			||||||
 | 
					            const rangeA = superiorMeta[i].range;
 | 
				
			||||||
 | 
					            const rangeB = inferiorMeta[i].range;
 | 
				
			||||||
 | 
					            const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
 | 
				
			||||||
 | 
					            const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
 | 
				
			||||||
 | 
					            const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
 | 
				
			||||||
 | 
					            const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
 | 
				
			||||||
 | 
					            buffA.Value = Math.trunc(newFval * 0x3fffffff);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (inferiorFingerprint.SubroutineIndex !== undefined) {
 | 
				
			||||||
 | 
					            const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
 | 
				
			||||||
 | 
					            if (!useSuperiorSubroutine) {
 | 
				
			||||||
 | 
					                fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
 | 
				
			||||||
 | 
					    inventoryChanges[category] = [superiorItem.toJSON() as any];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICrewShipFusionRequest {
 | 
				
			||||||
 | 
					    PartA: IOid;
 | 
				
			||||||
 | 
					    PartB: IOid;
 | 
				
			||||||
 | 
					    SourceRecipe: string;
 | 
				
			||||||
 | 
					    UseSubroutineA: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02];
 | 
				
			||||||
							
								
								
									
										87
									
								
								src/controllers/api/crewShipIdentifySalvageController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/controllers/api/crewShipIdentifySalvageController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    addCrewShipSalvagedWeaponSkin,
 | 
				
			||||||
 | 
					    addCrewShipRawSalvage,
 | 
				
			||||||
 | 
					    getInventory,
 | 
				
			||||||
 | 
					    addEquipment
 | 
				
			||||||
 | 
					} from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    ICrewShipComponentFingerprint,
 | 
				
			||||||
 | 
					    IInnateDamageFingerprint
 | 
				
			||||||
 | 
					} from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import { getRandomInt } from "../../services/rngService.ts";
 | 
				
			||||||
 | 
					import type { IFingerprintStat } from "../../helpers/rivenHelper.ts";
 | 
				
			||||||
 | 
					import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(
 | 
				
			||||||
 | 
					        accountId,
 | 
				
			||||||
 | 
					        "CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					    if (payload.ItemType in ExportCustoms) {
 | 
				
			||||||
 | 
					        const meta = ExportCustoms[payload.ItemType];
 | 
				
			||||||
 | 
					        let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] };
 | 
				
			||||||
 | 
					        if (meta.subroutines) {
 | 
				
			||||||
 | 
					            upgradeFingerprint = {
 | 
				
			||||||
 | 
					                SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1),
 | 
				
			||||||
 | 
					                ...upgradeFingerprint
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (const upgrade of meta.randomisedUpgrades!) {
 | 
				
			||||||
 | 
					            upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        addCrewShipSalvagedWeaponSkin(
 | 
				
			||||||
 | 
					            inventory,
 | 
				
			||||||
 | 
					            payload.ItemType,
 | 
				
			||||||
 | 
					            JSON.stringify(upgradeFingerprint),
 | 
				
			||||||
 | 
					            inventoryChanges
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const meta = ExportRailjackWeapons[payload.ItemType];
 | 
				
			||||||
 | 
					        let defaultOverwrites: Partial<IEquipmentDatabase> | undefined;
 | 
				
			||||||
 | 
					        if (meta.defaultUpgrades?.[0]) {
 | 
				
			||||||
 | 
					            const upgradeType = meta.defaultUpgrades[0].ItemType;
 | 
				
			||||||
 | 
					            const upgradeMeta = ExportUpgrades[upgradeType];
 | 
				
			||||||
 | 
					            const buffs: IFingerprintStat[] = [];
 | 
				
			||||||
 | 
					            for (const buff of upgradeMeta.upgradeEntries!) {
 | 
				
			||||||
 | 
					                buffs.push({
 | 
				
			||||||
 | 
					                    Tag: buff.tag,
 | 
				
			||||||
 | 
					                    Value: Math.trunc(Math.random() * 0x40000000)
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            defaultOverwrites = {
 | 
				
			||||||
 | 
					                UpgradeType: upgradeType,
 | 
				
			||||||
 | 
					                UpgradeFingerprint: JSON.stringify({
 | 
				
			||||||
 | 
					                    compat: payload.ItemType,
 | 
				
			||||||
 | 
					                    buffs
 | 
				
			||||||
 | 
					                } satisfies IInnateDamageFingerprint)
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inventoryChanges.CrewShipRawSalvage = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            ItemType: payload.ItemType,
 | 
				
			||||||
 | 
					            ItemCount: -1
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICrewShipIdentifySalvageRequest {
 | 
				
			||||||
 | 
					    ItemType: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest, getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const guild = (await Guild.findById(data.g, "DojoComponents Ranks"))!;
 | 
				
			||||||
 | 
					    const component = guild.DojoComponents.id(data.c)!;
 | 
				
			||||||
 | 
					    if (req.query.act == "f") {
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            results: component.Leaderboard ?? []
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else if (req.query.act == "p") {
 | 
				
			||||||
 | 
					        const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					        component.Leaderboard ??= [];
 | 
				
			||||||
 | 
					        const entry = component.Leaderboard.find(x => x.n == account.DisplayName);
 | 
				
			||||||
 | 
					        if (entry) {
 | 
				
			||||||
 | 
					            entry.s = data.s!;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            component.Leaderboard.push({
 | 
				
			||||||
 | 
					                s: data.s!,
 | 
				
			||||||
 | 
					                n: account.DisplayName,
 | 
				
			||||||
 | 
					                r: 0
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        component.Leaderboard.sort((a, b) => a.s - b.s); // In this case, the score is the time in milliseconds, so smaller is better.
 | 
				
			||||||
 | 
					        if (component.Leaderboard.length > 10) {
 | 
				
			||||||
 | 
					            component.Leaderboard.shift();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let r = 0;
 | 
				
			||||||
 | 
					        for (const entry of component.Leaderboard) {
 | 
				
			||||||
 | 
					            entry.r = ++r;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        await guild.save();
 | 
				
			||||||
 | 
					        res.status(200).end();
 | 
				
			||||||
 | 
					    } else if (req.query.act == "c") {
 | 
				
			||||||
 | 
					        // TOVERIFY: What clan permission is actually needed for this?
 | 
				
			||||||
 | 
					        const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
				
			||||||
 | 
					        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
 | 
				
			||||||
 | 
					            res.status(400).end();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        component.Leaderboard = undefined;
 | 
				
			||||||
 | 
					        await guild.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.status(200).end();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
				
			||||||
 | 
					        throw new Error(`unknown customObstacleCourseLeaderboard act: ${String(req.query.act)}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICustomObstacleCourseLeaderboardRequest {
 | 
				
			||||||
 | 
					    g: string;
 | 
				
			||||||
 | 
					    c: string;
 | 
				
			||||||
 | 
					    s?: number; // act=p
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/controllers/api/customizeGuildRanksController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/controllers/api/customizeGuildRanksController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { getGuildForRequest, hasGuildPermission } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IGuildRank } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const customizeGuildRanksController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequest(req);
 | 
				
			||||||
 | 
					    const payload = JSON.parse(String(req.body)) as ICustomizeGuildRanksRequest;
 | 
				
			||||||
 | 
					    if (!(await hasGuildPermission(guild, accountId, GuildPermission.Ruler))) {
 | 
				
			||||||
 | 
					        res.status(400).json("Invalid permission");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    guild.Ranks = payload.GuildRanks;
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ICustomizeGuildRanksRequest {
 | 
				
			||||||
 | 
					    GuildRanks: IGuildRank[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/controllers/api/declineAllianceInviteController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/controllers/api/declineAllianceInviteController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { AllianceMember, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const declineAllianceInviteController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    // Check requester is a warlord in their guild
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
 | 
				
			||||||
 | 
					    if (guildMember.rank > 1) {
 | 
				
			||||||
 | 
					        res.status(400).json({ Error: 104 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/controllers/api/declineGuildInviteController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/controllers/api/declineGuildInviteController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const declineGuildInviteController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await GuildMember.deleteOne({
 | 
				
			||||||
 | 
					        accountId: accountId,
 | 
				
			||||||
 | 
					        guildId: req.query.clanId as string
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/controllers/api/deleteSessionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/controllers/api/deleteSessionController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { deleteSession } from "../../managers/sessionManager.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deleteSessionController: RequestHandler = (_req, res) => {
 | 
				
			||||||
 | 
					    deleteSession(_req.query.sessionId as string);
 | 
				
			||||||
 | 
					    res.sendStatus(200);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { deleteSessionController };
 | 
				
			||||||
							
								
								
									
										51
									
								
								src/controllers/api/destroyDojoDecoController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/controllers/api/destroyDojoDecoController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getDojoClient,
 | 
				
			||||||
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
 | 
					    hasAccessToDojo,
 | 
				
			||||||
 | 
					    hasGuildPermission,
 | 
				
			||||||
 | 
					    refundDojoDeco,
 | 
				
			||||||
 | 
					    removeDojoDeco
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const destroyDojoDecoController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
 | 
				
			||||||
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
 | 
				
			||||||
 | 
					    if ("DecoType" in request) {
 | 
				
			||||||
 | 
					        removeDojoDeco(guild, request.ComponentId, request.DecoId);
 | 
				
			||||||
 | 
					    } else if (request.Act == "cObst") {
 | 
				
			||||||
 | 
					        const component = guild.DojoComponents.id(request.ComponentId)!;
 | 
				
			||||||
 | 
					        if (component.Decos) {
 | 
				
			||||||
 | 
					            for (const deco of component.Decos) {
 | 
				
			||||||
 | 
					                refundDojoDeco(guild, component, deco);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            component.Decos.splice(0, component.Decos.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.error(`unhandled destroyDojoDeco request`, request);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await guild.save();
 | 
				
			||||||
 | 
					    res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IDestroyDojoDecoRequest {
 | 
				
			||||||
 | 
					    DecoType: string;
 | 
				
			||||||
 | 
					    ComponentId: string;
 | 
				
			||||||
 | 
					    DecoId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IClearObstacleCourseRequest {
 | 
				
			||||||
 | 
					    ComponentId: string;
 | 
				
			||||||
 | 
					    Act: "cObst" | "maybesomethingelsewedontknowabout";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										67
									
								
								src/controllers/api/divvyAllianceVaultController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/controllers/api/divvyAllianceVaultController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { GuildPermission } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import { parallelForeach } from "../../utils/async-utils.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const divvyAllianceVaultController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    // Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits.
 | 
				
			||||||
 | 
					    if (req.query.credits == "1") {
 | 
				
			||||||
 | 
					        // Check requester is a warlord in their guild
 | 
				
			||||||
 | 
					        const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					        const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
 | 
				
			||||||
 | 
					        if (guildMember.rank > 1) {
 | 
				
			||||||
 | 
					            res.status(400).end();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check guild has treasurer permissions in the alliance
 | 
				
			||||||
 | 
					        const allianceMember = (await AllianceMember.findOne({
 | 
				
			||||||
 | 
					            allianceId: req.query.allianceId,
 | 
				
			||||||
 | 
					            guildId: guildMember.guildId
 | 
				
			||||||
 | 
					        }))!;
 | 
				
			||||||
 | 
					        if (!(allianceMember.Permissions & GuildPermission.Treasurer)) {
 | 
				
			||||||
 | 
					            res.status(400).end();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const allianceMembers = await AllianceMember.find({ allianceId: req.query.allianceId });
 | 
				
			||||||
 | 
					        const memberCounts: Record<string, number> = {};
 | 
				
			||||||
 | 
					        let totalMembers = 0;
 | 
				
			||||||
 | 
					        await parallelForeach(allianceMembers, async allianceMember => {
 | 
				
			||||||
 | 
					            const memberCount = await GuildMember.countDocuments({
 | 
				
			||||||
 | 
					                guildId: allianceMember.guildId
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            memberCounts[allianceMember.guildId.toString()] = memberCount;
 | 
				
			||||||
 | 
					            totalMembers += memberCount;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        logger.debug(`alliance has ${totalMembers} members between all its clans`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const alliance = (await Alliance.findById(allianceMember.allianceId, "VaultRegularCredits"))!;
 | 
				
			||||||
 | 
					        if (alliance.VaultRegularCredits) {
 | 
				
			||||||
 | 
					            let creditsHandedOutInTotal = 0;
 | 
				
			||||||
 | 
					            await parallelForeach(allianceMembers, async allianceMember => {
 | 
				
			||||||
 | 
					                const memberCount = memberCounts[allianceMember.guildId.toString()];
 | 
				
			||||||
 | 
					                const cutPercentage = memberCount / totalMembers;
 | 
				
			||||||
 | 
					                const creditsToHandOut = Math.trunc(alliance.VaultRegularCredits! * cutPercentage);
 | 
				
			||||||
 | 
					                logger.debug(
 | 
				
			||||||
 | 
					                    `${allianceMember.guildId.toString()} has ${memberCount} member(s) = ${Math.trunc(cutPercentage * 100)}% of alliance; giving ${creditsToHandOut} credit(s)`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                if (creditsToHandOut != 0) {
 | 
				
			||||||
 | 
					                    await Guild.updateOne(
 | 
				
			||||||
 | 
					                        { _id: allianceMember.guildId },
 | 
				
			||||||
 | 
					                        { $inc: { VaultRegularCredits: creditsToHandOut } }
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    creditsHandedOutInTotal += creditsToHandOut;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            alliance.VaultRegularCredits -= creditsHandedOutInTotal;
 | 
				
			||||||
 | 
					            logger.debug(
 | 
				
			||||||
 | 
					                `handed out ${creditsHandedOutInTotal} credits; alliance vault now has ${alliance.VaultRegularCredits} credit(s)`
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        await alliance.save();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/controllers/api/dojoComponentRushController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/controllers/api/dojoComponentRushController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import type { TGuildDatabaseDocument } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    getDojoClient,
 | 
				
			||||||
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
 | 
					    hasAccessToDojo,
 | 
				
			||||||
 | 
					    scaleRequiredCount
 | 
				
			||||||
 | 
					} from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IDojoContributable } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import type { IDojoBuild } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IDojoComponentRushRequest {
 | 
				
			||||||
 | 
					    DecoType?: string;
 | 
				
			||||||
 | 
					    DecoId?: string;
 | 
				
			||||||
 | 
					    ComponentId: string;
 | 
				
			||||||
 | 
					    Amount: number;
 | 
				
			||||||
 | 
					    VaultAmount: number;
 | 
				
			||||||
 | 
					    AllianceVaultAmount: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dojoComponentRushController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    if (!hasAccessToDojo(inventory)) {
 | 
				
			||||||
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const guild = await getGuildForRequestEx(req, inventory);
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest;
 | 
				
			||||||
 | 
					    const component = guild.DojoComponents.id(request.ComponentId)!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let platinumDonated = request.Amount;
 | 
				
			||||||
 | 
					    const inventoryChanges = updateCurrency(inventory, request.Amount, true);
 | 
				
			||||||
 | 
					    if (request.VaultAmount) {
 | 
				
			||||||
 | 
					        platinumDonated += request.VaultAmount;
 | 
				
			||||||
 | 
					        guild.VaultPremiumCredits! -= request.VaultAmount;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (request.DecoId) {
 | 
				
			||||||
 | 
					        const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
 | 
				
			||||||
 | 
					        const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
 | 
				
			||||||
 | 
					        processContribution(guild, deco, meta, platinumDonated);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
 | 
				
			||||||
 | 
					        processContribution(guild, component, meta, platinumDonated);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
 | 
				
			||||||
 | 
					        if (entry) {
 | 
				
			||||||
 | 
					            entry.dateTime = component.CompletionTime!;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!;
 | 
				
			||||||
 | 
					    guildMember.PremiumCreditsContributed ??= 0;
 | 
				
			||||||
 | 
					    guildMember.PremiumCreditsContributed += request.Amount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        ...(await getDojoClient(guild, 0, component._id)),
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const processContribution = (
 | 
				
			||||||
 | 
					    guild: TGuildDatabaseDocument,
 | 
				
			||||||
 | 
					    component: IDojoContributable,
 | 
				
			||||||
 | 
					    meta: IDojoBuild,
 | 
				
			||||||
 | 
					    platinumDonated: number
 | 
				
			||||||
 | 
					): void => {
 | 
				
			||||||
 | 
					    const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice);
 | 
				
			||||||
 | 
					    const fullDurationSeconds = meta.time;
 | 
				
			||||||
 | 
					    const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost;
 | 
				
			||||||
 | 
					    component.CompletionTime = new Date(
 | 
				
			||||||
 | 
					        component.CompletionTime!.getTime() - secondsPerPlatinum * platinumDonated * 1000
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    component.RushPlatinum ??= 0;
 | 
				
			||||||
 | 
					    component.RushPlatinum += platinumDonated;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/controllers/api/dojoController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/controllers/api/dojoController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dojoController: RequestHandler = (_req, res) => {
 | 
				
			||||||
 | 
					    res.json("-1"); // Tell client to use authorised request.
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setDojoURLController: RequestHandler = (_req, res) => {
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										145
									
								
								src/controllers/api/dronesController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/controllers/api/dronesController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,145 @@
 | 
				
			|||||||
 | 
					import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { fromStoreItem } from "../../services/itemDataService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getRandomInt, getRandomWeightedRewardUc } from "../../services/rngService.ts";
 | 
				
			||||||
 | 
					import type { IMongoDate, IOid } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import type { IDroneClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dronesController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    if ("GetActive" in req.query) {
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId, "Drones");
 | 
				
			||||||
 | 
					        const activeDrones: IActiveDrone[] = [];
 | 
				
			||||||
 | 
					        for (const drone of inventory.Drones) {
 | 
				
			||||||
 | 
					            if (drone.DeployTime) {
 | 
				
			||||||
 | 
					                activeDrones.push({
 | 
				
			||||||
 | 
					                    DeployTime: toMongoDate(drone.DeployTime),
 | 
				
			||||||
 | 
					                    System: drone.System!,
 | 
				
			||||||
 | 
					                    ItemId: toOid(drone._id),
 | 
				
			||||||
 | 
					                    ItemType: drone.ItemType,
 | 
				
			||||||
 | 
					                    CurrentHP: drone.CurrentHP,
 | 
				
			||||||
 | 
					                    DamageTime: toMongoDate(drone.DamageTime!),
 | 
				
			||||||
 | 
					                    PendingDamage: drone.PendingDamage!,
 | 
				
			||||||
 | 
					                    Resources: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            ItemType: drone.ResourceType!,
 | 
				
			||||||
 | 
					                            BinTotal: drone.ResourceCount!,
 | 
				
			||||||
 | 
					                            StartTime: toMongoDate(drone.DeployTime)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            ActiveDrones: activeDrones
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else if ("droneId" in req.query && "systemIndex" in req.query) {
 | 
				
			||||||
 | 
					        const inventory = await getInventory(
 | 
				
			||||||
 | 
					            accountId,
 | 
				
			||||||
 | 
					            "Drones instantResourceExtractorDrones noResourceExtractorDronesDamage"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        const drone = inventory.Drones.id(req.query.droneId as string)!;
 | 
				
			||||||
 | 
					        const droneMeta = ExportDrones[drone.ItemType];
 | 
				
			||||||
 | 
					        drone.DeployTime = inventory.instantResourceExtractorDrones ? new Date(0) : new Date();
 | 
				
			||||||
 | 
					        if (drone.RepairStart) {
 | 
				
			||||||
 | 
					            const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000;
 | 
				
			||||||
 | 
					            const hpPerMinute = droneMeta.repairRate / 60;
 | 
				
			||||||
 | 
					            drone.CurrentHP = Math.min(drone.CurrentHP + Math.round(repairMinutes * hpPerMinute), droneMeta.durability);
 | 
				
			||||||
 | 
					            drone.RepairStart = undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        drone.System = parseInt(req.query.systemIndex as string);
 | 
				
			||||||
 | 
					        const system = ExportSystems[drone.System - 1];
 | 
				
			||||||
 | 
					        drone.DamageTime = inventory.instantResourceExtractorDrones
 | 
				
			||||||
 | 
					            ? new Date()
 | 
				
			||||||
 | 
					            : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
 | 
				
			||||||
 | 
					        drone.PendingDamage =
 | 
				
			||||||
 | 
					            !inventory.noResourceExtractorDronesDamage && Math.random() < system.damageChance
 | 
				
			||||||
 | 
					                ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
 | 
				
			||||||
 | 
					                : 0;
 | 
				
			||||||
 | 
					        const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;
 | 
				
			||||||
 | 
					        //logger.debug(`drone rolled`, resource);
 | 
				
			||||||
 | 
					        drone.ResourceType = fromStoreItem(resource.StoreItem);
 | 
				
			||||||
 | 
					        const resourceMeta = ExportResources[drone.ResourceType];
 | 
				
			||||||
 | 
					        if (resourceMeta.pickupQuantity) {
 | 
				
			||||||
 | 
					            const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
 | 
				
			||||||
 | 
					            drone.ResourceCount = 0;
 | 
				
			||||||
 | 
					            for (let i = 0; i != pickupsToCollect; ++i) {
 | 
				
			||||||
 | 
					                drone.ResourceCount += getRandomInt(
 | 
				
			||||||
 | 
					                    resourceMeta.pickupQuantity.minValue,
 | 
				
			||||||
 | 
					                    resourceMeta.pickupQuantity.maxValue
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            drone.ResourceCount = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({});
 | 
				
			||||||
 | 
					    } else if ("collectDroneId" in req.query) {
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					        const drone = inventory.Drones.id(req.query.collectDroneId as string)!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (new Date() >= drone.DamageTime!) {
 | 
				
			||||||
 | 
					            drone.CurrentHP -= drone.PendingDamage!;
 | 
				
			||||||
 | 
					            drone.RepairStart = new Date();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					        if (drone.CurrentHP <= 0) {
 | 
				
			||||||
 | 
					            inventory.RegularCredits += 100;
 | 
				
			||||||
 | 
					            inventoryChanges.RegularCredits = 100;
 | 
				
			||||||
 | 
					            inventory.Drones.pull({ _id: req.query.collectDroneId as string });
 | 
				
			||||||
 | 
					            inventoryChanges.RemovedIdItems = [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ItemId: { $oid: req.query.collectDroneId }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const completionTime = drone.DeployTime!.getTime() + ExportDrones[drone.ItemType].fillRate * 3600_000;
 | 
				
			||||||
 | 
					            if (Date.now() >= completionTime) {
 | 
				
			||||||
 | 
					                const miscItemChanges = [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        ItemType: drone.ResourceType!,
 | 
				
			||||||
 | 
					                        ItemCount: drone.ResourceCount!
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					                addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					                inventoryChanges.MiscItems = miscItemChanges;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            drone.DeployTime = undefined;
 | 
				
			||||||
 | 
					            drone.System = undefined;
 | 
				
			||||||
 | 
					            drone.DamageTime = undefined;
 | 
				
			||||||
 | 
					            drone.PendingDamage = undefined;
 | 
				
			||||||
 | 
					            drone.ResourceType = undefined;
 | 
				
			||||||
 | 
					            drone.ResourceCount = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            inventoryChanges.Drones = [drone.toJSON<IDroneClient>()];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            InventoryChanges: inventoryChanges
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        throw new Error(`drones.php query not handled`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IActiveDrone {
 | 
				
			||||||
 | 
					    DeployTime: IMongoDate;
 | 
				
			||||||
 | 
					    System: number;
 | 
				
			||||||
 | 
					    ItemId: IOid;
 | 
				
			||||||
 | 
					    ItemType: string;
 | 
				
			||||||
 | 
					    CurrentHP: number;
 | 
				
			||||||
 | 
					    DamageTime: IMongoDate;
 | 
				
			||||||
 | 
					    PendingDamage: number;
 | 
				
			||||||
 | 
					    Resources: {
 | 
				
			||||||
 | 
					        ItemType: string;
 | 
				
			||||||
 | 
					        BinTotal: number;
 | 
				
			||||||
 | 
					        StartTime: IMongoDate;
 | 
				
			||||||
 | 
					    }[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										534
									
								
								src/controllers/api/endlessXpController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										534
									
								
								src/controllers/api/endlessXpController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,534 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { combineInventoryChanges, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    IEndlessXpReward,
 | 
				
			||||||
 | 
					    IInventoryClient,
 | 
				
			||||||
 | 
					    TEndlessXpCategory
 | 
				
			||||||
 | 
					} from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { ICountedStoreItem } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { ExportRewards } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { getRandomElement } from "../../services/rngService.ts";
 | 
				
			||||||
 | 
					import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const endlessXpController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
 | 
				
			||||||
 | 
					    if (payload.Mode == "r") {
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId, "EndlessXP");
 | 
				
			||||||
 | 
					        inventory.EndlessXP ??= [];
 | 
				
			||||||
 | 
					        let entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
 | 
				
			||||||
 | 
					        if (!entry) {
 | 
				
			||||||
 | 
					            entry = {
 | 
				
			||||||
 | 
					                Category: payload.Category,
 | 
				
			||||||
 | 
					                Earn: 0,
 | 
				
			||||||
 | 
					                Claim: 0,
 | 
				
			||||||
 | 
					                Choices: payload.Choices,
 | 
				
			||||||
 | 
					                PendingRewards: []
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            inventory.EndlessXP.push(entry);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000;
 | 
				
			||||||
 | 
					        const weekEnd = weekStart + 604800000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        entry.Earn = 0;
 | 
				
			||||||
 | 
					        entry.Claim = 0;
 | 
				
			||||||
 | 
					        entry.BonusAvailable = new Date(weekStart);
 | 
				
			||||||
 | 
					        entry.Expiry = new Date(weekEnd);
 | 
				
			||||||
 | 
					        entry.Choices = payload.Choices;
 | 
				
			||||||
 | 
					        entry.PendingRewards =
 | 
				
			||||||
 | 
					            payload.Category == "EXC_HARD"
 | 
				
			||||||
 | 
					                ? generateHardModeRewards(payload.Choices)
 | 
				
			||||||
 | 
					                : generateNormalModeRewards(payload.Choices);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            NewProgress: inventory.toJSON<IInventoryClient>().EndlessXP!.find(x => x.Category == payload.Category)!
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else if (payload.Mode == "c") {
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					        const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!;
 | 
				
			||||||
 | 
					        const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					        for (const reward of entry.PendingRewards) {
 | 
				
			||||||
 | 
					            if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) {
 | 
				
			||||||
 | 
					                combineInventoryChanges(
 | 
				
			||||||
 | 
					                    inventoryChanges,
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        await handleStoreItemAcquisition(
 | 
				
			||||||
 | 
					                            reward.Rewards[0].StoreItem,
 | 
				
			||||||
 | 
					                            inventory,
 | 
				
			||||||
 | 
					                            reward.Rewards[0].ItemCount
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ).InventoryChanges
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        entry.Claim = entry.Earn;
 | 
				
			||||||
 | 
					        await inventory.save();
 | 
				
			||||||
 | 
					        res.json({
 | 
				
			||||||
 | 
					            InventoryChanges: inventoryChanges,
 | 
				
			||||||
 | 
					            ClaimedXp: entry.Claim
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
				
			||||||
 | 
					        throw new Error(`unexpected endlessXp mode: ${payload.Mode}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type IEndlessXpRequest =
 | 
				
			||||||
 | 
					    | {
 | 
				
			||||||
 | 
					          Mode: "r";
 | 
				
			||||||
 | 
					          Category: TEndlessXpCategory;
 | 
				
			||||||
 | 
					          Choices: string[];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    | {
 | 
				
			||||||
 | 
					          Mode: "c" | "something else";
 | 
				
			||||||
 | 
					          Category: TEndlessXpCategory;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generateRandomRewards = (deckName: string): ICountedStoreItem[] => {
 | 
				
			||||||
 | 
					    const reward = getRandomElement(ExportRewards[deckName][0])!;
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            StoreItem: reward.type,
 | 
				
			||||||
 | 
					            ItemCount: reward.itemCount
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const normalModeChosenRewards: Record<string, string[]> = {
 | 
				
			||||||
 | 
					    Excalibur: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Trinity: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Ember: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Loki: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Mag: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Rhino: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Ash: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Frost: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Nyx: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Saryn: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Vauban: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Nova: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Nekros: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Valkyr: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Oberon: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Hydroid: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Mirage: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Limbo: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Mesa: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Chroma: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Atlas: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Ivara: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Inaros: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Titania: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Nidus: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Octavia: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Harrow: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Gara: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Khora: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Revenant: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Garuda: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Baruuk: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    Hildryn: [
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint",
 | 
				
			||||||
 | 
					        "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => {
 | 
				
			||||||
 | 
					    const choiceRewards = normalModeChosenRewards[choices[0]];
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 190,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 400,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: choiceRewards[0],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 630,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 890,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 1190,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: choiceRewards[1],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 1540,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 1950,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: choiceRewards[2],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 2430,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: choiceRewards[3],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 2990,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 3640,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: choiceRewards[4],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const hardModeChosenRewards: Record<string, string> = {
 | 
				
			||||||
 | 
					    Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
 | 
				
			||||||
 | 
					    CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
 | 
				
			||||||
 | 
					    DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
 | 
				
			||||||
 | 
					    DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
 | 
				
			||||||
 | 
					    AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
 | 
				
			||||||
 | 
					    NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
 | 
				
			||||||
 | 
					    Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
 | 
				
			||||||
 | 
					    RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
 | 
				
			||||||
 | 
					    RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
 | 
				
			||||||
 | 
					    RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
 | 
				
			||||||
 | 
					    Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 285,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 600,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 945,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 1335,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 1785,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: hardModeChosenRewards[choices[0]],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 2310,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 2925,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 3645,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 4485,
 | 
				
			||||||
 | 
					            Rewards: generateRandomRewards(
 | 
				
			||||||
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequiredTotalXp: 5460,
 | 
				
			||||||
 | 
					            Rewards: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: hardModeChosenRewards[choices[1]],
 | 
				
			||||||
 | 
					                    ItemCount: 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										52
									
								
								src/controllers/api/entratiLabConquestModeController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/controllers/api/entratiLabConquestModeController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { getInventory, updateEntratiVault } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const entratiLabConquestModeController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(
 | 
				
			||||||
 | 
					        accountId,
 | 
				
			||||||
 | 
					        "EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const body = getJSONfromString<IEntratiLabConquestModeRequest>(String(req.body));
 | 
				
			||||||
 | 
					    updateEntratiVault(inventory);
 | 
				
			||||||
 | 
					    if (body.BuyMode) {
 | 
				
			||||||
 | 
					        inventory.EntratiVaultCountLastPeriod! += 2;
 | 
				
			||||||
 | 
					        if (body.IsEchoesDeepArchemedea) {
 | 
				
			||||||
 | 
					            inventory.EchoesHexConquestUnlocked = 1;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            inventory.EntratiLabConquestUnlocked = 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (body.IsEchoesDeepArchemedea) {
 | 
				
			||||||
 | 
					        if (inventory.EchoesHexConquestUnlocked) {
 | 
				
			||||||
 | 
					            inventory.EchoesHexConquestActiveFrameVariants = body.EchoesHexConquestActiveFrameVariants!;
 | 
				
			||||||
 | 
					            inventory.EchoesHexConquestActiveStickers = body.EchoesHexConquestActiveStickers!;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        if (inventory.EntratiLabConquestUnlocked) {
 | 
				
			||||||
 | 
					            inventory.EntratiLabConquestActiveFrameVariants = body.EntratiLabConquestActiveFrameVariants!;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate!),
 | 
				
			||||||
 | 
					        EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod,
 | 
				
			||||||
 | 
					        EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked,
 | 
				
			||||||
 | 
					        EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission,
 | 
				
			||||||
 | 
					        EchoesHexConquestUnlocked: inventory.EchoesHexConquestUnlocked,
 | 
				
			||||||
 | 
					        EchoesHexConquestCacheScoreMission: inventory.EchoesHexConquestCacheScoreMission
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IEntratiLabConquestModeRequest {
 | 
				
			||||||
 | 
					    BuyMode?: number;
 | 
				
			||||||
 | 
					    IsEchoesDeepArchemedea?: number;
 | 
				
			||||||
 | 
					    EntratiLabConquestUnlocked?: number;
 | 
				
			||||||
 | 
					    EntratiLabConquestActiveFrameVariants?: string[];
 | 
				
			||||||
 | 
					    EchoesHexConquestUnlocked?: number;
 | 
				
			||||||
 | 
					    EchoesHexConquestActiveFrameVariants?: string[];
 | 
				
			||||||
 | 
					    EchoesHexConquestActiveStickers?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/controllers/api/evolveWeaponController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/controllers/api/evolveWeaponController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type { WeaponTypeInternal } from "../../services/itemDataService.ts";
 | 
				
			||||||
 | 
					import { getRecipe } from "../../services/itemDataService.ts";
 | 
				
			||||||
 | 
					import { EquipmentFeatures } from "../../types/equipmentTypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const evolveWeaponController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const payload = getJSONfromString<IEvolveWeaponRequest>(String(req.body));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const recipe = getRecipe(payload.Recipe)!;
 | 
				
			||||||
 | 
					    if (payload.Action == "EWA_INSTALL") {
 | 
				
			||||||
 | 
					        addMiscItems(
 | 
				
			||||||
 | 
					            inventory,
 | 
				
			||||||
 | 
					            recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const item = inventory[payload.Category].id(req.query.ItemId as string)!;
 | 
				
			||||||
 | 
					        item.Features ??= 0;
 | 
				
			||||||
 | 
					        item.Features |= EquipmentFeatures.INCARNON_GENESIS;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        item.SkillTree = "0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inventory.EvolutionProgress ??= [];
 | 
				
			||||||
 | 
					        if (!inventory.EvolutionProgress.find(entry => entry.ItemType == payload.EvoType)) {
 | 
				
			||||||
 | 
					            inventory.EvolutionProgress.push({
 | 
				
			||||||
 | 
					                Progress: 0,
 | 
				
			||||||
 | 
					                Rank: 1,
 | 
				
			||||||
 | 
					                ItemType: payload.EvoType
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else if (payload.Action == "EWA_UNINSTALL") {
 | 
				
			||||||
 | 
					        addMiscItems(inventory, [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ItemType: recipe.resultType,
 | 
				
			||||||
 | 
					                ItemCount: 1
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const item = inventory[payload.Category].id(req.query.ItemId as string)!;
 | 
				
			||||||
 | 
					        item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        throw new Error(`unexpected evolve weapon action: ${payload.Action}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IEvolveWeaponRequest {
 | 
				
			||||||
 | 
					    Action: string;
 | 
				
			||||||
 | 
					    Category: WeaponTypeInternal;
 | 
				
			||||||
 | 
					    Recipe: string; // e.g. "/Lotus/Types/Items/MiscItems/IncarnonAdapters/UnlockerBlueprints/DespairIncarnonBlueprint"
 | 
				
			||||||
 | 
					    UninstallRecipe: "";
 | 
				
			||||||
 | 
					    EvoType: string; // e.g. "/Lotus/Weapons/Tenno/ThrowingWeapons/StalkerKunai"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/controllers/api/findSessionsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/controllers/api/findSessionsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getSession } from "../../managers/sessionManager.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import type { IFindSessionRequest } from "../../types/session.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const findSessionsController: RequestHandler = (_req, res) => {
 | 
				
			||||||
 | 
					    const req = JSON.parse(String(_req.body)) as IFindSessionRequest;
 | 
				
			||||||
 | 
					    logger.debug("FindSession Request ", req);
 | 
				
			||||||
 | 
					    if (req.id != undefined) {
 | 
				
			||||||
 | 
					        logger.debug("Found ID");
 | 
				
			||||||
 | 
					        const session = getSession(req.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (session.length) res.json({ queryId: req.queryId, Sessions: session });
 | 
				
			||||||
 | 
					        else res.json({});
 | 
				
			||||||
 | 
					    } else if (req.originalSessionId != undefined) {
 | 
				
			||||||
 | 
					        logger.debug("Found OriginalSessionID");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const session = getSession(req.originalSessionId);
 | 
				
			||||||
 | 
					        if (session.length) res.json({ queryId: req.queryId, Sessions: session });
 | 
				
			||||||
 | 
					        else res.json({});
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.debug("Found SessionRequest");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const session = getSession(req);
 | 
				
			||||||
 | 
					        if (session.length) res.json({ queryId: req.queryId, Sessions: session });
 | 
				
			||||||
 | 
					        else res.json({});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/controllers/api/fishmongerController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/controllers/api/fishmongerController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { addMiscItems, addStanding, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportResources } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fishmongerController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const body = getJSONfromString<IFishmongerRequest>(String(req.body));
 | 
				
			||||||
 | 
					    const miscItemChanges: IMiscItem[] = [];
 | 
				
			||||||
 | 
					    let syndicateTag: string | undefined;
 | 
				
			||||||
 | 
					    let gainedStanding = 0;
 | 
				
			||||||
 | 
					    for (const fish of body.Fish) {
 | 
				
			||||||
 | 
					        const fishData = ExportResources[fish.ItemType];
 | 
				
			||||||
 | 
					        if (req.query.dissect == "1") {
 | 
				
			||||||
 | 
					            for (const part of fishData.dissectionParts!) {
 | 
				
			||||||
 | 
					                const partItem = miscItemChanges.find(x => x.ItemType == part.ItemType);
 | 
				
			||||||
 | 
					                if (partItem) {
 | 
				
			||||||
 | 
					                    partItem.ItemCount += part.ItemCount * fish.ItemCount;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    miscItemChanges.push({ ItemType: part.ItemType, ItemCount: part.ItemCount * fish.ItemCount });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            syndicateTag = fishData.syndicateTag!;
 | 
				
			||||||
 | 
					            gainedStanding += fishData.standingBonus! * fish.ItemCount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					    if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding);
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        InventoryChanges: {
 | 
				
			||||||
 | 
					            MiscItems: miscItemChanges
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        SyndicateTag: syndicateTag,
 | 
				
			||||||
 | 
					        StandingChange: gainedStanding
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IFishmongerRequest {
 | 
				
			||||||
 | 
					    Fish: IMiscItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										242
									
								
								src/controllers/api/focusController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								src/controllers/api/focusController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,242 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getInventory, addMiscItems, addEquipment, occupySlot } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IMiscItem, TFocusPolarity, TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import { ExportFocusUpgrades } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const focusController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    switch (req.query.op) {
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					            logger.error("Unhandled focus op type: " + String(req.query.op));
 | 
				
			||||||
 | 
					            logger.debug(String(req.body));
 | 
				
			||||||
 | 
					            res.end();
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        case FocusOperation.InstallLens: {
 | 
				
			||||||
 | 
					            const request = JSON.parse(String(req.body)) as ILensInstallRequest;
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					            const item = inventory[request.Category].id(request.WeaponId);
 | 
				
			||||||
 | 
					            if (item) {
 | 
				
			||||||
 | 
					                item.FocusLens = request.LensType;
 | 
				
			||||||
 | 
					                addMiscItems(inventory, [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        ItemType: request.LensType,
 | 
				
			||||||
 | 
					                        ItemCount: -1
 | 
				
			||||||
 | 
					                    } satisfies IMiscItem
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                weaponId: request.WeaponId,
 | 
				
			||||||
 | 
					                lensType: request.LensType
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.UnlockWay: {
 | 
				
			||||||
 | 
					            const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
 | 
				
			||||||
 | 
					            const focusPolarity = focusTypeToPolarity(focusType);
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId, "FocusAbility FocusUpgrades FocusXP");
 | 
				
			||||||
 | 
					            const cost = inventory.FocusAbility ? 50_000 : 0;
 | 
				
			||||||
 | 
					            inventory.FocusAbility ??= focusType;
 | 
				
			||||||
 | 
					            inventory.FocusUpgrades.push({ ItemType: focusType });
 | 
				
			||||||
 | 
					            if (cost) {
 | 
				
			||||||
 | 
					                inventory.FocusXP![focusPolarity]! -= cost;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                FocusUpgrade: { ItemType: focusType },
 | 
				
			||||||
 | 
					                FocusPointCosts: { [focusPolarity]: cost }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.ActivateWay: {
 | 
				
			||||||
 | 
					            const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await Inventory.updateOne(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    accountOwnerId: accountId
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    FocusAbility: focusType
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                FocusUpgrade: { ItemType: focusType }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.UnlockUpgrade: {
 | 
				
			||||||
 | 
					            const request = JSON.parse(String(req.body)) as IUnlockUpgradeRequest;
 | 
				
			||||||
 | 
					            const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					            let cost = 0;
 | 
				
			||||||
 | 
					            for (const focusType of request.FocusTypes) {
 | 
				
			||||||
 | 
					                cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
 | 
				
			||||||
 | 
					                inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            inventory.FocusXP![focusPolarity]! -= cost;
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                FocusTypes: request.FocusTypes,
 | 
				
			||||||
 | 
					                FocusPointCosts: { [focusPolarity]: cost }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.LevelUpUpgrade: {
 | 
				
			||||||
 | 
					            const request = JSON.parse(String(req.body)) as ILevelUpUpgradeRequest;
 | 
				
			||||||
 | 
					            const focusPolarity = focusTypeToPolarity(request.FocusInfos[0].ItemType);
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					            let cost = 0;
 | 
				
			||||||
 | 
					            for (const focusUpgrade of request.FocusInfos) {
 | 
				
			||||||
 | 
					                cost += focusUpgrade.FocusXpCost;
 | 
				
			||||||
 | 
					                const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
 | 
				
			||||||
 | 
					                focusUpgradeDb.Level = focusUpgrade.Level;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            inventory.FocusXP![focusPolarity]! -= cost;
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                FocusInfos: request.FocusInfos,
 | 
				
			||||||
 | 
					                FocusPointCosts: { [focusPolarity]: cost }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.SentTrainingAmplifier: {
 | 
				
			||||||
 | 
					            const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					            const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
 | 
				
			||||||
 | 
					                ModularParts: [
 | 
				
			||||||
 | 
					                    "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
 | 
				
			||||||
 | 
					                    "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
 | 
				
			||||||
 | 
					                    "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            occupySlot(inventory, InventorySlot.AMPS, false);
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json(inventoryChanges.OperatorAmps![0]);
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.UnbindUpgrade: {
 | 
				
			||||||
 | 
					            const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
 | 
				
			||||||
 | 
					            const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					            inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
 | 
				
			||||||
 | 
					            addMiscItems(inventory, [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
 | 
				
			||||||
 | 
					                    ItemCount: request.FocusTypes.length * -1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					            request.FocusTypes.forEach(type => {
 | 
				
			||||||
 | 
					                const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == type)!;
 | 
				
			||||||
 | 
					                focusUpgradeDb.IsUniversal = true;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                FocusTypes: request.FocusTypes,
 | 
				
			||||||
 | 
					                FocusPointCosts: {
 | 
				
			||||||
 | 
					                    [focusPolarity]: 750_000 * request.FocusTypes.length
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                MiscItemCosts: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
 | 
				
			||||||
 | 
					                        ItemCount: request.FocusTypes.length
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case FocusOperation.ConvertShard: {
 | 
				
			||||||
 | 
					            const request = JSON.parse(String(req.body)) as IConvertShardRequest;
 | 
				
			||||||
 | 
					            // Tally XP
 | 
				
			||||||
 | 
					            let xp = 0;
 | 
				
			||||||
 | 
					            for (const shard of request.Shards) {
 | 
				
			||||||
 | 
					                xp += shardValues[shard.ItemType as keyof typeof shardValues] * shard.ItemCount;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Send response
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                FocusPointGains: {
 | 
				
			||||||
 | 
					                    [request.Polarity]: xp
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                MiscItemCosts: request.Shards
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            // Commit added XP and removed shards to DB
 | 
				
			||||||
 | 
					            for (const shard of request.Shards) {
 | 
				
			||||||
 | 
					                shard.ItemCount *= -1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					            const polarity = request.Polarity;
 | 
				
			||||||
 | 
					            inventory.FocusXP ??= {};
 | 
				
			||||||
 | 
					            inventory.FocusXP[polarity] ??= 0;
 | 
				
			||||||
 | 
					            inventory.FocusXP[polarity] += xp;
 | 
				
			||||||
 | 
					            addMiscItems(inventory, request.Shards);
 | 
				
			||||||
 | 
					            await inventory.save();
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum FocusOperation {
 | 
				
			||||||
 | 
					    InstallLens = "1",
 | 
				
			||||||
 | 
					    UnlockWay = "2",
 | 
				
			||||||
 | 
					    UnlockUpgrade = "3",
 | 
				
			||||||
 | 
					    LevelUpUpgrade = "4",
 | 
				
			||||||
 | 
					    ActivateWay = "5",
 | 
				
			||||||
 | 
					    SentTrainingAmplifier = "7",
 | 
				
			||||||
 | 
					    UnbindUpgrade = "8",
 | 
				
			||||||
 | 
					    ConvertShard = "9"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// For UnlockWay & ActivateWay
 | 
				
			||||||
 | 
					interface IWayRequest {
 | 
				
			||||||
 | 
					    FocusType: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IUnlockUpgradeRequest {
 | 
				
			||||||
 | 
					    FocusTypes: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ILevelUpUpgradeRequest {
 | 
				
			||||||
 | 
					    FocusInfos: {
 | 
				
			||||||
 | 
					        ItemType: string;
 | 
				
			||||||
 | 
					        FocusXpCost: number;
 | 
				
			||||||
 | 
					        IsUniversal: boolean;
 | 
				
			||||||
 | 
					        Level: number;
 | 
				
			||||||
 | 
					        IsActiveAbility: boolean;
 | 
				
			||||||
 | 
					    }[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IUnbindUpgradeRequest {
 | 
				
			||||||
 | 
					    ShardTypes: string[];
 | 
				
			||||||
 | 
					    FocusTypes: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IConvertShardRequest {
 | 
				
			||||||
 | 
					    Shards: IMiscItem[];
 | 
				
			||||||
 | 
					    Polarity: TFocusPolarity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ISentTrainingAmplifierRequest {
 | 
				
			||||||
 | 
					    StartingWeaponType: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ILensInstallRequest {
 | 
				
			||||||
 | 
					    LensType: string;
 | 
				
			||||||
 | 
					    Category: TEquipmentKey;
 | 
				
			||||||
 | 
					    WeaponId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Works for ways & upgrades
 | 
				
			||||||
 | 
					const focusTypeToPolarity = (type: string): TFocusPolarity => {
 | 
				
			||||||
 | 
					    return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as TFocusPolarity;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const shardValues = {
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardCommonItem": 2_500,
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardSynthesizedItem": 5_000,
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem": 25_000,
 | 
				
			||||||
 | 
					    "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantTierTwoItem": 40_000
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/controllers/api/fusionTreasuresController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/controllers/api/fusionTreasuresController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportResources } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { addFusionTreasures, addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
				
			||||||
 | 
					import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IFusionTreasureRequest {
 | 
				
			||||||
 | 
					    oldTreasureName: string;
 | 
				
			||||||
 | 
					    newTreasureName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fusionTreasuresController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					    const request = JSON.parse(String(req.body)) as IFusionTreasureRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Swap treasures
 | 
				
			||||||
 | 
					    const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1);
 | 
				
			||||||
 | 
					    const newTreasure = parseFusionTreasure(request.newTreasureName, 1);
 | 
				
			||||||
 | 
					    const fusionTreasureChanges = [oldTreasure, newTreasure];
 | 
				
			||||||
 | 
					    addFusionTreasures(inventory, fusionTreasureChanges);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove consumed stars
 | 
				
			||||||
 | 
					    const miscItemChanges: IMiscItem[] = [];
 | 
				
			||||||
 | 
					    const filledSockets = newTreasure.Sockets & ~oldTreasure.Sockets;
 | 
				
			||||||
 | 
					    for (let i = 0; filledSockets >> i; ++i) {
 | 
				
			||||||
 | 
					        if ((filledSockets >> i) & 1) {
 | 
				
			||||||
 | 
					            //console.log("Socket", i, "has been filled with", ExportResources[oldTreasure.ItemType].sockets![i]);
 | 
				
			||||||
 | 
					            miscItemChanges.push({
 | 
				
			||||||
 | 
					                ItemType: ExportResources[oldTreasure.ItemType].sockets![i],
 | 
				
			||||||
 | 
					                ItemCount: -1
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    addMiscItems(inventory, miscItemChanges);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    // The response itself is the inventory changes for this endpoint.
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        MiscItems: miscItemChanges,
 | 
				
			||||||
 | 
					        FusionTreasures: fusionTreasureChanges
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/controllers/api/gardeningController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/controllers/api/gardeningController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { toStoreItem } from "../../services/itemDataService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
				
			||||||
 | 
					import type { IMongoDate } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import type { IMissionReward } from "../../types/missionTypes.ts";
 | 
				
			||||||
 | 
					import type { IGardeningClient, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
 | 
				
			||||||
 | 
					import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { dict_en, ExportResources } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const gardeningController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const data = getJSONfromString<IGardeningRequest>(String(req.body));
 | 
				
			||||||
 | 
					    if (data.Mode != "HarvestAll") {
 | 
				
			||||||
 | 
					        throw new Error(`unexpected gardening mode: ${data.Mode}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const [inventory, personalRooms] = await Promise.all([
 | 
				
			||||||
 | 
					        getInventory(accountId, "MiscItems"),
 | 
				
			||||||
 | 
					        getPersonalRooms(accountId, "Apartment")
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Harvest plants
 | 
				
			||||||
 | 
					    const inventoryChanges: IInventoryChanges = {};
 | 
				
			||||||
 | 
					    const rewards: Record<string, IMissionReward[][]> = {};
 | 
				
			||||||
 | 
					    for (const planter of personalRooms.Apartment.Gardening.Planters) {
 | 
				
			||||||
 | 
					        rewards[planter.Name] = [];
 | 
				
			||||||
 | 
					        for (const plant of planter.Plants) {
 | 
				
			||||||
 | 
					            const itemType =
 | 
				
			||||||
 | 
					                "/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" +
 | 
				
			||||||
 | 
					                plant.PlantType.substring(plant.PlantType.length - 1);
 | 
				
			||||||
 | 
					            const itemCount = Math.random() < 0.775 ? 2 : 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            addMiscItem(inventory, itemType, itemCount, inventoryChanges);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            rewards[planter.Name].push([
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StoreItem: toStoreItem(itemType),
 | 
				
			||||||
 | 
					                    TypeName: itemType,
 | 
				
			||||||
 | 
					                    ItemCount: itemCount,
 | 
				
			||||||
 | 
					                    DailyCooldown: false,
 | 
				
			||||||
 | 
					                    Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564,
 | 
				
			||||||
 | 
					                    TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`,
 | 
				
			||||||
 | 
					                    ProductCategory: "MiscItems"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Refresh garden
 | 
				
			||||||
 | 
					    personalRooms.Apartment.Gardening = createGarden();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all([inventory.save(), personalRooms.save()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1];
 | 
				
			||||||
 | 
					    const plant = planter.Plants[planter.Plants.length - 1];
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        GardenTagName: planter.Name,
 | 
				
			||||||
 | 
					        PlantType: plant.PlantType,
 | 
				
			||||||
 | 
					        PlotIndex: plant.PlotIndex,
 | 
				
			||||||
 | 
					        EndTime: toMongoDate(plant.EndTime),
 | 
				
			||||||
 | 
					        InventoryChanges: inventoryChanges,
 | 
				
			||||||
 | 
					        Gardening: personalRooms.toJSON<IPersonalRoomsClient>().Apartment.Gardening,
 | 
				
			||||||
 | 
					        Rewards: rewards
 | 
				
			||||||
 | 
					    } satisfies IGardeningResponse);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IGardeningRequest {
 | 
				
			||||||
 | 
					    Mode: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IGardeningResponse {
 | 
				
			||||||
 | 
					    GardenTagName: string;
 | 
				
			||||||
 | 
					    PlantType: string;
 | 
				
			||||||
 | 
					    PlotIndex: number;
 | 
				
			||||||
 | 
					    EndTime: IMongoDate;
 | 
				
			||||||
 | 
					    InventoryChanges: IInventoryChanges;
 | 
				
			||||||
 | 
					    Gardening: IGardeningClient;
 | 
				
			||||||
 | 
					    Rewards: Record<string, IMissionReward[][]>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/controllers/api/genericUpdateController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/controllers/api/genericUpdateController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { updateGeneric } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
				
			||||||
 | 
					import type { IGenericUpdate } from "../../types/genericUpdate.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This endpoint used to be /api/genericUpdate.php, but sometime around the Jade Shadows update, it was changed to /api/updateNodeIntros.php.
 | 
				
			||||||
 | 
					// SpaceNinjaServer supports both endpoints right now.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const genericUpdateController: RequestHandler = async (request, response) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(request);
 | 
				
			||||||
 | 
					    const update = getJSONfromString<IGenericUpdate>(String(request.body));
 | 
				
			||||||
 | 
					    response.json(await updateGeneric(update, accountId));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { genericUpdateController };
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/controllers/api/getAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/controllers/api/getAllianceController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import { Alliance, Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAllianceClient } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getAllianceController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId");
 | 
				
			||||||
 | 
					    if (inventory.GuildId) {
 | 
				
			||||||
 | 
					        const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!;
 | 
				
			||||||
 | 
					        if (guild.AllianceId) {
 | 
				
			||||||
 | 
					            const alliance = (await Alliance.findById(guild.AllianceId))!;
 | 
				
			||||||
 | 
					            res.json(await getAllianceClient(alliance, guild));
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// POST request since U27
 | 
				
			||||||
 | 
					/*interface IGetAllianceRequest {
 | 
				
			||||||
 | 
					    memberCount: number;
 | 
				
			||||||
 | 
					    clanLeaderName: string;
 | 
				
			||||||
 | 
					    clanLeaderId: string;
 | 
				
			||||||
 | 
					}*/
 | 
				
			||||||
							
								
								
									
										10
									
								
								src/controllers/api/getDailyDealStockLevelsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/controllers/api/getDailyDealStockLevelsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					import { DailyDeal } from "../../models/worldStateModel.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!;
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        StoreItem: req.query.productName,
 | 
				
			||||||
 | 
					        AmountSold: dailyDeal.AmountSold
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										54
									
								
								src/controllers/api/getFriendsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/controllers/api/getFriendsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { Friendship } from "../../models/friendModel.ts";
 | 
				
			||||||
 | 
					import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
				
			||||||
 | 
					import type { Request, RequestHandler, Response } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// POST with {} instead of GET as of 38.5.0
 | 
				
			||||||
 | 
					export const getFriendsController: RequestHandler = async (req: Request, res: Response) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const response: IGetFriendsResponse = {
 | 
				
			||||||
 | 
					        Current: [],
 | 
				
			||||||
 | 
					        IncomingFriendRequests: [],
 | 
				
			||||||
 | 
					        OutgoingFriendRequests: []
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const [internalFriendships, externalFriendships] = await Promise.all([
 | 
				
			||||||
 | 
					        Friendship.find({ owner: accountId }),
 | 
				
			||||||
 | 
					        Friendship.find({ friend: accountId }, "owner Note")
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					    for (const externalFriendship of externalFriendships) {
 | 
				
			||||||
 | 
					        if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
 | 
				
			||||||
 | 
					            response.IncomingFriendRequests.push({
 | 
				
			||||||
 | 
					                _id: toOid(externalFriendship.owner),
 | 
				
			||||||
 | 
					                Note: externalFriendship.Note
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const internalFriendship of internalFriendships) {
 | 
				
			||||||
 | 
					        const friendInfo: IFriendInfo = {
 | 
				
			||||||
 | 
					            _id: toOid(internalFriendship.friend)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        if (externalFriendships.find(x => x.owner.equals(internalFriendship.friend))) {
 | 
				
			||||||
 | 
					            response.Current.push(friendInfo);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            response.OutgoingFriendRequests.push(friendInfo);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const promises: Promise<void>[] = [];
 | 
				
			||||||
 | 
					    for (const arr of Object.values(response)) {
 | 
				
			||||||
 | 
					        for (const friendInfo of arr) {
 | 
				
			||||||
 | 
					            promises.push(addAccountDataToFriendInfo(friendInfo));
 | 
				
			||||||
 | 
					            promises.push(addInventoryDataToFriendInfo(friendInfo));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await Promise.all(promises);
 | 
				
			||||||
 | 
					    res.json(response);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// interface IGetFriendsResponse {
 | 
				
			||||||
 | 
					//     Current: IFriendInfo[];
 | 
				
			||||||
 | 
					//     IncomingFriendRequests: IFriendInfo[];
 | 
				
			||||||
 | 
					//     OutgoingFriendRequests: IFriendInfo[];
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					type IGetFriendsResponse = Record<"Current" | "IncomingFriendRequests" | "OutgoingFriendRequests", IFriendInfo[]>;
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/controllers/api/getGuildContributionsController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/controllers/api/getGuildContributionsController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import { GuildMember } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IGuildMemberClient } from "../../types/guildTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getGuildContributionsController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const guildId = (await getInventory(accountId, "GuildId")).GuildId;
 | 
				
			||||||
 | 
					    const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!;
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					        _id: { $oid: req.query.buddyId as string },
 | 
				
			||||||
 | 
					        RegularCreditsContributed: guildMember.RegularCreditsContributed,
 | 
				
			||||||
 | 
					        PremiumCreditsContributed: guildMember.PremiumCreditsContributed,
 | 
				
			||||||
 | 
					        MiscItemsContributed: guildMember.MiscItemsContributed,
 | 
				
			||||||
 | 
					        ConsumablesContributed: [], // ???
 | 
				
			||||||
 | 
					        ShipDecorationsContributed: guildMember.ShipDecorationsContributed
 | 
				
			||||||
 | 
					    } satisfies Partial<IGuildMemberClient>);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/controllers/api/getGuildController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/controllers/api/getGuildController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { logger } from "../../utils/logger.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { createUniqueClanName, getGuildClient } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getGuildController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(account._id.toString(), "GuildId");
 | 
				
			||||||
 | 
					    if (inventory.GuildId) {
 | 
				
			||||||
 | 
					        const guild = await Guild.findById(inventory.GuildId);
 | 
				
			||||||
 | 
					        if (guild) {
 | 
				
			||||||
 | 
					            // Handle guilds created before we added discriminators
 | 
				
			||||||
 | 
					            if (guild.Name.indexOf("#") == -1) {
 | 
				
			||||||
 | 
					                guild.Name = await createUniqueClanName(guild.Name);
 | 
				
			||||||
 | 
					                await guild.save();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (guild.CeremonyResetDate && Date.now() >= guild.CeremonyResetDate.getTime()) {
 | 
				
			||||||
 | 
					                logger.debug(`ascension ceremony is over`);
 | 
				
			||||||
 | 
					                guild.CeremonyEndo = undefined;
 | 
				
			||||||
 | 
					                guild.CeremonyContributors = undefined;
 | 
				
			||||||
 | 
					                guild.CeremonyResetDate = undefined;
 | 
				
			||||||
 | 
					                await guild.save();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            res.json(await getGuildClient(guild, account));
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/controllers/api/getGuildDojoController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/controllers/api/getGuildDojoController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
 | 
					import { Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getDojoClient } from "../../services/guildService.ts";
 | 
				
			||||||
 | 
					import { Account } from "../../models/loginModel.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getGuildDojoController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const guildId = req.query.guildId as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const guild = await Guild.findById(guildId);
 | 
				
			||||||
 | 
					    if (!guild) {
 | 
				
			||||||
 | 
					        res.status(404).end();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Populate dojo info if not present
 | 
				
			||||||
 | 
					    if (guild.DojoComponents.length == 0) {
 | 
				
			||||||
 | 
					        guild.DojoComponents.push({
 | 
				
			||||||
 | 
					            _id: new Types.ObjectId(),
 | 
				
			||||||
 | 
					            pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
 | 
				
			||||||
 | 
					            ppf: "",
 | 
				
			||||||
 | 
					            CompletionTime: new Date(Date.now()),
 | 
				
			||||||
 | 
					            DecoCapacity: 600
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await guild.save();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {};
 | 
				
			||||||
 | 
					    const account = await Account.findById(req.query.accountId as string);
 | 
				
			||||||
 | 
					    res.json(await getDojoClient(guild, 0, payload.ComponentId, account?.BuildLabel));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IGetGuildDojoRequest {
 | 
				
			||||||
 | 
					    ComponentId?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/controllers/api/getGuildEventScoreController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/controllers/api/getGuildEventScoreController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getGuildEventScoreController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(account._id.toString(), "GuildId");
 | 
				
			||||||
 | 
					    const guild = await Guild.findById(inventory.GuildId);
 | 
				
			||||||
 | 
					    const goalId = req.query.goalId as string;
 | 
				
			||||||
 | 
					    if (guild && guild.GoalProgress && goalId) {
 | 
				
			||||||
 | 
					        const goal = guild.GoalProgress.find(x => x.goalId.toString() == goalId);
 | 
				
			||||||
 | 
					        if (goal) {
 | 
				
			||||||
 | 
					            res.json({
 | 
				
			||||||
 | 
					                Tier: guild.Tier,
 | 
				
			||||||
 | 
					                GoalProgress: {
 | 
				
			||||||
 | 
					                    Count: goal.Count,
 | 
				
			||||||
 | 
					                    Tag: goal.Tag,
 | 
				
			||||||
 | 
					                    _id: { $oid: goal.goalId }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    res.json({});
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/controllers/api/getGuildLogController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/controllers/api/getGuildLogController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { Guild } from "../../models/guildModel.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IMongoDate } from "../../types/commonTypes.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getGuildLogController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "GuildId");
 | 
				
			||||||
 | 
					    if (inventory.GuildId) {
 | 
				
			||||||
 | 
					        const guild = await Guild.findById(inventory.GuildId);
 | 
				
			||||||
 | 
					        if (guild) {
 | 
				
			||||||
 | 
					            const log: Record<string, IGuildLogEntryClient[]> = {
 | 
				
			||||||
 | 
					                RoomChanges: [],
 | 
				
			||||||
 | 
					                TechChanges: [],
 | 
				
			||||||
 | 
					                RosterActivity: [],
 | 
				
			||||||
 | 
					                StandingsUpdates: [],
 | 
				
			||||||
 | 
					                ClassChanges: []
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            guild.RoomChanges?.forEach(entry => {
 | 
				
			||||||
 | 
					                log.RoomChanges.push({
 | 
				
			||||||
 | 
					                    dateTime: toMongoDate(entry.dateTime ?? new Date()),
 | 
				
			||||||
 | 
					                    entryType: entry.entryType,
 | 
				
			||||||
 | 
					                    details: entry.details
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            guild.TechChanges?.forEach(entry => {
 | 
				
			||||||
 | 
					                log.TechChanges.push({
 | 
				
			||||||
 | 
					                    dateTime: toMongoDate(entry.dateTime ?? new Date()),
 | 
				
			||||||
 | 
					                    entryType: entry.entryType,
 | 
				
			||||||
 | 
					                    details: entry.details
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            guild.RosterActivity?.forEach(entry => {
 | 
				
			||||||
 | 
					                log.RosterActivity.push({
 | 
				
			||||||
 | 
					                    dateTime: toMongoDate(entry.dateTime),
 | 
				
			||||||
 | 
					                    entryType: entry.entryType,
 | 
				
			||||||
 | 
					                    details: entry.details
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            guild.ClassChanges?.forEach(entry => {
 | 
				
			||||||
 | 
					                log.ClassChanges.push({
 | 
				
			||||||
 | 
					                    dateTime: toMongoDate(entry.dateTime),
 | 
				
			||||||
 | 
					                    entryType: entry.entryType,
 | 
				
			||||||
 | 
					                    details: entry.details
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            res.json(log);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    res.sendStatus(200);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IGuildLogEntryClient {
 | 
				
			||||||
 | 
					    dateTime: IMongoDate;
 | 
				
			||||||
 | 
					    entryType: number;
 | 
				
			||||||
 | 
					    details: number | string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/controllers/api/getIgnoredUsersController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/controllers/api/getIgnoredUsersController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					import { Account, Ignore } from "../../models/loginModel.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
				
			||||||
 | 
					import { parallelForeach } from "../../utils/async-utils.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getIgnoredUsersController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const ignores = await Ignore.find({ ignorer: accountId });
 | 
				
			||||||
 | 
					    const ignoredUsers: IFriendInfo[] = [];
 | 
				
			||||||
 | 
					    await parallelForeach(ignores, async ignore => {
 | 
				
			||||||
 | 
					        const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
 | 
				
			||||||
 | 
					        ignoredUsers.push({
 | 
				
			||||||
 | 
					            _id: toOid(ignore.ignoree),
 | 
				
			||||||
 | 
					            DisplayName: ignoreeAccount.DisplayName + ""
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    res.json({ IgnoredUsers: ignoredUsers });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/controllers/api/getNewRewardSeedController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/controllers/api/getNewRewardSeedController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { generateRewardSeed } from "../../services/rngService.ts";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getNewRewardSeedController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rewardSeed = generateRewardSeed();
 | 
				
			||||||
 | 
					    await Inventory.updateOne(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            accountOwnerId: accountId
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RewardSeed: rewardSeed
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    res.json({ rewardSeed: rewardSeed });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										62
									
								
								src/controllers/api/getPastWeeklyChallengesController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/controllers/api/getPastWeeklyChallengesController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
 | 
				
			||||||
 | 
					import { unixTimesInMs } from "../../constants/timeConstants.ts";
 | 
				
			||||||
 | 
					import type { ISeasonChallenge } from "../../types/worldStateTypes.ts";
 | 
				
			||||||
 | 
					import { ExportChallenges } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress");
 | 
				
			||||||
 | 
					    const worldState = getWorldState(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (worldState.SeasonInfo) {
 | 
				
			||||||
 | 
					        const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag);
 | 
				
			||||||
 | 
					        const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong);
 | 
				
			||||||
 | 
					        const nightwaveSeason = worldState.SeasonInfo.Season;
 | 
				
			||||||
 | 
					        const timeMs = worldState.Time * 1000;
 | 
				
			||||||
 | 
					        const completedChallengesIds = new Set<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inventory.SeasonChallengeHistory.forEach(challengeHistory => {
 | 
				
			||||||
 | 
					            const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1;
 | 
				
			||||||
 | 
					            if (nightwaveSeason == entryNightwaveSeason) {
 | 
				
			||||||
 | 
					                const meta = Object.entries(ExportChallenges).find(
 | 
				
			||||||
 | 
					                    ([key]) => key.split("/").pop() === challengeHistory.challenge
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                if (meta) {
 | 
				
			||||||
 | 
					                    const [, challengeMeta] = meta;
 | 
				
			||||||
 | 
					                    const challengeProgress = inventory.ChallengeProgress.find(
 | 
				
			||||||
 | 
					                        c => c.Name === challengeHistory.challenge
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) {
 | 
				
			||||||
 | 
					                        completedChallengesIds.add(challengeHistory.id);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const PastWeeklyChallenges: ISeasonChallenge[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) {
 | 
				
			||||||
 | 
					            const tempActs: ISeasonChallenge[] = [];
 | 
				
			||||||
 | 
					            pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (const act of tempActs) {
 | 
				
			||||||
 | 
					                if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) {
 | 
				
			||||||
 | 
					                    if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) {
 | 
				
			||||||
 | 
					                        act.Permanent = true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    PastWeeklyChallenges.push(act);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            week--;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.json({ PastWeeklyChallenges: PastWeeklyChallenges });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										39
									
								
								src/controllers/api/getShipController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/controllers/api/getShipController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { config } from "../../services/configService.ts";
 | 
				
			||||||
 | 
					import allShipFeatures from "../../../static/fixed_responses/allShipFeatures.json" with { type: "json" };
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
				
			||||||
 | 
					import type { IGetShipResponse, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
 | 
				
			||||||
 | 
					import { getLoadout } from "../../services/loadoutService.ts";
 | 
				
			||||||
 | 
					import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getShipController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const personalRoomsDb = await getPersonalRooms(accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future.
 | 
				
			||||||
 | 
					    if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) {
 | 
				
			||||||
 | 
					        personalRoomsDb.Apartment.Gardening = createGarden();
 | 
				
			||||||
 | 
					        await personalRoomsDb.save();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
 | 
				
			||||||
 | 
					    const loadout = await getLoadout(accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getShipResponse: IGetShipResponse = {
 | 
				
			||||||
 | 
					        ShipOwnerId: accountId,
 | 
				
			||||||
 | 
					        LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
 | 
				
			||||||
 | 
					        Ship: {
 | 
				
			||||||
 | 
					            ...personalRooms.Ship,
 | 
				
			||||||
 | 
					            ShipId: toOid(personalRoomsDb.activeShipId)
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        Apartment: personalRooms.Apartment,
 | 
				
			||||||
 | 
					        TailorShop: personalRooms.TailorShop
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (config.unlockAllShipFeatures) {
 | 
				
			||||||
 | 
					        getShipResponse.Ship.Features = allShipFeatures;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(getShipResponse);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/controllers/api/getVendorInfoController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/controllers/api/getVendorInfoController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					import type { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "../../services/serversideVendorsService.ts";
 | 
				
			||||||
 | 
					import { getInventory } from "../../services/inventoryService.ts";
 | 
				
			||||||
 | 
					import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
				
			||||||
 | 
					import { config } from "../../services/configService.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getVendorInfoController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    let manifest = getVendorManifestByTypeName(req.query.vendor as string);
 | 
				
			||||||
 | 
					    if (!manifest) {
 | 
				
			||||||
 | 
					        throw new Error(`Unknown vendor: ${req.query.vendor as string}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // For testing purposes, authenticating with this endpoint is optional here, but would be required on live.
 | 
				
			||||||
 | 
					    if (req.query.accountId) {
 | 
				
			||||||
 | 
					        const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					        const inventory = await getInventory(accountId);
 | 
				
			||||||
 | 
					        manifest = applyStandingToVendorManifest(inventory, manifest);
 | 
				
			||||||
 | 
					        if (config.dev?.keepVendorsExpired) {
 | 
				
			||||||
 | 
					            manifest = {
 | 
				
			||||||
 | 
					                VendorInfo: {
 | 
				
			||||||
 | 
					                    ...manifest.VendorInfo,
 | 
				
			||||||
 | 
					                    Expiry: { $date: { $numberLong: "0" } }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(manifest);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user