mirror of
https://github.com/PretendoNetwork/miiverse-api.git
synced 2026-07-17 09:02:25 -05:00
commit
2af8d74487
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
*.js
|
||||
59
.eslintrc.json
Normal file
59
.eslintrc.json
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"commonjs": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"globals": {
|
||||
"BigInt": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"require-atomic-updates": "warn",
|
||||
"no-case-declarations": "off",
|
||||
"no-empty": "off",
|
||||
"no-console": "off",
|
||||
"linebreak-style": "off",
|
||||
"no-global-assign": "off",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"no-extra-semi": "off",
|
||||
"@typescript-eslint/no-extra-semi": "error",
|
||||
"@typescript-eslint/no-empty-interface": "warn",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/typedef": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"keyword-spacing": "off",
|
||||
"@typescript-eslint/keyword-spacing": "error",
|
||||
"curly": "error",
|
||||
"brace-style": "error",
|
||||
"one-var": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
139
.gitignore
vendored
139
.gitignore
vendored
|
|
@ -1,5 +1,136 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# custom
|
||||
certs
|
||||
config.json
|
||||
src/logs
|
||||
logs
|
||||
dist
|
||||
newman
|
||||
4
.postman/api
Normal file
4
.postman/api
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
|
||||
apis[] = {"apiId":"093b0c82-779f-419f-9aca-029910a07e65"}
|
||||
configVersion = 1.0.0
|
||||
type = api
|
||||
20
.postman/api_093b0c82-779f-419f-9aca-029910a07e65
Normal file
20
.postman/api_093b0c82-779f-419f-9aca-029910a07e65
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY
|
||||
configVersion = 1.0.0
|
||||
type = apiEntityData
|
||||
|
||||
[config]
|
||||
id = 093b0c82-779f-419f-9aca-029910a07e65
|
||||
|
||||
[config.relations]
|
||||
|
||||
[config.relations.collections]
|
||||
rootDirectory = postman/collections
|
||||
files[] = {"id":"19511066-cb605935-6073-42c5-afa9-96d9a19c70a3","path":"Communities.json","metaData":{}}
|
||||
files[] = {"id":"19511066-2bb4ba1f-8e09-44b9-9ad1-5f59455c0ba7","path":"People.json","metaData":{}}
|
||||
|
||||
[config.relations.collections.metaData]
|
||||
|
||||
[config.relations.apiDefinition]
|
||||
rootDirectory = postman/schemas
|
||||
|
||||
[config.relations.apiDefinition.metaData]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"http": {
|
||||
"port": 8081
|
||||
},
|
||||
"account_server": "account.miiverse.cc:8080",
|
||||
"secret": "806590af0a2bf0cf9ce54b0b5c1493a1",
|
||||
"X-Nintendo-Client-ID": "a2efa818a34fa16b8afbc8a74eba3eda",
|
||||
"X-Nintendo-Client-Secret": "c91cdb5658bd4954ade78533a339cf9a",
|
||||
"mongoose": {
|
||||
"uri": "mongodb://localhost:27017",
|
||||
"database": "Miiverse",
|
||||
"options": {
|
||||
"useNewUrlParser": true,
|
||||
"useUnifiedTopology": true
|
||||
}
|
||||
},
|
||||
"authorized_PNIDs" : [
|
||||
1420636409, 1657414049, 1587572630
|
||||
]
|
||||
}
|
||||
8000
package-lock.json
generated
8000
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
109
package.json
109
package.json
|
|
@ -1,41 +1,72 @@
|
|||
{
|
||||
"name": "pretendoverse",
|
||||
"version": "1.0.0",
|
||||
"description": "Miiverse API Server",
|
||||
"main": "./src/server.js",
|
||||
"scripts": {
|
||||
"start": "node ./src/server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Jemma",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"colors": "^1.4.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.0",
|
||||
"express-subdomain": "^1.0.5",
|
||||
"fs-extra": "^9.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"mongoose": "^5.9.16",
|
||||
"mongoose-unique-validator": "^2.0.3",
|
||||
"morgan": "^1.10.0",
|
||||
"node-rsa": "^1.0.8",
|
||||
"node-snowflake": "0.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"pngjs": "^5.0.0",
|
||||
"sanitize": "^2.1.0",
|
||||
"tga": "^1.0.3",
|
||||
"xmlbuilder": "^13.0.2",
|
||||
"xmlbuilder2": "0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"multer": "^1.4.2",
|
||||
"object-to-xml": "^2.0.0",
|
||||
"request": "^2.88.2",
|
||||
"string-sanitizer": "^1.1.1",
|
||||
"xml2json": "^0.12.0"
|
||||
}
|
||||
"name": "miiverse-api",
|
||||
"version": "2.0.0",
|
||||
"description": "Miiverse API Server",
|
||||
"main": "./dist/server.js",
|
||||
"scripts": {
|
||||
"lint": "npx eslint .",
|
||||
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias",
|
||||
"clean": "rimraf ./dist",
|
||||
"start": "node --enable-source-maps .",
|
||||
"test": "ts-node test/test.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Pretendo Network",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Jemma Poffinbarger",
|
||||
"email": "contact@jemsoftware.dev",
|
||||
"url": "https://jemsoftware.dev/"
|
||||
},
|
||||
{
|
||||
"name": "Jonathan Barrow",
|
||||
"email": "jonbarrow1998@gmail.com",
|
||||
"url": "https://jonbarrow.dev/"
|
||||
}
|
||||
],
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@pretendonetwork/grpc": "^1.0.3",
|
||||
"aws-sdk": "^2.1204.0",
|
||||
"colors": "^1.4.0",
|
||||
"crc": "^4.3.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-subdomain": "^1.0.5",
|
||||
"fs-extra": "^9.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^6.10.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nice-grpc": "^2.0.0",
|
||||
"node-rsa": "^1.0.8",
|
||||
"node-snowflake": "0.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"pngjs": "^5.0.0",
|
||||
"tga": "^1.0.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/newman": "^5.3.4",
|
||||
"@types/node-rsa": "^1.1.1",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||
"@typescript-eslint/parser": "^5.59.0",
|
||||
"axios": "^1.3.6",
|
||||
"eslint": "^8.38.0",
|
||||
"newman": "^6.0.0",
|
||||
"ora": "^5.4.1",
|
||||
"postman-collection": "^4.1.7",
|
||||
"table": "^6.8.1",
|
||||
"ts-unused-exports": "^9.0.4",
|
||||
"tsc-alias": "^1.8.5",
|
||||
"typescript": "^5.0.4",
|
||||
"xmlbuilder2": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
525
postman/collections/Communities.json
Normal file
525
postman/collections/Communities.json
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "cb605935-6073-42c5-afa9-96d9a19c70a3",
|
||||
"name": "Communities",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_uid": "19511066-cb605935-6073-42c5-afa9-96d9a19c70a3"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "GET /v1/communities",
|
||||
"item": [
|
||||
{
|
||||
"name": "Sub-Community",
|
||||
"id": "0c9cf1fc-61a0-4deb-b910-e54701e39ae5",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_MarioVsDK}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Single Community",
|
||||
"id": "77083b95-581f-40cf-8d83-ade183923ba0",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Invalid Title ID",
|
||||
"id": "9bb6971f-f95a-4a53-960a-e9383854c2cc",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Bad_TID}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Invalid ParamPack Format",
|
||||
"id": "7e5058a3-5faa-4016-a86d-e288a1e3355f",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Bad Format}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "",
|
||||
"value": "",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"id": "9a31cc53-451e-4fb2-aace-ee3db9641243"
|
||||
},
|
||||
{
|
||||
"name": "GET /v1/communities/:id/posts",
|
||||
"item": [
|
||||
{
|
||||
"name": "No Params",
|
||||
"id": "3ea312f0-d205-4c25-b11a-218d0b28854d",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Limit",
|
||||
"id": "84d199be-78f5-4d65-955b-08f47b7ed1e7",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts?limit=2",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Search Key",
|
||||
"id": "3754a070-9dbc-4b35-b0b8-4480d5d5aa38",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_ACPlaza}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts?search_key=sza",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "search_key",
|
||||
"value": "sza"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Type memo",
|
||||
"id": "71362a5d-d976-402d-80d5-80e593d6ac0f",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_ACPlaza}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts?type=memo",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "memo"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "By Followings",
|
||||
"id": "70355c69-a29f-4fbc-bdb4-495b579b21f2",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_ACPlaza}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts?by=followings",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "by",
|
||||
"value": "followings"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "By Self",
|
||||
"id": "823b727d-f295-4cb3-9dbc-12c55a716ea1",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts?by=self",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "by",
|
||||
"value": "self"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Allow Spoiler",
|
||||
"id": "875cd973-3de9-429c-b61b-ba80320ffd22",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/communities/0/posts?allow_spoiler=1",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"communities",
|
||||
"0",
|
||||
"posts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "allow_spoiler",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"id": "9078f117-f05e-459b-ac03-59be51f48ecf"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "apikey",
|
||||
"apikey": [
|
||||
{
|
||||
"key": "value",
|
||||
"value": "{{ServiceToken}}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "X-Nintendo-ServiceToken",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"id": "64933ef4-dc38-4d6f-a8c5-54b93decd369",
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"id": "f00757f9-3faf-40da-89ff-0f3fb356678f",
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const headerSchema = {",
|
||||
" \"type\": \"object\",",
|
||||
" \"properties\": {",
|
||||
" \"result\": {",
|
||||
" \"type\": \"object\",",
|
||||
" \"properties\": {",
|
||||
" \"has_error\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 1",
|
||||
" },",
|
||||
" \"version\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 1",
|
||||
" },",
|
||||
" \"expire\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"pattern\": \"^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?\"",
|
||||
" },",
|
||||
" \"request_name\": {",
|
||||
" \"type\": \"string\"",
|
||||
" }",
|
||||
" },",
|
||||
" \"required\": [",
|
||||
" \"has_error\",",
|
||||
" \"version\",",
|
||||
" \"request_name\"",
|
||||
" ]",
|
||||
" },",
|
||||
"",
|
||||
" },",
|
||||
" \"required\": [",
|
||||
" \"result\"",
|
||||
" ]",
|
||||
"};",
|
||||
"",
|
||||
"const errorSchema = {",
|
||||
" \"type\": \"object\",",
|
||||
" \"properties\": {",
|
||||
" \"result\": {",
|
||||
" \"type\": \"object\",",
|
||||
" \"properties\": {",
|
||||
" \"has_error\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 1",
|
||||
" },",
|
||||
" \"version\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 1",
|
||||
" },",
|
||||
" \"code\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 3",
|
||||
" },",
|
||||
" \"error_code\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 4",
|
||||
" },",
|
||||
" \"message\": {",
|
||||
" \"type\": \"string\"",
|
||||
" }",
|
||||
" },",
|
||||
" \"required\": [",
|
||||
" \"has_error\",",
|
||||
" \"version\",",
|
||||
" \"code\",",
|
||||
" \"error_code\",",
|
||||
" \"message\"",
|
||||
" ]",
|
||||
" },",
|
||||
"",
|
||||
" },",
|
||||
" \"required\": [",
|
||||
" \"result\"",
|
||||
" ]",
|
||||
"};",
|
||||
"",
|
||||
"pm.test(\"Valid XML Response Header\", function () {",
|
||||
" var json = xml2Json(pm.response.text());",
|
||||
" console.log(pm.response.code);",
|
||||
" if(pm.response.code === 200)",
|
||||
" pm.expect(json).to.have.jsonSchema(headerSchema);",
|
||||
" else ",
|
||||
" pm.expect(json).to.have.jsonSchema(errorSchema);",
|
||||
" ",
|
||||
"})"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
167
postman/collections/People.json
Normal file
167
postman/collections/People.json
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "2bb4ba1f-8e09-44b9-9ad1-5f59455c0ba7",
|
||||
"name": "People",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_uid": "19511066-2bb4ba1f-8e09-44b9-9ad1-5f59455c0ba7"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "GET /v1/people",
|
||||
"item": [
|
||||
{
|
||||
"name": "Friends",
|
||||
"id": "688f1727-636a-4a41-ba96-a9f52c3fe2b9",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/people?relation=friend&distinct_pid=1",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"people"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "relation",
|
||||
"value": "friend"
|
||||
},
|
||||
{
|
||||
"key": "distinct_pid",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Following",
|
||||
"id": "00b11bd6-25f1-4c16-ac6f-d8f17895ca1c",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-Nintendo-ParamPack",
|
||||
"value": "{{PP_Splatoon}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{DOMAIN}}/v1/people?relation=following&distinct_pid=1",
|
||||
"host": [
|
||||
"{{DOMAIN}}"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"people"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "relation",
|
||||
"value": "following"
|
||||
},
|
||||
{
|
||||
"key": "distinct_pid",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"id": "826dbc90-90a0-483c-a94f-5d93f48a1804"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "apikey",
|
||||
"apikey": [
|
||||
{
|
||||
"key": "value",
|
||||
"value": "{{ServiceToken}}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "X-Nintendo-ServiceToken",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"id": "86097a74-4b68-4b97-b176-45bcbedd2abf",
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"id": "540f44d8-e877-4739-ad86-0e471f183a27",
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const headerSchema = {",
|
||||
" \"type\": \"object\",",
|
||||
" \"properties\": {",
|
||||
" \"result\": {",
|
||||
" \"type\": \"object\",",
|
||||
" \"properties\": {",
|
||||
" \"has_error\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 1",
|
||||
" },",
|
||||
" \"version\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"maxLength\": 1",
|
||||
" },",
|
||||
" \"expire\": {",
|
||||
" \"type\": \"string\",",
|
||||
" \"pattern\": \"^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?\"",
|
||||
" },",
|
||||
" \"request_name\": {",
|
||||
" \"type\": \"string\"",
|
||||
" }",
|
||||
" },",
|
||||
" \"required\": [",
|
||||
" \"has_error\",",
|
||||
" \"version\",",
|
||||
" \"request_name\"",
|
||||
" ]",
|
||||
" },",
|
||||
"",
|
||||
" },",
|
||||
" \"required\": [",
|
||||
" \"result\"",
|
||||
" ]",
|
||||
"};",
|
||||
"",
|
||||
"pm.test(\"Valid XML Response Header\", function () {",
|
||||
" var json = xml2Json(pm.response.text());",
|
||||
" pm.expect(json).to.have.jsonSchema(headerSchema);",
|
||||
"})"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
27
src/cache.ts
Normal file
27
src/cache.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export default class Cache<T> {
|
||||
private data?: T;
|
||||
private expireAt: number;
|
||||
private cacheTime: number;
|
||||
|
||||
constructor(cacheTime: number) {
|
||||
this.expireAt = Date.now() + cacheTime;
|
||||
this.cacheTime = cacheTime;
|
||||
}
|
||||
|
||||
valid(): boolean {
|
||||
if (!this.data || Date.now() >= this.expireAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
update(data: T): void {
|
||||
this.expireAt = Date.now() + this.cacheTime;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get(): T | undefined {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
113
src/config-manager.ts
Normal file
113
src/config-manager.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import fs from 'fs-extra';
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
import { LOG_INFO, LOG_WARN, LOG_ERROR } from '@/logger';
|
||||
import { Config } from '@/types/common/config';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
LOG_INFO('Loading config');
|
||||
|
||||
let mongooseConnectOptionsMain: mongoose.ConnectOptions = {};
|
||||
|
||||
if (process.env.PN_MIIVERSE_API_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH) {
|
||||
mongooseConnectOptionsMain = fs.readJSONSync(process.env.PN_MIIVERSE_API_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH);
|
||||
} else {
|
||||
LOG_WARN('No Mongoose connection options found for main connection. To add connection options, set PN_MIIVERSE_API_CONFIG_MONGOOSE_CONNECT_OPTIONS_PATH to the path of your options JSON file');
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
http: {
|
||||
port: Number(process.env.PN_MIIVERSE_API_CONFIG_HTTP_PORT || '')
|
||||
},
|
||||
account_server_address: process.env.PN_MIIVERSE_API_CONFIG_ACCOUNT_SERVER_ADDRESS || '',
|
||||
mongoose: {
|
||||
connection_string: process.env.PN_MIIVERSE_API_CONFIG_MONGO_CONNECTION_STRING || '',
|
||||
options: mongooseConnectOptionsMain
|
||||
},
|
||||
s3: {
|
||||
endpoint: process.env.PN_MIIVERSE_API_CONFIG_S3_ENDPOINT || '',
|
||||
key: process.env.PN_MIIVERSE_API_CONFIG_S3_ACCESS_KEY || '',
|
||||
secret: process.env.PN_MIIVERSE_API_CONFIG_S3_ACCESS_SECRET || ''
|
||||
},
|
||||
grpc: {
|
||||
friends: {
|
||||
ip: process.env.PN_MIIVERSE_API_CONFIG_GRPC_FRIENDS_IP || '',
|
||||
port: Number(process.env.PN_MIIVERSE_API_CONFIG_GRPC_FRIENDS_PORT || ''),
|
||||
api_key: process.env.PN_MIIVERSE_API_CONFIG_GRPC_FRIENDS_API_KEY || ''
|
||||
},
|
||||
account: {
|
||||
ip: process.env.PN_MIIVERSE_API_CONFIG_GRPC_ACCOUNT_IP || '',
|
||||
port: Number(process.env.PN_MIIVERSE_API_CONFIG_GRPC_ACCOUNT_PORT || ''),
|
||||
api_key: process.env.PN_MIIVERSE_API_CONFIG_GRPC_ACCOUNT_API_KEY || ''
|
||||
}
|
||||
},
|
||||
aes_key: process.env.PN_MIIVERSE_API_CONFIG_AES_KEY || ''
|
||||
};
|
||||
|
||||
LOG_INFO('Config loaded, checking integrity');
|
||||
|
||||
if (!config.http.port) {
|
||||
LOG_ERROR('Failed to find HTTP port. Set the PN_MIIVERSE_API_CONFIG_HTTP_PORT environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.account_server_address) {
|
||||
LOG_ERROR('Failed to find account server address. Set the PN_MIIVERSE_API_CONFIG_ACCOUNT_SERVER_ADDRESS environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.mongoose.connection_string) {
|
||||
LOG_ERROR('Failed to find MongoDB connection string. Set the PN_MIIVERSE_API_CONFIG_MONGO_CONNECTION_STRING environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.s3.endpoint) {
|
||||
LOG_ERROR('Failed to find s3 endpoint. Set the PN_MIIVERSE_API_CONFIG_S3_ENDPOINT environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.s3.key) {
|
||||
LOG_ERROR('Failed to find s3 key. Set the PN_MIIVERSE_API_CONFIG_S3_ACCESS_KEY environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.s3.secret) {
|
||||
LOG_ERROR('Failed to find s3 secret. Set the PN_MIIVERSE_API_CONFIG_S3_ACCESS_SECRET environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.grpc.friends.ip) {
|
||||
LOG_ERROR('Failed to find NEX Friends gRPC ip. Set the PN_MIIVERSE_API_CONFIG_GRPC_FRIENDS_IP environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.grpc.friends.port) {
|
||||
LOG_ERROR('Failed to find NEX Friends gRPC port. Set the PN_MIIVERSE_API_CONFIG_GRPC_FRIENDS_PORT environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.grpc.friends.api_key) {
|
||||
LOG_ERROR('Failed to find NEX Friends gRPC API key. Set the PN_MIIVERSE_API_CONFIG_GRPC_FRIENDS_API_KEY environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.grpc.account.ip) {
|
||||
LOG_ERROR('Failed to find account server gRPC ip. Set the PN_MIIVERSE_API_CONFIG_GRPC_ACCOUNT_IP environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.grpc.account.port) {
|
||||
LOG_ERROR('Failed to find account server gRPC port. Set the PN_MIIVERSE_API_CONFIG_GRPC_ACCOUNT_PORT environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.grpc.account.api_key) {
|
||||
LOG_ERROR('Failed to find account server gRPC API key. Set the PN_MIIVERSE_API_CONFIG_GRPC_ACCOUNT_API_KEY environment variable');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!config.aes_key) {
|
||||
LOG_ERROR('Token AES key is not set. Set the PN_MIIVERSE_API_CONFIG_AES_KEY environment variable to your AES-256-CBC key');
|
||||
process.exit(0);
|
||||
}
|
||||
371
src/database.js
371
src/database.js
|
|
@ -1,371 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { mongoose: mongooseConfig } = require('../config.json');
|
||||
const { ENDPOINT } = require('./models/endpoint');
|
||||
const { COMMUNITY } = require('./models/communities');
|
||||
const { POST } = require('./models/post');
|
||||
const { USER } = require('./models/user');
|
||||
const { CONVERSATION } = require('./models/conversation');
|
||||
const { uri, database, options } = mongooseConfig;
|
||||
const logger = require('./logger');
|
||||
|
||||
let connection;
|
||||
|
||||
async function connect() {
|
||||
await mongoose.connect(`${uri}/${database}`, options);
|
||||
connection = mongoose.connection;
|
||||
connection.on('connected', function () {
|
||||
logger.info(`MongoDB connected ${this.name}`);
|
||||
});
|
||||
connection.on('error', console.error.bind(console, 'connection error:'));
|
||||
connection.on('close', () => {
|
||||
connection.removeAllListeners();
|
||||
});
|
||||
}
|
||||
|
||||
function verifyConnected() {
|
||||
if (!connection) {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommunities(numberOfCommunities) {
|
||||
verifyConnected();
|
||||
if(numberOfCommunities === -1)
|
||||
return COMMUNITY.find({ parent: null });
|
||||
else
|
||||
return COMMUNITY.find({ parent: null }).limit(numberOfCommunities);
|
||||
}
|
||||
|
||||
async function getMostPopularCommunities(numberOfCommunities) {
|
||||
verifyConnected();
|
||||
return COMMUNITY.find({ parent: null }).sort({followers: -1}).limit(numberOfCommunities);
|
||||
}
|
||||
|
||||
async function getNewCommunities(numberOfCommunities) {
|
||||
verifyConnected();
|
||||
return COMMUNITY.find({ parent: null }).sort([['created_at', -1]]).limit(numberOfCommunities);
|
||||
}
|
||||
|
||||
async function getSubCommunities(communityID) {
|
||||
verifyConnected();
|
||||
return COMMUNITY.find({
|
||||
parent: communityID
|
||||
});
|
||||
}
|
||||
|
||||
async function getCommunityByTitleID(title_id) {
|
||||
verifyConnected();
|
||||
return COMMUNITY.findOne({
|
||||
title_id: title_id
|
||||
});
|
||||
}
|
||||
|
||||
async function getCommunityByID(community_id) {
|
||||
verifyConnected();
|
||||
return COMMUNITY.findOne({
|
||||
community_id: community_id
|
||||
});
|
||||
}
|
||||
|
||||
async function getTotalPostsByCommunity(community) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
parent: null
|
||||
}).countDocuments();
|
||||
}
|
||||
|
||||
async function getPostByID(postID) {
|
||||
verifyConnected();
|
||||
|
||||
return POST.findOne({
|
||||
id: postID
|
||||
});
|
||||
}
|
||||
|
||||
async function getPostsByUserID(userID) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
pid: userID,
|
||||
parent: null
|
||||
});
|
||||
}
|
||||
|
||||
async function getPostReplies(postID, number) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
parent: postID
|
||||
}).limit(number);
|
||||
}
|
||||
|
||||
async function getUserPostRepliesAfterTimestamp(post, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
parent: post.pid,
|
||||
created_at: { $lt: post.created_at }
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
async function getNumberUserPostsByID(userID, number) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
pid: userID,
|
||||
parent: null
|
||||
}).sort({ created_at: -1}).limit(number);
|
||||
}
|
||||
|
||||
async function getTotalPostsByUserID(userID) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
pid: userID,
|
||||
parent: null
|
||||
}).countDocuments();
|
||||
}
|
||||
|
||||
async function getHotPostsByCommunity(community, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
parent: null
|
||||
}).sort({empathy_count: -1}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
async function getNumberNewCommunityPostsByID(community, number) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
parent: null
|
||||
}).sort({ created_at: -1}).limit(number);
|
||||
}
|
||||
|
||||
async function getNumberPopularCommunityPostsByID(community, limit, offset) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
parent: null
|
||||
}).sort({ empathy_count: -1}).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
async function getNumberVerifiedCommunityPostsByID(community, limit, offset) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
verified: true,
|
||||
parent: null
|
||||
}).sort({ created_at: -1}).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
async function getPostsByCommunity(community, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
parent: null
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
async function getPostsByCommunityKey(community, numberOfPosts, search_key) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: community.title_id,
|
||||
search_key: search_key,
|
||||
parent: null
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
async function getNewPostsByCommunity(community, limit, offset) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
community_id: community.community_id,
|
||||
parent: null
|
||||
}).sort({ created_at: -1 }).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
async function getUserPostsAfterTimestamp(post, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
pid: post.pid,
|
||||
created_at: { $lt: post.created_at },
|
||||
parent: null
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
async function getUserPostsOffset(pid, limit, offset) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
pid: pid,
|
||||
parent: null
|
||||
}).sort({ created_at: -1}).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
async function getCommunityPostsAfterTimestamp(post, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
title_id: post.title_id,
|
||||
created_at: { $lt: post.created_at },
|
||||
parent: null
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
async function pushNewNotificationByPID(PID, content, link) {
|
||||
verifyConnected();
|
||||
return USER.update(
|
||||
{ pid: PID }, { $push: { notification_list: { content: content, link: link, read: false, created_at: Date() }}});
|
||||
}
|
||||
|
||||
async function pushNewNotificationToAll(content, link) {
|
||||
verifyConnected();
|
||||
return USER.updateMany(
|
||||
{}, { $push: { notification_list: { content: content, link: link, read: false, created_at: Date() }}});
|
||||
}
|
||||
|
||||
async function getDiscoveryHosts() {
|
||||
verifyConnected();
|
||||
return ENDPOINT.findOne({
|
||||
version: 1
|
||||
});
|
||||
}
|
||||
|
||||
async function getUsers(numberOfUsers) {
|
||||
verifyConnected();
|
||||
if(numberOfUsers === -1)
|
||||
return USER.find({});
|
||||
else
|
||||
return USER.find({}).limit(numberOfUsers);
|
||||
}
|
||||
|
||||
async function getFollowingUsers(user) {
|
||||
verifyConnected();
|
||||
return USER.find({
|
||||
pid: user.following_users
|
||||
});
|
||||
}
|
||||
async function getFollowedUsers(user) {
|
||||
verifyConnected();
|
||||
return USER.find({
|
||||
pid: user.followed_users
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserByPID(PID) {
|
||||
verifyConnected();
|
||||
|
||||
return USER.findOne({
|
||||
pid: PID
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserByUsername(user_id) {
|
||||
verifyConnected();
|
||||
|
||||
return USER.findOne({
|
||||
pnid: new RegExp(`^${user_id}$`, 'i')
|
||||
});
|
||||
}
|
||||
|
||||
async function getServerConfig() {
|
||||
verifyConnected();
|
||||
return ENDPOINT.findOne();
|
||||
}
|
||||
|
||||
async function getNewsFeed(user, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
$or: [
|
||||
{pid: user.followed_users},
|
||||
{pid: user.pid}
|
||||
],
|
||||
parent: null
|
||||
}).limit(numberOfPosts).sort({ created_at: -1});
|
||||
}
|
||||
|
||||
async function getNewsFeedAfterTimestamp(user, numberOfPosts, post) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
$or: [
|
||||
{pid: user.followed_users},
|
||||
{pid: user.pid}
|
||||
],
|
||||
created_at: { $lt: post.created_at },
|
||||
parent: null
|
||||
}).limit(numberOfPosts).sort({ created_at: -1});
|
||||
}
|
||||
|
||||
async function getNewsFeedOffset(user, limit, offset) {
|
||||
verifyConnected();
|
||||
return POST.find({
|
||||
$or: [
|
||||
{pid: user.followed_users},
|
||||
{pid: user.pid}
|
||||
],
|
||||
parent: null
|
||||
}).skip(offset).limit(limit).sort({ created_at: -1});
|
||||
}
|
||||
|
||||
async function getConversations(pid) {
|
||||
verifyConnected();
|
||||
return CONVERSATION.find({
|
||||
pids: pid
|
||||
});
|
||||
}
|
||||
|
||||
async function getConversation(pid, pid2) {
|
||||
verifyConnected();
|
||||
return CONVERSATION.find({
|
||||
$and: [
|
||||
{pids: pid},
|
||||
{pids: pid2}
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function getLatestMessage(pid, pid2) {
|
||||
verifyConnected();
|
||||
return POST.findOne({
|
||||
$or: [
|
||||
{pid: pid, message_to_pid: pid2},
|
||||
{pid: pid2, message_to_pid: pid}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
getCommunities,
|
||||
getMostPopularCommunities,
|
||||
getNewCommunities,
|
||||
getSubCommunities,
|
||||
getCommunityByTitleID,
|
||||
getCommunityByID,
|
||||
getTotalPostsByCommunity,
|
||||
getDiscoveryHosts,
|
||||
getPostsByCommunity,
|
||||
getHotPostsByCommunity,
|
||||
getNumberNewCommunityPostsByID,
|
||||
getNumberPopularCommunityPostsByID,
|
||||
getNumberVerifiedCommunityPostsByID,
|
||||
getNewPostsByCommunity,
|
||||
getPostsByCommunityKey,
|
||||
getPostsByUserID,
|
||||
getPostReplies,
|
||||
getUserPostRepliesAfterTimestamp,
|
||||
getNumberUserPostsByID,
|
||||
getTotalPostsByUserID,
|
||||
getPostByID,
|
||||
getUsers,
|
||||
getUserByPID,
|
||||
getUserByUsername,
|
||||
getUserPostsAfterTimestamp,
|
||||
getUserPostsOffset,
|
||||
getCommunityPostsAfterTimestamp,
|
||||
getServerConfig,
|
||||
pushNewNotificationByPID,
|
||||
pushNewNotificationToAll,
|
||||
getNewsFeed,
|
||||
getNewsFeedAfterTimestamp,
|
||||
getNewsFeedOffset,
|
||||
getFollowingUsers,
|
||||
getFollowedUsers,
|
||||
getConversations,
|
||||
getConversation,
|
||||
getLatestMessage,
|
||||
};
|
||||
179
src/database.ts
Normal file
179
src/database.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import mongoose from 'mongoose';
|
||||
import { LOG_INFO } from '@/logger';
|
||||
import { Community } from '@/models/community';
|
||||
import { Content } from '@/models/content';
|
||||
import { Conversation } from '@/models/conversation';
|
||||
import { Endpoint } from '@/models/endpoint';
|
||||
import { Post } from '@/models/post';
|
||||
import { Settings } from '@/models/settings';
|
||||
import { config } from '@/config-manager';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { HydratedPostDocument, IPost } from '@/types/mongoose/post';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
import { HydratedSettingsDocument } from '@/types/mongoose/settings';
|
||||
import { HydratedContentDocument } from '@/types/mongoose/content';
|
||||
import { HydratedConversationDocument } from '@/types/mongoose/conversation';
|
||||
|
||||
const { mongoose: mongooseConfig } = config;
|
||||
|
||||
let connection: mongoose.Connection;
|
||||
|
||||
export async function connect(): Promise<void> {
|
||||
await mongoose.connect(mongooseConfig.connection_string, mongooseConfig.options);
|
||||
|
||||
connection = mongoose.connection;
|
||||
connection.on('connected', () => {
|
||||
LOG_INFO('MongoDB connected');
|
||||
});
|
||||
connection.on('error', console.error.bind(console, 'connection error:'));
|
||||
connection.on('close', () => {
|
||||
connection.removeAllListeners();
|
||||
});
|
||||
}
|
||||
|
||||
function verifyConnected(): void {
|
||||
if (!connection) {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMostPopularCommunities(limit: number): Promise<HydratedCommunityDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.find({ parent: null, type: 0 }).sort({ followers: -1 }).limit(limit);
|
||||
}
|
||||
|
||||
export async function getNewCommunities(limit: number): Promise<HydratedCommunityDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.find({ parent: null, type: 0 }).sort([['created_at', -1]]).limit(limit);
|
||||
}
|
||||
|
||||
export async function getSubCommunities(parentCommunityID: string): Promise<HydratedCommunityDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.find({
|
||||
parent: parentCommunityID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommunityByTitleID(titleID: string): Promise<HydratedCommunityDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.findOne({
|
||||
title_id: titleID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommunityByTitleIDs(titleIDs: string[]): Promise<HydratedCommunityDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.findOne({
|
||||
title_id: { $in: titleIDs }
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommunityByID(communityID: string): Promise<HydratedCommunityDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.findOne({
|
||||
community_id: communityID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPostByID(postID: string): Promise<HydratedPostDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Post.findOne({
|
||||
id: postID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPostReplies(postID: string, limit: number): Promise<HydratedPostDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Post.find({
|
||||
parent: postID,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).limit(limit);
|
||||
}
|
||||
|
||||
export async function getDuplicatePosts(pid: number, post: IPost): Promise<HydratedPostDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Post.findOne({
|
||||
pid: pid,
|
||||
body: post.body,
|
||||
painting: post.painting,
|
||||
screenshot: post.screenshot,
|
||||
parent: null,
|
||||
removed: false
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPostsBytitleID(titleID: string[], limit: number): Promise<HydratedPostDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Post.find({
|
||||
title_id: titleID,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ created_at: -1 }).limit(limit);
|
||||
}
|
||||
|
||||
export async function getEndpoints(): Promise<HydratedEndpointDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Endpoint.find({});
|
||||
}
|
||||
|
||||
export async function getEndpoint(accessLevel: string): Promise<HydratedEndpointDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Endpoint.findOne({
|
||||
server_access_level: accessLevel
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserSettings(pid: number): Promise<HydratedSettingsDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Settings.findOne({ pid: pid });
|
||||
}
|
||||
|
||||
export async function getUserContent(pid: number): Promise<HydratedContentDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Content.findOne({ pid: pid });
|
||||
}
|
||||
|
||||
export async function getFollowedUsers(content: HydratedContentDocument): Promise<HydratedSettingsDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Settings.find({
|
||||
pid: content.followed_users
|
||||
});
|
||||
}
|
||||
|
||||
export async function getConversationByUsers(pids: number[]): Promise<HydratedConversationDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Conversation.findOne({
|
||||
$and: [
|
||||
{ 'users.pid': pids[0] },
|
||||
{ 'users.pid': pids[1] }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFriendMessages(pid: string, search_key: string[], limit: number): Promise<HydratedPostDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Post.find({
|
||||
message_to_pid: pid,
|
||||
search_key: search_key,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ created_at: 1 }).limit(limit);
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
const fs = require('fs-extra');
|
||||
require('colors');
|
||||
import fs from 'fs-extra';
|
||||
import colors from 'colors';
|
||||
|
||||
const root = __dirname;
|
||||
colors.enable();
|
||||
|
||||
const root = process.env.PN_MIIVERSE_API_LOGGER_PATH ? process.env.PN_MIIVERSE_API_LOGGER_PATH : `${__dirname}/..`;
|
||||
fs.ensureDirSync(`${root}/logs`);
|
||||
|
||||
const streams = {
|
||||
|
|
@ -10,9 +12,9 @@ const streams = {
|
|||
error: fs.createWriteStream(`${root}/logs/error.log`),
|
||||
warn: fs.createWriteStream(`${root}/logs/warn.log`),
|
||||
info: fs.createWriteStream(`${root}/logs/info.log`)
|
||||
};
|
||||
} as const;
|
||||
|
||||
function success(input) {
|
||||
export function LOG_SUCCESS(input: string): void {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
|
||||
streams.success.write(`${input}\n`);
|
||||
|
|
@ -20,7 +22,7 @@ function success(input) {
|
|||
console.log(`${input}`.green.bold);
|
||||
}
|
||||
|
||||
function error(input) {
|
||||
export function LOG_ERROR(input: string): void {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
|
||||
streams.error.write(`${input}\n`);
|
||||
|
|
@ -28,7 +30,7 @@ function error(input) {
|
|||
console.log(`${input}`.red.bold);
|
||||
}
|
||||
|
||||
function warn(input) {
|
||||
export function LOG_WARN(input: string): void {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
|
||||
streams.warn.write(`${input}\n`);
|
||||
|
|
@ -36,17 +38,10 @@ function warn(input) {
|
|||
console.log(`${input}`.yellow.bold);
|
||||
}
|
||||
|
||||
function info(input) {
|
||||
export function LOG_INFO(input: string): void {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
|
||||
streams.info.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.cyan.bold);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
success,
|
||||
error,
|
||||
warn,
|
||||
info
|
||||
};
|
||||
}
|
||||
175
src/middleware/auth.ts
Normal file
175
src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { z } from 'zod';
|
||||
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
|
||||
import { getEndpoint } from '@/database';
|
||||
import { getUserAccountData, getValueFromHeaders, decodeParamPack, getPIDFromServiceToken } from '@/util';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
|
||||
const ParamPackSchema = z.object({
|
||||
title_id: z.string(),
|
||||
access_key: z.string(),
|
||||
platform_id: z.string(),
|
||||
region_id: z.string(),
|
||||
language_id: z.string(),
|
||||
country_id: z.string(),
|
||||
area_id: z.string(),
|
||||
network_restriction: z.string(),
|
||||
friend_restriction: z.string(),
|
||||
rating_restriction: z.string(),
|
||||
rating_organization: z.string(),
|
||||
transferable_id: z.string(),
|
||||
tz_name: z.string(),
|
||||
utc_offset: z.string(),
|
||||
remaster_version: z.string().optional()
|
||||
});
|
||||
|
||||
async function auth(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
|
||||
if (request.path.includes('/v1/status')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// * Just don't care about the token here
|
||||
if (request.path === '/v1/topics') {
|
||||
return next();
|
||||
}
|
||||
|
||||
let encryptedToken = getValueFromHeaders(request.headers, 'x-nintendo-servicetoken');
|
||||
if (!encryptedToken) {
|
||||
encryptedToken = getValueFromHeaders(request.headers, 'olive service token');
|
||||
}
|
||||
|
||||
if (!encryptedToken) {
|
||||
return badAuth(response, 15, 'NO_TOKEN');
|
||||
}
|
||||
|
||||
const pid: number = getPIDFromServiceToken(encryptedToken);
|
||||
if (pid === 0) {
|
||||
return badAuth(response, 16, 'BAD_TOKEN');
|
||||
}
|
||||
|
||||
const paramPack = getValueFromHeaders(request.headers, 'x-nintendo-parampack');
|
||||
if (!paramPack) {
|
||||
return badAuth(response, 17, 'NO_PARAM');
|
||||
}
|
||||
|
||||
const paramPackData = decodeParamPack(paramPack);
|
||||
const paramPackCheck = ParamPackSchema.safeParse(paramPackData);
|
||||
if (!paramPackCheck.success) {
|
||||
console.log(paramPackCheck.error);
|
||||
return badAuth(response, 18, 'BAD_PARAM');
|
||||
}
|
||||
|
||||
let user: GetUserDataResponse;
|
||||
|
||||
try {
|
||||
user = await getUserAccountData(pid);
|
||||
} catch (error) {
|
||||
// TODO - Log this error
|
||||
console.log(error);
|
||||
return badAuth(response, 18, 'BAD_PARAM');
|
||||
}
|
||||
|
||||
let discovery: HydratedEndpointDocument | null;
|
||||
|
||||
if (user) {
|
||||
discovery = await getEndpoint(user.serverAccessLevel);
|
||||
} else {
|
||||
discovery = await getEndpoint('prod');
|
||||
}
|
||||
|
||||
if (!discovery) {
|
||||
return badAuth(response, 19, 'NO_DISCOVERY');
|
||||
}
|
||||
|
||||
if (discovery.status !== 0) {
|
||||
return serverError(response, discovery);
|
||||
}
|
||||
|
||||
// TODO - This is temp, testing something. Will be removed in the future
|
||||
if (request.path !== '/v1/endpoint') {
|
||||
if (user.serverAccessLevel !== 'test' && user.serverAccessLevel !== 'dev') {
|
||||
return badAuth(response, 16, 'BAD_TOKEN');
|
||||
}
|
||||
}
|
||||
|
||||
// * This is a false positive from ESLint.
|
||||
// * Since this middleware is only ever called
|
||||
// * per every request instance
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
request.pid = pid;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
request.paramPack = paramPackData;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
function badAuth(response: express.Response, errorCode: number, message: string): void {
|
||||
response.type('application/xml');
|
||||
response.status(400);
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: errorCode,
|
||||
message: message
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
}
|
||||
|
||||
function serverError(response: express.Response, discovery: HydratedEndpointDocument): void {
|
||||
let message = '';
|
||||
let error = 0;
|
||||
|
||||
switch (discovery.status) {
|
||||
case 1:
|
||||
message = 'SYSTEM_UPDATE_REQUIRED';
|
||||
error = 1;
|
||||
break;
|
||||
case 2:
|
||||
message = 'SETUP_NOT_COMPLETE';
|
||||
error = 2;
|
||||
break;
|
||||
case 3:
|
||||
message = 'SERVICE_MAINTENANCE';
|
||||
error = 3;
|
||||
break;
|
||||
case 4:
|
||||
message = 'SERVICE_CLOSED';
|
||||
error = 4;
|
||||
break;
|
||||
case 5:
|
||||
message = 'PARENTAL_CONTROLS_ENABLED';
|
||||
error = 5;
|
||||
break;
|
||||
case 6:
|
||||
message = 'POSTING_LIMITED_PARENTAL_CONTROLS';
|
||||
error = 6;
|
||||
break;
|
||||
case 7:
|
||||
message = 'NNID_BANNED';
|
||||
error = 7;
|
||||
break;
|
||||
default:
|
||||
message = 'SERVER_ERROR';
|
||||
error = 15;
|
||||
break;
|
||||
}
|
||||
|
||||
response.type('application/xml');
|
||||
response.status(400);
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: error,
|
||||
message: message
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
}
|
||||
|
||||
export default auth;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
const xmlbuilder = require('xmlbuilder');
|
||||
|
||||
const VALID_CLIENT_ID_SECRET_PAIRS = {
|
||||
// 'Key' is the client ID, 'Value' is the client secret
|
||||
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive?
|
||||
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive?
|
||||
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive?
|
||||
};
|
||||
|
||||
|
||||
function nintendoClientHeaderCheck(request, response, next) {
|
||||
response.set('Content-Type', 'text/xml');
|
||||
response.set('Server', 'Nintendo 3DS (http)');
|
||||
response.set('X-Nintendo-Date', new Date().getTime());
|
||||
|
||||
const {headers} = request;
|
||||
|
||||
if (
|
||||
!headers['x-nintendo-client-id'] ||
|
||||
!headers['x-nintendo-client-secret'] ||
|
||||
!VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
|
||||
headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
|
||||
) {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
cause: 'client_id',
|
||||
code: '0004',
|
||||
message: 'API application invalid or incorrect application credentials'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = nintendoClientHeaderCheck;
|
||||
44
src/middleware/client-header.ts
Normal file
44
src/middleware/client-header.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { getValueFromHeaders } from '@/util';
|
||||
|
||||
const VALID_CLIENT_ID_SECRET_PAIRS: { [key: string]: string } = {
|
||||
// * 'Key' is the client ID, 'Value' is the client secret
|
||||
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // * Possibly WiiU exclusive?
|
||||
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // * Possibly 3DS exclusive?
|
||||
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // * Possibly 3DS exclusive?
|
||||
};
|
||||
|
||||
|
||||
function nintendoClientHeaderCheck(request: express.Request, response: express.Response, next: express.NextFunction): void {
|
||||
response.type('text/xml');
|
||||
response.set('Server', 'Nintendo 3DS (http)');
|
||||
response.set('X-Nintendo-Date', new Date().getTime().toString());
|
||||
|
||||
const clientID = getValueFromHeaders(request.headers, 'x-nintendo-client-id');
|
||||
const clientSecret = getValueFromHeaders(request.headers, 'x-nintendo-client-secret');
|
||||
|
||||
if (
|
||||
!clientID ||
|
||||
!clientSecret ||
|
||||
!VALID_CLIENT_ID_SECRET_PAIRS[clientID] ||
|
||||
clientSecret !== VALID_CLIENT_ID_SECRET_PAIRS[clientID]
|
||||
) {
|
||||
response.type('application/xml');
|
||||
response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
cause: 'client_id',
|
||||
code: '0004',
|
||||
message: 'API application invalid or incorrect application credentials'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
export default nintendoClientHeaderCheck;
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
const xmlbuilder = require('xmlbuilder');
|
||||
const database = require('../database');
|
||||
|
||||
async function PNIDMiddleware(request, response, next) {
|
||||
const { headers } = request;
|
||||
|
||||
if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [type, token] = headers.authorization.split(' ');
|
||||
let user;
|
||||
|
||||
if (type === 'Basic') {
|
||||
user = await database.getUserBasic(token);
|
||||
} else {
|
||||
user = await database.getUserBearer(token);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
response.status(401);
|
||||
|
||||
if (type === 'Bearer') {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
cause: 'access_token',
|
||||
code: '0005',
|
||||
message: 'Invalid access token'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1105',
|
||||
message: 'Email address, username, or password, is not valid'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
request.pnid = user;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = PNIDMiddleware;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
// super basic and there's probably a much better way to do this
|
||||
|
||||
// this will only be used during the registration process, to track the progress of the user
|
||||
// express-session uses cookies which the WiiU does not support during the registration process
|
||||
|
||||
// temp, in-memory session storage
|
||||
const sessionStore = {};
|
||||
|
||||
function sessionMiddlware(request, response, next) {
|
||||
const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||
|
||||
if (!sessionStore[ip]) {
|
||||
sessionStore[ip] = {};
|
||||
}
|
||||
|
||||
const session = sessionStore[ip];
|
||||
|
||||
request.session = session;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = sessionMiddlware;
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
const { document: xmlParser } = require('xmlbuilder2');
|
||||
|
||||
function XMLMiddleware(request, response, next) {
|
||||
if (request.method == 'POST' || request.method == 'PUT') {
|
||||
const headers = request.headers;
|
||||
let body = '';
|
||||
|
||||
if (
|
||||
!headers['content-type'] ||
|
||||
!headers['content-type'].toLowerCase().includes('xml')
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
request.setEncoding('utf-8');
|
||||
request.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
request.on('end', () => {
|
||||
try {
|
||||
request.body = xmlParser(body);
|
||||
request.body = request.body.toObject();
|
||||
} catch (error) {
|
||||
return next();
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = XMLMiddleware;
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
const { Schema, model } = require('mongoose');
|
||||
|
||||
const CommunitySchema = new Schema({
|
||||
platform_id: Number,
|
||||
name: String,
|
||||
description: String,
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 0: Main Community
|
||||
* 1: Sub-Community
|
||||
* 2: Announcement Community
|
||||
*/
|
||||
type: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
parent: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
admins: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: new Date(),
|
||||
},
|
||||
empathy_count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
followers: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
has_shop_page: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
icon: String,
|
||||
title_ids: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
title_id: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
community_id: String,
|
||||
is_recommended: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
browser_icon: String,
|
||||
browser_thumbnail: String,
|
||||
CTR_browser_header: String,
|
||||
WiiU_browser_header: String
|
||||
});
|
||||
|
||||
CommunitySchema.methods.upEmpathy = async function() {
|
||||
const empathy = this.get('empathy_count');
|
||||
this.set('empathy_count', empathy + 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
CommunitySchema.methods.downEmpathy = async function() {
|
||||
const empathy = this.get('empathy_count');
|
||||
this.set('empathy_count', empathy - 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
CommunitySchema.methods.upFollower = async function() {
|
||||
const followers = this.get('followers');
|
||||
this.set('followers', followers + 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
CommunitySchema.methods.downFollower = async function() {
|
||||
const followers = this.get('followers');
|
||||
this.set('followers', followers - 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
const COMMUNITY = model('COMMUNITY', CommunitySchema);
|
||||
|
||||
module.exports = {
|
||||
CommunitySchema,
|
||||
COMMUNITY
|
||||
};
|
||||
103
src/models/community.ts
Normal file
103
src/models/community.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { CommunityData } from '@/types/miiverse/community';
|
||||
import { ICommunity, ICommunityMethods, CommunityModel, HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
|
||||
const CommunitySchema = new Schema<ICommunity, CommunityModel, ICommunityMethods>({
|
||||
platform_id: Number,
|
||||
name: String,
|
||||
description: String,
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
allows_comments: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 0: Main Community
|
||||
* 1: Sub-Community
|
||||
* 2: Announcement Community
|
||||
* 3: Private Community
|
||||
*/
|
||||
type: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
parent: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
admins: {
|
||||
type: [Number],
|
||||
default: undefined
|
||||
},
|
||||
owner: Number,
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: new Date(),
|
||||
},
|
||||
empathy_count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
followers: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
has_shop_page: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
icon: String,
|
||||
title_ids: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
title_id: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
community_id: String,
|
||||
olive_community_id: String,
|
||||
is_recommended: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
app_data: String,
|
||||
user_favorites: {
|
||||
type: [Number],
|
||||
default: []
|
||||
}
|
||||
});
|
||||
|
||||
CommunitySchema.method<HydratedCommunityDocument>('addUserFavorite', async function addUserFavorite(pid: number): Promise<void> {
|
||||
if (!this.user_favorites.includes(pid)) {
|
||||
this.user_favorites.push(pid);
|
||||
}
|
||||
|
||||
await this.save();
|
||||
});
|
||||
|
||||
CommunitySchema.method<HydratedCommunityDocument>('delUserFavorite', async function delUserFavorite(pid: number): Promise<void> {
|
||||
if (this.user_favorites.includes(pid)) {
|
||||
this.user_favorites.splice(this.user_favorites.indexOf(pid), 1);
|
||||
}
|
||||
|
||||
await this.save();
|
||||
});
|
||||
|
||||
CommunitySchema.method<HydratedCommunityDocument>('json', function json(): CommunityData {
|
||||
return {
|
||||
community_id: this.community_id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
icon: this.icon.replace(/[^A-Za-z0-9+/=\s]/g, ''),
|
||||
icon_3ds: '',
|
||||
pid: this.owner,
|
||||
app_data: this.app_data.replace(/[^A-Za-z0-9+/=\s]/g, ''),
|
||||
is_user_community: '0'
|
||||
};
|
||||
});
|
||||
|
||||
export const Community = model<ICommunity, CommunityModel>('Community', CommunitySchema);
|
||||
20
src/models/content.ts
Normal file
20
src/models/content.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { IContent, ContentModel } from '@/types/mongoose/content';
|
||||
|
||||
const ContentSchema = new Schema<IContent, ContentModel>({
|
||||
pid: Number,
|
||||
followed_communities: {
|
||||
type: [String],
|
||||
default: [0]
|
||||
},
|
||||
followed_users: {
|
||||
type: [Number],
|
||||
default: [0]
|
||||
},
|
||||
following_users: {
|
||||
type: [Number],
|
||||
default: [0]
|
||||
}
|
||||
});
|
||||
|
||||
export const Content = model<IContent, ContentModel>('Content', ContentSchema);
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
const { Schema, model } = require('mongoose');
|
||||
const snowflake = require('node-snowflake').Snowflake;
|
||||
|
||||
const ConversationSchema = new Schema({
|
||||
id: {
|
||||
type: String,
|
||||
default: snowflake.nextId()
|
||||
},
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: new Date(),
|
||||
},
|
||||
last_message_sent: {
|
||||
type: Date,
|
||||
default: new Date(),
|
||||
},
|
||||
message_preview: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
pid1: {
|
||||
pid: String,
|
||||
official: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
screen_name: String,
|
||||
read: Boolean
|
||||
},
|
||||
pid2: {
|
||||
pid: String,
|
||||
official: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
screen_name: String,
|
||||
read: Boolean
|
||||
}
|
||||
});
|
||||
|
||||
ConversationSchema.methods.newMessage = async function(message, fromPid) {
|
||||
const pid1 = this.get('pid1');
|
||||
const pid2 = this.get('pid2');
|
||||
if(pid1.pid === fromPid) {
|
||||
pid1.read = true
|
||||
pid2.read = false
|
||||
}
|
||||
else {
|
||||
pid1.read = false
|
||||
pid2.read = true
|
||||
}
|
||||
this.set('pid1', pid1);
|
||||
this.set('pid2', pid2);
|
||||
this.set('last_message_sent', new Date());
|
||||
this.set('message_preview', message);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
const CONVERSATION = model('CONVERSATION', ConversationSchema);
|
||||
|
||||
module.exports = {
|
||||
ConversationSchema: ConversationSchema,
|
||||
CONVERSATION: CONVERSATION
|
||||
};
|
||||
50
src/models/conversation.ts
Normal file
50
src/models/conversation.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { Snowflake } from 'node-snowflake';
|
||||
import { IConversation, IConversationMethods, ConversationModel, HydratedConversationDocument } from '@/types/mongoose/conversation';
|
||||
|
||||
const ConversationSchema = new Schema<IConversation, ConversationModel, IConversationMethods>({
|
||||
id: {
|
||||
type: String,
|
||||
default: Snowflake.nextId()
|
||||
},
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: new Date(),
|
||||
},
|
||||
last_updated: {
|
||||
type: Date,
|
||||
default: new Date(),
|
||||
},
|
||||
message_preview: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
users: [{
|
||||
pid: Number,
|
||||
official: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
read: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
ConversationSchema.method<HydratedConversationDocument>('newMessage', async function newMessage(message: string, senderPID: number) {
|
||||
if (this.users[0].pid === senderPID) {
|
||||
this.users[1].read = false;
|
||||
this.markModified('users[1].read');
|
||||
} else {
|
||||
this.users[0].read = false;
|
||||
this.markModified('users[0].read');
|
||||
}
|
||||
|
||||
this.last_updated = new Date();
|
||||
this.message_preview = message;
|
||||
|
||||
await this.save();
|
||||
});
|
||||
|
||||
export const Conversation = model<IConversation, ConversationModel>('Conversation', ConversationSchema);
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
const { Schema, model } = require('mongoose');
|
||||
|
||||
const endpointSchema = new Schema({
|
||||
has_error: Number,
|
||||
version: Number,
|
||||
endpoint: {
|
||||
host: String,
|
||||
api_host: String,
|
||||
portal_host: String,
|
||||
n3ds_host: String
|
||||
},
|
||||
guest: Boolean,
|
||||
});
|
||||
|
||||
endpointSchema.methods.updateHosts = async function({host, api_host, portal_host, n3ds_host}) {
|
||||
this.set('endpoint.host', host);
|
||||
this.set('endpoint.api_host', api_host);
|
||||
this.set('endpoint.portal_host', portal_host);
|
||||
this.set('endpoint.n3ds_host', n3ds_host);
|
||||
await this.save();
|
||||
};
|
||||
|
||||
endpointSchema.methods.updateGuest = async function(mode) {
|
||||
this.set('guest', mode);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
const ENDPOINT = model('ENDPOINT', endpointSchema);
|
||||
|
||||
module.exports = {
|
||||
endpointSchema,
|
||||
ENDPOINT
|
||||
};
|
||||
15
src/models/endpoint.ts
Normal file
15
src/models/endpoint.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { IEndpoint, EndpointModel } from '@/types/mongoose/endpoint';
|
||||
|
||||
const endpointSchema = new Schema<IEndpoint, EndpointModel>({
|
||||
status: Number,
|
||||
server_access_level: String,
|
||||
topics: Boolean,
|
||||
guest_access: Boolean,
|
||||
host: String,
|
||||
api_host: String,
|
||||
portal_host: String,
|
||||
n3ds_host: String
|
||||
});
|
||||
|
||||
export const Endpoint = model<IEndpoint, EndpointModel>('Endpoint', endpointSchema);
|
||||
17
src/models/notification.ts
Normal file
17
src/models/notification.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { INotification, NotificationModel } from '@/types/mongoose/notification';
|
||||
|
||||
const NotificationSchema = new Schema<INotification, NotificationModel>({
|
||||
pid: String,
|
||||
type: String,
|
||||
link: String,
|
||||
objectID: String,
|
||||
users: [{
|
||||
user: String,
|
||||
timestamp: Date
|
||||
}],
|
||||
read: Boolean,
|
||||
lastUpdated: Date
|
||||
});
|
||||
|
||||
export const Notification = model<INotification, NotificationModel>('Notification', NotificationSchema);
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
const { Schema, model } = require('mongoose');
|
||||
|
||||
const PostSchema = new Schema({
|
||||
title_id: String,
|
||||
screen_name: String,
|
||||
body: String,
|
||||
app_data: String,
|
||||
painting: String,
|
||||
painting_uri: String,
|
||||
screenshot: String,
|
||||
url: String,
|
||||
search_key: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
topic_tag: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
community_id: String,
|
||||
country_id: Number,
|
||||
created_at: Date,
|
||||
feeling_id: Number,
|
||||
id: String,
|
||||
is_autopost: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
is_community_private_autopost: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
is_spoiler: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
is_app_jumpable: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
empathy_count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
language_id: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
mii: String,
|
||||
mii_face_url: String,
|
||||
number: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pid: Number,
|
||||
platform_id: Number,
|
||||
region_id: Number,
|
||||
parent_post: String,
|
||||
reply_count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
verified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
parent: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
message_to_pid: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
conversation_id: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
PostSchema.methods.upEmpathy = async function() {
|
||||
const empathy = this.get('empathy_count');
|
||||
this.set('empathy_count', empathy + 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
PostSchema.methods.downEmpathy = async function() {
|
||||
const empathy = this.get('empathy_count');
|
||||
this.set('empathy_count', empathy - 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
PostSchema.methods.upReply = async function() {
|
||||
const replyCount = this.get('reply_count');
|
||||
this.set('reply_count', replyCount + 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
PostSchema.methods.downReply = async function() {
|
||||
const replyCount = this.get('reply_count');
|
||||
this.set('reply_count', replyCount - 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
const POST = model('POST', PostSchema);
|
||||
|
||||
module.exports = {
|
||||
PostSchema,
|
||||
POST
|
||||
};
|
||||
216
src/models/post.ts
Normal file
216
src/models/post.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import crypto from 'node:crypto';
|
||||
import moment from 'moment';
|
||||
import { Schema, model } from 'mongoose';
|
||||
import { HydratedPostDocument, IPost, IPostMethods, PostModel } from '@/types/mongoose/post';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { PostToJSONOptions } from '@/types/mongoose/post-to-json-options';
|
||||
import { PostData, PostPainting, PostScreenshot, PostTopicTag } from '@/types/miiverse/post';
|
||||
|
||||
const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
|
||||
id: String,
|
||||
title_id: String,
|
||||
screen_name: String,
|
||||
body: String,
|
||||
app_data: String,
|
||||
painting: String,
|
||||
screenshot: String,
|
||||
screenshot_length: Number,
|
||||
search_key: {
|
||||
type: [String],
|
||||
default: undefined
|
||||
},
|
||||
topic_tag: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
community_id: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
created_at: Date,
|
||||
feeling_id: Number,
|
||||
is_autopost: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
is_community_private_autopost: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
is_spoiler: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
is_app_jumpable: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
empathy_count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0
|
||||
},
|
||||
country_id: {
|
||||
type: Number,
|
||||
default: 49
|
||||
},
|
||||
language_id: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
mii: String,
|
||||
mii_face_url: String,
|
||||
pid: Number,
|
||||
platform_id: Number,
|
||||
region_id: Number,
|
||||
parent: String,
|
||||
reply_count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
verified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
message_to_pid: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
removed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
removed_reason: String,
|
||||
yeahs: [Number],
|
||||
number: Number
|
||||
}, {
|
||||
id: false // * Disables the .id() getter used by Mongoose in TypeScript. Needed to have our own .id field
|
||||
});
|
||||
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('del', async function del(reason: string) {
|
||||
this.removed = true;
|
||||
this.removed_reason = reason;
|
||||
await this.save();
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('generatePostUID', async function generatePostUID(length: number) {
|
||||
const id = Buffer.from(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(length * 2))), 'binary').toString('base64').replace(/[+/]/g, '').substring(0, length);
|
||||
|
||||
const inuse = await Post.findOne({ id });
|
||||
|
||||
if (inuse) {
|
||||
await this.generatePostUID(length);
|
||||
} else {
|
||||
this.id = id;
|
||||
}
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('cleanedBody', function cleanedBody(): string {
|
||||
return this.body ? this.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}+=,.<>/?;:'"[\]]/g, '').replace(/[\n\r]+/gm, '') : '';
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('cleanedMiiData', function cleanedMiiData(): string {
|
||||
return this.mii.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('cleanedPainting', function cleanedPainting(): string {
|
||||
return this.painting.replace(/[\n\r]+/gm, '').trim();
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('cleanedAppData', function cleanedAppData(): string {
|
||||
return this.app_data.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('formatPainting', function formatPainting(): PostPainting | undefined {
|
||||
if (this.painting) {
|
||||
return {
|
||||
format: 'tga',
|
||||
content: this.cleanedPainting(),
|
||||
size: this.painting.length,
|
||||
url: `https://pretendo-cdn.b-cdn.net/paintings/${this.pid}/${this.id}.png`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('formatScreenshot', function formatScreenshot(): PostScreenshot | undefined {
|
||||
if (this.screenshot && this.screenshot_length) {
|
||||
return {
|
||||
size: this.screenshot_length,
|
||||
url: `https://pretendo-cdn.b-cdn.net/screenshots/${this.pid}/${this.id}.jpg`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('formatTopicTag', function formatTopicTag(): PostTopicTag | undefined {
|
||||
if (this.topic_tag?.trim()) {
|
||||
return {
|
||||
name: this.topic_tag,
|
||||
title_id: this.title_id
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
PostSchema.method<HydratedPostDocument>('json', function json(options: PostToJSONOptions, community?: HydratedCommunityDocument): PostData {
|
||||
const post: PostData = {
|
||||
app_data: undefined, // TODO - I try to keep these fields in the real order they show up in, but idk where this one goes
|
||||
body: this.cleanedBody(),
|
||||
community_id: this.community_id, // TODO - This sucks
|
||||
country_id: this.country_id,
|
||||
created_at: moment(this.created_at).format('YYYY-MM-DD HH:MM:SS'),
|
||||
feeling_id: this.feeling_id,
|
||||
id: this.id,
|
||||
is_autopost: this.is_autopost ? 1 : 0,
|
||||
is_community_private_autopost: this.is_community_private_autopost ? 1 : 0,
|
||||
is_spoiler: this.is_spoiler ? 1 : 0,
|
||||
is_app_jumpable: this.is_app_jumpable ? 1 : 0,
|
||||
empathy_count: this.empathy_count || 0,
|
||||
language_id: this.language_id,
|
||||
mii: undefined, // * Conditionally set later
|
||||
mii_face_url: undefined, // * Conditionally set later
|
||||
number: 0,
|
||||
painting: this.formatPainting(),
|
||||
pid: this.pid,
|
||||
platform_id: this.platform_id,
|
||||
region_id: this.region_id,
|
||||
reply_count: this.reply_count || 0,
|
||||
screen_name: this.screen_name,
|
||||
screenshot: this.formatScreenshot(),
|
||||
topic_tag: undefined, // * Conditionally set later
|
||||
title_id: this.title_id,
|
||||
};
|
||||
|
||||
if (options.app_data) {
|
||||
post.app_data = this.cleanedAppData();
|
||||
}
|
||||
|
||||
if (options.with_mii) {
|
||||
post.mii = this.cleanedMiiData();
|
||||
post.mii_face_url = this.mii_face_url;
|
||||
}
|
||||
|
||||
if (options.topic_tag) {
|
||||
post.topic_tag = this.formatTopicTag();
|
||||
}
|
||||
|
||||
if (community) {
|
||||
post.community_id = community.community_id;
|
||||
}
|
||||
|
||||
// * Some sanity checks
|
||||
if (post.feeling_id > 5) {
|
||||
post.feeling_id = 0;
|
||||
}
|
||||
|
||||
return post;
|
||||
});
|
||||
|
||||
PostSchema.pre('save', async function(next) {
|
||||
if (!this.id) {
|
||||
await this.generatePostUID(21);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
export const Post = model<IPost, PostModel>('Post', PostSchema);
|
||||
14
src/models/report.ts
Normal file
14
src/models/report.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { IReport, ReportModel } from '@/types/mongoose/report';
|
||||
|
||||
const ReportSchema = new Schema<IReport, ReportModel>({
|
||||
pid: String,
|
||||
post_id: String,
|
||||
reason: Number,
|
||||
created_at: {
|
||||
type: Date,
|
||||
default: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
export const Report = model<IReport, ReportModel>('Report', ReportSchema);
|
||||
59
src/models/settings.ts
Normal file
59
src/models/settings.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Schema, model } from 'mongoose';
|
||||
import { SettingsData } from '@/types/miiverse/settings';
|
||||
import { HydratedSettingsDocument, ISettings, ISettingsMethods, SettingsModel } from '@/types/mongoose/settings';
|
||||
|
||||
const SettingsSchema = new Schema<ISettings, SettingsModel, ISettingsMethods>({
|
||||
pid: Number,
|
||||
screen_name: String,
|
||||
account_status: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ban_lift_date: Date,
|
||||
ban_reason: String,
|
||||
profile_comment: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
profile_comment_visibility: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
game_skill: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
game_skill_visibility: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
birthday_visibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
relationship_visibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
country_visibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
profile_favorite_community_visibility: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
receive_notifications: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
SettingsSchema.method<HydratedSettingsDocument>('json', function json(): SettingsData {
|
||||
return {
|
||||
pid: this.pid,
|
||||
screen_name: this.screen_name
|
||||
};
|
||||
});
|
||||
|
||||
export const Settings = model<ISettings, SettingsModel>('Settings', SettingsSchema);
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
const { Schema, model } = require('mongoose');
|
||||
|
||||
const notification = new Schema({
|
||||
content: String,
|
||||
link: String,
|
||||
read: Boolean,
|
||||
created_at: Date,
|
||||
});
|
||||
|
||||
const UserSchema = new Schema({
|
||||
pid: Number,
|
||||
created_at: Date,
|
||||
user_id: String,
|
||||
pnid: String,
|
||||
birthday: Date,
|
||||
country: String,
|
||||
pfp_uri: String,
|
||||
mii: String,
|
||||
mii_face_url: String,
|
||||
/**
|
||||
* Account Status
|
||||
* 0 - Fine
|
||||
* 1 - Limited from Posting
|
||||
* 2 - Temporary Ban
|
||||
* 3 - Forever Ban
|
||||
*/
|
||||
account_status: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ban_lift_date: Date,
|
||||
ban_reason: String,
|
||||
official: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
profile_comment: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
game_skill: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
game_skill_visibility: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
profile_comment_visibility: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
birthday_visibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
relationship_visibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
country_visibility: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
profile_favorite_community_visibility: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
notifications: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
likes: {
|
||||
type: [String],
|
||||
default: [0]
|
||||
},
|
||||
followed_communities: {
|
||||
type: [String],
|
||||
default: [0]
|
||||
},
|
||||
followed_users: {
|
||||
type: [String],
|
||||
default: [0]
|
||||
},
|
||||
following_users: {
|
||||
type: [String],
|
||||
default: [0]
|
||||
},
|
||||
followers: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
following: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
notification_list: {
|
||||
type: [notification],
|
||||
default: [{
|
||||
content: 'This is your notifications! You\'ll see more stuff here soon!',
|
||||
link: '/users/me',
|
||||
read: false,
|
||||
created_at: new Date(),
|
||||
}]
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
UserSchema.methods.getAccountStatus = async function() {
|
||||
return this.get('account_status');
|
||||
};
|
||||
|
||||
UserSchema.methods.setAccountStatus = async function(accountStatus) {
|
||||
this.set('account_status', accountStatus);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getBanDate = async function() {
|
||||
return this.get('ban_lift_date');
|
||||
};
|
||||
|
||||
UserSchema.methods.setBanData = async function(banDate) {
|
||||
this.set('ban_lift_date', banDate);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getProfileComment = async function() {
|
||||
return this.get('profile_comment');
|
||||
};
|
||||
UserSchema.methods.setProfileComment = async function(profileComment) {
|
||||
this.set('profile_comment', profileComment);
|
||||
await this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.getGameSkill = async function() {
|
||||
return this.get('game_skill');
|
||||
};
|
||||
|
||||
UserSchema.methods.setGameSkill = async function(gameSkill) {
|
||||
this.set('game_skill', gameSkill);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getGameSkillVisibility = async function() {
|
||||
return this.get('game_skill_visibility');
|
||||
};
|
||||
|
||||
UserSchema.methods.setGameSkillVisibility = async function(gameSkillVisibility) {
|
||||
this.set('game_skill_visibility', gameSkillVisibility);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getProfileCommentVisibility = async function() {
|
||||
return this.get('profile_comment_visibility');
|
||||
};
|
||||
|
||||
UserSchema.methods.setProfileCommentVisibility = async function(profileCommentVisibility) {
|
||||
this.set('profile_comment_visibility', profileCommentVisibility);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getBirthdayVisibility = async function() {
|
||||
return this.get('birthday_visibility');
|
||||
};
|
||||
|
||||
UserSchema.methods.setBirthdayVisibility = async function(birthdayVisibility) {
|
||||
this.set('birthday_visibility', birthdayVisibility);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getRelationshipVisibility = async function() {
|
||||
return this.get('relationship_visibility');
|
||||
};
|
||||
|
||||
UserSchema.methods.setRelationshipVisibility = async function(accountStatus) {
|
||||
this.set('relationship_visibility', accountStatus);
|
||||
await this.save();
|
||||
};
|
||||
UserSchema.methods.getFavoriteCommunityVisibility = async function() {
|
||||
return this.get('profile_favorite_community_visibility');
|
||||
};
|
||||
|
||||
UserSchema.methods.setFavoriteCommunityVisibility = async function(favoriteCommunityVisibility) {
|
||||
this.set('profile_favorite_community_visibility', favoriteCommunityVisibility);
|
||||
await this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.getCountryVisibility = async function() {
|
||||
return this.get('country_visibility');
|
||||
};
|
||||
|
||||
UserSchema.methods.setCountryVisibility = async function(countryVisibility) {
|
||||
this.set('country_visibility', countryVisibility);
|
||||
await this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.addToLikes = async function(postID) {
|
||||
const likes = this.get('likes');
|
||||
likes.addToSet(postID);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.removeFromLike = async function(postID) {
|
||||
const likes = this.get('likes');
|
||||
likes.pull(postID);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.addToCommunities = async function(postID) {
|
||||
const communities = this.get('followed_communities');
|
||||
communities.addToSet(postID);
|
||||
await this.upFollowing();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.removeFromCommunities = async function(postID) {
|
||||
const communities = this.get('followed_communities');
|
||||
communities.pull(postID);
|
||||
await this.downFollowing();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.addToUsers = async function(postID) {
|
||||
const users = this.get('followed_users');
|
||||
users.addToSet(postID);
|
||||
await this.upFollowing();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.removeFromUsers = async function(postID) {
|
||||
const users = this.get('followed_users');
|
||||
users.pull(postID);
|
||||
await this.downFollowing();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.addToFollowers = async function(postID) {
|
||||
const users = this.get('following_users');
|
||||
users.addToSet(postID);
|
||||
await this.upFollower();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.removeFromFollowers = async function(postID) {
|
||||
const users = this.get('following_users');
|
||||
users.pull(postID);
|
||||
await this.downFollower();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
UserSchema.methods.upFollower = async function() {
|
||||
const followers = this.get('followers');
|
||||
this.set('followers', followers + 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.downFollower = async function() {
|
||||
const followers = this.get('followers');
|
||||
if(followers > 0)
|
||||
this.set('followers', followers - 1);
|
||||
await this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.upFollowing = async function() {
|
||||
const following = this.get('following');
|
||||
this.set('following', following + 1);
|
||||
|
||||
await this.save();
|
||||
};
|
||||
|
||||
UserSchema.methods.downFollowing = async function() {
|
||||
const following = this.get('following');
|
||||
if(following > 0)
|
||||
this.set('following', following - 1);
|
||||
await this.save();
|
||||
};
|
||||
|
||||
const USER = model('USER', UserSchema);
|
||||
|
||||
module.exports = {
|
||||
UserSchema,
|
||||
USER
|
||||
};
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
process.title = 'Pretendo - Miiverse';
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const xmlparser = require('./middleware/xml-parser');
|
||||
const database = require('./database');
|
||||
const logger = require('./logger');
|
||||
const config = require('../config.json');
|
||||
|
||||
const { http: { port } } = config;
|
||||
const app = express();
|
||||
|
||||
const miiverse = require('./services/miiverse-api');
|
||||
|
||||
app.set('etag', false);
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Create router
|
||||
logger.info('Setting up Middleware');
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
|
||||
app.use(express.urlencoded({
|
||||
extended: true,
|
||||
limit: '5mb',
|
||||
parameterLimit: 100000
|
||||
}));
|
||||
app.use(xmlparser);
|
||||
|
||||
// import the servers into one
|
||||
app.use(miiverse);
|
||||
|
||||
// 404 handler
|
||||
logger.info('Creating 404 status handler');
|
||||
app.use((request, response) => {
|
||||
//logger.warn(request.protocol + '://' + request.get('host') + request.originalUrl);
|
||||
response.status(404);
|
||||
response.send();
|
||||
});
|
||||
|
||||
// non-404 error handler
|
||||
logger.info('Creating non-404 status handler');
|
||||
app.use((error, request, response) => {
|
||||
const status = error.status || 500;
|
||||
|
||||
response.status(status);
|
||||
|
||||
response.json({
|
||||
app: 'api',
|
||||
status,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
|
||||
// Starts the server
|
||||
logger.info('Starting server');
|
||||
|
||||
database.connect().then(() => {
|
||||
app.listen(port, () => {
|
||||
logger.success(`Server started on port ${port}`);
|
||||
});
|
||||
});
|
||||
81
src/server.ts
Normal file
81
src/server.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
process.title = 'Pretendo - Miiverse';
|
||||
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { connect as connectDatabase } from '@/database';
|
||||
import { LOG_INFO, LOG_SUCCESS } from '@/logger';
|
||||
import auth from '@/middleware/auth';
|
||||
|
||||
import discovery from '@/services/discovery';
|
||||
import api from '@/services/api';
|
||||
|
||||
import { config } from '@/config-manager';
|
||||
|
||||
const { http: { port } } = config;
|
||||
const app = express();
|
||||
|
||||
app.set('etag', false);
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Create router
|
||||
LOG_INFO('Setting up Middleware');
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
|
||||
app.use(express.urlencoded({
|
||||
extended: true,
|
||||
limit: '5mb',
|
||||
parameterLimit: 100000
|
||||
}));
|
||||
app.use(auth);
|
||||
|
||||
// import the servers into one
|
||||
app.use(discovery);
|
||||
app.use(api);
|
||||
|
||||
// 404 handler
|
||||
LOG_INFO('Creating 404 status handler');
|
||||
app.use((_request: express.Request, response: express.Response) => {
|
||||
response.type('application/xml');
|
||||
response.status(404);
|
||||
|
||||
return response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: 'Not Found'
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
});
|
||||
|
||||
// non-404 error handler
|
||||
LOG_INFO('Creating non-404 status handler');
|
||||
app.use((_error: unknown, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
|
||||
const status = 500;
|
||||
response.type('application/xml');
|
||||
response.status(404);
|
||||
|
||||
return response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: status,
|
||||
message: 'Not Found'
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// Starts the server
|
||||
LOG_INFO('Starting server');
|
||||
|
||||
await connectDatabase();
|
||||
|
||||
app.listen(port, () => {
|
||||
LOG_SUCCESS(`Server started on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
35
src/services/api/index.ts
Normal file
35
src/services/api/index.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { LOG_INFO } from '@/logger';
|
||||
|
||||
import postsHandlers from '@/services/api/routes/posts';
|
||||
import friendMessagesHandlers from '@/services/api/routes/friend_messages';
|
||||
import communitiesHandlers from '@/services/api/routes/communities';
|
||||
import peopleHandlers from '@/services/api/routes/people';
|
||||
import topicsHandlers from '@/services/api/routes/topics';
|
||||
import usersHandlers from '@/services/api/routes/users';
|
||||
import statusHandlers from '@/services/api/routes/status';
|
||||
|
||||
// Main router for endpointsindex.js
|
||||
const router = express.Router();
|
||||
|
||||
// Router to handle the subdomain restriction
|
||||
const api = express.Router();
|
||||
|
||||
// Create subdomains
|
||||
LOG_INFO('[MIIVERSE] Creating \'api\' subdomain');
|
||||
router.use(subdomain('api.olv', api));
|
||||
router.use(subdomain('api-test.olv', api));
|
||||
router.use(subdomain('api-dev.olv', api));
|
||||
|
||||
// Setup routes
|
||||
api.use('/v1/posts', postsHandlers);
|
||||
api.use('/v1/posts.search', postsHandlers);
|
||||
api.use('/v1/friend_messages', friendMessagesHandlers);
|
||||
api.use('/v1/communities/', communitiesHandlers);
|
||||
api.use('/v1/people/', peopleHandlers);
|
||||
api.use('/v1/topics/', topicsHandlers);
|
||||
api.use('/v1/users/', usersHandlers);
|
||||
api.use('/v1/status/', statusHandlers);
|
||||
|
||||
export default router;
|
||||
481
src/services/api/routes/communities.ts
Normal file
481
src/services/api/routes/communities.ts
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import multer from 'multer';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
getMostPopularCommunities,
|
||||
getNewCommunities,
|
||||
getCommunityByTitleID,
|
||||
getUserContent,
|
||||
} from '@/database';
|
||||
import { getValueFromQueryString } from '@/util';
|
||||
import { LOG_WARN } from '@/logger';
|
||||
import { Community } from '@/models/community';
|
||||
import { Post } from '@/models/post';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { SubCommunityQuery } from '@/types/mongoose/subcommunity-query';
|
||||
import { CommunityPostsQuery } from '@/types/mongoose/community-posts-query';
|
||||
import { HydratedPostDocument, IPost } from '@/types/mongoose/post';
|
||||
import { ParamPack } from '@/types/common/param-pack';
|
||||
import { CommunitiesResult, CommunityPostsResult } from '@/types/miiverse/community';
|
||||
|
||||
const createNewCommunitySchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string(),
|
||||
app_data: z.string().optional()
|
||||
});
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function respondCommunityError(response: express.Response, httpStatusCode: number, errorCode: number): void {
|
||||
response.status(httpStatusCode).send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: httpStatusCode,
|
||||
error_code: errorCode,
|
||||
message: 'COMMUNITY_ERROR' // This field is unused by the entire nn_olv.rpl
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
}
|
||||
|
||||
function respondCommunityNotFound(response: express.Response): void {
|
||||
respondCommunityError(response, 404, 919);
|
||||
}
|
||||
|
||||
async function commonGetSubCommunity(paramPack: ParamPack, communityID: string | undefined): Promise<HydratedCommunityDocument | null> {
|
||||
|
||||
const parentCommunity = await getCommunityByTitleID(paramPack.title_id);
|
||||
|
||||
if (!parentCommunity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = {
|
||||
parent: parentCommunity.olive_community_id,
|
||||
community_id: communityID
|
||||
};
|
||||
|
||||
const community = await Community.findOne(query);
|
||||
|
||||
if (!community) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return community;
|
||||
}
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const parentCommunity = await getCommunityByTitleID(request.paramPack.title_id);
|
||||
if (!parentCommunity) {
|
||||
respondCommunityNotFound(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = getValueFromQueryString(request.query, 'type')[0];
|
||||
const limitString = getValueFromQueryString(request.query, 'limit')[0];
|
||||
|
||||
let limit = 4;
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 4;
|
||||
}
|
||||
|
||||
if (limit > 16) {
|
||||
limit = 16;
|
||||
}
|
||||
|
||||
const query: SubCommunityQuery = {
|
||||
parent: parentCommunity.olive_community_id
|
||||
};
|
||||
|
||||
if (type === 'my') {
|
||||
query.owner = request.pid;
|
||||
} else if (type === 'favorite') {
|
||||
query.user_favorites = request.pid;
|
||||
}
|
||||
|
||||
const communities = await Community.find(query).limit(limit);
|
||||
|
||||
const result: CommunitiesResult = {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'communities',
|
||||
communities: []
|
||||
};
|
||||
|
||||
for (const community of communities) {
|
||||
result.communities.push({
|
||||
community: community.json()
|
||||
});
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result
|
||||
}, {
|
||||
separateArrayItems: true
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
router.get('/popular', async function (_request: express.Request, response: express.Response): Promise<void> {
|
||||
const popularCommunities = await getMostPopularCommunities(100);
|
||||
|
||||
response.type('application/json');
|
||||
response.send(popularCommunities);
|
||||
});
|
||||
|
||||
router.get('/new', async function (_request: express.Request, response: express.Response): Promise<void> {
|
||||
const newCommunities = await getNewCommunities(100);
|
||||
|
||||
response.type('application/json');
|
||||
response.send(newCommunities);
|
||||
});
|
||||
|
||||
router.get('/:communityID/posts', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
let community = await Community.findOne({
|
||||
community_id: request.params.communityID
|
||||
});
|
||||
|
||||
if (!community) {
|
||||
community = await getCommunityByTitleID(request.paramPack.title_id);
|
||||
}
|
||||
|
||||
if (!community) {
|
||||
return respondCommunityNotFound(response);
|
||||
}
|
||||
|
||||
const query: CommunityPostsQuery = {
|
||||
community_id: community.olive_community_id,
|
||||
removed: false,
|
||||
app_data: { $ne: null },
|
||||
message_to_pid: { $eq: null }
|
||||
};
|
||||
|
||||
const searchKey = getValueFromQueryString(request.query, 'search_key')[0];
|
||||
const allowSpoiler = getValueFromQueryString(request.query, 'allow_spoiler')[0];
|
||||
const postType = getValueFromQueryString(request.query, 'type')[0];
|
||||
const queryBy = getValueFromQueryString(request.query, 'by')[0];
|
||||
const distinctPID = getValueFromQueryString(request.query, 'distinct_pid')[0];
|
||||
const limitString = getValueFromQueryString(request.query, 'limit')[0];
|
||||
const withMii = getValueFromQueryString(request.query, 'with_mii')[0];
|
||||
|
||||
let limit = 10;
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
if (searchKey) {
|
||||
query.search_key = searchKey;
|
||||
}
|
||||
|
||||
if (!allowSpoiler) {
|
||||
query.is_spoiler = 0;
|
||||
}
|
||||
|
||||
//TODO: There probably is a type for text and screenshots too, will have to investigate
|
||||
if (postType === 'memo') {
|
||||
query.painting = { $ne: null };
|
||||
}
|
||||
|
||||
if (queryBy === 'followings') {
|
||||
const userContent = await getUserContent(request.pid);
|
||||
|
||||
if (!userContent) {
|
||||
LOG_WARN(`USER PID ${request.pid} HAS NO USER CONTENT`);
|
||||
query.pid = [];
|
||||
} else {
|
||||
query.pid = userContent.following_users;
|
||||
}
|
||||
} else if (queryBy === 'self') {
|
||||
query.pid = request.pid;
|
||||
}
|
||||
|
||||
let posts: HydratedPostDocument[];
|
||||
|
||||
if (distinctPID && distinctPID === '1') {
|
||||
const unhydratedPosts = await Post.aggregate<IPost>([
|
||||
{ $match: query }, // filter based on input query
|
||||
{ $sort: { created_at: -1 } }, // sort by 'created_at' in descending order
|
||||
{ $group: { _id: '$pid', doc: { $first: '$$ROOT' } } }, // remove any duplicate 'pid' elements
|
||||
{ $replaceRoot: { newRoot: '$doc' } }, // replace the root with the 'doc' field
|
||||
{ $limit: limit } // only return the top 10 results
|
||||
]);
|
||||
posts = unhydratedPosts.map((post: IPost) => Post.hydrate(post));
|
||||
} else {
|
||||
posts = await Post.find(query).sort({ created_at: -1 }).limit(limit);
|
||||
}
|
||||
|
||||
const result: CommunityPostsResult = {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'posts',
|
||||
topic: {
|
||||
community_id: community.community_id
|
||||
},
|
||||
posts: []
|
||||
};
|
||||
|
||||
for (const post of posts) {
|
||||
result.posts.push({
|
||||
post: post.json({
|
||||
with_mii: withMii === '1',
|
||||
app_data: true,
|
||||
topic_tag: true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result
|
||||
}, {
|
||||
separateArrayItems: true
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
// Handler for POST on '/v1/communities'
|
||||
router.post('/', multer().none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const parentCommunity = await getCommunityByTitleID(request.paramPack.title_id);
|
||||
if (!parentCommunity) {
|
||||
return respondCommunityNotFound(response);
|
||||
}
|
||||
|
||||
// TODO - Better error codes, maybe do defaults?
|
||||
const bodyCheck = createNewCommunitySchema.safeParse(request.body);
|
||||
if (!bodyCheck.success) {
|
||||
return respondCommunityError(response, 400, 20);
|
||||
}
|
||||
|
||||
request.body.name = request.body.name.trim();
|
||||
request.body.icon = request.body.icon.trim();
|
||||
|
||||
if (request.body.description) {
|
||||
request.body.description = request.body.description.trim();
|
||||
}
|
||||
|
||||
if (request.body.app_data) {
|
||||
request.body.app_data = request.body.app_data.trim();
|
||||
}
|
||||
|
||||
// Name must be at least 4 character long
|
||||
if (request.body.name.length < 4) {
|
||||
return respondCommunityError(response, 400, 20);
|
||||
}
|
||||
|
||||
// Each user can only have 4 subcommunities per title
|
||||
const ownedQuery = {
|
||||
parent: parentCommunity.olive_community_id,
|
||||
owner: request.pid
|
||||
};
|
||||
|
||||
const ownedSubcommunityCount = await Community.countDocuments(ownedQuery);
|
||||
if (ownedSubcommunityCount >= 4) {
|
||||
return respondCommunityError(response, 401, 911);
|
||||
}
|
||||
|
||||
// Each user can only have 16 favorite subcommunities per title
|
||||
const favoriteQuery = {
|
||||
parent: parentCommunity.olive_community_id,
|
||||
user_favorites: request.pid
|
||||
};
|
||||
|
||||
const ownedFavoriteCount = await Community.countDocuments(favoriteQuery);
|
||||
if (ownedFavoriteCount >= 16) {
|
||||
return respondCommunityError(response, 401, 912);
|
||||
}
|
||||
|
||||
const communitiesCount = await Community.count();
|
||||
const communityID = (parseInt(parentCommunity.community_id) + (5000 * communitiesCount)); // Change this to auto increment
|
||||
const community = await Community.create({
|
||||
platform_id: 0, // WiiU
|
||||
name: request.body.name,
|
||||
description: request.body.description || '',
|
||||
open: true,
|
||||
allows_comments: true,
|
||||
type: 1,
|
||||
parent: parentCommunity.olive_community_id,
|
||||
admins: parentCommunity.admins,
|
||||
owner: request.pid,
|
||||
icon: request.body.icon,
|
||||
title_id: request.paramPack.title_id,
|
||||
community_id: communityID.toString(),
|
||||
olive_community_id: communityID.toString(),
|
||||
app_data: request.body.app_data || '',
|
||||
user_favorites: [request.pid]
|
||||
});
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
request_name: 'community',
|
||||
community: community.json()
|
||||
}
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
router.post('/:community_id.delete', multer().none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const community = await commonGetSubCommunity(request.paramPack, request.params.community_id);
|
||||
|
||||
if (!community) {
|
||||
respondCommunityNotFound(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (community.owner != request.pid) {
|
||||
response.sendStatus(403); // Forbidden
|
||||
return;
|
||||
}
|
||||
|
||||
await Community.deleteOne({ _id: community._id });
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
request_name: 'community',
|
||||
community: community.json()
|
||||
}
|
||||
}).end({ pretty: true, allowEmpty: true }));
|
||||
});
|
||||
|
||||
router.post('/:community_id.favorite', multer().none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const community = await commonGetSubCommunity(request.paramPack, request.params.community_id);
|
||||
|
||||
if (!community) {
|
||||
respondCommunityNotFound(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Each user can only have 16 favorite subcommunities per title
|
||||
const favoriteQuery = {
|
||||
parent: community.parent,
|
||||
user_favorites: request.pid
|
||||
};
|
||||
|
||||
const ownedFavoriteCount = await Community.countDocuments(favoriteQuery);
|
||||
if (ownedFavoriteCount >= 16) {
|
||||
return respondCommunityError(response, 401, 914);
|
||||
}
|
||||
|
||||
await community.addUserFavorite(request.pid);
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
request_name: 'community',
|
||||
community: community.json()
|
||||
}
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
router.post('/:community_id.unfavorite', multer().none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const community = await commonGetSubCommunity(request.paramPack, request.params.community_id);
|
||||
if (!community) {
|
||||
respondCommunityNotFound(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// You can't remove from your favorites a community you own
|
||||
if (community.owner === request.pid) {
|
||||
return respondCommunityError(response, 401, 916);
|
||||
}
|
||||
|
||||
await community.delUserFavorite(request.pid);
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
request_name: 'community',
|
||||
community: community.json()
|
||||
}
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
router.post('/:community_id', multer().none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const community = await commonGetSubCommunity(request.paramPack, request.params.community_id);
|
||||
|
||||
if (!community) {
|
||||
respondCommunityNotFound(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (community.owner != request.pid) {
|
||||
response.sendStatus(403); // Forbidden
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.body.name) {
|
||||
community.name = request.body.name.trim();
|
||||
}
|
||||
|
||||
if (request.body.description) {
|
||||
community.description = request.body.description.trim();
|
||||
}
|
||||
|
||||
if (request.body.icon) {
|
||||
community.icon = request.body.icon.trim();
|
||||
}
|
||||
|
||||
if (request.body.app_data) {
|
||||
community.app_data = request.body.app_data.trim();
|
||||
}
|
||||
|
||||
await community.save();
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
request_name: 'community',
|
||||
community: community.json()
|
||||
}
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
export default router;
|
||||
300
src/services/api/routes/friend_messages.ts
Normal file
300
src/services/api/routes/friend_messages.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { Snowflake } from 'node-snowflake';
|
||||
import moment from 'moment';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { z } from 'zod';
|
||||
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
|
||||
import { getUserFriendPIDs, getUserAccountData, processPainting, uploadCDNAsset, getValueFromQueryString } from '@/util';
|
||||
import { getConversationByUsers, getUserSettings, getFriendMessages } from '@/database';
|
||||
import { LOG_WARN } from '@/logger';
|
||||
import { Post } from '@/models/post';
|
||||
import { Conversation } from '@/models/conversation';
|
||||
import { FormattedMessage } from '@/types/common/formatted-message';
|
||||
|
||||
const sendMessageSchema = z.object({
|
||||
message_to_pid: z.string().transform(Number),
|
||||
body: z.string(),
|
||||
painting: z.string().optional(),
|
||||
screenshot: z.string().optional(),
|
||||
app_data: z.string().optional()
|
||||
});
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer();
|
||||
|
||||
router.post('/', upload.none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
// TODO - Better error codes, maybe do defaults?
|
||||
const bodyCheck = sendMessageSchema.safeParse(request.body);
|
||||
|
||||
if (!bodyCheck.success) {
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientPID = bodyCheck.data.message_to_pid;
|
||||
let messageBody = bodyCheck.data.body;
|
||||
const painting = bodyCheck.data.painting?.replace(/\0/g, '').trim() || '';
|
||||
const screenshot = bodyCheck.data.screenshot?.trim().replace(/\0/g, '').trim() || '';
|
||||
const appData = bodyCheck.data.app_data?.replace(/[^A-Za-z0-9+/=\s]/g, '').trim() || '';
|
||||
|
||||
if (isNaN(recipientPID)) {
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
let sender: GetUserDataResponse;
|
||||
|
||||
try {
|
||||
sender = await getUserAccountData(request.pid);
|
||||
} catch (error) {
|
||||
// TODO - Log this error
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sender.mii) {
|
||||
// * This should never happen, but TypeScript complains so check anyway
|
||||
// TODO - Better errors
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
let recipient: GetUserDataResponse;
|
||||
|
||||
try {
|
||||
recipient = await getUserAccountData(request.pid);
|
||||
} catch (error) {
|
||||
// TODO - Log this error
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
let conversation = await getConversationByUsers([sender.pid, recipient.pid]);
|
||||
|
||||
if (!conversation) {
|
||||
const userSettings = await getUserSettings(request.pid);
|
||||
const user2Settings = await getUserSettings(recipient.pid);
|
||||
|
||||
if (!sender || !recipient || userSettings || user2Settings) {
|
||||
response.sendStatus(422);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation = await Conversation.create({
|
||||
id: Snowflake.nextId(),
|
||||
users: [
|
||||
{
|
||||
pid: sender.pid,
|
||||
official: (sender.accessLevel === 2 || sender.accessLevel === 3),
|
||||
read: true
|
||||
},
|
||||
{
|
||||
pid: recipient.pid,
|
||||
official: (recipient.accessLevel === 2 || recipient.accessLevel === 3),
|
||||
read: false
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (!conversation) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const friendPIDs = await getUserFriendPIDs(recipient.pid);
|
||||
|
||||
if (friendPIDs.indexOf(request.pid) === -1) {
|
||||
response.sendStatus(422);
|
||||
return;
|
||||
}
|
||||
|
||||
let miiFace = 'normal_face.png';
|
||||
switch (parseInt(request.body.feeling_id)) {
|
||||
case 1:
|
||||
miiFace = 'smile_open_mouth.png';
|
||||
break;
|
||||
case 2:
|
||||
miiFace = 'wink_left.png';
|
||||
break;
|
||||
case 3:
|
||||
miiFace = 'surprise_open_mouth.png';
|
||||
break;
|
||||
case 4:
|
||||
miiFace = 'frustrated.png';
|
||||
break;
|
||||
case 5:
|
||||
miiFace = 'sorrow.png';
|
||||
break;
|
||||
}
|
||||
|
||||
if (messageBody) {
|
||||
messageBody = messageBody.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}‛¨ƒºª«»“”„¿¡←→↑↓√§¶†‡¦–—⇒⇔¤¢€£¥™©®+×÷=±∞ˇ˘˙¸˛˜′″µ°¹²³♭♪•…¬¯‰¼½¾♡♥●◆■▲▼☆★♀♂,./?;:'"\\<>]/g, '');
|
||||
}
|
||||
|
||||
if (messageBody.length > 280) {
|
||||
messageBody = messageBody.substring(0, 280);
|
||||
}
|
||||
|
||||
if (messageBody === '' && painting === '' && screenshot === '') {
|
||||
response.status(422);
|
||||
response.redirect(`/friend_messages/${conversation.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await Post.create({
|
||||
title_id: request.paramPack.title_id,
|
||||
community_id: conversation.id,
|
||||
screen_name: sender.mii.name,
|
||||
body: messageBody,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
screenshot: '',
|
||||
screenshot_length: 0,
|
||||
country_id: request.paramPack.country_id,
|
||||
created_at: new Date(),
|
||||
feeling_id: request.body.feeling_id,
|
||||
search_key: request.body.search_key,
|
||||
topic_tag: request.body.topic_tag,
|
||||
is_autopost: request.body.is_autopost,
|
||||
is_spoiler: (request.body.spoiler) ? 1 : 0,
|
||||
is_app_jumpable: request.body.is_app_jumpable,
|
||||
language_id: request.body.language_id,
|
||||
mii: sender.mii.data,
|
||||
mii_face_url: `https://mii.olv.pretendo.cc/mii/${sender.pid}/${miiFace}`,
|
||||
pid: request.pid,
|
||||
platform_id: request.paramPack.platform_id,
|
||||
region_id: request.paramPack.region_id,
|
||||
verified: (sender.accessLevel === 2 || sender.accessLevel === 3),
|
||||
message_to_pid: request.body.message_to_pid,
|
||||
parent: null,
|
||||
removed: false
|
||||
});
|
||||
|
||||
if (painting) {
|
||||
const paintingBuffer = await processPainting(painting);
|
||||
|
||||
if (paintingBuffer) {
|
||||
await uploadCDNAsset('pn-cdn', `paintings/${request.pid}/${post.id}.png`, paintingBuffer, 'public-read');
|
||||
} else {
|
||||
LOG_WARN(`PAINTING FOR POST ${post.id} FAILED TO PROCESS`);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshot) {
|
||||
const screenshotBuffer = Buffer.from(screenshot, 'base64');
|
||||
|
||||
await uploadCDNAsset('pn-cdn', `screenshots/${request.pid}/${post.id}.jpg`, screenshotBuffer, 'public-read');
|
||||
|
||||
post.screenshot = `/screenshots/${request.pid}/${post.id}.jpg`;
|
||||
post.screenshot_length = screenshot.length;
|
||||
|
||||
await post.save();
|
||||
}
|
||||
|
||||
let postPreviewText = messageBody;
|
||||
if (painting) {
|
||||
postPreviewText = 'sent a Drawing';
|
||||
} else if (messageBody.length > 25) {
|
||||
postPreviewText = messageBody.substring(0, 25) + '...';
|
||||
}
|
||||
|
||||
await conversation.newMessage(postPreviewText, recipientPID);
|
||||
|
||||
response.sendStatus(200);
|
||||
});
|
||||
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const limitString = getValueFromQueryString(request.query, 'limit')[0];
|
||||
|
||||
// TODO - Is this the limit?
|
||||
let limit = 10;
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
if (!request.query.search_key) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const searchKey = getValueFromQueryString(request.query, 'search_key');
|
||||
|
||||
const messages = await getFriendMessages(request.pid.toString(), searchKey, limit);
|
||||
|
||||
const postBody: FormattedMessage[] = [];
|
||||
for (const message of messages) {
|
||||
postBody.push({
|
||||
post: {
|
||||
body: message.body,
|
||||
country_id: message.country_id || 0,
|
||||
created_at: moment(message.created_at).format('YYYY-MM-DD HH:MM:SS'),
|
||||
feeling_id: message.feeling_id || 0,
|
||||
id: message.id,
|
||||
is_autopost: message.is_autopost,
|
||||
is_spoiler: message.is_spoiler,
|
||||
is_app_jumpable: message.is_app_jumpable,
|
||||
empathy_added: message.empathy_count,
|
||||
language_id: message.language_id,
|
||||
message_to_pid: message.message_to_pid,
|
||||
mii: message.mii,
|
||||
mii_face_url: message.mii_face_url,
|
||||
number: message.number || 0,
|
||||
pid: message.pid,
|
||||
platform_id: message.platform_id || 0,
|
||||
region_id: message.region_id || 0,
|
||||
reply_count: message.reply_count,
|
||||
screen_name: message.screen_name,
|
||||
topic_tag: {
|
||||
name: message.topic_tag,
|
||||
title_id: 0
|
||||
},
|
||||
title_id: message.title_id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'friend_messages',
|
||||
posts: postBody
|
||||
}
|
||||
}, { separateArrayItems: true }).end({ pretty: true }));
|
||||
});
|
||||
|
||||
router.post('/:post_id/empathies', upload.none(), async function (_request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
// TODO - FOR JEMMA! FIX THIS! MISSING MONGOOSE SCHEMA METHODS
|
||||
// * Remove the underscores from request and response to make them seen by eslint again
|
||||
/*
|
||||
let pid = getPIDFromServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
const post = await getPostByID(req.params.post_id);
|
||||
if(pid === null) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
let user = await getUserByPID(pid);
|
||||
if(user.likes.indexOf(post.id) === -1 && user.id !== post.pid)
|
||||
{
|
||||
post.upEmpathy();
|
||||
user.addToLikes(post.id)
|
||||
res.sendStatus(200);
|
||||
}
|
||||
else
|
||||
res.sendStatus(403);
|
||||
*/
|
||||
});
|
||||
|
||||
export default router;
|
||||
151
src/services/api/routes/people.ts
Normal file
151
src/services/api/routes/people.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import moment from 'moment';
|
||||
import { getUserContent, getFollowedUsers } from '@/database';
|
||||
import { getValueFromQueryString, getUserFriendPIDs } from '@/util';
|
||||
import { Post } from '@/models/post';
|
||||
import { CommunityPostsQuery } from '@/types/mongoose/community-posts-query';
|
||||
import { HydratedPostDocument, IPost } from '@/types/mongoose/post';
|
||||
import { PeopleFollowingResult, PeoplePostsResult } from '@/types/miiverse/people';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const userContent = await getUserContent(request.pid);
|
||||
|
||||
if (!userContent) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const query: CommunityPostsQuery = {
|
||||
removed: false,
|
||||
is_spoiler: 0,
|
||||
app_data: { $eq: null },
|
||||
parent: { $eq: null },
|
||||
message_to_pid: { $eq: null }
|
||||
};
|
||||
|
||||
const relation = getValueFromQueryString(request.query, 'relation')[0];
|
||||
const distinctPID = getValueFromQueryString(request.query, 'distinct_pid')[0];
|
||||
const limitString = getValueFromQueryString(request.query, 'limit')[0];
|
||||
const withMii = getValueFromQueryString(request.query, 'with_mii')[0];
|
||||
|
||||
let limit = 10;
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
if (relation === 'friend') {
|
||||
query.pid = { $in: await getUserFriendPIDs(request.pid) };
|
||||
} else if (relation === 'following') {
|
||||
query.pid = { $in: userContent.followed_users };
|
||||
} else if (request.query.pid) {
|
||||
const pidInputs = getValueFromQueryString(request.query, 'pid');
|
||||
const pids = pidInputs.map(pid => Number(pid)).filter(pid => !isNaN(pid));
|
||||
|
||||
query.pid = { $in: pids };
|
||||
}
|
||||
|
||||
let posts: HydratedPostDocument[];
|
||||
|
||||
if (distinctPID === '1') {
|
||||
const unhydratedPosts = await Post.aggregate<IPost>([
|
||||
{ $match: query }, // filter based on input query
|
||||
{ $sort: { created_at: -1 } }, // sort by 'created_at' in descending order
|
||||
{ $group: { _id: '$pid', doc: { $first: '$$ROOT' } } }, // remove any duplicate 'pid' elements
|
||||
{ $replaceRoot: { newRoot: '$doc' } }, // replace the root with the 'doc' field
|
||||
{ $limit: limit } // only return the top 10 results
|
||||
]);
|
||||
|
||||
posts = unhydratedPosts.map((post: IPost) => Post.hydrate(post));
|
||||
} else if (request.query.is_hot === '1') {
|
||||
posts = await Post.find(query).sort({ empathy_count: -1}).limit(limit);
|
||||
} else {
|
||||
posts = await Post.find(query).sort({ created_at: -1}).limit(limit);
|
||||
}
|
||||
|
||||
const result: PeoplePostsResult = {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
expire: moment().add(1, 'days').format('YYYY-MM-DD HH:MM:SS'),
|
||||
request_name: 'posts',
|
||||
people: []
|
||||
};
|
||||
|
||||
for (const post of posts) {
|
||||
result.people.push({
|
||||
person: {
|
||||
posts: [
|
||||
{
|
||||
post: post.json({
|
||||
with_mii: withMii === '1',
|
||||
topic_tag: true
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result
|
||||
}, {
|
||||
separateArrayItems: true
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
router.get('/:pid/following', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const pid = parseInt(request.params.pid);
|
||||
|
||||
if (isNaN(pid)) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const userContent = await getUserContent(pid);
|
||||
|
||||
if (!userContent) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const people = await getFollowedUsers(userContent);
|
||||
|
||||
const result: PeopleFollowingResult = {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'user_infos',
|
||||
people: []
|
||||
};
|
||||
|
||||
for (const person of people) {
|
||||
result.people.push({
|
||||
person: person.json()
|
||||
});
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result
|
||||
}, {
|
||||
separateArrayItems: true
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
export default router;
|
||||
421
src/services/api/routes/posts.ts
Normal file
421
src/services/api/routes/posts.ts
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { z } from 'zod';
|
||||
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
|
||||
import { getUserAccountData, processPainting, uploadCDNAsset, getValueFromQueryString } from '@/util';
|
||||
import {
|
||||
getPostByID,
|
||||
getUserContent,
|
||||
getPostReplies,
|
||||
getUserSettings,
|
||||
getCommunityByID,
|
||||
getCommunityByTitleID,
|
||||
getDuplicatePosts
|
||||
} from '@/database';
|
||||
import { LOG_WARN } from '@/logger';
|
||||
import { Post } from '@/models/post';
|
||||
import { Community } from '@/models/community';
|
||||
import { HydratedPostDocument } from '@/types/mongoose/post';
|
||||
import { PostRepliesResult } from '@/types/miiverse/post';
|
||||
|
||||
const newPostSchema = z.object({
|
||||
community_id: z.string().optional(),
|
||||
app_data: z.string().optional(),
|
||||
painting: z.string().optional(),
|
||||
screenshot: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
feeling_id: z.string(),
|
||||
search_key: z.string().array().or(z.string()).optional(),
|
||||
topic_tag: z.string().optional(),
|
||||
is_autopost: z.string(),
|
||||
is_spoiler: z.string().optional(),
|
||||
is_app_jumpable: z.string().optional(),
|
||||
language_id: z.string()
|
||||
});
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer();
|
||||
|
||||
/* GET post titles. */
|
||||
router.post('/', upload.none(), newPost);
|
||||
|
||||
router.post('/:post_id/replies', upload.none(), newPost);
|
||||
|
||||
router.post('/:post_id.delete', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const post = await getPostByID(request.params.post_id);
|
||||
const userContent = await getUserContent(request.pid);
|
||||
|
||||
if (!post || !userContent) {
|
||||
response.sendStatus(504);
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.pid === userContent.pid) {
|
||||
await post.del('User requested removal');
|
||||
response.sendStatus(200);
|
||||
} else {
|
||||
response.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:post_id/empathies', upload.none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const post = await getPostByID(request.params.post_id);
|
||||
|
||||
if (!post) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.yeahs?.indexOf(request.pid) === -1) {
|
||||
await Post.updateOne({
|
||||
id: post.id,
|
||||
yeahs: {
|
||||
$ne: request.pid
|
||||
}
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
empathy_count: 1
|
||||
},
|
||||
$push: {
|
||||
yeahs: request.pid
|
||||
}
|
||||
});
|
||||
} else if (post.yeahs?.indexOf(request.pid) !== -1) {
|
||||
await Post.updateOne({
|
||||
id: post.id,
|
||||
yeahs: {
|
||||
$eq: request.pid
|
||||
}
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
empathy_count: -1
|
||||
},
|
||||
$pull: {
|
||||
yeahs: request.pid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
response.sendStatus(200);
|
||||
});
|
||||
|
||||
router.get('/:post_id/replies', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const limitString = getValueFromQueryString(request.query, 'limit')[0];
|
||||
|
||||
let limit = 10; // TODO - Is there a real limit?
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
const post = await getPostByID(request.params.post_id);
|
||||
|
||||
if (!post) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const posts = await getPostReplies(post.id, limit);
|
||||
if (posts.length === 0) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const result: PostRepliesResult = {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'replies',
|
||||
posts: []
|
||||
};
|
||||
|
||||
for (const post of posts) {
|
||||
result.posts.push({
|
||||
post: post.json({
|
||||
with_mii: request.query.with_mii as string === '1',
|
||||
topic_tag: true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result
|
||||
}, {
|
||||
separateArrayItems: true
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
}));
|
||||
});
|
||||
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
const postID = getValueFromQueryString(request.query, 'post_id')[0];
|
||||
|
||||
if (!postID) {
|
||||
response.type('application/xml');
|
||||
response.status(404);
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: 'Not Found'
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await getPostByID(postID);
|
||||
|
||||
if (!post) {
|
||||
response.status(404);
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: 'Not Found'
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
request_name: 'posts.search',
|
||||
posts: {
|
||||
post: post.json({ with_mii: true })
|
||||
}
|
||||
}
|
||||
}).end({ pretty: true, allowEmpty: true }));
|
||||
});
|
||||
|
||||
async function newPost(request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
let user: GetUserDataResponse;
|
||||
|
||||
try {
|
||||
user = await getUserAccountData(request.pid);
|
||||
} catch (error) {
|
||||
// TODO - Log this error
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.mii) {
|
||||
// * This should never happen, but TypeScript complains so check anyway
|
||||
// TODO - Better errors
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
const userSettings = await getUserSettings(request.pid);
|
||||
const bodyCheck = newPostSchema.safeParse(request.body);
|
||||
|
||||
if (!userSettings || !bodyCheck.success) {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
const communityID = bodyCheck.data.community_id || '';
|
||||
let messageBody = bodyCheck.data.body;
|
||||
const painting = bodyCheck.data.painting?.replace(/\0/g, '').trim() || '';
|
||||
const screenshot = bodyCheck.data.screenshot?.replace(/\0/g, '').trim() || '';
|
||||
const appData = bodyCheck.data.app_data?.replace(/[^A-Za-z0-9+/=\s]/g, '').trim() || '';
|
||||
const feelingID = parseInt(bodyCheck.data.feeling_id);
|
||||
let searchKey = bodyCheck.data.search_key || [];
|
||||
const topicTag = bodyCheck.data.topic_tag || '';
|
||||
const autopost = bodyCheck.data.is_autopost;
|
||||
const spoiler = bodyCheck.data.is_spoiler;
|
||||
const jumpable = bodyCheck.data.is_app_jumpable;
|
||||
const languageID = parseInt(bodyCheck.data.language_id);
|
||||
const countryID = parseInt(request.paramPack.country_id);
|
||||
const platformID = parseInt(request.paramPack.platform_id);
|
||||
const regionID = parseInt(request.paramPack.region_id);
|
||||
|
||||
if (
|
||||
isNaN(feelingID) ||
|
||||
isNaN(languageID) ||
|
||||
isNaN(countryID) ||
|
||||
isNaN(platformID) ||
|
||||
isNaN(regionID)
|
||||
) {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
let community = await getCommunityByID(communityID);
|
||||
if (!community) {
|
||||
community = await Community.findOne({
|
||||
olive_community_id: communityID
|
||||
});
|
||||
}
|
||||
|
||||
if (!community) {
|
||||
community = await getCommunityByTitleID(request.paramPack.title_id);
|
||||
}
|
||||
|
||||
if (!community || userSettings.account_status !== 0 || community.community_id === 'announcements') {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
let parentPost: HydratedPostDocument | null = null;
|
||||
if (request.params.post_id) {
|
||||
parentPost = await getPostByID(request.params.post_id.toString());
|
||||
|
||||
if (!parentPost) {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - Clean this up
|
||||
// * Nesting this because of how manu checks there are, extremely unreadable otherwise
|
||||
if (!(community.admins && community.admins.indexOf(request.pid) !== -1 && userSettings.account_status === 0)) {
|
||||
if (community.type >= 2) {
|
||||
if (!(parentPost && community.allows_comments && community.open)) {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let miiFace = 'normal_face.png';
|
||||
switch (parseInt(request.body.feeling_id)) {
|
||||
case 1:
|
||||
miiFace = 'smile_open_mouth.png';
|
||||
break;
|
||||
case 2:
|
||||
miiFace = 'wink_left.png';
|
||||
break;
|
||||
case 3:
|
||||
miiFace = 'surprise_open_mouth.png';
|
||||
break;
|
||||
case 4:
|
||||
miiFace = 'frustrated.png';
|
||||
break;
|
||||
case 5:
|
||||
miiFace = 'sorrow.png';
|
||||
break;
|
||||
}
|
||||
|
||||
if (messageBody) {
|
||||
messageBody = messageBody.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}‛¨ƒºª«»“”„¿¡←→↑↓√§¶†‡¦–—⇒⇔¤¢€£¥™©®+×÷=±∞ˇ˘˙¸˛˜′″µ°¹²³♭♪•…¬¯‰¼½¾♡♥●◆■▲▼☆★♀♂,./?;:'"\\<>]/g, '');
|
||||
}
|
||||
|
||||
if (messageBody && messageBody.length > 280) {
|
||||
messageBody = messageBody.substring(0, 280);
|
||||
}
|
||||
|
||||
if ((!messageBody || messageBody === '') && painting === '' && screenshot === '') {
|
||||
response.status(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(searchKey)) {
|
||||
searchKey = [searchKey];
|
||||
}
|
||||
|
||||
const document = {
|
||||
id: '', // * This gets changed when saving the document for the first time
|
||||
title_id: request.paramPack.title_id,
|
||||
community_id: community.olive_community_id,
|
||||
screen_name: userSettings.screen_name,
|
||||
body: messageBody ? messageBody : '',
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
screenshot: '',
|
||||
screenshot_length: 0,
|
||||
country_id: countryID,
|
||||
created_at: new Date(),
|
||||
feeling_id: feelingID,
|
||||
search_key: searchKey,
|
||||
topic_tag: topicTag,
|
||||
is_autopost: (autopost) ? 1 : 0,
|
||||
is_spoiler: (spoiler === '1') ? 1 : 0,
|
||||
is_app_jumpable: (jumpable) ? 1 : 0,
|
||||
language_id: languageID,
|
||||
mii: user.mii.data,
|
||||
mii_face_url: `https://mii.olv.pretendo.cc/mii/${user.pid}/${miiFace}`,
|
||||
pid: request.pid,
|
||||
platform_id: platformID,
|
||||
region_id: regionID,
|
||||
verified: (user.accessLevel === 2 || user.accessLevel === 3),
|
||||
parent: parentPost ? parentPost.id : null,
|
||||
removed: false
|
||||
};
|
||||
|
||||
const duplicatePost = await getDuplicatePosts(request.pid, document);
|
||||
|
||||
if (duplicatePost) {
|
||||
response.status(400);
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: 'DUPLICATE_POST'
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await Post.create(document);
|
||||
|
||||
if (painting) {
|
||||
const paintingBuffer = await processPainting(painting);
|
||||
|
||||
if (paintingBuffer) {
|
||||
await uploadCDNAsset('pn-cdn', `paintings/${request.pid}/${post.id}.png`, paintingBuffer, 'public-read');
|
||||
} else {
|
||||
LOG_WARN(`PAINTING FOR POST ${post.id} FAILED TO PROCESS`);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshot) {
|
||||
const screenshotBuffer = Buffer.from(screenshot, 'base64');
|
||||
|
||||
await uploadCDNAsset('pn-cdn', `screenshots/${request.pid}/${post.id}.jpg`, screenshotBuffer, 'public-read');
|
||||
|
||||
post.screenshot = `/screenshots/${request.pid}/${post.id}.jpg`;
|
||||
post.screenshot_length = screenshot.length;
|
||||
|
||||
await post.save();
|
||||
}
|
||||
|
||||
if (parentPost) {
|
||||
parentPost.reply_count = (parentPost.reply_count || 0) + 1;
|
||||
parentPost.save();
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: '0',
|
||||
version: '1',
|
||||
post: {
|
||||
post: post.json({ with_mii: true })
|
||||
}
|
||||
}
|
||||
}).end({ pretty: true, allowEmpty: true }));
|
||||
}
|
||||
|
||||
export default router;
|
||||
20
src/services/api/routes/status.ts
Normal file
20
src/services/api/routes/status.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import express from 'express';
|
||||
import { getEndpoints } from '@/database';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', function(_request: express.Request, response: express.Response): void {
|
||||
response.send('Pong!');
|
||||
});
|
||||
|
||||
router.get('/database', async function(_request: express.Request, response: express.Response): Promise<void> {
|
||||
const endpoints = await getEndpoints();
|
||||
|
||||
if (endpoints && endpoints.length <= 0) {
|
||||
response.send('DB Connection Working! :D');
|
||||
} else {
|
||||
response.send('DB Connection Not Working! D:');
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
286
src/services/api/routes/topics.ts
Normal file
286
src/services/api/routes/topics.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
import express from 'express';
|
||||
import moment from 'moment';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import Cache from '@/cache';
|
||||
import { Post } from '@/models/post';
|
||||
import { Community } from '@/models/community';
|
||||
import { IPost } from '@/types/mongoose/post';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { WWPResult, WWPTopic } from '@/types/miiverse/wara-wara-plaza';
|
||||
|
||||
const router = express.Router();
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
const WARA_WARA_PLAZA_CACHE = new Cache<WWPResult>(ONE_HOUR);
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
// * Commented out for now because we just don't
|
||||
// * need this data here. WWP does not use the
|
||||
// * current users data atm. Also some users have
|
||||
// * BOSS tasks with outdated tokens, which aren't
|
||||
// * usable and thus break this request. This is
|
||||
// * done as a quick/hacky fix around that
|
||||
// TODO - Re-enable this and filter out the current users posts
|
||||
//let user: GetUserDataResponse;
|
||||
//
|
||||
//try {
|
||||
// user = await getUserAccountData(request.pid);
|
||||
//} catch (error) {
|
||||
// // TODO - Log this error
|
||||
// response.sendStatus(403);
|
||||
// return;
|
||||
//}
|
||||
//
|
||||
//let discovery: HydratedEndpointDocument | null;
|
||||
//
|
||||
//if (user) {
|
||||
// discovery = await getEndpoint(user.serverAccessLevel);
|
||||
//} else {
|
||||
// discovery = await getEndpoint('prod');
|
||||
//}
|
||||
//
|
||||
//if (!discovery || !discovery.topics) {
|
||||
// response.sendStatus(404);
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (!WARA_WARA_PLAZA_CACHE.valid()) {
|
||||
const communities = await calculateMostPopularCommunities(24, 10);
|
||||
|
||||
if (communities.length < 10) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
WARA_WARA_PLAZA_CACHE.update(await generateTopicsData(communities));
|
||||
}
|
||||
|
||||
const result = WARA_WARA_PLAZA_CACHE.get() || {};
|
||||
const xml = xmlbuilder.create({
|
||||
result: result
|
||||
}, {
|
||||
separateArrayItems: true
|
||||
}).end({
|
||||
pretty: true,
|
||||
allowEmpty: true
|
||||
});
|
||||
|
||||
response.send(xml);
|
||||
});
|
||||
|
||||
async function generateTopicsData(communities: HydratedCommunityDocument[]): Promise<WWPResult> {
|
||||
const topics: {
|
||||
topic: WWPTopic;
|
||||
}[] = [];
|
||||
|
||||
const seenPeople: number[] = [];
|
||||
|
||||
for (let i = 0; i < communities.length; i++) {
|
||||
const community = communities[i];
|
||||
|
||||
const empathies = await Post.aggregate<{ _id: null; total: number; }>([
|
||||
{
|
||||
$match: {
|
||||
community_id: community.olive_community_id
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
total: {
|
||||
$sum: '$empathy_count'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit: 1
|
||||
}
|
||||
]);
|
||||
|
||||
const topic: WWPTopic = {
|
||||
empathy_count: empathies[0]?.total || 0,
|
||||
has_shop_page: community.has_shop_page ? 1 : 0,
|
||||
icon: community.icon,
|
||||
title_ids: [],
|
||||
title_id: community.title_id[0],
|
||||
community_id: 0xFFFFFFFF, // * This is how it was in the real WWP. Unsure why, but it works
|
||||
is_recommended: community.is_recommended ? 1 : 0,
|
||||
name: community.name,
|
||||
people: [],
|
||||
position: i+1
|
||||
};
|
||||
|
||||
community.title_id.forEach(title_id => {
|
||||
// * Just in case
|
||||
if (title_id) {
|
||||
topic.title_ids.push({ title_id });
|
||||
}
|
||||
});
|
||||
|
||||
const people = await getCommunityPeople(community, seenPeople);
|
||||
|
||||
for (const person of people) {
|
||||
const post = Post.hydrate(person.post).json({
|
||||
with_mii: true,
|
||||
topic_tag: true
|
||||
});
|
||||
|
||||
post.community_id = 0xFFFFFFFF; // * Make this match above. This is how it was in the real WWP. Unsure why, but it works
|
||||
|
||||
topic.people.push({
|
||||
person: {
|
||||
posts: [
|
||||
{
|
||||
post
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
seenPeople.push(person._id);
|
||||
}
|
||||
|
||||
topics.push({
|
||||
topic: topic
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
expire: moment().add(2, 'days').format('YYYY-MM-DD HH:MM:SS'),
|
||||
request_name: 'topics',
|
||||
topics
|
||||
};
|
||||
}
|
||||
|
||||
async function getCommunityPeople(community: HydratedCommunityDocument, seenPeople: number[], hours = 24): Promise<{ _id: number; post: IPost }[]> {
|
||||
const now = new Date();
|
||||
const last24Hours = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
||||
const people = await Post.aggregate<{ _id: number; post: IPost }>([
|
||||
{
|
||||
$match: {
|
||||
title_id: {
|
||||
$in: community.title_id
|
||||
},
|
||||
created_at: {
|
||||
$gte: last24Hours
|
||||
},
|
||||
message_to_pid: null,
|
||||
parent: null,
|
||||
removed: false,
|
||||
pid: {
|
||||
// * Exclude people we have seen in other communities.
|
||||
// * This increases generation time, but ensures the
|
||||
// * max number of slots we can fill end up getting used
|
||||
$nin: seenPeople
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$pid',
|
||||
post: {
|
||||
$first: '$$ROOT'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit: 70 // * Arbitrary
|
||||
}
|
||||
]);
|
||||
|
||||
// TODO - Remove this check once out of beta and have more users
|
||||
// * We only do this because Juxtaposition is not super active
|
||||
// * due to it being in beta. If we don't expand the search
|
||||
// * time range then WWP still ends up fairly empty
|
||||
// *
|
||||
// * Ensure we have at *least* 20 people. Arbitrary.
|
||||
// * If the year is less than 2020, assume we've gone
|
||||
// * too far back. There are no more posts, just return
|
||||
// * what was found
|
||||
if (people.length < 20 && last24Hours.getFullYear() >= 2020) {
|
||||
// * Double the search range each time to get
|
||||
// * exponentially more posts. This speeds up
|
||||
// * the search at the cost of using older posts
|
||||
return getCommunityPeople(community, seenPeople, hours * 2);
|
||||
}
|
||||
|
||||
return people;
|
||||
}
|
||||
|
||||
async function calculateMostPopularCommunities(hours: number, limit: number): Promise<HydratedCommunityDocument[]> {
|
||||
const now = new Date();
|
||||
const last24Hours = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
||||
|
||||
if (!last24Hours) {
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
|
||||
const validCommunities = await Community.aggregate<{ _id: null; communities: string[]; }>([
|
||||
{
|
||||
$match: {
|
||||
type: 0,
|
||||
parent: null
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
communities: {
|
||||
$push: '$olive_community_id'
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const communityIDs = validCommunities[0].communities;
|
||||
|
||||
if (!communityIDs) {
|
||||
throw new Error('No communities found');
|
||||
}
|
||||
|
||||
const popularCommunities = await Post.aggregate<{ _id: null; count: number; }>([
|
||||
{
|
||||
$match: {
|
||||
created_at: {
|
||||
$gte: last24Hours
|
||||
},
|
||||
message_to_pid: null,
|
||||
community_id: {
|
||||
$in: communityIDs
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$community_id',
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit: limit
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
count: -1
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
if (popularCommunities.length < limit) {
|
||||
return calculateMostPopularCommunities(hours + hours, limit);
|
||||
}
|
||||
|
||||
return Community.find({
|
||||
olive_community_id: {
|
||||
$in: popularCommunities.map(({ _id }) => _id)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default router;
|
||||
26
src/services/api/routes/users.ts
Normal file
26
src/services/api/routes/users.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { getValueFromQueryString } from '@/util';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:pid/notifications', function(request: express.Request, response: express.Response): void {
|
||||
const type = getValueFromQueryString(request.query, 'type')[0];
|
||||
const titleID = getValueFromQueryString(request.query, 'title_id')[0];
|
||||
const pid = getValueFromQueryString(request.query, 'pid')[0];
|
||||
|
||||
console.log(type);
|
||||
console.log(titleID);
|
||||
console.log(pid);
|
||||
|
||||
response.type('application/xml');
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
posts: ' '
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
});
|
||||
|
||||
export default router;
|
||||
22
src/services/discovery/index.ts
Normal file
22
src/services/discovery/index.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import { LOG_INFO } from '@/logger';
|
||||
|
||||
import discoveryHandlers from '@/services/discovery/routes/discovery';
|
||||
|
||||
// Main router for endpointsindex.js
|
||||
const router = express.Router();
|
||||
|
||||
// Router to handle the subdomain restriction
|
||||
const discovery = express.Router();
|
||||
|
||||
// Create subdomains
|
||||
LOG_INFO('[MIIVERSE] Creating \'discovery\' subdomain');
|
||||
router.use(subdomain('discovery.olv', discovery));
|
||||
router.use(subdomain('discovery-test.olv', discovery));
|
||||
router.use(subdomain('discovery-dev.olv', discovery));
|
||||
|
||||
// Setup routes
|
||||
discovery.use('/v1/endpoint', discoveryHandlers);
|
||||
|
||||
export default router;
|
||||
104
src/services/discovery/routes/discovery.ts
Normal file
104
src/services/discovery/routes/discovery.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
|
||||
import { getUserAccountData } from '@/util';
|
||||
import { getEndpoint } from '@/database';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/* GET discovery server. */
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
response.type('application/xml');
|
||||
|
||||
let user: GetUserDataResponse;
|
||||
|
||||
try {
|
||||
user = await getUserAccountData(request.pid);
|
||||
} catch (error) {
|
||||
// TODO - Log this error
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
let discovery: HydratedEndpointDocument | null;
|
||||
|
||||
if (user) {
|
||||
discovery = await getEndpoint(user.serverAccessLevel);
|
||||
} else {
|
||||
discovery = await getEndpoint('prod');
|
||||
}
|
||||
|
||||
// TODO - Better error
|
||||
if (!discovery) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = '';
|
||||
let errorCode = 0;
|
||||
switch (discovery.status) {
|
||||
case 0:
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
endpoint: {
|
||||
host: discovery.host,
|
||||
api_host: discovery.api_host,
|
||||
portal_host: discovery.portal_host,
|
||||
n3ds_host: discovery.n3ds_host
|
||||
}
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
|
||||
return ;
|
||||
case 1:
|
||||
message = 'SYSTEM_UPDATE_REQUIRED';
|
||||
errorCode = 1;
|
||||
break;
|
||||
case 2:
|
||||
message = 'SETUP_NOT_COMPLETE';
|
||||
errorCode = 2;
|
||||
break;
|
||||
case 3:
|
||||
message = 'SERVICE_MAINTENANCE';
|
||||
errorCode = 3;
|
||||
break;
|
||||
case 4:
|
||||
message = 'SERVICE_CLOSED';
|
||||
errorCode = 4;
|
||||
break;
|
||||
case 5:
|
||||
message = 'PARENTAL_CONTROLS_ENABLED';
|
||||
errorCode = 5;
|
||||
break;
|
||||
case 6:
|
||||
message = 'POSTING_LIMITED_PARENTAL_CONTROLS';
|
||||
errorCode = 6;
|
||||
break;
|
||||
case 7:
|
||||
message = 'NNID_BANNED';
|
||||
errorCode = 7;
|
||||
response.type('application/xml');
|
||||
break;
|
||||
default:
|
||||
message = 'SERVER_ERROR';
|
||||
errorCode = 15;
|
||||
response.type('application/xml');
|
||||
break;
|
||||
}
|
||||
|
||||
response.status(400);
|
||||
response.send(xmlbuilder.create({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: errorCode,
|
||||
message: message
|
||||
}
|
||||
}).end({ pretty: true }));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
const express = require('express');
|
||||
const subdomain = require('express-subdomain');
|
||||
const sessionMiddleware = require('../../middleware/session');
|
||||
const pnidMiddleware = require('../../middleware/pnid');
|
||||
const logger = require('../../logger');
|
||||
const routes = require('./routes');
|
||||
|
||||
// Main router for endpointsindex.js
|
||||
const router = express.Router();
|
||||
|
||||
// Router to handle the subdomain restriction
|
||||
const discovery = express.Router();
|
||||
const api = express.Router();
|
||||
|
||||
// Create subdomains
|
||||
logger.info('[MIIVERSE] Creating \'discovery\' subdomain');
|
||||
router.use(subdomain('discovery.olv', discovery));
|
||||
logger.info('[MIIVERSE] Creating \'api\' subdomain');
|
||||
router.use(subdomain('api.olv', api));
|
||||
|
||||
|
||||
logger.info('[MIIVERSE] Importing middleware');
|
||||
discovery.use(sessionMiddleware);
|
||||
discovery.use(pnidMiddleware);
|
||||
|
||||
// Setup routes
|
||||
discovery.use('/v1/endpoint', routes.DISCOVERY);
|
||||
api.use('/v1/posts', routes.POST);
|
||||
api.use('/v1/friend_messages', routes.MESSAGE);
|
||||
api.use('/v1/communities/', routes.COMMUNITY);
|
||||
api.use('/v1/people/', routes.PEOPLE);
|
||||
api.use('/v1/topics/', routes.TOPICS);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const database = require('../../../database');
|
||||
const comPostGen = require('../../../util/CommunityPostGen');
|
||||
const processHeaders = require('../../../util/util');
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (req, res) {
|
||||
const paramPack = processHeaders.data.decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let community = await database.getCommunityByTitleID(paramPack.title_id);
|
||||
if (community != null) {
|
||||
let response = await comPostGen.Communities(community);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
} else {
|
||||
res.status(404);
|
||||
res.send();
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/popular', async function (req, res) {
|
||||
let community = await database.getMostPopularCommunities(100);
|
||||
if (community != null) {
|
||||
res.contentType("application/json");
|
||||
res.send(community);
|
||||
} else {
|
||||
res.status(404);
|
||||
res.send();
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/new', async function (req, res) {
|
||||
let community = await database.getNewCommunities(100);
|
||||
if (community != null) {
|
||||
res.contentType("application/json");
|
||||
res.send(community);
|
||||
} else {
|
||||
res.status(404);
|
||||
res.send();
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/0/posts', async function (req, res) {
|
||||
const paramPack = processHeaders.data.decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let community = await database.getCommunityByTitleID(paramPack.title_id);
|
||||
if(community != null)
|
||||
{
|
||||
let posts;
|
||||
if(req.query.search_key)
|
||||
posts = await database.getPostsByCommunityKey(community, parseInt(req.query.limit), req.query.search_key);
|
||||
else
|
||||
posts = await database.getPostsByCommunity(community, parseInt(req.query.limit));
|
||||
|
||||
/* Build formatted response and send it off. */
|
||||
let response;
|
||||
if(req.query.with_mii === 1)
|
||||
response = await comPostGen.PostsResponseWithMii(posts, community);
|
||||
else
|
||||
response = await comPostGen.PostsResponse(posts, community);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.status(404);
|
||||
res.send();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
var express = require('express');
|
||||
var xml = require('object-to-xml');
|
||||
const database = require('../../../database');
|
||||
const util = require('../../../util/util');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET discovery server. */
|
||||
router.get('/', async function (req, res) {
|
||||
const discovery = await database.getDiscoveryHosts();
|
||||
try
|
||||
{
|
||||
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
let usrObj;
|
||||
if(pid == null)
|
||||
{
|
||||
throw new Error('The User token was not valid');
|
||||
}
|
||||
else
|
||||
{
|
||||
usrObj = await util.data.processUser(pid);
|
||||
if(!usrObj) {
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 2,
|
||||
message: "SETUP_NOT_COMPLETE"
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
}
|
||||
switch (usrObj.account_status) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: "POSTING_FROM_NNID"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(e);
|
||||
}
|
||||
switch(discovery.has_error)
|
||||
{
|
||||
case 0 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
response = {
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: discovery.version,
|
||||
endpoint: {
|
||||
host: discovery.endpoint.host,
|
||||
api_host: discovery.endpoint.api_host,
|
||||
portal_host: discovery.endpoint.portal_host,
|
||||
n3ds_host: discovery.endpoint.n3ds_host
|
||||
}
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 1 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 1,
|
||||
message: "SYSTEM_UPDATE_REQUIRED"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 2 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 2,
|
||||
message: "SETUP_NOT_COMPLETE"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 3 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 3,
|
||||
message: "SERVICE_MAINTENANCE"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 4:
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 4,
|
||||
message: "SERVICE_CLOSED"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 5 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 5,
|
||||
message: "PARENTAL_CONTROLS_ENABLED"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 6 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 6,
|
||||
message: "POSTING_LIMITED_PARENTAL_CONTROLS"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
case 7 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: "PNID_BANNED"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
default :
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 15,
|
||||
message: "SERVER_ERROR"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/posts', function (req, res) {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
module.exports = {
|
||||
DISCOVERY: require('./discovery'),
|
||||
POST: require('./post'),
|
||||
MESSAGE: require('./message'),
|
||||
COMMUNITY: require('./communities'),
|
||||
PEOPLE: require('./people'),
|
||||
TOPICS: require('./topics')
|
||||
};
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const moment = require('moment');
|
||||
var xml = require('object-to-xml');
|
||||
const { POST } = require('../../../models/post');
|
||||
const { CONVERSATION } = require('../../../models/conversation');
|
||||
const util = require('../../../util/util');
|
||||
const database = require('../../../database');
|
||||
var multer = require('multer');
|
||||
const snowflake = require('node-snowflake').Snowflake;
|
||||
var upload = multer();
|
||||
|
||||
/* GET post titles. */
|
||||
router.post('/', upload.none(), async function (req, res, next) {
|
||||
try
|
||||
{
|
||||
let paramPackData = util.data.decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
if(pid === null)
|
||||
{
|
||||
throw new Error('The User token was not valid');
|
||||
}
|
||||
else
|
||||
{
|
||||
let conversation = await database.getConversation(pid.toString(), req.body.message_to_pid.toString())
|
||||
if(!conversation) {
|
||||
let user = await database.getUserByPID(pid);
|
||||
let user2 = await database.getUserByPID(req.body.message_to_pid);
|
||||
if(!user || !user2)
|
||||
return res.sendStatus(422)
|
||||
let doc = {
|
||||
message_preview: req.body.body,
|
||||
pids: [
|
||||
{
|
||||
pid: user.pid.toString(),
|
||||
official: user.official,
|
||||
screen_name: user.user_id,
|
||||
read: true
|
||||
},
|
||||
{
|
||||
pid: user2.pid.toString(),
|
||||
official: user2.official,
|
||||
screen_name: user2.user_id,
|
||||
read: false
|
||||
}
|
||||
]
|
||||
}
|
||||
const newConversation = new CONVERSATION(doc);
|
||||
await newConversation.save();
|
||||
}
|
||||
else {
|
||||
let messageType = '';
|
||||
if(req.body.screenshot)
|
||||
messageType = '(Screenshot)';
|
||||
else if(req.body.painting)
|
||||
messageType = '(Drawing)';
|
||||
else
|
||||
messageType = req.body.body;
|
||||
await conversation.newMessage(messageType, req.pid.toString())
|
||||
}
|
||||
conversation = await database.getConversation(pid.toString(), req.body.message_to_pid.toString())
|
||||
let user = await util.data.processUser(pid);
|
||||
let appData = "";
|
||||
if (req.body.app_data) {
|
||||
appData = req.body.app_data.replace(/\0/g, "").trim();
|
||||
}
|
||||
let painting = "";
|
||||
if (req.body.painting) {
|
||||
painting = req.body.painting.replace(/\0/g, "").trim();
|
||||
}
|
||||
let paintingURI = "";
|
||||
if (req.body.painting) {
|
||||
paintingURI = await util.data.processPainting(painting);
|
||||
}
|
||||
let screenshot = "";
|
||||
if (req.body.screenshot) {
|
||||
screenshot = req.body.screenshot.replace(/\0/g, "").trim();
|
||||
}
|
||||
|
||||
let miiFace;
|
||||
console.log(parseInt(req.body.feeling_id))
|
||||
switch (parseInt(req.body.feeling_id)) {
|
||||
case 1:
|
||||
miiFace = 'smile_open_mouth.png';
|
||||
break;
|
||||
case 2:
|
||||
miiFace = 'wink_left.png';
|
||||
break;
|
||||
case 3:
|
||||
miiFace = 'surprise_open_mouth.png';
|
||||
break;
|
||||
case 4:
|
||||
miiFace = 'frustrated.png';
|
||||
break;
|
||||
case 5:
|
||||
miiFace = 'sorrow.png';
|
||||
break;
|
||||
default:
|
||||
miiFace = 'normal_face.png';
|
||||
break;
|
||||
}
|
||||
|
||||
const document = {
|
||||
title_id: paramPackData.title_id,
|
||||
screen_name: user.user_id,
|
||||
body: req.body.body,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
painting_uri: paintingURI,
|
||||
screenshot: screenshot,
|
||||
url: req.body.url,
|
||||
search_key: req.body.search_key,
|
||||
topic_tag: req.body.topic_tag,
|
||||
country_id: paramPackData.country_id,
|
||||
created_at: new Date(),
|
||||
feeling_id: req.body.feeling_id,
|
||||
id: snowflake.nextId(),
|
||||
is_autopost: req.body.is_autopost,
|
||||
is_spoiler: req.body.is_spoiler,
|
||||
is_app_jumpable: req.body.is_app_jumpable,
|
||||
language_id: req.body.language_id,
|
||||
mii: user.mii,
|
||||
mii_face_url: `http://mii.olv.pretendo.cc/mii/${user.pid}/${miiFace}`,
|
||||
pid: user.pid,
|
||||
verified: user.official,
|
||||
platform_id: paramPackData.platform_id,
|
||||
region_id: paramPackData.region_id,
|
||||
parent: null,
|
||||
message_to_pid: req.body.message_to_pid,
|
||||
conversation_id: conversation.id
|
||||
};
|
||||
const newPost = new POST(document);
|
||||
newPost.save();
|
||||
res.sendStatus(200);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(e);
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: "POSTING_FROM_NNID"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
router.post('/:post_id/empathies', upload.none(), async function (req, res, next) {
|
||||
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
const post = await database.getPostByID(req.params.post_id);
|
||||
if(pid === null) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
let user = await database.getUserByPID(pid);
|
||||
if(user.likes.indexOf(post.id) === -1 && user.id !== post.pid)
|
||||
{
|
||||
post.upEmpathy();
|
||||
user.addToLikes(post.id)
|
||||
res.sendStatus(200);
|
||||
}
|
||||
else
|
||||
res.sendStatus(403);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const database = require('../../../database');
|
||||
const pplPostGen = require('../../../util/peoplePostGen');
|
||||
const processHeaders = require('../../../util/util');
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (req, res) {
|
||||
let community = await database.getCommunityByTitleID(1407375153321472);
|
||||
if(community != null)
|
||||
{
|
||||
let posts;
|
||||
if(req.query.search_key)
|
||||
posts = await database.getPostsByCommunityKey(community, parseInt(req.query.limit), req.query.search_key);
|
||||
else
|
||||
posts = await database.getPostsByCommunity(community, parseInt(req.query.limit));
|
||||
|
||||
/* Build formatted response and send it off. */
|
||||
let response;
|
||||
if(req.query.with_mii === 1)
|
||||
response = await pplPostGen.PostsResponseWithMii(posts, community);
|
||||
else
|
||||
response = await pplPostGen.PostsResponse(posts, community);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.status(404);
|
||||
res.send();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const moment = require('moment');
|
||||
var xml = require('object-to-xml');
|
||||
const { POST } = require('../../../models/post');
|
||||
const util = require('../../../util/util');
|
||||
const database = require('../../../database');
|
||||
var multer = require('multer');
|
||||
const snowflake = require('node-snowflake').Snowflake;
|
||||
var upload = multer();
|
||||
|
||||
/* GET post titles. */
|
||||
router.post('/', upload.none(), async function (req, res, next) {
|
||||
try
|
||||
{
|
||||
let paramPackData = util.data.decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
if(pid === null)
|
||||
{
|
||||
throw new Error('The User token was not valid');
|
||||
}
|
||||
else
|
||||
{
|
||||
let user = await util.data.processUser(pid);
|
||||
let community = await database.getCommunityByTitleID(paramPackData.title_id)
|
||||
if(community.community_id === 'announcements')
|
||||
return res.sendStatus(403)
|
||||
let appData = "";
|
||||
if (req.body.app_data) {
|
||||
appData = req.body.app_data.replace(/\0/g, "").replace(/\r?\n|\r/g, "").trim();
|
||||
}
|
||||
let painting = "";
|
||||
if (req.body.painting) {
|
||||
painting = req.body.painting.replace(/\0/g, "").replace(/\r?\n|\r/g, "").trim();
|
||||
}
|
||||
let paintingURI = "";
|
||||
if (req.body.painting) {
|
||||
paintingURI = await util.data.processPainting(painting);
|
||||
}
|
||||
let screenshot = "";
|
||||
if (req.body.screenshot) {
|
||||
screenshot = req.body.screenshot.replace(/\0/g, "").trim();
|
||||
}
|
||||
|
||||
let miiFace;
|
||||
console.log(parseInt(req.body.feeling_id))
|
||||
switch (parseInt(req.body.feeling_id)) {
|
||||
case 1:
|
||||
miiFace = 'smile_open_mouth.png';
|
||||
break;
|
||||
case 2:
|
||||
miiFace = 'wink_left.png';
|
||||
break;
|
||||
case 3:
|
||||
miiFace = 'surprise_open_mouth.png';
|
||||
break;
|
||||
case 4:
|
||||
miiFace = 'frustrated.png';
|
||||
break;
|
||||
case 5:
|
||||
miiFace = 'sorrow.png';
|
||||
break;
|
||||
default:
|
||||
miiFace = 'normal_face.png';
|
||||
break;
|
||||
}
|
||||
|
||||
const document = {
|
||||
title_id: paramPackData.title_id,
|
||||
screen_name: user.user_id,
|
||||
body: req.body.body,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
painting_uri: paintingURI,
|
||||
screenshot: screenshot,
|
||||
url: req.body.url,
|
||||
search_key: req.body.search_key,
|
||||
topic_tag: req.body.topic_tag,
|
||||
community_id: community.community_id,
|
||||
country_id: paramPackData.country_id,
|
||||
created_at: new Date(),
|
||||
feeling_id: req.body.feeling_id,
|
||||
id: snowflake.nextId(),
|
||||
is_autopost: req.body.is_autopost,
|
||||
is_spoiler: req.body.is_spoiler,
|
||||
is_app_jumpable: req.body.is_app_jumpable,
|
||||
language_id: req.body.language_id,
|
||||
mii: user.mii,
|
||||
mii_face_url: `http://mii.olv.pretendo.cc/mii/${user.pid}/${miiFace}`,
|
||||
pid: user.pid,
|
||||
verified: user.official,
|
||||
platform_id: paramPackData.platform_id,
|
||||
region_id: paramPackData.region_id,
|
||||
parent: null,
|
||||
};
|
||||
const newPost = new POST(document);
|
||||
newPost.save();
|
||||
res.sendStatus(200);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(e);
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: "POSTING_FROM_NNID"
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:post_id/empathies', upload.none(), async function (req, res, next) {
|
||||
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
const post = await database.getPostByID(req.params.post_id);
|
||||
if(pid === null) {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
let user = await database.getUserByPID(pid);
|
||||
if(user.likes.indexOf(post.id) === -1 && user.id !== post.pid)
|
||||
{
|
||||
post.upEmpathy();
|
||||
user.addToLikes(post.id)
|
||||
res.sendStatus(200);
|
||||
}
|
||||
else
|
||||
res.sendStatus(403);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const database = require('../../../database');
|
||||
const comPostGen = require('../../../util/CommunityPostGen');
|
||||
const processHeaders = require('../../../util/util');
|
||||
const xmlbuilder = require("xmlbuilder");
|
||||
const moment = require("moment");
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (req, res) {
|
||||
let communities = await database.getCommunities(10);
|
||||
if(communities === null)
|
||||
return res.sendStatus(404);
|
||||
let response = await comPostGen.topics(communities);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
30
src/types/common/config.ts
Normal file
30
src/types/common/config.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import mongoose from 'mongoose';
|
||||
|
||||
export interface Config {
|
||||
http: {
|
||||
port: number;
|
||||
};
|
||||
account_server_address: string;
|
||||
mongoose: {
|
||||
connection_string: string;
|
||||
options: mongoose.ConnectOptions;
|
||||
};
|
||||
s3: {
|
||||
endpoint: string;
|
||||
key: string;
|
||||
secret: string;
|
||||
};
|
||||
grpc: {
|
||||
friends: {
|
||||
ip: string;
|
||||
port: number;
|
||||
api_key: string;
|
||||
};
|
||||
account: {
|
||||
ip: string;
|
||||
port: number;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
aes_key: string;
|
||||
}
|
||||
28
src/types/common/formatted-message.ts
Normal file
28
src/types/common/formatted-message.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export interface FormattedMessage {
|
||||
post: {
|
||||
body: string;
|
||||
country_id: number;
|
||||
created_at: string;
|
||||
feeling_id: number;
|
||||
id: string;
|
||||
is_autopost: number;
|
||||
is_spoiler: number;
|
||||
is_app_jumpable: number;
|
||||
empathy_added?: number; // * Only optional because they are optional in Posts
|
||||
language_id: number;
|
||||
message_to_pid?: string; // * Only optional because they are optional in Posts
|
||||
mii: string;
|
||||
mii_face_url: string;
|
||||
number: number;
|
||||
pid: number;
|
||||
platform_id: number;
|
||||
region_id: number;
|
||||
reply_count?: number; // * Only optional because they are optional in Posts
|
||||
screen_name: string;
|
||||
topic_tag: {
|
||||
name: string;
|
||||
title_id: number
|
||||
};
|
||||
title_id: string;
|
||||
};
|
||||
}
|
||||
16
src/types/common/param-pack.ts
Normal file
16
src/types/common/param-pack.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export interface ParamPack {
|
||||
title_id: string;
|
||||
access_key: string;
|
||||
platform_id: string;
|
||||
region_id: string;
|
||||
language_id: string;
|
||||
country_id: string;
|
||||
area_id: string;
|
||||
network_restriction: string;
|
||||
friend_restriction: string;
|
||||
rating_restriction: string;
|
||||
rating_organization: string;
|
||||
transferable_id: string;
|
||||
tz_name: string;
|
||||
utc_offset: string;
|
||||
}
|
||||
8
src/types/common/token.ts
Normal file
8
src/types/common/token.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface Token {
|
||||
system_type: number;
|
||||
token_type: number;
|
||||
pid: number;
|
||||
access_level: number;
|
||||
title_id: bigint;
|
||||
expire_time: bigint;
|
||||
}
|
||||
16
src/types/express-subdomain.d.ts
vendored
Normal file
16
src/types/express-subdomain.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// * Credit to https://github.com/bmullan91/express-subdomain/pull/61 for the types!
|
||||
|
||||
declare module 'express-subdomain'{
|
||||
import type { Request, Response, Router } from 'express';
|
||||
|
||||
/**
|
||||
* @description The subdomain function.
|
||||
* @param subdomain The subdomain to listen on.
|
||||
* @param fn The listener function, takes a response and request.
|
||||
* @returns A function call to the value passed as FN, or void (the next function).
|
||||
*/
|
||||
export default function subdomain(
|
||||
subdomain: string,
|
||||
fn: Router
|
||||
): (req: Request, res: Response, next: () => void) => void | typeof fn;
|
||||
}
|
||||
10
src/types/express.d.ts
vendored
Normal file
10
src/types/express.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { ParamPack } from '@/types/common/param-pack';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
pid: number;
|
||||
paramPack: ParamPack
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/types/miiverse/community.ts
Normal file
33
src/types/miiverse/community.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { PostData } from '@/types/miiverse/post';
|
||||
|
||||
export type CommunityData = {
|
||||
community_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
icon_3ds: string;
|
||||
pid: number;
|
||||
app_data: string;
|
||||
is_user_community: string;
|
||||
};
|
||||
|
||||
export type CommunitiesResult = {
|
||||
has_error: 0 | 1;
|
||||
version: 1;
|
||||
request_name: 'communities';
|
||||
communities: {
|
||||
community: CommunityData;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type CommunityPostsResult = {
|
||||
has_error: 0 | 1;
|
||||
version: 1;
|
||||
request_name: 'posts';
|
||||
topic: {
|
||||
community_id: string;
|
||||
};
|
||||
posts: {
|
||||
post: PostData;
|
||||
}[];
|
||||
};
|
||||
27
src/types/miiverse/people.ts
Normal file
27
src/types/miiverse/people.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { PostData } from '@/types/miiverse/post';
|
||||
import { SettingsData } from '@/types/miiverse/settings';
|
||||
|
||||
export type PersonPosts = {
|
||||
person: {
|
||||
posts: {
|
||||
post: PostData;
|
||||
}[];
|
||||
}
|
||||
};
|
||||
|
||||
export type PeoplePostsResult = {
|
||||
has_error: 0 | 1;
|
||||
version: 1;
|
||||
expire: string;
|
||||
request_name: 'posts';
|
||||
people: PersonPosts[];
|
||||
};
|
||||
|
||||
export type PeopleFollowingResult = {
|
||||
has_error: 0 | 1;
|
||||
version: 1;
|
||||
request_name: 'user_infos';
|
||||
people: {
|
||||
person: SettingsData;
|
||||
}[];
|
||||
};
|
||||
53
src/types/miiverse/post.ts
Normal file
53
src/types/miiverse/post.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
export type PostData = {
|
||||
app_data?: string; // TODO - I try to keep these fields in the real order they show up in, but idk where this one goes
|
||||
body?: string;
|
||||
community_id: number | string; // TODO - Remove this union. Only done to bypass some errors which don't break anything
|
||||
country_id: number;
|
||||
created_at: string;
|
||||
feeling_id: number;
|
||||
id: string;
|
||||
is_autopost: 0 | 1;
|
||||
is_community_private_autopost: 0 | 1;
|
||||
is_spoiler: 0 | 1;
|
||||
is_app_jumpable: 0 | 1;
|
||||
empathy_count: number;
|
||||
language_id: number;
|
||||
mii?: string;
|
||||
mii_face_url?: string;
|
||||
number: number;
|
||||
painting?: PostPainting;
|
||||
pid: number;
|
||||
platform_id: number;
|
||||
region_id: number;
|
||||
reply_count: number;
|
||||
screen_name: string;
|
||||
screenshot?: PostScreenshot;
|
||||
topic_tag?: PostTopicTag;
|
||||
title_id: string;
|
||||
};
|
||||
|
||||
export type PostPainting = {
|
||||
format: string;
|
||||
content: string;
|
||||
size: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type PostScreenshot = {
|
||||
size: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type PostTopicTag = {
|
||||
name: string;
|
||||
title_id: string;
|
||||
};
|
||||
|
||||
export type PostRepliesResult = {
|
||||
has_error: 0 | 1;
|
||||
version: 1;
|
||||
request_name: 'replies';
|
||||
posts: {
|
||||
post: PostData;
|
||||
}[];
|
||||
};
|
||||
4
src/types/miiverse/settings.ts
Normal file
4
src/types/miiverse/settings.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type SettingsData = {
|
||||
pid: number;
|
||||
screen_name: string;
|
||||
};
|
||||
27
src/types/miiverse/wara-wara-plaza.ts
Normal file
27
src/types/miiverse/wara-wara-plaza.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { PersonPosts } from '@/types/miiverse/people';
|
||||
|
||||
export type WWPTopic = {
|
||||
empathy_count: number;
|
||||
has_shop_page: 0 | 1;
|
||||
icon: string;
|
||||
title_ids: {
|
||||
title_id: string;
|
||||
}[];
|
||||
title_id: string;
|
||||
community_id: number;
|
||||
is_recommended: 0 | 1;
|
||||
name: string;
|
||||
people: PersonPosts[];
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type WWPResult = {
|
||||
has_error: 0 | 1;
|
||||
version: 1;
|
||||
expire: string;
|
||||
request_name: 'topics';
|
||||
topics: {
|
||||
topic: WWPTopic;
|
||||
}[];
|
||||
};
|
||||
|
||||
24
src/types/mongoose/community-posts-query.ts
Normal file
24
src/types/mongoose/community-posts-query.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// TODO - Make this more generic
|
||||
|
||||
export interface CommunityPostsQuery {
|
||||
community_id?: string;
|
||||
removed: boolean;
|
||||
app_data?: {
|
||||
$ne?: null;
|
||||
$eq?: null;
|
||||
};
|
||||
message_to_pid?: {
|
||||
$eq: null;
|
||||
};
|
||||
search_key?: string;
|
||||
is_spoiler?: 0 | 1;
|
||||
painting?: {
|
||||
$ne: null;
|
||||
};
|
||||
pid?: number | number[] | {
|
||||
$in: number[];
|
||||
};
|
||||
parent?: {
|
||||
$eq: null
|
||||
};
|
||||
}
|
||||
43
src/types/mongoose/community.ts
Normal file
43
src/types/mongoose/community.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Model, Types, HydratedDocument } from 'mongoose';
|
||||
import { CommunityData } from '@/types/miiverse/community';
|
||||
|
||||
enum COMMUNITY_TYPE {
|
||||
Main = 0,
|
||||
Sub = 1,
|
||||
Announcement = 2,
|
||||
Private = 3
|
||||
}
|
||||
|
||||
export interface ICommunity {
|
||||
platform_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
open: boolean;
|
||||
allows_comments: boolean;
|
||||
type: COMMUNITY_TYPE;
|
||||
parent: string;
|
||||
admins: Types.Array<number>;
|
||||
owner: number;
|
||||
created_at: Date;
|
||||
empathy_count: number;
|
||||
followers: number;
|
||||
has_shop_page: number;
|
||||
icon: string;
|
||||
title_ids: Types.Array<string>;
|
||||
title_id: Types.Array<string>;
|
||||
community_id: string;
|
||||
olive_community_id: string;
|
||||
is_recommended: number;
|
||||
app_data: string;
|
||||
user_favorites: Types.Array<number>;
|
||||
}
|
||||
|
||||
export interface ICommunityMethods {
|
||||
addUserFavorite(pid: number): Promise<void>;
|
||||
delUserFavorite(pid: number): Promise<void>;
|
||||
json(): CommunityData;
|
||||
}
|
||||
|
||||
export type CommunityModel = Model<ICommunity, object, ICommunityMethods>;
|
||||
|
||||
export type HydratedCommunityDocument = HydratedDocument<ICommunity, ICommunityMethods>;
|
||||
12
src/types/mongoose/content.ts
Normal file
12
src/types/mongoose/content.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Model, Types, HydratedDocument } from 'mongoose';
|
||||
|
||||
export interface IContent {
|
||||
pid: number;
|
||||
followed_communities: Types.Array<string>;
|
||||
followed_users: Types.Array<number>;
|
||||
following_users: Types.Array<number>;
|
||||
}
|
||||
|
||||
export type ContentModel = Model<IContent>;
|
||||
|
||||
export type HydratedContentDocument = HydratedDocument<IContent>;
|
||||
23
src/types/mongoose/conversation.ts
Normal file
23
src/types/mongoose/conversation.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Model, Types, HydratedDocument } from 'mongoose';
|
||||
|
||||
export type ConversationUser = {
|
||||
pid: number;
|
||||
official: boolean;
|
||||
read: boolean;
|
||||
};
|
||||
|
||||
export interface IConversation {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
last_updated: Date;
|
||||
message_preview: string,
|
||||
users: Types.Array<ConversationUser>;
|
||||
}
|
||||
|
||||
export interface IConversationMethods {
|
||||
newMessage(message: string, senderPID: number): Promise<void>;
|
||||
}
|
||||
|
||||
export type ConversationModel = Model<IConversation, object, IConversationMethods>;
|
||||
|
||||
export type HydratedConversationDocument = HydratedDocument<IConversation, IConversationMethods>;
|
||||
16
src/types/mongoose/endpoint.ts
Normal file
16
src/types/mongoose/endpoint.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
export interface IEndpoint {
|
||||
status: number;
|
||||
server_access_level: string;
|
||||
topics: boolean;
|
||||
guest_access: boolean;
|
||||
host: string;
|
||||
api_host: string;
|
||||
portal_host: string;
|
||||
n3ds_host: string;
|
||||
}
|
||||
|
||||
export type EndpointModel = Model<IEndpoint>;
|
||||
|
||||
export type HydratedEndpointDocument = HydratedDocument<IEndpoint>;
|
||||
20
src/types/mongoose/notification.ts
Normal file
20
src/types/mongoose/notification.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Model, Types, HydratedDocument } from 'mongoose';
|
||||
|
||||
export type NotificationUser = {
|
||||
user: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface INotification {
|
||||
pid: string;
|
||||
type: string;
|
||||
link: string;
|
||||
objectID: string;
|
||||
users: Types.Array<NotificationUser>;
|
||||
read: boolean;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export type NotificationModel = Model<INotification>;
|
||||
|
||||
export type HydratedNotificationDocument = HydratedDocument<INotification>;
|
||||
5
src/types/mongoose/post-to-json-options.ts
Normal file
5
src/types/mongoose/post-to-json-options.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface PostToJSONOptions {
|
||||
with_mii: boolean;
|
||||
app_data?: boolean;
|
||||
topic_tag?: boolean;
|
||||
}
|
||||
57
src/types/mongoose/post.ts
Normal file
57
src/types/mongoose/post.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Model, Types, HydratedDocument } from 'mongoose';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { PostToJSONOptions } from '@/types/mongoose/post-to-json-options';
|
||||
import { PostData, PostPainting, PostScreenshot, PostTopicTag } from '@/types/miiverse/post';
|
||||
|
||||
export interface IPost {
|
||||
id: string;
|
||||
title_id: string;
|
||||
screen_name: string;
|
||||
body: string;
|
||||
app_data: string;
|
||||
painting: string;
|
||||
screenshot: string;
|
||||
screenshot_length: number;
|
||||
search_key: string[];
|
||||
topic_tag: string;
|
||||
community_id: string;
|
||||
created_at: Date;
|
||||
feeling_id: number;
|
||||
is_autopost: number;
|
||||
is_community_private_autopost?: number;
|
||||
is_spoiler: number;
|
||||
is_app_jumpable: number;
|
||||
empathy_count?: number;
|
||||
country_id: number;
|
||||
language_id: number;
|
||||
mii: string;
|
||||
mii_face_url: string;
|
||||
pid: number;
|
||||
platform_id: number;
|
||||
region_id: number;
|
||||
parent: string;
|
||||
reply_count?: number;
|
||||
verified: boolean;
|
||||
message_to_pid?: string;
|
||||
removed: boolean;
|
||||
removed_reason?: string;
|
||||
yeahs?: Types.Array<number>;
|
||||
number?: number;
|
||||
}
|
||||
|
||||
export interface IPostMethods {
|
||||
del(reason: string): Promise<void>;
|
||||
generatePostUID(length: number): Promise<void>;
|
||||
cleanedBody(): string;
|
||||
cleanedMiiData(): string;
|
||||
cleanedPainting(): string;
|
||||
cleanedAppData(): string;
|
||||
formatPainting(): PostPainting | undefined;
|
||||
formatScreenshot(): PostScreenshot | undefined;
|
||||
formatTopicTag(): PostTopicTag | undefined;
|
||||
json(options: PostToJSONOptions, community?: HydratedCommunityDocument): PostData;
|
||||
}
|
||||
|
||||
export type PostModel = Model<IPost, object, IPostMethods>;
|
||||
|
||||
export type HydratedPostDocument = HydratedDocument<IPost, IPostMethods>;
|
||||
12
src/types/mongoose/report.ts
Normal file
12
src/types/mongoose/report.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
export interface IReport {
|
||||
pid: string;
|
||||
post_id: string;
|
||||
reason: number;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export type ReportModel = Model<IReport>;
|
||||
|
||||
export type HydratedReportDocument = HydratedDocument<IReport>;
|
||||
27
src/types/mongoose/settings.ts
Normal file
27
src/types/mongoose/settings.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Model, HydratedDocument } from 'mongoose';
|
||||
import { SettingsData } from '@/types/miiverse/settings';
|
||||
|
||||
export interface ISettings {
|
||||
pid: number;
|
||||
screen_name: string;
|
||||
account_status: number;
|
||||
ban_lift_date: Date;
|
||||
ban_reason: string;
|
||||
profile_comment: string;
|
||||
profile_comment_visibility: boolean;
|
||||
game_skill: number;
|
||||
game_skill_visibility: boolean;
|
||||
birthday_visibility: boolean;
|
||||
relationship_visibility: boolean;
|
||||
country_visibility: boolean;
|
||||
profile_favorite_community_visibility: boolean;
|
||||
receive_notifications: boolean;
|
||||
}
|
||||
|
||||
export interface ISettingsMethods {
|
||||
json(): SettingsData;
|
||||
}
|
||||
|
||||
export type SettingsModel = Model<ISettings, object, ISettingsMethods>;
|
||||
|
||||
export type HydratedSettingsDocument = HydratedDocument<ISettings, ISettingsMethods>;
|
||||
9
src/types/mongoose/subcommunity-query.ts
Normal file
9
src/types/mongoose/subcommunity-query.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// TODO - Make this more generic
|
||||
|
||||
export interface SubCommunityQuery {
|
||||
parent: string;
|
||||
owner?: number;
|
||||
user_favorites?: number;
|
||||
olive_community_id?: string;
|
||||
community_id?: string;
|
||||
}
|
||||
14
src/types/node-snowflake.d.ts
vendored
Normal file
14
src/types/node-snowflake.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
declare module 'node-snowflake' {
|
||||
export interface SnowflakeInitConfig {
|
||||
worker_id: number;
|
||||
data_center_id: number;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
export function Server(port: number): void;
|
||||
|
||||
export const Snowflake: {
|
||||
init: (config: SnowflakeInitConfig) => void;
|
||||
nextId: (workerId?: number, dataCenterId?: number, sequence?: number) => string;
|
||||
};
|
||||
}
|
||||
58
src/types/tga.d.ts
vendored
Normal file
58
src/types/tga.d.ts
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
declare module 'tga' {
|
||||
export interface TGAHeader {
|
||||
idLength: number;
|
||||
colorMapType: number;
|
||||
dataType: number;
|
||||
colorMapOrigin: number;
|
||||
colorMapLength: number;
|
||||
colorMapDepth: number;
|
||||
xOrigin: number;
|
||||
yOrigin: number;
|
||||
width: number;
|
||||
height: number;
|
||||
bitsPerPixel: number;
|
||||
flags: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default class TGA {
|
||||
constructor(buf: Buffer, opt?: { dontFixAlpha: boolean });
|
||||
|
||||
static createTgaBuffer(width: number, height: number, pixels: Uint8Array, dontFlipY: boolean): Buffer;
|
||||
|
||||
parseHeader(): void;
|
||||
parseFooter(): void;
|
||||
parseExtension(extensionAreaOffset: number): void;
|
||||
readColor(offset: number, bytesPerPixel: number): Uint8Array;
|
||||
readColorWithColorMap(offset: number): Uint8Array;
|
||||
readColorAuto(offset: number, bytesPerPixel: number, isUsingColorMap: boolean): Uint8Array;
|
||||
parseColorMap(): void;
|
||||
setPixel(pixels: Uint8Array, idx: number, color: Uint8Array): void;
|
||||
parsePixels(): void;
|
||||
parse(): void;
|
||||
fixForAlpha(): void;
|
||||
|
||||
public dontFixAlpha: boolean;
|
||||
public _buff: Buffer;
|
||||
public data: Uint8Array;
|
||||
public currentOffset: number;
|
||||
|
||||
public header: TGAHeader;
|
||||
|
||||
public width: number;
|
||||
public height: number;
|
||||
|
||||
public isUsingColorMap: boolean;
|
||||
public isUsingRLE: boolean;
|
||||
public isGray: boolean;
|
||||
|
||||
public hasAlpha: boolean;
|
||||
|
||||
public isFlipX: boolean;
|
||||
public isFlipY: boolean;
|
||||
|
||||
public colorMap: Uint8Array;
|
||||
|
||||
public pixels: Uint8Array;
|
||||
}
|
||||
}
|
||||
190
src/util.ts
Normal file
190
src/util.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import crypto from 'node:crypto';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import TGA from 'tga';
|
||||
import pako from 'pako';
|
||||
import { PNG } from 'pngjs';
|
||||
import aws from 'aws-sdk';
|
||||
import { createChannel, createClient, Metadata } from 'nice-grpc';
|
||||
import { ParsedQs } from 'qs';
|
||||
import crc32 from 'crc/crc32';
|
||||
import { ParamPack } from '@/types/common/param-pack';
|
||||
import { config } from '@/config-manager';
|
||||
import { Token } from '@/types/common/token';
|
||||
|
||||
import { FriendsDefinition } from '@pretendonetwork/grpc/friends/friends_service';
|
||||
import { FriendRequest } from '@pretendonetwork/grpc/friends/friend_request';
|
||||
|
||||
import { AccountDefinition } from '@pretendonetwork/grpc/account/account_service';
|
||||
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
|
||||
|
||||
// * nice-grpc doesn't export ChannelImplementation so this can't be typed
|
||||
const gRPCFriendsChannel = createChannel(`${config.grpc.friends.ip}:${config.grpc.friends.port}`);
|
||||
const gRPCFriendsClient = createClient(FriendsDefinition, gRPCFriendsChannel);
|
||||
|
||||
const gRPCAccountChannel = createChannel(`${config.grpc.account.ip}:${config.grpc.account.port}`);
|
||||
const gRPCAccountClient = createClient(AccountDefinition, gRPCAccountChannel);
|
||||
|
||||
const s3 = new aws.S3({
|
||||
endpoint: new aws.Endpoint(config.s3.endpoint),
|
||||
accessKeyId: config.s3.key,
|
||||
secretAccessKey: config.s3.secret
|
||||
});
|
||||
|
||||
export function decodeParamPack(paramPack: string): ParamPack {
|
||||
const values = Buffer.from(paramPack, 'base64').toString().split('\\');
|
||||
const entries = values.filter(value => value).reduce((entries: string[][], value: string, index: number) => {
|
||||
if (0 === index % 2) {
|
||||
entries.push([value]);
|
||||
} else {
|
||||
entries[Math.ceil(index / 2 - 1)].push(value);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}, []);
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
export function getPIDFromServiceToken(token: string): number {
|
||||
try {
|
||||
const decryptedToken = decryptToken(Buffer.from(token, 'base64'));
|
||||
|
||||
if (!decryptedToken) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const unpackedToken = unpackToken(decryptedToken);
|
||||
|
||||
return unpackedToken.pid;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptToken(token: Buffer): Buffer {
|
||||
const iv = Buffer.alloc(16);
|
||||
|
||||
const expectedChecksum = token.readUint32BE();
|
||||
const encryptedBody = token.subarray(4);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(config.aes_key, 'hex'), iv);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encryptedBody),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
if (expectedChecksum !== crc32(decrypted)) {
|
||||
throw new Error('Checksum did not match. Failed decrypt. Are you using the right key?');
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
export function unpackToken(token: Buffer): Token {
|
||||
return {
|
||||
system_type: token.readUInt8(0x0),
|
||||
token_type: token.readUInt8(0x1),
|
||||
pid: token.readUInt32LE(0x2),
|
||||
expire_time: token.readBigUInt64LE(0x6),
|
||||
title_id: token.readBigUInt64LE(0xE),
|
||||
access_level: token.readInt8(0x16)
|
||||
};
|
||||
}
|
||||
|
||||
export function processPainting(painting: string): Buffer | null {
|
||||
const paintingBuffer = Buffer.from(painting, 'base64');
|
||||
let output: Uint8Array;
|
||||
|
||||
try {
|
||||
output = pako.inflate(paintingBuffer);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const tga = new TGA(Buffer.from(output));
|
||||
const png = new PNG({
|
||||
width: tga.width,
|
||||
height: tga.height
|
||||
});
|
||||
|
||||
png.data = Buffer.from(tga.pixels);
|
||||
|
||||
return PNG.sync.write(png);
|
||||
}
|
||||
|
||||
export async function uploadCDNAsset(bucket: string, key: string, data: Buffer, acl: string): Promise<void> {
|
||||
const awsPutParams = {
|
||||
Body: data,
|
||||
Key: key,
|
||||
Bucket: bucket,
|
||||
ACL: acl
|
||||
};
|
||||
|
||||
await s3.putObject(awsPutParams).promise();
|
||||
}
|
||||
|
||||
export async function getUserFriendPIDs(pid: number): Promise<number[]> {
|
||||
const response = await gRPCFriendsClient.getUserFriendPIDs({
|
||||
pid: pid
|
||||
}, {
|
||||
metadata: Metadata({
|
||||
'X-API-Key': config.grpc.friends.api_key
|
||||
})
|
||||
});
|
||||
|
||||
return response.pids;
|
||||
}
|
||||
|
||||
export async function getUserFriendRequestsIncoming(pid: number): Promise<FriendRequest[]> {
|
||||
const response = await gRPCFriendsClient.getUserFriendRequestsIncoming({
|
||||
pid: pid
|
||||
}, {
|
||||
metadata: Metadata({
|
||||
'X-API-Key': config.grpc.friends.api_key
|
||||
})
|
||||
});
|
||||
|
||||
return response.friendRequests;
|
||||
}
|
||||
|
||||
export function getUserAccountData(pid: number): Promise<GetUserDataResponse> {
|
||||
return gRPCAccountClient.getUserData({
|
||||
pid: pid
|
||||
}, {
|
||||
metadata: Metadata({
|
||||
'X-API-Key': config.grpc.account.api_key
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function getValueFromQueryString(qs: ParsedQs, key: string): string[] {
|
||||
const property = qs[key] as string | string[];
|
||||
|
||||
if (property) {
|
||||
if (Array.isArray(property)) {
|
||||
return property;
|
||||
} else {
|
||||
return [property];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getValueFromHeaders(headers: IncomingHttpHeaders, key: string): string | undefined {
|
||||
let header = headers[key];
|
||||
let value: string | undefined;
|
||||
|
||||
if (header) {
|
||||
if (Array.isArray(header)) {
|
||||
header = header[0];
|
||||
}
|
||||
|
||||
value = header;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
const xmlbuilder = require("xmlbuilder");
|
||||
const moment = require("moment");
|
||||
const database = require('../database');
|
||||
|
||||
class CommunityPostGen {
|
||||
/* TODO lots of stubs and constants in here */
|
||||
static async PostsResponse(posts, community) {
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "posts").up()
|
||||
.e("topic")
|
||||
.e("community_id", community.community_id).up()
|
||||
.up()
|
||||
.e("posts");
|
||||
for (let i = 0; i < posts.length; i++) {
|
||||
xml = xml.e("post")
|
||||
.e("app_data", posts[i].app_data).up()
|
||||
.e("body", posts[i].body).up()
|
||||
.e("community_id", community.id).up()
|
||||
.e("country_id", "254").up()
|
||||
.e("created_at", moment(posts[i].created_at).format("YYYY-MM-DD hh:mm:ss")).up()
|
||||
.e("feeling_id", "1").up()
|
||||
.e("id", posts[i].id).up()
|
||||
.e("is_autopost", "0").up()
|
||||
.e("is_community_private_autopost", "0").up()
|
||||
.e("is_spoiler", "0").up()
|
||||
.e("is_app_jumpable", "0").up()
|
||||
.e("empathy_count", posts[i].empathy_count).up()
|
||||
.e("language_id", "1").up()
|
||||
.e("number", "0").up();
|
||||
if (posts[i].painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", posts[i].painting).up()
|
||||
.e("size", posts[i].painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", posts[i].pid).up()
|
||||
.e("platform_id", "1").up()
|
||||
.e("region_id", "4").up()
|
||||
.e("reply_count", "0").up()
|
||||
.e("screen_name", posts[i].screen_name).up()
|
||||
.e("title_id", community.title_ids[0]).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static async PostsResponseWithMii(posts, community) {
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "posts").up()
|
||||
.e("topic")
|
||||
.e("community_id", community.community_id).up()
|
||||
.up()
|
||||
.e("posts");
|
||||
for (let i = 0; i < posts.length; i++) {
|
||||
xml = xml.e("post")
|
||||
.e("app_data", posts[i].app_data).up()
|
||||
.e("body", posts[i].body).up()
|
||||
.e("community_id", community.community_id).up()
|
||||
.e("country_id", "254").up()
|
||||
.e("created_at", moment(posts[i].created_at).format('YYYY-MM-DD HH:MM:SS')).up()
|
||||
.e("feeling_id", "1").up()
|
||||
.e("id", posts[i].id).up()
|
||||
.e("is_autopost", "0").up()
|
||||
.e("is_community_private_autopost", "0").up()
|
||||
.e("is_spoiler", "0").up()
|
||||
.e("is_app_jumpable", "0").up()
|
||||
.e("empathy_count", posts[i].empathy_count).up()
|
||||
.e("language_id", "1").up()
|
||||
.e("mii", posts[i].mii).up()
|
||||
.e("mii_face_url", posts[i].mii_face_url).up()
|
||||
.e("number", "0").up();
|
||||
if (posts[i].painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", posts[i].painting).up()
|
||||
.e("size", posts[i].painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", posts[i].id).up()
|
||||
.e("platform_id", posts[i].platform_id).up()
|
||||
.e("region_id", posts[i].region_id).up()
|
||||
.e("reply_count", posts[i].reply_count).up()
|
||||
.e("screen_name", posts[i].screen_name).up()
|
||||
.e("title_id", posts[i].title_id).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static async EmptyResponse() {
|
||||
const xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static async Communities(community) {
|
||||
const xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "communities").up()
|
||||
.e("communities")
|
||||
.e("community")
|
||||
.e('olive_community_id', community.community_id).up()
|
||||
.e('community_id', community.community_id).up()
|
||||
.e("name", community.name).up()
|
||||
.e("description", community.description).up()
|
||||
.e("icon").up()
|
||||
.e("icon_3ds").up()
|
||||
.e("pid").up()
|
||||
.e("app_data").up()
|
||||
.e("is_user_community", 0).up()
|
||||
.up()
|
||||
.e("community")
|
||||
.e('olive_community_id', community.community_id + 100).up()
|
||||
.e('community_id', community.community_id + 100).up()
|
||||
.e("name", community.name + '- Nintendo Levels').up()
|
||||
.e("description", community.description).up()
|
||||
.e("icon").up()
|
||||
.e("icon_3ds").up()
|
||||
.e("pid").up()
|
||||
.e("app_data", 'TVZNSQI').up()
|
||||
.e("is_user_community", 0).up()
|
||||
.up()
|
||||
.e("community")
|
||||
.e('olive_community_id', community.community_id + 200).up()
|
||||
.e('community_id', community.community_id + 200).up()
|
||||
.e("name", community.name + '- User Levels').up()
|
||||
.e("description", community.description).up()
|
||||
.e("icon").up()
|
||||
.e("icon_3ds").up()
|
||||
.e("pid").up()
|
||||
.e("app_data", 'TVZNSQE').up()
|
||||
.e("is_user_community", 0).up()
|
||||
.up()
|
||||
.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
/* TODO Again, some constants */
|
||||
static async SinglePostResponse(post) {
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("post");
|
||||
if (post.app_data) {
|
||||
xml = xml.e("app_data", post.app_data).up();
|
||||
}
|
||||
xml = xml.e("body", post.body).up()
|
||||
.e("community_id", post.community_id).up()
|
||||
.e("country_id", "254").up()
|
||||
.e("created_at", post.created_at).up()
|
||||
.e("feeling_id", "1").up()
|
||||
.e("id", post.id).up()
|
||||
.e("is_autopost", "0").up()
|
||||
.e("is_community_private_autopost", "0").up()
|
||||
.e("is_spoiler", "0").up()
|
||||
.e("is_app_jumpable", "0").up()
|
||||
.e("empathy_count", post.empathy_count).up()
|
||||
.e("language_id", "1").up()
|
||||
.e("number", "0").up();
|
||||
if (post.painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", post.painting).up()
|
||||
.e("size", post.painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", post.pid).up()
|
||||
.e("platform_id", "1").up()
|
||||
.e("region_id", "4").up()
|
||||
.e("reply_count", "0").up()
|
||||
.e("screen_name", post.screen_name).up()
|
||||
.e("title_id", post.title_id).up()
|
||||
.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static async topics(communities) {
|
||||
const expirationDate = moment().add(2, 'days');
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "topics").up()
|
||||
.e("expire", expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
|
||||
.e("topics");
|
||||
for (const community of communities) {
|
||||
let posts = await database.getPostsByCommunity(community, 30);
|
||||
xml = xml.e('topic')
|
||||
.e('empathy_count', community.empathy_count).up()
|
||||
.e('has_shop_page', community.has_shop_page).up()
|
||||
.e('icon', community.icon).up()
|
||||
.e('title_ids');
|
||||
community.title_ids.forEach(function (title_id) {
|
||||
xml = xml.e('title_id', title_id).up()
|
||||
})
|
||||
xml = xml.up()
|
||||
.e('title_id', community.title_ids[0]).up()
|
||||
.e('community_id', community.community_id).up()
|
||||
.e('is_recommended', community.is_recommended).up()
|
||||
.e('name', community.name).up()
|
||||
.e("people");
|
||||
for (const post of posts) {
|
||||
let newBody = '';
|
||||
if(post.body)
|
||||
newBody = post.body.replace( /[\r\n]+/gm, '');
|
||||
xml = xml.e("person")
|
||||
.e("posts")
|
||||
.e("post")
|
||||
.e("body", newBody).up()
|
||||
.e("community_id", community.community_id).up()
|
||||
.e("country_id", post.country_id).up()
|
||||
.e("created_at", moment(post.created_at).format('YYYY-MM-DD HH:MM:SS')).up()
|
||||
.e("feeling_id", post.feeling_id).up()
|
||||
.e("id", post.id).up()
|
||||
.e("is_autopost", post.is_autopost).up()
|
||||
.e("is_community_private_autopost", post.is_community_private_autopost).up()
|
||||
.e("is_spoiler", post.is_spoiler).up()
|
||||
.e("is_app_jumpable", post.is_app_jumpable).up()
|
||||
.e("empathy_count", post.empathy_count).up()
|
||||
.e("language_id", post.language_id).up()
|
||||
.e("mii", post.mii).up()
|
||||
.e("mii_face_url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up();
|
||||
xml = xml.e("number", "0").up();
|
||||
if (post.painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", post.painting.replace( /[\r\n]+/gm, "" )).up()
|
||||
.e("size", post.painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", post.pid).up()
|
||||
.e("platform_id", post.platform_id).up()
|
||||
.e("region_id", post.region_id).up()
|
||||
.e("reply_count", post.reply_count).up()
|
||||
.e("screen_name", post.screen_name).up()
|
||||
.e("title_id", post.title_id).up()
|
||||
.up().up().up();
|
||||
}
|
||||
xml = xml.up().up()
|
||||
}
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CommunityPostGen;
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
const xmlbuilder = require("xmlbuilder");
|
||||
const moment = require("moment");
|
||||
|
||||
class CommunityPostGen {
|
||||
/* TODO lots of stubs and constants in here */
|
||||
static async PostsResponse(posts, community) {
|
||||
const expirationDate = moment().add(11, 'days');
|
||||
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("expire", expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
|
||||
.e("request_name", "people").up()
|
||||
.e("people");
|
||||
for (let i = 0; i < posts.length; i++) {
|
||||
xml = xml.e("person")
|
||||
.e("posts")
|
||||
.e("post")
|
||||
.e("body", posts[i].body).up()
|
||||
.e("community_id", community.community_id).up()
|
||||
.e("country_id", "254").up()
|
||||
.e("created_at", posts[i].created_at).up()
|
||||
.e("feeling_id", "1").up()
|
||||
.e("id", posts[i].id).up()
|
||||
.e("is_autopost", "0").up()
|
||||
.e("is_community_private_autopost", "0").up()
|
||||
.e("is_spoiler", "0").up()
|
||||
.e("is_app_jumpable", "0").up()
|
||||
.e("empathy_count", posts[i].empathy_count).up()
|
||||
.e("language_id", "1").up();
|
||||
xml = xml.e("number", "0").up();
|
||||
if (posts[i].painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", posts[i].painting).up()
|
||||
.e("size", posts[i].painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", posts[i].pid).up()
|
||||
.e("platform_id", "1").up()
|
||||
.e("region_id", "4").up()
|
||||
.e("reply_count", "0").up()
|
||||
.e("screen_name", posts[i].screen_name).up()
|
||||
.e("title_id", posts[i].title_id).up()
|
||||
.up().up().up();
|
||||
}
|
||||
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static async PostsResponseWithMii(posts, community) {
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "posts").up()
|
||||
.e("topic")
|
||||
.e("community_id", community.community_id).up()
|
||||
.up()
|
||||
.e("posts");
|
||||
for (let i = 0; i < posts.length; i++) {
|
||||
xml = xml.e("person")
|
||||
.e("posts")
|
||||
.e("post")
|
||||
.e("body", posts[i].body).up()
|
||||
.e("community_id", community.community_id).up()
|
||||
.e("country_id", "254").up()
|
||||
.e("created_at", posts[i].created_at).up()
|
||||
.e("feeling_id", "1").up()
|
||||
.e("id", posts[i].id).up()
|
||||
.e("is_autopost", "0").up()
|
||||
.e("is_community_private_autopost", "0").up()
|
||||
.e("is_spoiler", "0").up()
|
||||
.e("is_app_jumpable", "0").up()
|
||||
.e("empathy_count", posts[i].empathy_count).up()
|
||||
.e("language_id", "1").up()
|
||||
.e("mii", posts[i].mii).up()
|
||||
.e("mii_face_url", posts[i].mii_face_url).up()
|
||||
.e("number", "0").up();
|
||||
if (posts[i].painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", posts[i].painting).up()
|
||||
.e("size", posts[i].painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", posts[i].pid).up()
|
||||
.e("platform_id", "1").up()
|
||||
.e("region_id", "4").up()
|
||||
.e("reply_count", "0").up()
|
||||
.e("screen_name", posts[i].screen_name).up()
|
||||
.e("title_id", posts[i].title_id).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static async EmptyResponse() {
|
||||
const xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
/* TODO Again, some constants */
|
||||
static async SinglePostResponse(post) {
|
||||
let xml = xmlbuilder.create("result")
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("post");
|
||||
if (post.app_data) {
|
||||
xml = xml.e("app_data", post.app_data).up();
|
||||
}
|
||||
xml = xml.e("body", post.body).up()
|
||||
.e("community_id", post.community_id).up()
|
||||
.e("country_id", "254").up()
|
||||
.e("created_at", post.created_at).up()
|
||||
.e("feeling_id", "1").up()
|
||||
.e("id", post.id).up()
|
||||
.e("is_autopost", "0").up()
|
||||
.e("is_community_private_autopost", "0").up()
|
||||
.e("is_spoiler", "0").up()
|
||||
.e("is_app_jumpable", "0").up()
|
||||
.e("empathy_count", post.empathy_count).up()
|
||||
.e("language_id", "1").up()
|
||||
.e("number", "0").up();
|
||||
if (post.painting) {
|
||||
xml = xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", post.painting).up()
|
||||
.e("size", post.painting.length).up()
|
||||
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
|
||||
.up();
|
||||
}
|
||||
xml = xml.e("pid", post.pid).up()
|
||||
.e("platform_id", "1").up()
|
||||
.e("region_id", "4").up()
|
||||
.e("reply_count", "0").up()
|
||||
.e("screen_name", post.screen_name).up()
|
||||
.e("title_id", post.title_id).up()
|
||||
.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CommunityPostGen;
|
||||
}
|
||||
164
src/util/util.js
164
src/util/util.js
|
|
@ -1,164 +0,0 @@
|
|||
const crypto = require('crypto');
|
||||
const NodeRSA = require('node-rsa');
|
||||
const fs = require('fs-extra');
|
||||
const database = require('../database');
|
||||
const config = require('../../config.json');
|
||||
const xmlParser = require('xml2json');
|
||||
const request = require("request");
|
||||
const moment = require('moment');
|
||||
const { USER } = require('../models/user');
|
||||
let TGA = require('tga');
|
||||
let pako = require('pako');
|
||||
let PNG = require('pngjs').PNG;
|
||||
const path = require('path')
|
||||
|
||||
|
||||
let methods = {
|
||||
processUser: function(pid) {
|
||||
return new Promise(async function(resolve, reject) {
|
||||
let userObject = await database.getUserByPID(pid);
|
||||
if(userObject != null)
|
||||
resolve(userObject);
|
||||
else
|
||||
{
|
||||
await request({
|
||||
url: "http://" + config.account_server + "/v1/api/miis?pids=" + pid,
|
||||
headers: {
|
||||
'X-Nintendo-Client-ID': 'a2efa818a34fa16b8afbc8a74eba3eda',
|
||||
'X-Nintendo-Client-Secret': 'c91cdb5658bd4954ade78533a339cf9a'
|
||||
}
|
||||
}, function (error, response, body) {
|
||||
if (!error && response.statusCode === 200) {
|
||||
let xml = xmlParser.toJson(body, {object: true});
|
||||
const newUsr = {
|
||||
pid: pid,
|
||||
created_at: moment().format('YYYY-MM-DD HH:mm:SS'),
|
||||
user_id: xml.miis.mii.user_id,
|
||||
account_status: 0,
|
||||
mii: xml.miis.mii.data,
|
||||
official: false
|
||||
};
|
||||
const newUsrObj = new USER(newUsr);
|
||||
newUsrObj.save();
|
||||
resolve(newUsr);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('fail');
|
||||
reject();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
decodeParamPack: function (paramPack) {
|
||||
/* Decode base64 */
|
||||
let dec = Buffer.from(paramPack, "base64").toString("ascii");
|
||||
/* Remove starting and ending '/', split into array */
|
||||
dec = dec.slice(1, -1).split("\\");
|
||||
/* Parameters are in the format [name, val, name, val]. Copy into out{}. */
|
||||
const out = {};
|
||||
for (let i = 0; i < dec.length; i += 2) {
|
||||
out[dec[i].trim()] = dec[i + 1].trim();
|
||||
}
|
||||
return out;
|
||||
},
|
||||
processServiceToken: function(token) {
|
||||
try
|
||||
{
|
||||
let B64token = Buffer.from(token, 'base64');
|
||||
let decryptedToken = this.decryptToken(B64token);
|
||||
return decryptedToken.readUInt32LE(0x2);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(e)
|
||||
return null;
|
||||
}
|
||||
|
||||
},
|
||||
decryptToken: function(token) {
|
||||
// Access and refresh tokens use a different format since they must be much smaller
|
||||
// Assume a small length means access or refresh token
|
||||
if (token.length <= 32) {
|
||||
const cryptoPath = path.normalize(`${__dirname}/../certs/access`);
|
||||
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
|
||||
|
||||
const iv = Buffer.alloc(16);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
|
||||
|
||||
let decryptedBody = decipher.update(token);
|
||||
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
|
||||
|
||||
return decryptedBody;
|
||||
}
|
||||
|
||||
const cryptoPath = path.normalize(`${__dirname}/../certs/access`);
|
||||
|
||||
const cryptoOptions = {
|
||||
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
|
||||
hmac_secret: config.secret
|
||||
};
|
||||
|
||||
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
|
||||
environment: 'browser',
|
||||
encryptionScheme: {
|
||||
'hash': 'sha256',
|
||||
}
|
||||
});
|
||||
|
||||
const cryptoConfig = token.subarray(0, 0x82);
|
||||
const signature = token.subarray(0x82, 0x96);
|
||||
const encryptedBody = token.subarray(0x96);
|
||||
|
||||
const encryptedAESKey = cryptoConfig.subarray(0, 128);
|
||||
const point1 = cryptoConfig.readInt8(0x80);
|
||||
const point2 = cryptoConfig.readInt8(0x81);
|
||||
|
||||
const iv = Buffer.concat([
|
||||
Buffer.from(encryptedAESKey.subarray(point1, point1 + 8)),
|
||||
Buffer.from(encryptedAESKey.subarray(point2, point2 + 8))
|
||||
]);
|
||||
|
||||
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
|
||||
|
||||
let decryptedBody = decipher.update(encryptedBody);
|
||||
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
|
||||
|
||||
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
|
||||
const calculatedSignature = hmac.digest();
|
||||
|
||||
if (Buffer.compare(calculatedSignature, signature) !== 0) {
|
||||
console.log('Token signature did not match');
|
||||
return null;
|
||||
}
|
||||
|
||||
return decryptedBody;
|
||||
},
|
||||
processPainting: function (painting) {
|
||||
let paintingBuffer = Buffer.from(painting, 'base64');
|
||||
let output = '';
|
||||
try
|
||||
{
|
||||
output = pako.inflate(paintingBuffer);
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.error(err);
|
||||
}
|
||||
let tga = new TGA(Buffer.from(output));
|
||||
let png = new PNG({
|
||||
width: tga.width,
|
||||
height: tga.height
|
||||
});
|
||||
png.data = tga.pixels;
|
||||
let pngBuffer = PNG.sync.write(png);
|
||||
return `data:image/png;base64,${pngBuffer.toString('base64')}`;
|
||||
},
|
||||
};
|
||||
exports.data = methods;
|
||||
298
test/test.ts
Normal file
298
test/test.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import newman from 'newman';
|
||||
import { Collection, CollectionDefinition } from 'postman-collection';
|
||||
import qs from 'qs';
|
||||
import axios from 'axios';
|
||||
import { create as parseXML } from 'xmlbuilder2';
|
||||
import { table } from 'table';
|
||||
import ora from 'ora';
|
||||
import dotenv from 'dotenv';
|
||||
import colors from 'colors';
|
||||
|
||||
import communitiesCollection from '../postman/collections/Communities.json';
|
||||
import peopleCollection from '../postman/collections/People.json';
|
||||
|
||||
const PeopleCollection: CollectionDefinition = peopleCollection as CollectionDefinition;
|
||||
const CommunitiesCollection: CollectionDefinition = communitiesCollection as CollectionDefinition;
|
||||
|
||||
dotenv.config();
|
||||
colors.enable();
|
||||
|
||||
interface TestResult {
|
||||
collection: string;
|
||||
name: string;
|
||||
url: string;
|
||||
query: string;
|
||||
assertion: string;
|
||||
error?: string
|
||||
}
|
||||
|
||||
const USERNAME = process.env.PN_MIIVERSE_API_TESTING_USERNAME?.trim() || '';
|
||||
const PASSWORD = process.env.PN_MIIVERSE_API_TESTING_PASSWORD?.trim() || '';
|
||||
const DEVICE_ID = process.env.PN_MIIVERSE_API_TESTING_DEVICE_ID?.trim() || '';
|
||||
const SERIAL_NUMBER = process.env.PN_MIIVERSE_API_TESTING_SERIAL_NUMBER?.trim() || '';
|
||||
const CERTIFICATE = process.env.PN_MIIVERSE_API_TESTING_CONSOLE_CERT?.trim() || '';
|
||||
|
||||
if (!USERNAME) {
|
||||
throw new Error('PNID username missing. Required for requesting service tokens. Set PN_MIIVERSE_API_TESTING_USERNAME');
|
||||
}
|
||||
|
||||
if (!PASSWORD) {
|
||||
throw new Error('PNID password missing. Required for requesting service tokens. Set PN_MIIVERSE_API_TESTING_PASSWORD');
|
||||
}
|
||||
|
||||
if (!DEVICE_ID) {
|
||||
throw new Error('Console device ID missing. Required for requesting service tokens. Set PN_MIIVERSE_API_TESTING_DEVICE_ID');
|
||||
}
|
||||
|
||||
if (!SERIAL_NUMBER) {
|
||||
throw new Error('Console serial number missing. Required for requesting service tokens. Set PN_MIIVERSE_API_TESTING_SERIAL_NUMBER');
|
||||
}
|
||||
|
||||
if (!CERTIFICATE) {
|
||||
throw new Error('Console certificate missing. Required for requesting service tokens. Set PN_MIIVERSE_API_TESTING_CONSOLE_CERT');
|
||||
}
|
||||
|
||||
const BASE_URL = 'https://account.pretendo.cc';
|
||||
const API_URL = `${BASE_URL}/v1/api`;
|
||||
const MAPPED_IDS_URL = `${API_URL}/admin/mapped_ids`;
|
||||
const ACCESS_TOKEN_URL = `${API_URL}/oauth20/access_token/generate`;
|
||||
const SERVICE_TOKEN_URL = `${API_URL}/provider/service_token/@me?client_id=87cd32617f1985439ea608c2746e4610`;
|
||||
|
||||
const DEFAULT_HEADERS = {
|
||||
'X-Nintendo-Client-ID': 'a2efa818a34fa16b8afbc8a74eba3eda',
|
||||
'X-Nintendo-Client-Secret': 'c91cdb5658bd4954ade78533a339cf9a',
|
||||
'X-Nintendo-Device-ID': DEVICE_ID,
|
||||
'X-Nintendo-Serial-Number': SERIAL_NUMBER,
|
||||
'X-Nintendo-Device-Cert': CERTIFICATE
|
||||
};
|
||||
|
||||
export function nintendoPasswordHash(password: string, pid: number): string {
|
||||
const pidBuffer = Buffer.alloc(4);
|
||||
pidBuffer.writeUInt32LE(pid);
|
||||
|
||||
const unpacked = Buffer.concat([
|
||||
pidBuffer,
|
||||
Buffer.from('\x02\x65\x43\x46'),
|
||||
Buffer.from(password)
|
||||
]);
|
||||
|
||||
return crypto.createHash('sha256').update(unpacked).digest().toString('hex');
|
||||
}
|
||||
|
||||
async function apiGetRequest(url: string, headers = {}): Promise<Record<string, any>> {
|
||||
const response = await axios.get(url, {
|
||||
headers: Object.assign(headers, DEFAULT_HEADERS),
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
const data: Record<string, any> = parseXML(response.data).end({ format: 'object' });
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(data.errors.error.message);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function apiPostRequest(url: string, body: string): Promise<Record<string, any>> {
|
||||
const response = await axios.post(url, body, {
|
||||
headers: DEFAULT_HEADERS,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
const data: Record<string, any> = parseXML(response.data).end({ format: 'object' });
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(data.errors.error.message);
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getPID(username: string): Promise<number> {
|
||||
const response = await apiGetRequest(`${MAPPED_IDS_URL}?input_type=user_id&output_type=pid&input=${username}`);
|
||||
|
||||
return Number(response.mapped_ids.mapped_id.out_id);
|
||||
}
|
||||
|
||||
async function getAccessToken(username: string, passwordHash: string): Promise<string> {
|
||||
const data = qs.stringify({
|
||||
grant_type: 'password',
|
||||
user_id: username,
|
||||
password: passwordHash,
|
||||
password_type: 'hash',
|
||||
});
|
||||
|
||||
const response = await apiPostRequest(ACCESS_TOKEN_URL, data);
|
||||
|
||||
return response.OAuth20.access_token.token;
|
||||
}
|
||||
|
||||
async function getMiiverseServiceToken(accessToken: string): Promise<string> {
|
||||
const response = await apiGetRequest(SERVICE_TOKEN_URL, {
|
||||
'X-Nintendo-Title-ID': '0005001010040100',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
});
|
||||
|
||||
return response.service_token.token;
|
||||
}
|
||||
|
||||
function runNewmanTest(collection: string | Collection | CollectionDefinition, variables: Record<string, string>): Promise<TestResult[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
newman.run({
|
||||
collection: collection,
|
||||
reporters: ['json'],
|
||||
envVar: Object.entries(variables).map(entry => ({ key: entry[0], value: entry[1] })),
|
||||
globals: variables,
|
||||
globalVar: Object.entries(variables).map(entry => ({ key: entry[0], value: entry[1] })),
|
||||
}, (error, summary) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(createTestResults(summary));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function communitiesRoutesTest(serviceToken: string): Promise<TestResult[]> {
|
||||
// TODO - Make this more dynamic?
|
||||
return runNewmanTest(CommunitiesCollection, {
|
||||
DOMAIN: 'api.olv.pretendo.cc',
|
||||
ServiceToken: serviceToken,
|
||||
// TODO - Change these names. Should not be game-specific
|
||||
PP_Splatoon: 'XHRpdGxlX2lkXDE0MDczNzUxNTM1MjI5NDRcYWNjZXNzX2tleVwwXHBsYXRmb3JtX2lkXDFccmVnaW9uX2lkXDJcbGFuZ3VhZ2VfaWRcMVxjb3VudHJ5X2lkXDExMFxhcmVhX2lkXDBcbmV0d29ya19yZXN0cmljdGlvblwwXGZyaWVuZF9yZXN0cmljdGlvblwwXHJhdGluZ19yZXN0cmljdGlvblwyMFxyYXRpbmdfb3JnYW5pemF0aW9uXDBcdHJhbnNmZXJhYmxlX2lkXDEyNzU2MTQ0ODg0NDUzODk4NzgyXHR6X25hbWVcQW1lcmljYS9OZXdfWW9ya1x1dGNfb2Zmc2V0XC0xNDQwMFxyZW1hc3Rlcl92ZXJzaW9uXDBc',
|
||||
PP_MarioVsDK: 'XHRpdGxlX2lkXDE0MDczNzUxNTMzMzcwODhcYWNjZXNzX2tleVw2OTI0NzQ1MTBccGxhdGZvcm1faWRcMVxyZWdpb25faWRcMlxsYW5ndWFnZV9pZFwxXGNvdW50cnlfaWRcNDlcYXJlYV9pZFwwXG5ldHdvcmtfcmVzdHJpY3Rpb25cMFxmcmllbmRfcmVzdHJpY3Rpb25cMFxyYXRpbmdfcmVzdHJpY3Rpb25cMTdccmF0aW5nX29yZ2FuaXphdGlvblwxXHRyYW5zZmVyYWJsZV9pZFw3NjA4MjAyOTE2MDc1ODg0NDI1XHR6X25hbWVcUGFjaWZpYy9NaWR3YXlcdXRjX29mZnNldFwtMzk2MDBc',
|
||||
PP_Bad_TID: 'XHRpdGxlX2lkXDEyMzRcYWNjZXNzX2tleVwwXHBsYXRmb3JtX2lkXDFccmVnaW9uX2lkXDJcbGFuZ3VhZ2VfaWRcMVxjb3VudHJ5X2lkXDExMFxhcmVhX2lkXDBcbmV0d29ya19yZXN0cmljdGlvblwwXGZyaWVuZF9yZXN0cmljdGlvblwwXHJhdGluZ19yZXN0cmljdGlvblwyMFxyYXRpbmdfb3JnYW5pemF0aW9uXDBcdHJhbnNmZXJhYmxlX2lkXDEyNzU2MTQ0ODg0NDUzODk4NzgyXHR6X25hbWVcQW1lcmljYS9OZXdfWW9ya1x1dGNfb2Zmc2V0XC0xNDQwMFxyZW1hc3Rlcl92ZXJzaW9uXDBc',
|
||||
PP_ACPlaza: 'XHRpdGxlX2lkXDE0MDczNzUxNTMzMjE0NzJcYWNjZXNzX2tleVwwXHBsYXRmb3JtX2lkXDFccmVnaW9uX2lkXDJcbGFuZ3VhZ2VfaWRcMVxjb3VudHJ5X2lkXDExMFxhcmVhX2lkXDBcbmV0d29ya19yZXN0cmljdGlvblwwXGZyaWVuZF9yZXN0cmljdGlvblwwXHJhdGluZ19yZXN0cmljdGlvblwyMFxyYXRpbmdfb3JnYW5pemF0aW9uXDBcdHJhbnNmZXJhYmxlX2lkXDEyNzU2MTQ0ODg0NDUzODk4NzgyXHR6X25hbWVcQW1lcmljYS9OZXdfWW9ya1x1dGNfb2Zmc2V0XC0xNDQwMFxyZW1hc3Rlcl92ZXJzaW9uXDBc',
|
||||
'PP_Bad Format': 'XHR'
|
||||
});
|
||||
}
|
||||
|
||||
function peopleRoutesTest(serviceToken: string): Promise<TestResult[]> {
|
||||
// TODO - Make this more dynamic?
|
||||
return runNewmanTest(PeopleCollection, {
|
||||
DOMAIN: 'api.olv.pretendo.cc',
|
||||
ServiceToken: serviceToken,
|
||||
// TODO - Change this name. Should not be game-specific
|
||||
PP_Splatoon: 'XHRpdGxlX2lkXDE0MDczNzUxNTM1MjI5NDRcYWNjZXNzX2tleVwwXHBsYXRmb3JtX2lkXDFccmVnaW9uX2lkXDJcbGFuZ3VhZ2VfaWRcMVxjb3VudHJ5X2lkXDExMFxhcmVhX2lkXDBcbmV0d29ya19yZXN0cmljdGlvblwwXGZyaWVuZF9yZXN0cmljdGlvblwwXHJhdGluZ19yZXN0cmljdGlvblwyMFxyYXRpbmdfb3JnYW5pemF0aW9uXDBcdHJhbnNmZXJhYmxlX2lkXDEyNzU2MTQ0ODg0NDUzODk4NzgyXHR6X25hbWVcQW1lcmljYS9OZXdfWW9ya1x1dGNfb2Zmc2V0XC0xNDQwMFxyZW1hc3Rlcl92ZXJzaW9uXDBc',
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const tokensSpinner = ora('Acquiring account tokens').start();
|
||||
|
||||
const pid = await getPID(USERNAME);
|
||||
const passwordHash = nintendoPasswordHash(PASSWORD, pid);
|
||||
const accessToken = await getAccessToken(USERNAME, passwordHash);
|
||||
const serviceToken = await getMiiverseServiceToken(accessToken);
|
||||
|
||||
tokensSpinner.succeed();
|
||||
|
||||
const testsSpinner = ora('Running tests').start();
|
||||
|
||||
const results: TestResult[] = [
|
||||
...await communitiesRoutesTest(serviceToken),
|
||||
...await peopleRoutesTest(serviceToken)
|
||||
];
|
||||
|
||||
const passed = results.filter(result => !result.error);
|
||||
const failed = results.filter(result => result.error);
|
||||
|
||||
if (failed.length !== 0) {
|
||||
testsSpinner.warn('Some tests have failed! See below for details');
|
||||
} else {
|
||||
testsSpinner.succeed('All tests passed!');
|
||||
}
|
||||
|
||||
const testsOverviewData = [
|
||||
['Tests Ran'.cyan, results.length.toString().cyan],
|
||||
['Passed'.green, passed.length.toString().green]
|
||||
];
|
||||
|
||||
if (failed.length === 0) {
|
||||
testsOverviewData.push(['Failed'.red, failed.length.toString().green]);
|
||||
} else {
|
||||
testsOverviewData.push(['Failed'.red, failed.length.toString().red]);
|
||||
}
|
||||
|
||||
const config = {
|
||||
singleLine: true,
|
||||
border: {
|
||||
topBody: '─',
|
||||
topJoin: '┬',
|
||||
topLeft: '┌',
|
||||
topRight: '┐',
|
||||
|
||||
bottomBody: '─',
|
||||
bottomJoin: '┴',
|
||||
bottomLeft: '└',
|
||||
bottomRight: '┘',
|
||||
|
||||
bodyLeft: '│',
|
||||
bodyRight: '│',
|
||||
bodyJoin: '│',
|
||||
|
||||
joinBody: '─',
|
||||
joinLeft: '├',
|
||||
joinRight: '┤',
|
||||
joinJoin: '┼'
|
||||
}
|
||||
};
|
||||
|
||||
console.log(table(testsOverviewData, config));
|
||||
|
||||
if (failed.length !== 0) {
|
||||
console.log('Failed tests:\n'.red.underline.italic.bold);
|
||||
for (const test of failed) {
|
||||
console.log('Collection:'.bold, test.collection.red.bold);
|
||||
console.log('Test Name:'.bold, test.name.red.bold);
|
||||
console.log('URL:'.bold, `${test.url}${test.query ? '?' + test.query : ''}`.red.bold);
|
||||
console.log('Message:'.bold, test.error?.red.bold);
|
||||
console.log('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
function createTestResults(summary: newman.NewmanRunSummary): TestResult[] {
|
||||
const results: TestResult[] = [];
|
||||
|
||||
for (const execution of summary.run.executions) {
|
||||
const request = execution.request;
|
||||
for (const assertion of execution.assertions) {
|
||||
const result: TestResult = {
|
||||
collection: summary.collection.name,
|
||||
name: execution.item.name,
|
||||
url: `${request.url.protocol}://${request.url.host?.join('.')}/${request.url.path?.join('/')}`,
|
||||
query: qs.stringify(request.url.query.all().reduce((object: Record<string, string>, item: { disabled?: boolean; key: string | null; value: string | null; }) => {
|
||||
if (!item.disabled && item.key && item.value) {
|
||||
object[item.key] = item.value;
|
||||
}
|
||||
return object;
|
||||
}, {})),
|
||||
assertion: assertion.assertion
|
||||
};
|
||||
|
||||
if (assertion.error) {
|
||||
result.error = `${assertion.error.name}: ${assertion.error.message}`;
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "src",
|
||||
"outDir": "dist",
|
||||
"allowJs": true,
|
||||
"target": "es2022",
|
||||
"noEmitOnError": true,
|
||||
"noImplicitAny": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user