mirror of
https://github.com/PretendoNetwork/miiverse-api.git
synced 2026-04-25 07:36:49 -05:00
Full conversion to TypeScript
This commit is contained in:
parent
c13383812e
commit
106b581fe3
|
|
@ -30,7 +30,12 @@
|
|||
"@typescript-eslint/no-extra-semi": "error",
|
||||
"@typescript-eslint/no-empty-interface": "warn",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@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"
|
||||
|
|
|
|||
108
package-lock.json
generated
108
package-lock.json
generated
|
|
@ -34,13 +34,15 @@
|
|||
"sanitize": "^2.1.0",
|
||||
"tga": "^1.0.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"xmlbuilder2": "0.0.4"
|
||||
"xmlbuilder2": "0.0.4",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bmp-js": "^0.1.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node-rsa": "^1.1.1",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
"object-to-xml": "^2.0.0",
|
||||
"request": "^2.88.2",
|
||||
"string-sanitizer": "^1.1.1",
|
||||
"ts-unused-exports": "^9.0.4",
|
||||
"tsc-alias": "^1.8.5",
|
||||
"typescript": "^5.0.4",
|
||||
"xml2json": "^0.12.0"
|
||||
|
|
@ -1207,11 +1210,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader/node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||
|
|
@ -1517,6 +1515,12 @@
|
|||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/jsonfile": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
|
||||
|
|
@ -1546,6 +1550,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/multer": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
|
||||
"integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz",
|
||||
|
|
@ -3830,6 +3843,18 @@
|
|||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
|
|
@ -3918,9 +3943,9 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
|
|
@ -4551,7 +4576,7 @@
|
|||
},
|
||||
"node_modules/pretendo-grpc-ts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+ssh://git@github.com/PretendoNetwork/grpc-ts.git#3daba789d43f1feed91abde775d68904173dad56",
|
||||
"resolved": "git+ssh://git@github.com/PretendoNetwork/grpc-ts.git#660e3600db111746fa9eeb6ec763d2497d0f6bd5",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
@ -4559,6 +4584,11 @@
|
|||
"protobufjs": "^7.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pretendo-grpc-ts/node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
|
|
@ -4587,6 +4617,11 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs/node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
|
@ -5127,6 +5162,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
|
@ -5263,6 +5307,30 @@
|
|||
"resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz",
|
||||
"integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="
|
||||
},
|
||||
"node_modules/ts-unused-exports": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-9.0.4.tgz",
|
||||
"integrity": "sha512-/PPy0B1zhOJkDTUd1XVyaCqE/yA3IL2FrQ8W5/6cQ2g0kKC/06q8LEoPeXI6ELfI6Bivmv3MMvsUup5u3WH+BQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"tsconfig-paths": "^3.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"ts-unused-exports": "bin/ts-unused-exports"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/pzavolinsky/ts-unused-exports?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=3.8.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsc-alias": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.5.tgz",
|
||||
|
|
@ -5280,6 +5348,18 @@
|
|||
"tsc-alias": "dist/bin/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||
"integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json5": "^0.0.29",
|
||||
"json5": "^1.0.2",
|
||||
"minimist": "^1.2.6",
|
||||
"strip-bom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
|
|
@ -5701,6 +5781,14 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,15 @@
|
|||
"sanitize": "^2.1.0",
|
||||
"tga": "^1.0.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"xmlbuilder2": "0.0.4"
|
||||
"xmlbuilder2": "0.0.4",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bmp-js": "^0.1.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node-rsa": "^1.1.1",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
"object-to-xml": "^2.0.0",
|
||||
"request": "^2.88.2",
|
||||
"string-sanitizer": "^1.1.1",
|
||||
"ts-unused-exports": "^9.0.4",
|
||||
"tsc-alias": "^1.8.5",
|
||||
"typescript": "^5.0.4",
|
||||
"xml2json": "^0.12.0"
|
||||
|
|
|
|||
|
|
@ -6,34 +6,35 @@ const { account_db: mongooseConfig } = config;
|
|||
|
||||
export let pnidConnection: mongoose.Connection;
|
||||
|
||||
export function connect() {
|
||||
if(!pnidConnection)
|
||||
pnidConnection = makeNewConnection(mongooseConfig.connection_string);
|
||||
export function connect(): void {
|
||||
if (!pnidConnection) {
|
||||
pnidConnection = makeNewConnection(mongooseConfig.connection_string);
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyConnected() {
|
||||
if (!pnidConnection) {
|
||||
throw new Error('Cannot make database requests without being connected');
|
||||
}
|
||||
export function verifyConnected(): void {
|
||||
if (!pnidConnection) {
|
||||
throw new Error('Cannot make database requests without being connected');
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNewConnection(uri) {
|
||||
pnidConnection = mongoose.createConnection(uri, mongooseConfig.options);
|
||||
export function makeNewConnection(uri: string): mongoose.Connection {
|
||||
pnidConnection = mongoose.createConnection(uri, mongooseConfig.options);
|
||||
|
||||
pnidConnection.on('error', function (error) {
|
||||
LOG_ERROR(`MongoDB connection ${this.name} ${JSON.stringify(error)}`);
|
||||
pnidConnection.close().catch(() =>LOG_ERROR(`MongoDB failed to close connection ${this.name}`));
|
||||
});
|
||||
pnidConnection.on('error', error => {
|
||||
LOG_ERROR(`MongoDB connection ${JSON.stringify(error)}`);
|
||||
pnidConnection.close().catch(error => LOG_ERROR(JSON.stringify(error)));
|
||||
});
|
||||
|
||||
pnidConnection.on('connected', function () {
|
||||
LOG_INFO(`MongoDB connected ${this.name} / ${uri}`);
|
||||
});
|
||||
pnidConnection.on('connected', () => {
|
||||
LOG_INFO(`MongoDB connected ${uri}`);
|
||||
});
|
||||
|
||||
pnidConnection.on('disconnected', function () {
|
||||
LOG_INFO(`MongoDB disconnected ${this.name}`);
|
||||
});
|
||||
pnidConnection.on('disconnected', () => {
|
||||
LOG_INFO('MongoDB disconnected');
|
||||
});
|
||||
|
||||
return pnidConnection;
|
||||
return pnidConnection;
|
||||
}
|
||||
|
||||
pnidConnection = makeNewConnection(mongooseConfig.connection_string);
|
||||
|
|
|
|||
572
src/database.ts
572
src/database.ts
|
|
@ -5,488 +5,186 @@ import { Community } from '@/models/community';
|
|||
import { Content } from '@/models/content';
|
||||
import { Conversation } from '@/models/conversation';
|
||||
import { Endpoint } from '@/models/endpoint';
|
||||
import { Notification } from '@/models/notification';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { Post } from '@/models/post';
|
||||
import { Settings } from '@/models/settings';
|
||||
import { config } from '@/config-manager';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
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;
|
||||
let connection: mongoose.Connection;
|
||||
|
||||
export async function connect() {
|
||||
await mongoose.connect(mongooseConfig.connection_string, mongooseConfig.options);
|
||||
connection = mongoose.connection;
|
||||
connection.on('connected', function () {
|
||||
LOG_INFO(`MongoDB connected ${this.name}`);
|
||||
});
|
||||
connection.on('error', console.error.bind(console, 'connection error:'));
|
||||
connection.on('close', () => {
|
||||
connection.removeAllListeners();
|
||||
});
|
||||
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() {
|
||||
if (!connection) {
|
||||
connect();
|
||||
}
|
||||
function verifyConnected(): void {
|
||||
if (!connection) {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommunities(numberOfCommunities) {
|
||||
verifyConnected();
|
||||
if (numberOfCommunities === -1)
|
||||
return Community.find({ parent: null, type: 0 });
|
||||
else
|
||||
return Community.find({ parent: null, type: 0 }).limit(numberOfCommunities);
|
||||
export async function getMostPopularCommunities(limit: number): Promise<HydratedCommunityDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.find({ parent: null, type: 0 }).sort({ followers: -1 }).limit(limit);
|
||||
}
|
||||
|
||||
export async function getMostPopularCommunities(numberOfCommunities) {
|
||||
verifyConnected();
|
||||
return Community.find({ parent: null, type: 0 }).sort({ followers: -1 }).limit(numberOfCommunities);
|
||||
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 getNewCommunities(numberOfCommunities) {
|
||||
verifyConnected();
|
||||
return Community.find({ parent: null, type: 0 }).sort([['created_at', -1]]).limit(numberOfCommunities);
|
||||
export async function getSubCommunities(parentCommunityID: string): Promise<HydratedCommunityDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.find({
|
||||
parent: parentCommunityID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSubCommunities(communityID) {
|
||||
verifyConnected();
|
||||
return Community.find({
|
||||
parent: communityID
|
||||
});
|
||||
export async function getCommunityByTitleID(titleID: string): Promise<HydratedCommunityDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.findOne({
|
||||
title_id: titleID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommunityByTitleID(title_id) {
|
||||
verifyConnected();
|
||||
return Community.findOne({
|
||||
title_id: title_id
|
||||
});
|
||||
export async function getCommunityByTitleIDs(titleIDs: string[]): Promise<HydratedCommunityDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.findOne({
|
||||
title_ids: { $in: titleIDs }
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommunityByTitleIDs(title_ids) {
|
||||
verifyConnected();
|
||||
return Community.findOne({
|
||||
title_ids: { $in: title_ids }
|
||||
});
|
||||
export async function getCommunityByID(communityID: string): Promise<HydratedCommunityDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Community.findOne({
|
||||
community_id: communityID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCommunityByID(community_id) {
|
||||
verifyConnected();
|
||||
return Community.findOne({
|
||||
community_id: community_id
|
||||
});
|
||||
export async function getPostByID(postID: string): Promise<HydratedPostDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Post.findOne({
|
||||
id: postID
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTotalPostsByCommunity(community) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
title_id: community.title_id,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).countDocuments();
|
||||
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 getPostByID(postID) {
|
||||
verifyConnected();
|
||||
return Post.findOne({
|
||||
id: postID
|
||||
});
|
||||
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 getPostsByUserID(userID) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: userID,
|
||||
parent: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
});
|
||||
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 getPostReplies(postID, number) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
parent: postID,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).limit(number);
|
||||
export async function getEndpoints(): Promise<HydratedEndpointDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Endpoint.find({});
|
||||
}
|
||||
|
||||
export async function getDuplicatePosts(pid, post) {
|
||||
verifyConnected();
|
||||
return Post.findOne({
|
||||
pid: pid,
|
||||
body: post.body,
|
||||
painting: post.painting,
|
||||
screenshot: post.screenshot,
|
||||
parent: null,
|
||||
removed: false
|
||||
});
|
||||
export async function getEndpoint(accessLevel: string): Promise<HydratedEndpointDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Endpoint.findOne({
|
||||
server_access_level: accessLevel
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserPostRepliesAfterTimestamp(post, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
parent: post.pid,
|
||||
created_at: { $lt: post.created_at },
|
||||
message_to_pid: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).limit(numberOfPosts);
|
||||
export async function getUserSettings(pid: number): Promise<HydratedSettingsDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Settings.findOne({ pid: pid });
|
||||
}
|
||||
|
||||
export async function getNumberUserPostsByID(userID, number) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: userID,
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false
|
||||
}).sort({ created_at: -1 }).limit(number);
|
||||
export async function getUserContent(pid: number): Promise<HydratedContentDocument | null> {
|
||||
verifyConnected();
|
||||
|
||||
return Content.findOne({ pid: pid });
|
||||
}
|
||||
|
||||
export async function getTotalPostsByUserID(userID) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: userID,
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false
|
||||
}).countDocuments();
|
||||
export async function getFollowedUsers(content: HydratedContentDocument): Promise<HydratedSettingsDocument[]> {
|
||||
verifyConnected();
|
||||
|
||||
return Settings.find({
|
||||
pid: content.followed_users
|
||||
});
|
||||
}
|
||||
|
||||
export async function getHotPostsByCommunity(community, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
title_id: community.title_id,
|
||||
parent: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).sort({ empathy_count: -1 }).limit(numberOfPosts);
|
||||
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 getNumberNewCommunityPostsByID(community, number) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
title_id: community.title_id,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ created_at: -1 }).limit(number);
|
||||
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);
|
||||
}
|
||||
|
||||
export async function getNumberPopularCommunityPostsByID(community, limit, offset) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
title_id: community.title_id,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ empathy_count: -1 }).skip(offset).limit(limit);
|
||||
}
|
||||
export async function getPNID(pid: number): Promise<HydratedPNIDDocument | null> {
|
||||
accountDBVerifyConnected();
|
||||
|
||||
export async function getNumberVerifiedCommunityPostsByID(community, limit, offset) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
title_id: community.title_id,
|
||||
verified: true,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ created_at: -1 }).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
export async function getPostsByCommunity(community, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
community_id: community.olive_community_id,
|
||||
parent: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).sort({ created_at: -1 }).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
export async function getPostsByCommunityKey(community, numberOfPosts, search_key) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
community_id: community.olive_community_id,
|
||||
search_key: search_key,
|
||||
parent: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).sort({ created_at: -1 }).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
export async function getNewPostsByCommunity(community, limit, offset) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
community_id: community.olive_community_id,
|
||||
parent: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).sort({ created_at: -1 }).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
export async function getAllUserPosts(pid) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: pid,
|
||||
message_to_pid: null,
|
||||
app_data: { $ne: null }
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRemovedUserPosts(pid) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: pid,
|
||||
message_to_pid: null,
|
||||
removed: true
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserPostsAfterTimestamp(post, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: post.pid,
|
||||
created_at: { $lt: post.created_at },
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
export async function getUserPostsOffset(pid, limit, offset) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
pid: pid,
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).skip(offset).limit(limit).sort({ created_at: -1 });
|
||||
}
|
||||
|
||||
export async function getCommunityPostsAfterTimestamp(post, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
title_id: post.title_id,
|
||||
created_at: { $lt: post.created_at },
|
||||
parent: null,
|
||||
removed: false,
|
||||
app_data: { $ne: null }
|
||||
}).limit(numberOfPosts);
|
||||
}
|
||||
|
||||
export async function getEndpoints() {
|
||||
verifyConnected();
|
||||
return Endpoint.find({});
|
||||
}
|
||||
|
||||
export async function getEndPoint(accessLevel) {
|
||||
verifyConnected();
|
||||
return Endpoint.findOne({
|
||||
server_access_level: accessLevel
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUsersSettings(numberOfUsers) {
|
||||
verifyConnected();
|
||||
if (numberOfUsers === -1)
|
||||
return Settings.find({});
|
||||
else
|
||||
return Settings.find({}).limit(numberOfUsers);
|
||||
}
|
||||
|
||||
export async function getUsersContent(numberOfUsers) {
|
||||
verifyConnected();
|
||||
if (numberOfUsers === -1)
|
||||
return Settings.find({});
|
||||
else
|
||||
return Settings.find({}).limit(numberOfUsers);
|
||||
}
|
||||
|
||||
export async function getUserSettings(pid) {
|
||||
verifyConnected();
|
||||
return Settings.findOne({ pid: pid });
|
||||
}
|
||||
|
||||
export async function getUserContent(pid) {
|
||||
verifyConnected();
|
||||
return Content.findOne({ pid: pid });
|
||||
}
|
||||
|
||||
export async function getFollowingUsers(content) {
|
||||
verifyConnected();
|
||||
return Settings.find({
|
||||
pid: content.following_users
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFollowedUsers(content) {
|
||||
verifyConnected();
|
||||
return Settings.find({
|
||||
pid: content.followed_users
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserByUsername(user_id) {
|
||||
verifyConnected();
|
||||
return PNID.findOne({
|
||||
"username": new RegExp(`^${user_id}$`, 'i')
|
||||
});
|
||||
}
|
||||
|
||||
export async function getNewsFeed(content, numberOfPosts) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
$or: [
|
||||
{ pid: content.followed_users },
|
||||
{ pid: content.pid },
|
||||
{ community_id: content.followed_communities },
|
||||
],
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false
|
||||
}).limit(numberOfPosts).sort({ created_at: -1 });
|
||||
}
|
||||
|
||||
export async function getNewsFeedAfterTimestamp(content, numberOfPosts, post) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
$or: [
|
||||
{ pid: content.followed_users },
|
||||
{ pid: content.pid },
|
||||
{ community_id: content.followed_communities },
|
||||
],
|
||||
created_at: { $lt: post.created_at },
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false
|
||||
}).limit(numberOfPosts).sort({ created_at: -1 });
|
||||
}
|
||||
|
||||
export async function getNewsFeedOffset(content, limit, offset) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
$or: [
|
||||
{ pid: content.followed_users },
|
||||
{ pid: content.pid },
|
||||
{ community_id: content.followed_communities },
|
||||
],
|
||||
parent: null,
|
||||
message_to_pid: null,
|
||||
removed: false
|
||||
}).skip(offset).limit(limit).sort({ created_at: -1 });
|
||||
}
|
||||
|
||||
export async function getConversations(pid) {
|
||||
verifyConnected();
|
||||
return Conversation.find({
|
||||
"users.pid": pid
|
||||
}).sort({ last_updated: -1 });
|
||||
}
|
||||
|
||||
export async function getUnreadConversationCount(pid) {
|
||||
verifyConnected();
|
||||
return Conversation.find({
|
||||
"users": {
|
||||
$elemMatch: {
|
||||
'pid': pid,
|
||||
'read': false
|
||||
}
|
||||
}
|
||||
|
||||
}).countDocuments();
|
||||
}
|
||||
|
||||
export async function getConversationByID(community_id) {
|
||||
verifyConnected();
|
||||
return Conversation.findOne({
|
||||
type: 3,
|
||||
id: community_id
|
||||
});
|
||||
}
|
||||
|
||||
export async function getConversationMessages(community_id, limit, offset) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
community_id: community_id,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ created_at: 1 }).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
export async function getConversationByUsers(pids) {
|
||||
verifyConnected();
|
||||
return Conversation.findOne({
|
||||
$and: [
|
||||
{ 'users.pid': pids[0] },
|
||||
{ 'users.pid': pids[1] }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLatestMessage(pid, pid2) {
|
||||
verifyConnected();
|
||||
return Post.findOne({
|
||||
$or: [
|
||||
{ pid: pid, message_to_pid: pid2 },
|
||||
{ pid: pid2, message_to_pid: pid }
|
||||
],
|
||||
removed: false
|
||||
})
|
||||
}
|
||||
|
||||
export async function getFriendMessages(pid, search_key, limit) {
|
||||
verifyConnected();
|
||||
return Post.find({
|
||||
message_to_pid: pid,
|
||||
search_key: search_key,
|
||||
parent: null,
|
||||
removed: false
|
||||
}).sort({ created_at: 1 }).limit(limit);
|
||||
}
|
||||
|
||||
export async function getPNIDS() {
|
||||
accountDBVerifyConnected();
|
||||
return PNID.find({});
|
||||
}
|
||||
|
||||
export async function getPNID(pid) {
|
||||
accountDBVerifyConnected();
|
||||
return PNID.findOne({
|
||||
pid: pid
|
||||
});
|
||||
}
|
||||
|
||||
export async function getNotifications(pid, limit, offset) {
|
||||
verifyConnected();
|
||||
return Notification.find({
|
||||
pid: pid,
|
||||
}).sort({ created_at: 1 }).skip(offset).limit(limit);
|
||||
}
|
||||
|
||||
export async function getNotification(pid, type, reference_id) {
|
||||
verifyConnected();
|
||||
return Notification.findOne({
|
||||
pid: pid,
|
||||
type: type,
|
||||
reference_id: reference_id
|
||||
})
|
||||
}
|
||||
|
||||
export async function getLastNotification(pid) {
|
||||
verifyConnected();
|
||||
return Notification.findOne({
|
||||
pid: pid
|
||||
}).sort({ created_at: -1 }).limit(1);
|
||||
}
|
||||
|
||||
export async function getUnreadNotificationCount(pid) {
|
||||
verifyConnected();
|
||||
return Notification.find({
|
||||
pid: pid,
|
||||
read: false
|
||||
}).countDocuments();
|
||||
return PNID.findOne({
|
||||
pid: pid
|
||||
});
|
||||
}
|
||||
|
|
@ -1,124 +1,150 @@
|
|||
import express from 'express';
|
||||
import xml from 'object-to-xml';
|
||||
import { getPNID, getEndPoint } from '@/database';
|
||||
import { decodeParamPack, processServiceToken } from '@/util';
|
||||
import { z } from 'zod';
|
||||
import { getPNID, getEndpoint } from '@/database';
|
||||
import { getValueFromHeaders, decodeParamPack, getPIDFromServiceToken } from '@/util';
|
||||
import { ParamPack } from '@/types/common/param-pack';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
async function auth(req, res, next) {
|
||||
if(/*req.path.includes('/topics') || */req.path.includes('/v1/status'))
|
||||
return next();
|
||||
const token = req.headers["x-nintendo-servicetoken"] || req.headers['olive service token'];
|
||||
let paramPackData = req.headers["x-nintendo-parampack"];
|
||||
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()
|
||||
});
|
||||
|
||||
if(paramPackData)
|
||||
paramPackData = paramPackData = decodeParamPack(paramPackData);
|
||||
else if(req.path.includes('/users/'))
|
||||
return next();
|
||||
async function auth(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
|
||||
if (request.path.includes('/v1/status')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if(!token || !paramPackData && req.path.includes('/v1/endpoint'))
|
||||
return next();
|
||||
let token: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-servicetoken');
|
||||
if (!token) {
|
||||
token = getValueFromHeaders(request.headers, 'olive service token');
|
||||
}
|
||||
|
||||
if(!token || !paramPackData)
|
||||
badAuth(res);
|
||||
else {
|
||||
const pid = processServiceToken(token);
|
||||
if (!token) {
|
||||
return badAuth(response);
|
||||
}
|
||||
|
||||
if(pid === null)
|
||||
badAuth(res);
|
||||
else {
|
||||
let user = await getPNID(pid), discovery;
|
||||
if(user)
|
||||
discovery = await getEndPoint(user.server_access_level);
|
||||
else
|
||||
discovery = await getEndPoint('prod');
|
||||
const paramPack: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-parampack');
|
||||
if (!paramPack) {
|
||||
return badAuth(response);
|
||||
}
|
||||
|
||||
if(discovery.status !== 0) return serverError(res, discovery);
|
||||
const paramPackData: ParamPack = decodeParamPack(paramPack);
|
||||
const paramPackCheck: z.SafeParseReturnType<ParamPack, ParamPack> = ParamPackSchema.safeParse(paramPackData);
|
||||
if (!paramPackCheck.success) {
|
||||
return badAuth(response);
|
||||
}
|
||||
|
||||
req.pid = pid;
|
||||
req.paramPackData = paramPackData;
|
||||
return next();
|
||||
}
|
||||
}
|
||||
const pid: number = getPIDFromServiceToken(token);
|
||||
|
||||
if (pid === 0) {
|
||||
return badAuth(response);
|
||||
}
|
||||
|
||||
const user: HydratedPNIDDocument | null = await getPNID(pid);
|
||||
let discovery: HydratedEndpointDocument | null;
|
||||
if (user) {
|
||||
discovery = await getEndpoint(user.server_access_level);
|
||||
} else {
|
||||
discovery = await getEndpoint('prod');
|
||||
}
|
||||
|
||||
if (!discovery) {
|
||||
return badAuth(response);
|
||||
}
|
||||
|
||||
if (discovery.status !== 0) {
|
||||
return serverError(response, discovery);
|
||||
}
|
||||
|
||||
request.pid = pid;
|
||||
request.paramPack = paramPackData;
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
function badAuth(res) {
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: "POSTING_FROM_NNID"
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
function badAuth(response: express.Response): void {
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 400;
|
||||
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: 'POSTING_FROM_NNID'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function serverError(res, discovery) {
|
||||
let message = '', error = 0;
|
||||
switch(discovery.status) {
|
||||
case 0 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
let response = {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
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;
|
||||
res.set("Content-Type", "application/xml");
|
||||
break;
|
||||
default :
|
||||
message = 'SERVER_ERROR';
|
||||
error = 15;
|
||||
res.set("Content-Type", "application/xml");
|
||||
break;
|
||||
}
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: error,
|
||||
message: message
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
function serverError(response: express.Response, discovery: HydratedEndpointDocument): void {
|
||||
let message: string = '';
|
||||
let error: number = 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;
|
||||
response.set('Content-Type', 'application/xml');
|
||||
break;
|
||||
default :
|
||||
message = 'SERVER_ERROR';
|
||||
error = 15;
|
||||
response.set('Content-Type', 'application/xml');
|
||||
break;
|
||||
}
|
||||
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 400;
|
||||
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: error,
|
||||
message: message
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export default auth;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
export default sessionMiddlware;
|
||||
|
|
@ -2,21 +2,21 @@ import { Schema, model } from 'mongoose';
|
|||
import { INotification, INotificationMethods, NotificationModel } from '@/types/mongoose/notification';
|
||||
|
||||
const NotificationSchema = new Schema<INotification, NotificationModel, INotificationMethods>({
|
||||
pid: String,
|
||||
type: String,
|
||||
link: String,
|
||||
objectID: String,
|
||||
users: [{
|
||||
user: String,
|
||||
timestamp: Date
|
||||
}],
|
||||
read: Boolean,
|
||||
lastUpdated: Date
|
||||
pid: String,
|
||||
type: String,
|
||||
link: String,
|
||||
objectID: String,
|
||||
users: [{
|
||||
user: String,
|
||||
timestamp: Date
|
||||
}],
|
||||
read: Boolean,
|
||||
lastUpdated: Date
|
||||
});
|
||||
|
||||
NotificationSchema.method('markRead', async function markRead() {
|
||||
this.set('read', true);
|
||||
await this.save();
|
||||
this.set('read', true);
|
||||
await this.save();
|
||||
});
|
||||
|
||||
export const Notification: NotificationModel = model<INotification, NotificationModel>('Notification', NotificationSchema);
|
||||
|
|
|
|||
|
|
@ -2,114 +2,116 @@ import { Schema, model } from 'mongoose';
|
|||
import { IPost, IPostMethods, PostModel } from '@/types/mongoose/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: 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
|
||||
});
|
||||
|
||||
PostSchema.method('upReply', async function upReply() {
|
||||
const replyCount = this.get('reply_count');
|
||||
if(replyCount + 1 < 0)
|
||||
this.set('reply_count', 0);
|
||||
else
|
||||
this.set('reply_count', replyCount + 1);
|
||||
const replyCount = this.get('reply_count');
|
||||
if (replyCount + 1 < 0) {
|
||||
this.set('reply_count', 0);
|
||||
} else {
|
||||
this.set('reply_count', replyCount + 1);
|
||||
}
|
||||
|
||||
await this.save();
|
||||
await this.save();
|
||||
});
|
||||
|
||||
PostSchema.method('downReply', async function downReply() {
|
||||
const replyCount = this.get('reply_count');
|
||||
if(replyCount - 1 < 0)
|
||||
this.set('reply_count', 0);
|
||||
else
|
||||
this.set('reply_count', replyCount - 1);
|
||||
const replyCount = this.get('reply_count');
|
||||
if (replyCount - 1 < 0) {
|
||||
this.set('reply_count', 0);
|
||||
} else {
|
||||
this.set('reply_count', replyCount - 1);
|
||||
}
|
||||
|
||||
await this.save();
|
||||
await this.save();
|
||||
});
|
||||
|
||||
PostSchema.method('remove', async function remove(reason) {
|
||||
this.set('remove', true);
|
||||
this.set('removed_reason', reason)
|
||||
await this.save();
|
||||
this.set('remove', true);
|
||||
this.set('removed_reason', reason);
|
||||
await this.save();
|
||||
});
|
||||
|
||||
PostSchema.method('unRemove', async function unRemove(reason) {
|
||||
this.set('remove', false);
|
||||
this.set('removed_reason', reason)
|
||||
await this.save();
|
||||
this.set('remove', false);
|
||||
this.set('removed_reason', reason);
|
||||
await this.save();
|
||||
});
|
||||
|
||||
export const Post: PostModel = model<IPost, PostModel>('Post', PostSchema);
|
||||
|
|
|
|||
|
|
@ -36,36 +36,35 @@ app.use(miiverse);
|
|||
|
||||
// 404 handler
|
||||
LOG_INFO('Creating 404 status handler');
|
||||
app.use((req, res) => {
|
||||
//logger.warn(request.protocol + '://' + request.get('host') + request.originalUrl);
|
||||
res.set('Content-Type', 'application/xml');
|
||||
res.statusCode = 404;
|
||||
const response = {
|
||||
app.use((_request: express.Request, response: express.Response) => {
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 404;
|
||||
|
||||
return response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: 'Not Found'
|
||||
}
|
||||
};
|
||||
return res.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml(response));
|
||||
}));
|
||||
});
|
||||
|
||||
// non-404 error handler
|
||||
LOG_INFO('Creating non-404 status handler');
|
||||
app.use((error, req, res, _next) => {
|
||||
const status = error.status || 500;
|
||||
res.set('Content-Type', 'application/xml');
|
||||
res.statusCode = 404;
|
||||
const response = {
|
||||
app.use((error: any, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
|
||||
const status: number = error.status || 500;
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 404;
|
||||
|
||||
return response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: status,
|
||||
message: 'Not Found'
|
||||
}
|
||||
};
|
||||
return res.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml(response));
|
||||
}));
|
||||
});
|
||||
|
||||
// Starts the server
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import express from 'express';
|
||||
import subdomain from 'express-subdomain';
|
||||
import sessionMiddleware from '@/middleware/session';
|
||||
import { LOG_INFO } from '@/logger';
|
||||
|
||||
import DISCOVERY from '@/services/miiverse-api/routes/discovery';
|
||||
|
|
@ -27,9 +26,6 @@ router.use(subdomain('api.olv', api));
|
|||
router.use(subdomain('api-test.olv', api));
|
||||
router.use(subdomain('api-dev.olv', api));
|
||||
|
||||
LOG_INFO('[MIIVERSE] Importing middleware');
|
||||
discovery.use(sessionMiddleware);
|
||||
|
||||
// Setup routes
|
||||
discovery.use('/v1/endpoint', DISCOVERY);
|
||||
api.use('/v1/posts', POST);
|
||||
|
|
|
|||
|
|
@ -1,133 +1,193 @@
|
|||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
getSubCommunities,
|
||||
getMostPopularCommunities,
|
||||
getNewCommunities,
|
||||
getCommunityByTitleID,
|
||||
getUserContent,
|
||||
getCommunityByTitleIDs
|
||||
getSubCommunities,
|
||||
getMostPopularCommunities,
|
||||
getNewCommunities,
|
||||
getCommunityByTitleID,
|
||||
getUserContent,
|
||||
getCommunityByTitleIDs
|
||||
} from '@/database';
|
||||
import comPostGen from '@/util/xmlResponseGenerator';
|
||||
import { decodeParamPack } from '@/util';
|
||||
import { Community } from "@/models/community";
|
||||
import { Post } from "@/models/post";
|
||||
import { getValueFromQueryString } from '@/util';
|
||||
import { LOG_WARN } from '@/logger';
|
||||
import { Community } from '@/models/community';
|
||||
import { Post } from '@/models/post';
|
||||
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
|
||||
import { CreateNewCommunityBody } from '@/types/common/create-new-community-body';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { CommunityPostsQuery } from '@/types/mongoose/community-posts-query';
|
||||
import { HydratedContentDocument } from '@/types/mongoose/content';
|
||||
import { HydratedPostDocument } from '@/types/mongoose/post';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const createNewCommunitySchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
app_data: z.string()
|
||||
});
|
||||
|
||||
const router: express.Router = express.Router();
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (req, res) {
|
||||
const paramPack = decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let community = await getCommunityByTitleID(paramPack.title_id);
|
||||
if(!community) res.sendStatus(404);
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const community: HydratedCommunityDocument | null = await getCommunityByTitleID(request.paramPack.title_id);
|
||||
if (!community) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
let communities = await getSubCommunities(community.olive_community_id);
|
||||
if(!communities) res.sendStatus(404);
|
||||
communities.unshift(community);
|
||||
let response = await comPostGen.Communities(communities);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
const subCommunities: HydratedCommunityDocument[] = await getSubCommunities(community.olive_community_id);
|
||||
subCommunities.unshift(community);
|
||||
|
||||
const communities: string = await comPostGen.Communities(subCommunities);
|
||||
|
||||
response.contentType('application/xml');
|
||||
response.send(communities);
|
||||
});
|
||||
|
||||
router.get('/popular', async function (req, res) {
|
||||
let community = await getMostPopularCommunities(100);
|
||||
if (community != null) {
|
||||
res.contentType("application/json");
|
||||
res.send(community);
|
||||
} else res.sendStatus(404);
|
||||
router.get('/popular', async function (_request: express.Request, response: express.Response): Promise<void> {
|
||||
const popularCommunities: HydratedCommunityDocument[] = await getMostPopularCommunities(100);
|
||||
|
||||
response.contentType('application/json');
|
||||
response.send(popularCommunities);
|
||||
});
|
||||
|
||||
router.get('/new', async function (req, res) {
|
||||
let community = await getNewCommunities(100);
|
||||
if (community != null) {
|
||||
res.contentType("application/json");
|
||||
res.send(community);
|
||||
} else res.sendStatus(404);
|
||||
router.get('/new', async function (_request: express.Request, response: express.Response): Promise<void> {
|
||||
const newCommunities: HydratedCommunityDocument[] = await getNewCommunities(100);
|
||||
|
||||
response.contentType('application/json');
|
||||
response.send(newCommunities);
|
||||
});
|
||||
|
||||
router.get('/:appID/posts', async function (req, res) {
|
||||
const paramPack = decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let community = await Community.findOne({ community_id: req.params.appID });
|
||||
if(!community)
|
||||
community = await getCommunityByTitleID(paramPack.title_id);
|
||||
if(!community)
|
||||
res.sendStatus(404);
|
||||
let query = {
|
||||
community_id: community.olive_community_id,
|
||||
removed: false,
|
||||
app_data: { $ne: null },
|
||||
message_to_pid: { $eq: null },
|
||||
search_key: null,
|
||||
is_spoiler: null,
|
||||
painting: null,
|
||||
pid: null
|
||||
}
|
||||
router.get('/:appID/posts', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
let community: HydratedCommunityDocument | null = await Community.findOne({
|
||||
community_id: request.params.appID
|
||||
});
|
||||
|
||||
if(req.query.search_key)
|
||||
query.search_key = req.query.search_key;
|
||||
if(!req.query.allow_spoiler)
|
||||
query.is_spoiler = 0;
|
||||
//TODO: There probably is a type for text and screenshots too, will have to investigate
|
||||
if(req.query.type === 'memo')
|
||||
query.painting = { $ne: null };
|
||||
if(req.query.by === 'followings') {
|
||||
let userContent = await getUserContent(req.pid);
|
||||
query.pid = userContent.following_users;
|
||||
}
|
||||
else if(req.query.by === 'self')
|
||||
query.pid = req.pid;
|
||||
if (!community) {
|
||||
community = await getCommunityByTitleID(request.paramPack.title_id);
|
||||
}
|
||||
|
||||
let posts;
|
||||
if(req.query.distinct_pid === '1')
|
||||
posts = await Post.aggregate([
|
||||
{ $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: (req.query.limit ? Number(req.query.limit) : 10) } // only return the top 10 results
|
||||
]);
|
||||
else
|
||||
posts = await Post.find(query).sort({ created_at: -1}).limit(parseInt(req.query.limit as string));
|
||||
if (!community) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Build formatted response and send it off. */
|
||||
let options = {
|
||||
name: 'posts',
|
||||
with_mii: req.query.with_mii === '1',
|
||||
app_data: true,
|
||||
topic_tag: true
|
||||
}
|
||||
res.contentType("application/xml");
|
||||
res.send(await comPostGen.PostsResponse(posts, community, options));
|
||||
const query: CommunityPostsQuery = {
|
||||
community_id: community.olive_community_id,
|
||||
removed: false,
|
||||
app_data: { $ne: null },
|
||||
message_to_pid: { $eq: null }
|
||||
};
|
||||
|
||||
const searchKey: string | undefined = getValueFromQueryString(request.query, 'search_key');
|
||||
const allowSpoiler: string | undefined = getValueFromQueryString(request.query, 'allow_spoiler');
|
||||
const postType: string | undefined = getValueFromQueryString(request.query, 'type');
|
||||
const queryBy: string | undefined = getValueFromQueryString(request.query, 'by');
|
||||
const distinctPID: string | undefined = getValueFromQueryString(request.query, 'distinct_pid');
|
||||
const limitString: string | undefined = getValueFromQueryString(request.query, 'limit');
|
||||
const withMii: string | undefined = getValueFromQueryString(request.query, 'with_mii');
|
||||
|
||||
let limit: number = 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: HydratedContentDocument | null = 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') {
|
||||
posts = await Post.aggregate([
|
||||
{ $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
|
||||
]);
|
||||
} else {
|
||||
posts = await Post.find(query).sort({ created_at: -1}).limit(limit);
|
||||
}
|
||||
|
||||
/* Build formatted response and send it off. */
|
||||
const options: XMLResponseGeneratorOptions = {
|
||||
name: 'posts',
|
||||
with_mii: withMii === '1',
|
||||
app_data: true,
|
||||
topic_tag: true
|
||||
};
|
||||
response.contentType('application/xml');
|
||||
response.send(await comPostGen.PostsResponse(posts, community, options));
|
||||
});
|
||||
|
||||
// Handler for POST on '/v1/communities'
|
||||
router.post('/', multer().none(), async function (req, res) {
|
||||
const paramPack = decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let parent_community = await getCommunityByTitleIDs(paramPack.title_id);
|
||||
if(!parent_community) res.sendStatus(404);
|
||||
router.post('/', multer().none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const parentCommunity: HydratedCommunityDocument | null = await getCommunityByTitleIDs([request.paramPack.title_id]);
|
||||
|
||||
let num_communities = await Community.count();
|
||||
let new_community = new Community({
|
||||
platform_id: 0, // WiiU
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
open: true,
|
||||
allows_comments: true,
|
||||
type: 1,
|
||||
parent: parent_community.community_id,
|
||||
admins: parent_community.admins,
|
||||
icon: req.body.icon,
|
||||
title_id: paramPack.title_id,
|
||||
community_id: (parseInt(parent_community.community_id) + (5000 * num_communities)).toString(),
|
||||
olive_community_id: (parseInt(parent_community.community_id) + (5000 * num_communities)).toString(),
|
||||
app_data: req.body.app_data.replace(/[^A-Za-z0-9+/=\s]/g, ""),
|
||||
});
|
||||
if (!parentCommunity) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
await new_community.save();
|
||||
// TODO - Better error codes, maybe do defaults?
|
||||
const bodyCheck: z.SafeParseReturnType<CreateNewCommunityBody, CreateNewCommunityBody> = createNewCommunitySchema.safeParse(request.body);
|
||||
if (!bodyCheck.success) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await comPostGen.Community(new_community);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
const communitiesCount: number = await Community.count();
|
||||
const community: HydratedCommunityDocument = new Community({
|
||||
platform_id: 0, // WiiU
|
||||
name: request.body.name,
|
||||
description: request.body.description,
|
||||
open: true,
|
||||
allows_comments: true,
|
||||
type: 1,
|
||||
parent: parentCommunity.community_id,
|
||||
admins: parentCommunity.admins,
|
||||
icon: request.body.icon,
|
||||
title_id: request.paramPack.title_id,
|
||||
community_id: (parseInt(parentCommunity.community_id) + (5000 * communitiesCount)).toString(),
|
||||
olive_community_id: (parseInt(parentCommunity.community_id) + (5000 * communitiesCount)).toString(),
|
||||
app_data: request.body.app_data.replace(/[^A-Za-z0-9+/=\s]/g, ''),
|
||||
});
|
||||
|
||||
await community.save();
|
||||
|
||||
response.contentType('application/xml');
|
||||
response.send(await comPostGen.Community(community));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,83 +1,94 @@
|
|||
import express from 'express';
|
||||
import xml from 'object-to-xml';
|
||||
import { getPNID, getEndPoint } from '@/database';
|
||||
import { getPNID, getEndpoint } from '@/database';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
|
||||
const router = express.Router();
|
||||
const router: express.Router = express.Router();
|
||||
|
||||
/* GET discovery server. */
|
||||
router.get('/', async function (req, res) {
|
||||
let user = await getPNID(req.pid);
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const user: HydratedPNIDDocument | null = await getPNID(request.pid);
|
||||
|
||||
let discovery;
|
||||
if(user)
|
||||
discovery = await getEndPoint(user.server_access_level);
|
||||
else
|
||||
discovery = await getEndPoint('prod');
|
||||
let discovery: HydratedEndpointDocument | null;
|
||||
|
||||
let message = '', error = 0;
|
||||
switch(discovery.status) {
|
||||
case 0 :
|
||||
res.set("Content-Type", "application/xml");
|
||||
let response = {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
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;
|
||||
res.set("Content-Type", "application/xml");
|
||||
break;
|
||||
default :
|
||||
message = 'SERVER_ERROR';
|
||||
error = 15;
|
||||
res.set("Content-Type", "application/xml");
|
||||
break;
|
||||
}
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: error,
|
||||
message: message
|
||||
}
|
||||
};
|
||||
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
if (user) {
|
||||
discovery = await getEndpoint(user.server_access_level);
|
||||
} else {
|
||||
discovery = await getEndpoint('prod');
|
||||
}
|
||||
|
||||
// TODO - Better error
|
||||
if (!discovery) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
let message: string = '';
|
||||
let errorCode: number = 0;
|
||||
switch (discovery.status) {
|
||||
case 0 :
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
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
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
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.set('Content-Type', 'application/xml');
|
||||
break;
|
||||
default :
|
||||
message = 'SERVER_ERROR';
|
||||
errorCode = 15;
|
||||
response.set('Content-Type', 'application/xml');
|
||||
break;
|
||||
}
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 400;
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: errorCode,
|
||||
message: message
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,210 +1,300 @@
|
|||
import crypto from "node:crypto";
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import multer from 'multer';
|
||||
import { Snowflake } from 'node-snowflake';
|
||||
import moment from 'moment';
|
||||
import xml from 'object-to-xml';
|
||||
import { getFriends, decodeParamPack, processPainting, uploadCDNAsset } from '@/util';
|
||||
import { getPNID, getConversationByUsers, getUserSettings, getConversationByID, getFriendMessages } from '@/database';
|
||||
import { z } from 'zod';
|
||||
import { ParsedQs } from 'qs';
|
||||
import { getUserFriendPIDs, processPainting, uploadCDNAsset, getValueFromQueryString } from '@/util';
|
||||
import { getPNID, getConversationByUsers, getUserSettings, getFriendMessages } from '@/database';
|
||||
import { LOG_WARN } from '@/logger';
|
||||
import { Post } from '@/models/post';
|
||||
import { Conversation } from '@/models/conversation';
|
||||
import { SendMessageBody } from '@/types/common/send-message-body';
|
||||
import { FormattedMessage } from '@/types/common/formatted-message';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import { HydratedConversationDocument } from '@/types/mongoose/conversation';
|
||||
import { HydratedSettingsDocument } from '@/types/mongoose/settings';
|
||||
import { HydratedPostDocument } from '@/types/mongoose/post';
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer();
|
||||
|
||||
router.post('/', upload.none(), async function (req, res) {
|
||||
let user = await getPNID(req.pid);
|
||||
let user2 = await getPNID(req.body.message_to_pid);
|
||||
let conversation = await getConversationByUsers([user.pid, user2.pid]);
|
||||
let userSettings = await getUserSettings(req.pid), user2Settings = await getUserSettings(user2.pid), postID = await generatePostUID(21);
|
||||
let friends = await getFriends(user2.pid);
|
||||
if(!conversation) {
|
||||
if(!user || !user2 || userSettings || userSettings)
|
||||
return res.sendStatus(422)
|
||||
let document = {
|
||||
id: Snowflake.nextId(),
|
||||
users: [
|
||||
{
|
||||
pid: user.pid,
|
||||
official: (user.access_level === 2 || user.access_level === 3),
|
||||
read: true
|
||||
},
|
||||
{
|
||||
pid: user2.pid,
|
||||
official: (user2.access_level === 2 || user2.access_level === 3),
|
||||
read: false
|
||||
},
|
||||
]
|
||||
};
|
||||
const newConversations = new Conversation(document);
|
||||
await newConversations.save();
|
||||
conversation = await getConversationByID(document.id);
|
||||
}
|
||||
if(!conversation)
|
||||
return res.sendStatus(404);
|
||||
if(!friends || friends.pids.indexOf(req.pid) === -1)
|
||||
return res.sendStatus(422);
|
||||
|
||||
if(req.body.body === '' && req.body.painting === '' && req.body.screenshot === '') {
|
||||
res.status(422);
|
||||
return res.redirect(`/friend_messages/${conversation.id}`);
|
||||
}
|
||||
let paramPackData = decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let appData = "", painting = "", paintingURI, screenshot = null;
|
||||
if (req.body.app_data)
|
||||
appData = req.body.app_data.replace(/[^A-Za-z0-9+/=\s]/g, "");
|
||||
if (req.body.painting) {
|
||||
painting = req.body.painting.replace(/\0/g, "").trim();
|
||||
paintingURI = await processPainting(painting, true);
|
||||
await uploadCDNAsset('pn-cdn', `paintings/${req.pid}/${postID}.png`, paintingURI, 'public-read');
|
||||
}
|
||||
if (req.body.screenshot) {
|
||||
screenshot = req.body.screenshot.replace(/\0/g, "").trim();
|
||||
await uploadCDNAsset('pn-cdn', `screenshots/${req.pid}/${postID}.jpg`, Buffer.from(screenshot, 'base64'), 'public-read');
|
||||
}
|
||||
|
||||
let miiFace;
|
||||
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;
|
||||
}
|
||||
let body = req.body.body;
|
||||
if(body)
|
||||
body = req.body.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}‛¨ƒºª«»“”„¿¡←→↑↓√§¶†‡¦–—⇒⇔¤¢€£¥™©®+×÷=±∞ˇ˘˙¸˛˜′″µ°¹²³♭♪•…¬¯‰¼½¾♡♥●◆■▲▼☆★♀♂,./?;:'"\\<>]/g, "");
|
||||
if(body.length > 280)
|
||||
body = body.substring(0,280);
|
||||
const document = {
|
||||
title_id: paramPackData.title_id,
|
||||
community_id: conversation.id,
|
||||
screen_name: user.mii.name,
|
||||
body: body,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
screenshot: screenshot ? `/screenshots/${req.pid}/${postID}.jpg`: "",
|
||||
screenshot_length: screenshot ? screenshot.length : null,
|
||||
country_id: paramPackData.country_id,
|
||||
created_at: new Date(),
|
||||
feeling_id: req.body.feeling_id,
|
||||
id: postID,
|
||||
search_key: req.body.search_key,
|
||||
topic_tag: req.body.topic_tag,
|
||||
is_autopost: req.body.is_autopost,
|
||||
is_spoiler: (req.body.spoiler) ? 1 : 0,
|
||||
is_app_jumpable: req.body.is_app_jumpable,
|
||||
language_id: req.body.language_id,
|
||||
mii: user.mii.data,
|
||||
mii_face_url: `https://mii.olv.pretendo.cc/mii/${user.pid}/${miiFace}`,
|
||||
pid: req.pid,
|
||||
platform_id: paramPackData.platform_id,
|
||||
region_id: paramPackData.region_id,
|
||||
verified: (user.access_level === 2 || user.access_level === 3),
|
||||
message_to_pid: req.body.message_to_pid,
|
||||
parent: null,
|
||||
removed: false
|
||||
};
|
||||
const newPost = new Post(document);
|
||||
newPost.save();
|
||||
res.sendStatus(200);
|
||||
let postPreviewText;
|
||||
if(document.painting)
|
||||
postPreviewText = 'sent a Drawing'
|
||||
else if(document.body.length > 25)
|
||||
postPreviewText = document.body.substring(0, 25) + '...';
|
||||
else
|
||||
postPreviewText = document.body;
|
||||
await conversation.newMessage(postPreviewText, document.message_to_pid);
|
||||
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()
|
||||
});
|
||||
|
||||
router.get('/', async function(req, res) {
|
||||
let limit = parseInt(req.query.limit as string), search_key = req.query.search_key;
|
||||
let posts = await getFriendMessages(req.pid, search_key, limit);
|
||||
const router: express.Router = express.Router();
|
||||
const upload: multer.Multer = multer();
|
||||
|
||||
let postBody = [];
|
||||
for(let post of posts) {
|
||||
console.log(post)
|
||||
postBody.push({
|
||||
post: {
|
||||
body: post.body,
|
||||
country_id: post.country_id || 0,
|
||||
created_at: moment(post.created_at).format('YYYY-MM-DD HH:MM:SS'),
|
||||
feeling_id: post.feeling_id || 0,
|
||||
id: post.id,
|
||||
is_autopost: post.is_autopost,
|
||||
is_spoiler: post.is_spoiler,
|
||||
is_app_jumpable: post.is_app_jumpable,
|
||||
empathy_added: post.empathy_count,
|
||||
language_id: post.language_id,
|
||||
message_to_pid: post.message_to_pid,
|
||||
mii: post.mii,
|
||||
mii_face_url: post.mii_face_url,
|
||||
number: post.number || 0,
|
||||
pid: post.pid,
|
||||
platform_id: post.platform_id || 0,
|
||||
region_id: post.region_id || 0,
|
||||
reply_count: post.reply_count,
|
||||
screen_name: post.screen_name,
|
||||
topic_tag: {
|
||||
name: post.topic_tag,
|
||||
title_id: 0
|
||||
},
|
||||
title_id: post.title_id
|
||||
}
|
||||
});
|
||||
}
|
||||
res.set("Content-Type", "application/xml");
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'friend_messages',
|
||||
posts: postBody
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
router.post('/', upload.none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
// TODO - Better error codes, maybe do defaults?
|
||||
const bodyCheck: z.SafeParseReturnType<SendMessageBody, SendMessageBody> = sendMessageSchema.safeParse(request.body);
|
||||
|
||||
if (!bodyCheck.success) {
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientPID: number = bodyCheck.data.message_to_pid;
|
||||
let messageBody: string = bodyCheck.data.body;
|
||||
let painting: string = bodyCheck.data.painting?.trim() || '';
|
||||
let screenshot: string = bodyCheck.data.screenshot?.trim() || '';
|
||||
let appData: string = bodyCheck.data.app_data?.trim() || '';
|
||||
|
||||
if (isNaN(recipientPID)) {
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
const sender: HydratedPNIDDocument | null = await getPNID(request.pid);
|
||||
const recipient: HydratedPNIDDocument | null = await getPNID(recipientPID);
|
||||
|
||||
if (!sender || !recipient) {
|
||||
response.status(422);
|
||||
return;
|
||||
}
|
||||
|
||||
let conversation: HydratedConversationDocument | null = await getConversationByUsers([sender.pid, recipient.pid]);
|
||||
|
||||
if (!conversation) {
|
||||
const userSettings: HydratedSettingsDocument | null = await getUserSettings(request.pid);
|
||||
const user2Settings: HydratedSettingsDocument | null = await getUserSettings(recipient.pid);
|
||||
|
||||
if (!sender || !recipient || userSettings || user2Settings) {
|
||||
response.sendStatus(422);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation = new Conversation({
|
||||
id: Snowflake.nextId(),
|
||||
users: [
|
||||
{
|
||||
pid: sender.pid,
|
||||
official: (sender.access_level === 2 || sender.access_level === 3),
|
||||
read: true
|
||||
},
|
||||
{
|
||||
pid: recipient.pid,
|
||||
official: (recipient.access_level === 2 || recipient.access_level === 3),
|
||||
read: false
|
||||
},
|
||||
]
|
||||
});
|
||||
await conversation.save();
|
||||
}
|
||||
|
||||
if (!conversation) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const friendPIDs: number[] = await getUserFriendPIDs(recipient.pid);
|
||||
|
||||
if (friendPIDs.indexOf(request.pid) === -1) {
|
||||
response.sendStatus(422);
|
||||
return;
|
||||
}
|
||||
|
||||
if (appData) {
|
||||
appData = appData.replace(/[^A-Za-z0-9+/=\s]/g, '');
|
||||
}
|
||||
|
||||
const postID: string = await generatePostUID(21);
|
||||
|
||||
if (painting) {
|
||||
painting = painting.replace(/\0/g, '').trim();
|
||||
const paintingBuffer: Buffer | null = await processPainting(painting);
|
||||
|
||||
if (paintingBuffer) {
|
||||
await uploadCDNAsset('pn-cdn', `paintings/${request.pid}/${postID}.png`, paintingBuffer, 'public-read');
|
||||
} else {
|
||||
LOG_WARN(`PAINTING FOR POST ${postID} FAILED TO PROCESS`);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshot) {
|
||||
screenshot = screenshot.replace(/\0/g, '').trim();
|
||||
const screenshotBuffer: Buffer = Buffer.from(screenshot, 'base64');
|
||||
|
||||
await uploadCDNAsset('pn-cdn', `screenshots/${request.pid}/${postID}.jpg`, screenshotBuffer, 'public-read');
|
||||
}
|
||||
|
||||
let miiFace: string = '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 newPost = new Post({
|
||||
title_id: request.paramPack.title_id,
|
||||
community_id: conversation.id,
|
||||
screen_name: sender.mii.name,
|
||||
body: messageBody,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
screenshot: screenshot ? `/screenshots/${request.pid}/${postID}.jpg` : '',
|
||||
screenshot_length: screenshot ? screenshot.length : null,
|
||||
country_id: request.paramPack.country_id,
|
||||
created_at: new Date(),
|
||||
feeling_id: request.body.feeling_id,
|
||||
id: postID,
|
||||
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.access_level === 2 || sender.access_level === 3),
|
||||
message_to_pid: request.body.message_to_pid,
|
||||
parent: null,
|
||||
removed: false
|
||||
});
|
||||
newPost.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.post('/:post_id/empathies', upload.none(), async function (req, res) {
|
||||
// TODO - FOR JEMMA! FIX THIS! MISSING SCHEMA METHODS
|
||||
/*
|
||||
let pid = processServiceToken(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);
|
||||
*/
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const limitString: string | undefined = getValueFromQueryString(request.query, 'limit');
|
||||
|
||||
// TODO - Is this the limit?
|
||||
let limit: number = 10;
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
// TODO - Update getValueFromQueryString to return arrays optionally
|
||||
const searchKey: string | ParsedQs | string[] | ParsedQs[] | undefined = request.query.search_key;
|
||||
|
||||
if (!searchKey) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const messages: HydratedPostDocument[] = await getFriendMessages(request.pid.toString(), searchKey as string[], limit);
|
||||
|
||||
const postBody: FormattedMessage[] = [];
|
||||
for (const message of messages) {
|
||||
console.log(message);
|
||||
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.set('Content-Type', 'application/xml');
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
request_name: 'friend_messages',
|
||||
posts: postBody
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
async function generatePostUID(length) {
|
||||
let 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 });
|
||||
id = (inuse ? await generatePostUID(length) : id);
|
||||
return id;
|
||||
router.post('/:post_id/empathies', upload.none(), async function (_request: express.Request, _response: express.Response): Promise<void> {
|
||||
// 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);
|
||||
*/
|
||||
});
|
||||
|
||||
async function generatePostUID(length: number): Promise<string> {
|
||||
let id: string = Buffer.from(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(length * 2))), 'binary').toString('base64').replace(/[+/]/g, '').substring(0, length);
|
||||
const inuse: HydratedPostDocument | null = await Post.findOne({ id });
|
||||
|
||||
id = (inuse ? await generatePostUID(length) : id);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,65 +1,101 @@
|
|||
import express from 'express';
|
||||
import xmlGenerator from '@/util/xmlResponseGenerator';
|
||||
import { getUserContent, getFollowedUsers } from '@/database';
|
||||
import { getFriends } from '@/util';
|
||||
import { Post } from "@/models/post";
|
||||
import { getValueFromQueryString, getUserFriendPIDs } from '@/util';
|
||||
import { Post } from '@/models/post';
|
||||
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
|
||||
import { HydratedContentDocument } from '@/types/mongoose/content';
|
||||
import { CommunityPostsQuery } from '@/types/mongoose/community-posts-query';
|
||||
import { HydratedPostDocument } from '@/types/mongoose/post';
|
||||
import { HydratedSettingsDocument } from '@/types/mongoose/settings';
|
||||
|
||||
const router = express.Router();
|
||||
const router: express.Router = express.Router();
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (req, res) {
|
||||
let userContent = await getUserContent(req.pid);
|
||||
if(!userContent) return res.sendStatus(404);
|
||||
let query = {
|
||||
removed: false,
|
||||
is_spoiler: 0,
|
||||
app_data: { $eq: null },
|
||||
parent: { $eq: null },
|
||||
message_to_pid: { $eq: null },
|
||||
pid: null
|
||||
}
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const userContent: HydratedContentDocument | null = await getUserContent(request.pid);
|
||||
|
||||
if(req.query.relation === 'friend') {
|
||||
let friends = await getFriends(req.pid);
|
||||
if(!friends) return res.sendStatus(204);
|
||||
query.pid = { $in: friends.pids };
|
||||
}
|
||||
else if(req.query.relation === 'following') {
|
||||
query.pid = { $in: userContent.followed_users.map(i=>Number(i)) };
|
||||
}
|
||||
else if(req.query.pid) {
|
||||
query.pid = { $in: (req.query.pid as string[]).map(i=>Number(i)) }
|
||||
}
|
||||
let posts;
|
||||
if(req.query.distinct_pid === '1')
|
||||
posts = await Post.aggregate([
|
||||
{ $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: (req.query.limit ? Number(req.query.limit) : 10) } // only return the top 10 results
|
||||
]);
|
||||
else if(req.query.is_hot === '1')
|
||||
posts = await Post.find(query).sort({ empathy_count: -1}).limit(parseInt(req.query.limit as string));
|
||||
else
|
||||
posts = await Post.find(query).sort({ created_at: -1}).limit(parseInt(req.query.limit as string));
|
||||
if (!userContent) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Build formatted response and send it off. */
|
||||
let options = {
|
||||
name: 'posts',
|
||||
with_mii: req.query.with_mii === '1',
|
||||
topic_tag: true
|
||||
}
|
||||
res.contentType("application/xml");
|
||||
res.send(await xmlGenerator.People(posts, options));
|
||||
const query: CommunityPostsQuery = {
|
||||
removed: false,
|
||||
is_spoiler: 0,
|
||||
app_data: { $eq: null },
|
||||
parent: { $eq: null },
|
||||
message_to_pid: { $eq: null }
|
||||
};
|
||||
|
||||
const relation: string | undefined = getValueFromQueryString(request.query, 'relation');
|
||||
const distinctPID: string | undefined = getValueFromQueryString(request.query, 'distinct_pid');
|
||||
const limitString: string | undefined = getValueFromQueryString(request.query, 'limit');
|
||||
const withMii: string | undefined = getValueFromQueryString(request.query, 'with_mii');
|
||||
|
||||
let limit: number = 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) {
|
||||
// TODO - Update getValueFromQueryString to return arrays optionally
|
||||
query.pid = { $in: (request.query.pid as string[]).map(pid => Number(pid)) };
|
||||
}
|
||||
|
||||
let posts: HydratedPostDocument[];
|
||||
if (distinctPID === '1') {
|
||||
posts = await Post.aggregate([
|
||||
{ $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
|
||||
]);
|
||||
} 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);
|
||||
}
|
||||
|
||||
/* Build formatted response and send it off. */
|
||||
const options: XMLResponseGeneratorOptions = {
|
||||
name: 'posts',
|
||||
with_mii: withMii === '1',
|
||||
topic_tag: true
|
||||
};
|
||||
|
||||
response.contentType('application/xml');
|
||||
response.send(await xmlGenerator.People(posts, options));
|
||||
});
|
||||
|
||||
router.get('/:pid/following', async function (req, res) {
|
||||
let user = await getUserContent(req.params.pid);
|
||||
if(!user) res.sendStatus(404);
|
||||
let people = await getFollowedUsers(user);
|
||||
if(!people) res.sendStatus(404);
|
||||
res.send(await xmlGenerator.Following(people));
|
||||
router.get('/:pid/following', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const pid: number = parseInt(request.params.pid);
|
||||
|
||||
if (isNaN(pid)) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const userContent: HydratedContentDocument | null = await getUserContent(pid);
|
||||
|
||||
if (!userContent) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const people: HydratedSettingsDocument[] = await getFollowedUsers(userContent);
|
||||
|
||||
response.send(await xmlGenerator.Following(people));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
import express from 'express';
|
||||
import { getEndpoints } from '@/database';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
|
||||
const router = express.Router();
|
||||
const router: express.Router = express.Router();
|
||||
|
||||
router.get('/', async function(req, res) {
|
||||
res.send('Pong!');
|
||||
router.get('/', function(_request: express.Request, response: express.Response): void {
|
||||
response.send('Pong!');
|
||||
});
|
||||
|
||||
router.get('/database', async function(req, res) {
|
||||
let document = await getEndpoints();
|
||||
if(document)
|
||||
res.send('DB Connection Working! :D');
|
||||
router.get('/database', async function(_request: express.Request, response: express.Response): Promise<void> {
|
||||
const endpoints: HydratedEndpointDocument[] = await getEndpoints();
|
||||
|
||||
if (endpoints && endpoints.length <= 0) {
|
||||
response.send('DB Connection Working! :D');
|
||||
} else {
|
||||
response.send('DB Connection Not Working! D:');
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,243 +1,377 @@
|
|||
import crypto from "node:crypto";
|
||||
import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { Snowflake } from 'node-snowflake';
|
||||
import xml from 'object-to-xml';
|
||||
import communityPostGen from '@/util/xmlResponseGenerator';
|
||||
import { processServiceToken, decodeParamPack, processPainting, uploadCDNAsset } from '@/util';
|
||||
import { z } from 'zod';
|
||||
import { processPainting, uploadCDNAsset, getValueFromQueryString } from '@/util';
|
||||
import {
|
||||
getPostByID,
|
||||
getUserContent,
|
||||
getPostReplies,
|
||||
getPNID,
|
||||
getUserSettings,
|
||||
getCommunityByID,
|
||||
getCommunityByTitleID,
|
||||
getDuplicatePosts
|
||||
getPostByID,
|
||||
getUserContent,
|
||||
getPostReplies,
|
||||
getPNID,
|
||||
getUserSettings,
|
||||
getCommunityByID,
|
||||
getCommunityByTitleID,
|
||||
getDuplicatePosts
|
||||
} from '@/database';
|
||||
import { LOG_WARN } from '@/logger';
|
||||
import { Post } from '@/models/post';
|
||||
const { Community } = require("@/models/community");
|
||||
import { Community } from '@/models/community';
|
||||
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
|
||||
import { HydratedPostDocument, IPost } from '@/types/mongoose/post';
|
||||
import { HydratedContentDocument } from '@/types/mongoose/content';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import { HydratedSettingsDocument } from '@/types/mongoose/settings';
|
||||
|
||||
const router = express.Router();
|
||||
const newPostSchema = z.object({
|
||||
community_id: z.string(),
|
||||
app_data: z.string().optional(),
|
||||
painting: z.string().optional(),
|
||||
screenshot: z.string().optional(),
|
||||
body: z.string(),
|
||||
feeling_id: z.string(),
|
||||
search_key: z.string().array(),
|
||||
topic_tag: z.string(),
|
||||
is_autopost: z.string(),
|
||||
spoiler: z.string().optional(),
|
||||
is_app_jumpable: z.string(),
|
||||
language_id: z.string()
|
||||
});
|
||||
|
||||
const upload = multer();
|
||||
const router: express.Router = express.Router();
|
||||
const upload: multer.Multer = multer();
|
||||
|
||||
/* GET post titles. */
|
||||
router.post('/', upload.none(), async function (req, res) { await newPost(req, res)});
|
||||
router.post('/', upload.none(), newPost);
|
||||
|
||||
router.post('/:post_id/replies', upload.none(), async function (req, res) { await newPost(req, res)});
|
||||
router.post('/:post_id/replies', upload.none(), newPost);
|
||||
|
||||
router.post('/:post_id.delete', async function (req, res) {
|
||||
const post = await getPostByID(req.params.post_id);
|
||||
let user = await getUserContent(req.pid);
|
||||
if(!post || !user)
|
||||
return res.sendStatus(504);
|
||||
if(post.pid === user.pid) {
|
||||
await post.remove('User requested removal');
|
||||
res.sendStatus(200);
|
||||
}
|
||||
router.post('/:post_id.delete', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const post: HydratedPostDocument | null = await getPostByID(request.params.post_id);
|
||||
const userContent: HydratedContentDocument | null = await getUserContent(request.pid);
|
||||
|
||||
else res.sendStatus(401)
|
||||
if (!post || !userContent) {
|
||||
response.sendStatus(504);
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.pid === userContent.pid) {
|
||||
await post.remove('User requested removal');
|
||||
response.sendStatus(200);
|
||||
} else {
|
||||
response.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:post_id/empathies', upload.none(), async function (req, res) {
|
||||
const post = await getPostByID(req.params.post_id);
|
||||
if(!post) res.sendStatus(404);
|
||||
if(post.yeahs.indexOf(req.pid) === -1) {
|
||||
await Post.updateOne({
|
||||
id: post.id,
|
||||
yeahs: {
|
||||
$ne: req.pid
|
||||
}
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
empathy_count: 1
|
||||
},
|
||||
$push: {
|
||||
yeahs: req.pid
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(post.yeahs.indexOf(req.pid) !== -1) {
|
||||
await Post.updateOne({
|
||||
id: post.id,
|
||||
yeahs: {
|
||||
$eq: req.pid
|
||||
}
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
empathy_count: -1
|
||||
},
|
||||
$pull: {
|
||||
yeahs: req.pid
|
||||
}
|
||||
});
|
||||
}
|
||||
res.sendStatus(200);
|
||||
router.post('/:post_id/empathies', upload.none(), async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const post: HydratedPostDocument | null = 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 (req, res) {
|
||||
let pid = processServiceToken(req.headers["x-nintendo-servicetoken"]);
|
||||
const post = await getPostByID(req.params.post_id);
|
||||
if(!post)
|
||||
return res.sendStatus(404);
|
||||
const posts = await getPostReplies(post.id, req.query.limit)
|
||||
if(!posts || posts.length === 0)
|
||||
return res.sendStatus(404);
|
||||
let options = {
|
||||
name: 'replies',
|
||||
with_mii: req.query.with_mii as string === '1',
|
||||
topic_tag: true
|
||||
}
|
||||
/* Build formatted response and send it off. */
|
||||
let response = await communityPostGen.RepliesResponse(posts, options)
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
router.get('/:post_id/replies', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const limitString: string | undefined = getValueFromQueryString(request.query, 'limit');
|
||||
|
||||
let limit: number = 10; // TODO - Is there a real limit?
|
||||
|
||||
if (limitString) {
|
||||
limit = parseInt(limitString);
|
||||
}
|
||||
|
||||
if (isNaN(limit)) {
|
||||
limit = 10;
|
||||
}
|
||||
|
||||
const post: HydratedPostDocument | null = await getPostByID(request.params.post_id);
|
||||
|
||||
if (!post) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const posts: HydratedPostDocument[] = await getPostReplies(post.id, limit);
|
||||
if (posts.length === 0) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const options: XMLResponseGeneratorOptions = {
|
||||
name: 'replies',
|
||||
with_mii: request.query.with_mii as string === '1',
|
||||
topic_tag: true
|
||||
};
|
||||
|
||||
response.contentType('application/xml');
|
||||
response.send(await communityPostGen.RepliesResponse(posts, options));
|
||||
});
|
||||
|
||||
router.get('', async function (req, res) {
|
||||
const post = await getPostByID(req.query.post_id);
|
||||
if(!post) {
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 404;
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: "Not Found"
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
}
|
||||
else res.send(await communityPostGen.QueryResponse(post));
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const postID: string | undefined = getValueFromQueryString(request.query, 'post_id');
|
||||
|
||||
if (!postID) {
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 404;
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: 'Not Found'
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const post: HydratedPostDocument | null = await getPostByID(postID);
|
||||
|
||||
if (!post) {
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 404;
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 404,
|
||||
message: 'Not Found'
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
response.send(await communityPostGen.QueryResponse(post));
|
||||
}
|
||||
});
|
||||
|
||||
async function newPost(req, res) {
|
||||
let PNID = await getPNID(req.pid), userSettings = await getUserSettings(req.pid), postID = await generatePostUID(21), parentPost = null;
|
||||
let paramPackData = decodeParamPack(req.headers["x-nintendo-parampack"]);
|
||||
let community_id = req.body.community_id;
|
||||
async function newPost(request: express.Request, response: express.Response): Promise<void> {
|
||||
const PNID: HydratedPNIDDocument | null = await getPNID(request.pid);
|
||||
const userSettings: HydratedSettingsDocument | null = await getUserSettings(request.pid);
|
||||
const bodyCheck = newPostSchema.safeParse(request.body);
|
||||
|
||||
let community = await getCommunityByID(community_id)
|
||||
if(!community)
|
||||
community = await Community.findOne({olive_community_id: community_id});
|
||||
if(!community)
|
||||
community = await getCommunityByTitleID(paramPackData.title_id);
|
||||
if (!PNID || !userSettings || !bodyCheck.success) {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!community || userSettings.account_status !== 0 || community.community_id === 'announcements')
|
||||
return res.sendStatus(403);
|
||||
if(req.params.post_id) {
|
||||
parentPost = await getPostByID(req.params.post_id.toString());
|
||||
if(!parentPost)
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
const communityID: string = bodyCheck.data.community_id;
|
||||
let messageBody: string = bodyCheck.data.body;
|
||||
let painting: string = bodyCheck.data.painting?.trim() || '';
|
||||
let screenshot: string = bodyCheck.data.screenshot?.trim() || '';
|
||||
let appData: string = bodyCheck.data.app_data?.trim() || '';
|
||||
const feelingID: number = parseInt(bodyCheck.data.feeling_id);
|
||||
const searchKey: string[] = bodyCheck.data.search_key;
|
||||
const topicTag: string = bodyCheck.data.topic_tag;
|
||||
const autopost: string = bodyCheck.data.is_autopost;
|
||||
const spoiler: string | undefined = bodyCheck.data.spoiler;
|
||||
const jumpable: string = bodyCheck.data.is_app_jumpable;
|
||||
const languageID: number = parseInt(bodyCheck.data.language_id);
|
||||
const countryID: number = parseInt(request.paramPack.country_id);
|
||||
const platformID: number = parseInt(request.paramPack.platform_id);
|
||||
const regionID: number = parseInt(request.paramPack.region_id);
|
||||
|
||||
if(!(community.admins && community.admins.indexOf(req.pid) !== -1 && userSettings.account_status === 0)
|
||||
&& (community.type >= 2) && !(parentPost && community.allows_comments && community.open)) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
if (
|
||||
isNaN(feelingID) ||
|
||||
isNaN(languageID) ||
|
||||
isNaN(countryID) ||
|
||||
isNaN(platformID) ||
|
||||
isNaN(regionID)
|
||||
) {
|
||||
response.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
let appData = "", painting = "", paintingURI, screenshot = null;
|
||||
if (req.body.app_data)
|
||||
appData = req.body.app_data.replace(/[^A-Za-z0-9+/=\s]/g, "");
|
||||
if (req.body.painting) {
|
||||
painting = req.body.painting.replace(/\0/g, "").trim();
|
||||
paintingURI = await processPainting(painting, true);
|
||||
await uploadCDNAsset('pn-cdn', `paintings/${req.pid}/${postID}.png`, paintingURI, 'public-read');
|
||||
}
|
||||
if (req.body.screenshot) {
|
||||
screenshot = req.body.screenshot.replace(/\0/g, "").trim();
|
||||
await uploadCDNAsset('pn-cdn', `screenshots/${req.pid}/${postID}.jpg`, Buffer.from(screenshot, 'base64'), 'public-read');
|
||||
}
|
||||
let community = await getCommunityByID(communityID);
|
||||
if (!community) {
|
||||
community = await Community.findOne({
|
||||
olive_community_id: communityID
|
||||
});
|
||||
}
|
||||
|
||||
let miiFace;
|
||||
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;
|
||||
}
|
||||
let body = req.body.body;
|
||||
if(body)
|
||||
body = req.body.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}‛¨ƒºª«»“”„¿¡←→↑↓√§¶†‡¦–—⇒⇔¤¢€£¥™©®+×÷=±∞ˇ˘˙¸˛˜′″µ°¹²³♭♪•…¬¯‰¼½¾♡♥●◆■▲▼☆★♀♂,./?;:'"\\<>]/g, "").trim();
|
||||
if(body && body.length > 280)
|
||||
body = body.substring(0,280);
|
||||
if(!body && !painting && !screenshot)
|
||||
return res.sendStatus(400);
|
||||
const document = {
|
||||
title_id: paramPackData.title_id,
|
||||
community_id: community.olive_community_id,
|
||||
screen_name: userSettings.screen_name,
|
||||
body: body,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
screenshot: screenshot ? `/screenshots/${req.pid}/${postID}.jpg`: "",
|
||||
screenshot_length: screenshot ? screenshot.length : null,
|
||||
country_id: paramPackData.country_id,
|
||||
created_at: new Date(),
|
||||
feeling_id: req.body.feeling_id,
|
||||
id: postID,
|
||||
search_key: req.body.search_key,
|
||||
topic_tag: req.body.topic_tag,
|
||||
is_autopost: req.body.is_autopost,
|
||||
is_spoiler: (req.body.spoiler) ? 1 : 0,
|
||||
is_app_jumpable: req.body.is_app_jumpable,
|
||||
language_id: req.body.language_id,
|
||||
mii: PNID.mii.data,
|
||||
mii_face_url: `https://mii.olv.pretendo.cc/mii/${PNID.pid}/${miiFace}`,
|
||||
pid: req.pid,
|
||||
platform_id: paramPackData.platform_id,
|
||||
region_id: paramPackData.region_id,
|
||||
verified: (PNID.access_level === 2 || PNID.access_level === 3),
|
||||
parent: parentPost ? parentPost.id : null,
|
||||
removed: false
|
||||
};
|
||||
let duplicatePost = await getDuplicatePosts(req.pid, document);
|
||||
if(duplicatePost || document.body === '' && document.painting === '' && document.screenshot === '') {
|
||||
res.set("Content-Type", "application/xml");
|
||||
res.statusCode = 400;
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: "DUPLICATE_POST"
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
}
|
||||
const newPost = new Post(document);
|
||||
newPost.save();
|
||||
if(parentPost) {
|
||||
parentPost.reply_count = parentPost.reply_count + 1;
|
||||
parentPost.save();
|
||||
}
|
||||
res.send(await communityPostGen.SinglePostResponse(newPost));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appData) {
|
||||
appData = appData.replace(/[^A-Za-z0-9+/=\s]/g, '');
|
||||
}
|
||||
|
||||
const postID: string = await generatePostUID(21);
|
||||
|
||||
if (painting) {
|
||||
painting = painting.replace(/\0/g, '').trim();
|
||||
const paintingBuffer: Buffer | null = await processPainting(painting);
|
||||
|
||||
if (paintingBuffer) {
|
||||
await uploadCDNAsset('pn-cdn', `paintings/${request.pid}/${postID}.png`, paintingBuffer, 'public-read');
|
||||
} else {
|
||||
LOG_WARN(`PAINTING FOR POST ${postID} FAILED TO PROCESS`);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshot) {
|
||||
screenshot = screenshot.replace(/\0/g, '').trim();
|
||||
const screenshotBuffer: Buffer = Buffer.from(screenshot, 'base64');
|
||||
|
||||
await uploadCDNAsset('pn-cdn', `screenshots/${request.pid}/${postID}.jpg`, screenshotBuffer, 'public-read');
|
||||
}
|
||||
|
||||
let miiFace: string = '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(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const document: IPost = {
|
||||
title_id: request.paramPack.title_id,
|
||||
community_id: community.olive_community_id,
|
||||
screen_name: userSettings.screen_name,
|
||||
body: messageBody,
|
||||
app_data: appData,
|
||||
painting: painting,
|
||||
screenshot: screenshot ? `/screenshots/${request.pid}/${postID}.jpg`: '',
|
||||
screenshot_length: screenshot ? screenshot.length : 0,
|
||||
country_id: countryID,
|
||||
created_at: new Date(),
|
||||
feeling_id: feelingID,
|
||||
id: postID,
|
||||
search_key: searchKey,
|
||||
topic_tag: topicTag,
|
||||
is_autopost: (autopost) ? 1 : 0,
|
||||
is_spoiler: (spoiler) ? 1 : 0,
|
||||
is_app_jumpable: (jumpable) ? 1 : 0,
|
||||
language_id: languageID,
|
||||
mii: PNID.mii.data,
|
||||
mii_face_url: `https://mii.olv.pretendo.cc/mii/${PNID.pid}/${miiFace}`,
|
||||
pid: request.pid,
|
||||
platform_id: platformID,
|
||||
region_id: regionID,
|
||||
verified: (PNID.access_level === 2 || PNID.access_level === 3),
|
||||
parent: parentPost ? parentPost.id : null,
|
||||
removed: false
|
||||
};
|
||||
|
||||
const duplicatePost = await getDuplicatePosts(request.pid, document);
|
||||
|
||||
if (duplicatePost) {
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.statusCode = 400;
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 1,
|
||||
version: 1,
|
||||
code: 400,
|
||||
error_code: 7,
|
||||
message: 'DUPLICATE_POST'
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const newPost = new Post(document);
|
||||
newPost.save();
|
||||
|
||||
if (parentPost) {
|
||||
parentPost.reply_count = (parentPost.reply_count || 0) + 1;
|
||||
parentPost.save();
|
||||
}
|
||||
|
||||
response.send(await communityPostGen.SinglePostResponse(newPost));
|
||||
}
|
||||
|
||||
async function generatePostUID(length) {
|
||||
let 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 });
|
||||
id = (inuse ? await generatePostUID(length) : id);
|
||||
return id;
|
||||
}
|
||||
async function generatePostUID(length: number): Promise<string> {
|
||||
let id: string = Buffer.from(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(length * 2))), 'binary').toString('base64').replace(/[+/]/g, '').substring(0, length);
|
||||
const inuse: HydratedPostDocument | null = await Post.findOne({ id });
|
||||
|
||||
id = (inuse ? await generatePostUID(length) : id);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,59 +1,89 @@
|
|||
import express from 'express';
|
||||
import memoize from 'memoizee';
|
||||
import { getPNID, getEndPoint } from '@/database';
|
||||
import { getPNID, getEndpoint } from '@/database';
|
||||
import { Post } from '@/models/post';
|
||||
import { Community } from '@/models/community';
|
||||
import comPostGen from '@/util/xmlResponseGenerator';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { HydratedPostDocument } from '@/types/mongoose/post';
|
||||
|
||||
const router = express.Router();
|
||||
const router: express.Router = express.Router();
|
||||
|
||||
// TODO - Need to add types to memoize in @/types/memoize.d.ts
|
||||
const memoized = memoize(comPostGen.topics, { async: true, maxAge: 1000 * 60 * 60 });
|
||||
|
||||
/* GET post titles. */
|
||||
router.get('/', async function (req, res) {
|
||||
let user = await getPNID(req.pid), discovery;
|
||||
if(user)
|
||||
discovery = await getEndPoint(user.server_access_level);
|
||||
else
|
||||
discovery = await getEndPoint('prod');
|
||||
if(!discovery.topics) return res.sendStatus(404);
|
||||
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
|
||||
const user: HydratedPNIDDocument | null = await getPNID(request.pid);
|
||||
let discovery: HydratedEndpointDocument | null;
|
||||
|
||||
let communities = await calculateMostPopularCommunities(24, 10);
|
||||
if(communities === null || communities.length < 10) return res.sendStatus(404);
|
||||
if (user) {
|
||||
discovery = await getEndpoint(user.server_access_level);
|
||||
} else {
|
||||
discovery = await getEndpoint('prod');
|
||||
}
|
||||
|
||||
let response = await memoized(communities);
|
||||
res.contentType("application/xml");
|
||||
res.send(response);
|
||||
if (!discovery || !discovery.topics) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const communities: HydratedCommunityDocument[] = await calculateMostPopularCommunities(24, 10);
|
||||
|
||||
if (communities.length < 10) {
|
||||
response.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
response.contentType('application/xml');
|
||||
response.send(await memoized(communities));
|
||||
});
|
||||
|
||||
async function calculateMostPopularCommunities(hours, limit) {
|
||||
const now = new Date();
|
||||
const last24Hours = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
||||
const posts = await Post.find({ created_at: { $gte: last24Hours }, message_to_pid: null });
|
||||
if(!posts) return;
|
||||
const communityIds = {};
|
||||
for (const post of posts) {
|
||||
const communityId = post.community_id;
|
||||
communityIds[communityId] = (communityIds[communityId] || 0) + 1;
|
||||
}
|
||||
const communities = Object.entries(communityIds)
|
||||
.sort((a, b) => (b[1] as number) - (a[1] as number))
|
||||
.map((entry) => entry[0]);
|
||||
if(communities.length < limit)
|
||||
return Community.find().limit(limit).sort({followers: -1});
|
||||
async function calculateMostPopularCommunities(hours: number, limit: number): Promise<HydratedCommunityDocument[]> {
|
||||
const now: Date = new Date();
|
||||
const last24Hours: Date = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
||||
const posts: HydratedPostDocument[] = await Post.find({ created_at: { $gte: last24Hours }, message_to_pid: null });
|
||||
|
||||
let response = await Community.aggregate([
|
||||
{ $match: { olive_community_id: { $in: communities }, parent: null } },
|
||||
{$addFields: {
|
||||
index: { $indexOfArray: [ communities, "$olive_community_id" ] }
|
||||
}},
|
||||
{ $sort: { index: 1 } },
|
||||
{ $limit : limit },
|
||||
{ $project: { index: 0, _id: 0 } }
|
||||
]);
|
||||
if(response.length < limit)
|
||||
return calculateMostPopularCommunities(hours + hours, limit);
|
||||
else return response;
|
||||
if (!posts.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const communityIDCounts: {
|
||||
[key: string]: number
|
||||
} = {};
|
||||
|
||||
for (const post of posts) {
|
||||
const communityID: string = post.community_id;
|
||||
communityIDCounts[communityID] = (communityIDCounts[communityID] || 0) + 1;
|
||||
}
|
||||
|
||||
const popularCommunitiesSorted: string[] = Object.entries(communityIDCounts)
|
||||
.sort((a, b) => (b[1] as number) - (a[1] as number))
|
||||
.map((entry) => entry[0]);
|
||||
|
||||
if (popularCommunitiesSorted.length < limit) {
|
||||
return Community.find().limit(limit).sort({
|
||||
followers: -1
|
||||
});
|
||||
}
|
||||
|
||||
const response: HydratedCommunityDocument[] = await Community.aggregate([
|
||||
{ $match: { olive_community_id: { $in: popularCommunitiesSorted }, parent: null } },
|
||||
{$addFields: {
|
||||
index: { $indexOfArray: [ popularCommunitiesSorted, '$olive_community_id' ] }
|
||||
}},
|
||||
{ $sort: { index: 1 } },
|
||||
{ $limit : limit },
|
||||
{ $project: { index: 0, _id: 0 } }
|
||||
]);
|
||||
|
||||
if (response.length < limit) {
|
||||
return calculateMostPopularCommunities(hours + hours, limit);
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
import express from 'express';
|
||||
import xml from 'object-to-xml';
|
||||
import { getValueFromQueryString } from '@/util';
|
||||
|
||||
const router = express.Router();
|
||||
const router: express.Router = express.Router();
|
||||
|
||||
router.get('/:pid/notifications', async function(req, res) {
|
||||
let type = req.query.type, title_id = req.query.title_id;
|
||||
console.log(type);
|
||||
console.log(title_id);
|
||||
console.log(req.params.pid);
|
||||
router.get('/:pid/notifications', function(request: express.Request, response: express.Response): void {
|
||||
const type: string | undefined = getValueFromQueryString(request.query, 'type');
|
||||
const titleID: string | undefined = getValueFromQueryString(request.query, 'title_id');
|
||||
const pid: string | undefined = getValueFromQueryString(request.query, 'pid');
|
||||
|
||||
res.set("Content-Type", "application/xml");
|
||||
let response = {
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
posts: " "
|
||||
}
|
||||
};
|
||||
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
|
||||
console.log(type);
|
||||
console.log(titleID);
|
||||
console.log(pid);
|
||||
|
||||
response.set('Content-Type', 'application/xml');
|
||||
response.send('<?xml version="1.0" encoding="UTF-8"?>\n' + xml({
|
||||
result: {
|
||||
has_error: 0,
|
||||
version: 1,
|
||||
posts: ' '
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
6
src/types/common/create-new-community-body.ts
Normal file
6
src/types/common/create-new-community-body.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface CreateNewCommunityBody {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
app_data: string;
|
||||
}
|
||||
4
src/types/common/crypto-options.ts
Normal file
4
src/types/common/crypto-options.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface CryptoOptions {
|
||||
private_key: Buffer
|
||||
hmac_secret: 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;
|
||||
};
|
||||
}
|
||||
7
src/types/common/send-message-body.ts
Normal file
7
src/types/common/send-message-body.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface SendMessageBody {
|
||||
message_to_pid: number;
|
||||
body: string;
|
||||
painting?: string;
|
||||
screenshot?: string;
|
||||
app_data?: string;
|
||||
}
|
||||
7
src/types/common/xml-response-generator-options.ts
Normal file
7
src/types/common/xml-response-generator-options.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface XMLResponseGeneratorOptions {
|
||||
name?: string;
|
||||
with_mii: boolean;
|
||||
app_data?: boolean;
|
||||
topic_tag?: boolean;
|
||||
topics?: boolean;
|
||||
}
|
||||
3
src/types/express-subdomain.d.ts
vendored
Normal file
3
src/types/express-subdomain.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
declare module 'express-subdomain';
|
||||
|
||||
// TODO - Add proper types
|
||||
4
src/types/express.d.ts
vendored
4
src/types/express.d.ts
vendored
|
|
@ -1,10 +1,10 @@
|
|||
// to make the file a module and avoid the TypeScript error
|
||||
export {};
|
||||
import { ParamPack } from '@/types/common/param-pack';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
pid: number;
|
||||
paramPack: ParamPack
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
@ -9,16 +9,16 @@ export interface IPost {
|
|||
painting: string;
|
||||
screenshot: string;
|
||||
screenshot_length: number;
|
||||
search_key: Types.Array<string>;
|
||||
search_key: string[];
|
||||
topic_tag: string;
|
||||
community_id: string;
|
||||
created_at: number;
|
||||
created_at: Date;
|
||||
feeling_id: number;
|
||||
is_autopost: number;
|
||||
is_community_private_autopost: number;
|
||||
is_community_private_autopost?: number;
|
||||
is_spoiler: number;
|
||||
is_app_jumpable: number;
|
||||
empathy_count: number;
|
||||
empathy_count?: number;
|
||||
country_id: number;
|
||||
language_id: number;
|
||||
mii: string;
|
||||
|
|
@ -27,13 +27,13 @@ export interface IPost {
|
|||
platform_id: number;
|
||||
region_id: number;
|
||||
parent: string;
|
||||
reply_count: number;
|
||||
reply_count?: number;
|
||||
verified: boolean;
|
||||
message_to_pid: string;
|
||||
message_to_pid?: string;
|
||||
removed: boolean;
|
||||
removed_reason: string;
|
||||
yeahs: Types.Array<number>;
|
||||
number: number;
|
||||
removed_reason?: string;
|
||||
yeahs?: Types.Array<number>;
|
||||
number?: number;
|
||||
}
|
||||
|
||||
export interface IPostMethods {
|
||||
|
|
|
|||
3
src/types/node-snowflake.d.ts
vendored
Normal file
3
src/types/node-snowflake.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
declare module 'node-snowflake';
|
||||
|
||||
// TODO - Add proper types
|
||||
230
src/util.ts
230
src/util.ts
|
|
@ -5,99 +5,85 @@ import fs from 'fs-extra';
|
|||
import TGA from 'tga';
|
||||
import pako from 'pako';
|
||||
import { PNG } from 'pngjs';
|
||||
import bmp from 'bmp-js';
|
||||
import aws from 'aws-sdk';
|
||||
import { createChannel, createClient, Metadata } from 'nice-grpc';
|
||||
import { friends } from 'pretendo-grpc-ts';
|
||||
import { ParsedQs } from 'qs';
|
||||
import { getPNID } from '@/database';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { Settings } from '@/models/settings';
|
||||
import { Content } from '@/models/content';
|
||||
import { SafeQs } from '@/types/common/safe-qs';
|
||||
import { ParamPack } from '@/types/common/param-pack';
|
||||
import { config } from '@/config-manager';
|
||||
import { CryptoOptions } from '@/types/common/crypto-options';
|
||||
|
||||
import { FriendsClient, FriendsDefinition } from 'pretendo-grpc-ts/dist/friends/friends_service';
|
||||
import { GetUserFriendPIDsResponse } from 'pretendo-grpc-ts/dist/friends/get_user_friend_pids_rpc';
|
||||
import { GetUserFriendRequestsIncomingResponse } from 'pretendo-grpc-ts/dist/friends/get_user_friend_requests_incoming_rpc';
|
||||
import { FriendRequest } from 'pretendo-grpc-ts/dist/friends/friend_request';
|
||||
|
||||
const { FriendsService } = friends;
|
||||
const { ip, port, api_key } = config.grpc.friends;
|
||||
|
||||
const channel = createChannel(`${ip}:${port}`);
|
||||
const client = createClient(FriendsService.FriendsDefinition, channel);
|
||||
const gRPCChannel = createChannel(`${ip}:${port}`); // * nice-grpc doesn't export ChannelImplementation so this can't be typed
|
||||
const gRPCFriendsClient: FriendsClient = createClient(FriendsDefinition, gRPCChannel);
|
||||
|
||||
const s3 = new aws.S3({
|
||||
const s3: aws.S3 = new aws.S3({
|
||||
endpoint: new aws.Endpoint(config.s3.endpoint),
|
||||
accessKeyId: config.s3.key,
|
||||
secretAccessKey: config.s3.secret
|
||||
});
|
||||
|
||||
export async function create_user(pid, experience, notifications, region) {
|
||||
const pnid = await getPNID(pid);
|
||||
if (!pnid) {
|
||||
return;
|
||||
}
|
||||
const newSettings = {
|
||||
pid: pid,
|
||||
screen_name: pnid.mii.name,
|
||||
game_skill: experience,
|
||||
receive_notifications: notifications,
|
||||
};
|
||||
const newContent = {
|
||||
pid: pid
|
||||
};
|
||||
const newSettingsObj = new Settings(newSettings);
|
||||
await newSettingsObj.save();
|
||||
export function decodeParamPack(paramPack: string): ParamPack {
|
||||
const values: string[] = Buffer.from(paramPack, 'base64').toString().split('\\');
|
||||
const entries: string[][] = 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);
|
||||
}
|
||||
|
||||
const newContentObj = new Content(newContent);
|
||||
await newContentObj.save();
|
||||
return entries;
|
||||
}, []);
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
export function decodeParamPack(paramPack): ParamPack {
|
||||
/* Decode base64 */
|
||||
const dec = Buffer.from(paramPack, 'base64').toString('ascii').slice(1, -1).split('\\');
|
||||
/* Remove starting and ending '/', split into array */
|
||||
/* 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 as ParamPack;
|
||||
}
|
||||
|
||||
export function processServiceToken(token) {
|
||||
export function getPIDFromServiceToken(token: string): number {
|
||||
try {
|
||||
const B64token = Buffer.from(token, 'base64');
|
||||
const decryptedToken = this.decryptToken(B64token);
|
||||
const decoded: Buffer = Buffer.from(token, 'base64');
|
||||
const decryptedToken: Buffer | null = decryptToken(decoded);
|
||||
|
||||
if (!decryptedToken) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return decryptedToken.readUInt32LE(0x2);
|
||||
} catch (e) {
|
||||
return null;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptToken(token) {
|
||||
export function decryptToken(token: Buffer): Buffer | null {
|
||||
const cryptoPath: string = `${__dirname}/../certs/access`;
|
||||
|
||||
// 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 = `${__dirname}/../certs/access`;
|
||||
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
|
||||
const aesKey: Buffer = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
|
||||
|
||||
const iv = Buffer.alloc(16);
|
||||
const iv: Buffer = Buffer.alloc(16);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
|
||||
const decipher: crypto.Decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
|
||||
|
||||
let decryptedBody = decipher.update(token);
|
||||
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
|
||||
|
||||
return decryptedBody;
|
||||
return Buffer.concat([
|
||||
decipher.update(token),
|
||||
decipher.final()
|
||||
]);
|
||||
}
|
||||
|
||||
const cryptoPath = `${__dirname}/../certs/access`;
|
||||
|
||||
const cryptoOptions = {
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
|
||||
hmac_secret: config.account_server_secret
|
||||
};
|
||||
|
||||
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
|
||||
const privateKey: NodeRSA = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
|
||||
environment: 'browser',
|
||||
encryptionScheme: {
|
||||
scheme: 'pkcs1_oaep',
|
||||
|
|
@ -105,29 +91,31 @@ export function decryptToken(token) {
|
|||
}
|
||||
});
|
||||
|
||||
const cryptoConfig = token.subarray(0, 0x82);
|
||||
const signature = token.subarray(0x82, 0x96);
|
||||
const encryptedBody = token.subarray(0x96);
|
||||
const cryptoConfig: Buffer = token.subarray(0, 0x82);
|
||||
const signature: Buffer = token.subarray(0x82, 0x96);
|
||||
const encryptedBody: Buffer = token.subarray(0x96);
|
||||
|
||||
const encryptedAESKey = cryptoConfig.subarray(0, 128);
|
||||
const point1 = cryptoConfig.readInt8(0x80);
|
||||
const point2 = cryptoConfig.readInt8(0x81);
|
||||
const encryptedAESKey: Buffer = cryptoConfig.subarray(0, 128);
|
||||
const point1: number = cryptoConfig.readInt8(0x80);
|
||||
const point2: number = cryptoConfig.readInt8(0x81);
|
||||
|
||||
const iv = Buffer.concat([
|
||||
const iv: Buffer = Buffer.concat([
|
||||
Buffer.from(encryptedAESKey.subarray(point1, point1 + 8)),
|
||||
Buffer.from(encryptedAESKey.subarray(point2, point2 + 8))
|
||||
]);
|
||||
|
||||
try {
|
||||
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
|
||||
const decryptedAESKey: Buffer = privateKey.decrypt(encryptedAESKey);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
|
||||
const decipher: crypto.Decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
|
||||
|
||||
let decryptedBody = decipher.update(encryptedBody);
|
||||
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
|
||||
const decryptedBody: Buffer = Buffer.concat([
|
||||
decipher.update(encryptedBody),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
|
||||
const calculatedSignature = hmac.digest();
|
||||
const hmac: crypto.Hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
|
||||
const calculatedSignature: Buffer = hmac.digest();
|
||||
|
||||
if (Buffer.compare(calculatedSignature, signature) !== 0) {
|
||||
LOG_ERROR('Token signature did not match');
|
||||
|
|
@ -135,89 +123,36 @@ export function decryptToken(token) {
|
|||
}
|
||||
|
||||
return decryptedBody;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
LOG_ERROR('Failed to decrypt token. Probably a NNID from the topics request');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function processPainting(painting, isTGA) {
|
||||
if (isTGA) {
|
||||
const paintingBuffer = Buffer.from(painting, 'base64');
|
||||
let output;
|
||||
try {
|
||||
output = pako.inflate(paintingBuffer);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
const tga = new TGA(Buffer.from(output));
|
||||
const png = new PNG({
|
||||
width: tga.width,
|
||||
height: tga.height
|
||||
});
|
||||
png.data = tga.pixels;
|
||||
return PNG.sync.write(png);
|
||||
//return `data:image/png;base64,${pngBuffer.toString('base64')}`;
|
||||
} else {
|
||||
const paintingBuffer = Buffer.from(painting, 'base64');
|
||||
const bitmap = bmp.decode(paintingBuffer);
|
||||
const tga = this.createBMPTgaBuffer(bitmap.width, bitmap.height, bitmap.data, false);
|
||||
export function processPainting(painting: string): Buffer | null {
|
||||
const paintingBuffer: Buffer = Buffer.from(painting, 'base64');
|
||||
let output: Uint8Array;
|
||||
|
||||
let output;
|
||||
try {
|
||||
output = pako.deflate(tga, {level: 6});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return new Buffer(output).toString('base64');
|
||||
}
|
||||
}
|
||||
|
||||
export function nintendoPasswordHash(password, pid) {
|
||||
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');
|
||||
}
|
||||
|
||||
export function createBMPTgaBuffer(width, height, pixels, dontFlipY) {
|
||||
const buffer = Buffer.alloc(18 + pixels.length);
|
||||
// write header
|
||||
buffer.writeInt8(0, 0);
|
||||
buffer.writeInt8(0, 1);
|
||||
buffer.writeInt8(2, 2);
|
||||
buffer.writeInt16LE(0, 3);
|
||||
buffer.writeInt16LE(0, 5);
|
||||
buffer.writeInt8(0, 7);
|
||||
buffer.writeInt16LE(0, 8);
|
||||
buffer.writeInt16LE(0, 10);
|
||||
buffer.writeInt16LE(width, 12);
|
||||
buffer.writeInt16LE(height, 14);
|
||||
buffer.writeInt8(32, 16);
|
||||
buffer.writeInt8(8, 17);
|
||||
|
||||
let offset = 18;
|
||||
for (let i = 0; i < height; i++) {
|
||||
for (let j = 0; j < width; j++) {
|
||||
const idx = ((dontFlipY ? i : height - i - 1) * width + j) * 4;
|
||||
buffer.writeUInt8(pixels[idx + 1], offset++); // b
|
||||
buffer.writeUInt8(pixels[idx + 2], offset++); // g
|
||||
buffer.writeUInt8(pixels[idx + 3], offset++); // r
|
||||
buffer.writeUInt8(255, offset++); // a
|
||||
}
|
||||
try {
|
||||
output = pako.inflate(paintingBuffer);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
const tga = new TGA(Buffer.from(output));
|
||||
const png: PNG = new PNG({
|
||||
width: tga.width,
|
||||
height: tga.height
|
||||
});
|
||||
|
||||
png.data = tga.pixels;
|
||||
|
||||
return PNG.sync.write(png);
|
||||
}
|
||||
|
||||
export async function uploadCDNAsset(bucket, key, data, acl) {
|
||||
const awsPutParams = {
|
||||
export async function uploadCDNAsset(bucket: string, key: string, data: Buffer, acl: string): Promise<void> {
|
||||
const awsPutParams: aws.S3.PutObjectRequest = {
|
||||
Body: data,
|
||||
Key: key,
|
||||
Bucket: bucket,
|
||||
|
|
@ -227,24 +162,27 @@ export async function uploadCDNAsset(bucket, key, data, acl) {
|
|||
await s3.putObject(awsPutParams).promise();
|
||||
}
|
||||
|
||||
export async function getFriends(pid) {
|
||||
return await client.getUserFriendPIDs({
|
||||
export async function getUserFriendPIDs(pid: number): Promise<number[]> {
|
||||
const response: GetUserFriendPIDsResponse = await gRPCFriendsClient.getUserFriendPIDs({
|
||||
pid: pid
|
||||
}, {
|
||||
metadata: Metadata({
|
||||
'X-API-Key': api_key
|
||||
})
|
||||
});
|
||||
|
||||
return response.pids;
|
||||
}
|
||||
|
||||
export async function getFriendRequests(pid) {
|
||||
const requests = await client.getUserFriendRequestsIncoming({
|
||||
export async function getUserFriendRequestsIncoming(pid: number): Promise<FriendRequest[]> {
|
||||
const requests: GetUserFriendRequestsIncomingResponse = await gRPCFriendsClient.getUserFriendRequestsIncoming({
|
||||
pid: pid
|
||||
}, {
|
||||
metadata: Metadata({
|
||||
'X-API-Key': api_key
|
||||
})
|
||||
});
|
||||
|
||||
return requests.friendRequests;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,292 +1,282 @@
|
|||
import xmlbuilder from 'xmlbuilder';
|
||||
import moment from 'moment';
|
||||
import { getNumberNewCommunityPostsByID } from '@/database';
|
||||
import { getPostsBytitleID } from '@/database';
|
||||
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
|
||||
import { HydratedPostDocument } from '@/types/mongoose/post';
|
||||
import { HydratedCommunityDocument } from '@/types/mongoose/community';
|
||||
import { HydratedSettingsDocument } from '@/types/mongoose/settings';
|
||||
|
||||
class XmlResponseGenerator {
|
||||
/**
|
||||
* Generate response to reply request
|
||||
* @param posts
|
||||
* @param options
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async RepliesResponse(posts, options) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "replies").up()
|
||||
.e("posts");
|
||||
for (const post of posts) {
|
||||
postObj(xml, post, options, {});
|
||||
}
|
||||
xml = xml.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
static RepliesResponse(posts: HydratedPostDocument[], options: XMLResponseGeneratorOptions): string {
|
||||
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('request_name', 'replies').up()
|
||||
.e('posts');
|
||||
|
||||
/**
|
||||
* Generate response to community posts response
|
||||
* @param posts
|
||||
* @param community
|
||||
* @param options
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async PostsResponse(posts, community, options) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", options.name).up()
|
||||
.e("topic")
|
||||
.e("community_id", community.community_id).up()
|
||||
.up()
|
||||
.e("posts");
|
||||
for (const post of posts) {
|
||||
postObj(xml, post, options, {});
|
||||
}
|
||||
xml = xml.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
for (const post of posts) {
|
||||
postObj(xml, post, options, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate empty xml response
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async EmptyResponse() {
|
||||
const xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates response to list of communities request
|
||||
* @param communities
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async Communities(communities) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "communities").up()
|
||||
.e("communities");
|
||||
for(let community of communities) {
|
||||
xml = xml.e("community")
|
||||
.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", community.app_data).up()
|
||||
.e("is_user_community", 0).up()
|
||||
.up()
|
||||
}
|
||||
return xml.up().end({ pretty: true, allowEmpty: true});
|
||||
}
|
||||
static PostsResponse(posts: HydratedPostDocument[], community: HydratedCommunityDocument, options: XMLResponseGeneratorOptions): string {
|
||||
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('request_name', options.name).up()
|
||||
.e('topic')
|
||||
.e('community_id', community.community_id).up()
|
||||
.up()
|
||||
.e('posts');
|
||||
|
||||
/**
|
||||
* Generates response to a acommunity request
|
||||
* @param communities
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async Community(community) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "community").up()
|
||||
.e("community")
|
||||
.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", community.app_data).up()
|
||||
.e("is_user_community", 0)
|
||||
.up()
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true});
|
||||
}
|
||||
for (const post of posts) {
|
||||
postObj(xml, post, options, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate response to request for single post
|
||||
* @param post
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async SinglePostResponse(post) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("post");
|
||||
postObj(xml, post, { with_mii: true }, {});
|
||||
xml = xml.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate response to search for post
|
||||
* @param post
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async QueryResponse(post) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "posts.search").up()
|
||||
.e("posts");
|
||||
postObj(xml, post, { with_mii: true }, {});
|
||||
xml = xml.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
static EmptyResponse(): string {
|
||||
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up();
|
||||
|
||||
/**
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static Communities(communities: HydratedCommunityDocument[]): string {
|
||||
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('request_name', 'communities').up()
|
||||
.e('communities');
|
||||
|
||||
for (const community of communities) {
|
||||
xml = xml.e('community')
|
||||
.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', community.app_data).up()
|
||||
.e('is_user_community', 0).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static Community(community: HydratedCommunityDocument): string {
|
||||
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('request_name', 'community').up()
|
||||
.e('community')
|
||||
.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', community.app_data).up()
|
||||
.e('is_user_community', 0)
|
||||
.up();
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static SinglePostResponse(post: HydratedPostDocument): string {
|
||||
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('post');
|
||||
|
||||
const options: XMLResponseGeneratorOptions = {
|
||||
with_mii: true
|
||||
};
|
||||
|
||||
postObj(xml, post, options, null);
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static QueryResponse(post: HydratedPostDocument): string {
|
||||
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('request_name', 'posts.search').up()
|
||||
.e('posts');
|
||||
|
||||
const options: XMLResponseGeneratorOptions = {
|
||||
with_mii: true
|
||||
};
|
||||
|
||||
postObj(xml, post, options, null);
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate response to /v1/topics
|
||||
* @param communities
|
||||
* @returns xml
|
||||
*/
|
||||
static async topics(communities) {
|
||||
const expirationDate = moment().add(1, 'days');
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.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 getNumberNewCommunityPostsByID(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_id.forEach(function (title_id) {
|
||||
if(title_id !== '')
|
||||
xml = xml.e('title_id', title_id).up();
|
||||
})
|
||||
xml = xml.up()
|
||||
.e('title_id', community.title_id[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) {
|
||||
xml = xml.e("person")
|
||||
.e("posts")
|
||||
postObj(xml, post, { with_mii: true, app_data: false, topic_tag: false, topics: true }, community);
|
||||
xml = xml.up().up();
|
||||
}
|
||||
xml = xml.up().up()
|
||||
}
|
||||
return xml.end({ pretty: false, allowEmpty: true });
|
||||
}
|
||||
static async topics(communities: HydratedCommunityDocument[]): Promise<string> {
|
||||
const expirationDate = moment().add(1, 'days');
|
||||
|
||||
/**
|
||||
* Generate response to /v1/users/:pid/following
|
||||
* @param people
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async Following(people) {
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("request_name", "user_infos").up()
|
||||
.e("people");
|
||||
for(let person of people) {
|
||||
xml = xml.e("person")
|
||||
.e('pid', person.pid).up()
|
||||
.e('screen_name', person.screen_name).up()
|
||||
.up()
|
||||
}
|
||||
return xml.up().end({ pretty: true, allowEmpty: true});
|
||||
}
|
||||
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.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');
|
||||
|
||||
/**
|
||||
* Generate response to /v1/people
|
||||
* @param posts
|
||||
* @param options
|
||||
* @returns xml
|
||||
* @constructor
|
||||
*/
|
||||
static async People(posts, options) {
|
||||
const expirationDate = moment().add(1, 'days');
|
||||
let xml = xmlbuilder.create("result", { encoding: 'UTF-8' })
|
||||
.e("has_error", "0").up()
|
||||
.e("version", "1").up()
|
||||
.e("expire", expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
|
||||
.e("request_name", options.name).up()
|
||||
.e("people");
|
||||
for (const post of posts) {
|
||||
xml = xml.e("person")
|
||||
.e("posts")
|
||||
postObj(xml, post, options, {});
|
||||
xml = xml.up().up();
|
||||
}
|
||||
xml = xml.up();
|
||||
return xml.end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
for (const community of communities) {
|
||||
const posts = await getPostsBytitleID(community.title_id, 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_id.forEach(function (title_id: string) {
|
||||
if (title_id !== '') {
|
||||
xml = xml.e('title_id', title_id).up();
|
||||
}
|
||||
});
|
||||
|
||||
xml = xml.up()
|
||||
.e('title_id', community.title_id[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) {
|
||||
xml = xml.e('person').e('posts');
|
||||
|
||||
const options: XMLResponseGeneratorOptions = { with_mii: true,
|
||||
app_data: false,
|
||||
topic_tag: false,
|
||||
topics: true
|
||||
};
|
||||
|
||||
postObj(xml, post, options, community);
|
||||
xml = xml.up().up();
|
||||
}
|
||||
|
||||
xml = xml.up().up();
|
||||
}
|
||||
|
||||
return xml.end({ pretty: false, allowEmpty: true });
|
||||
}
|
||||
|
||||
static Following(people: HydratedSettingsDocument[]): string {
|
||||
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('request_name', 'user_infos').up()
|
||||
.e('people');
|
||||
|
||||
for (const person of people) {
|
||||
xml = xml.e('person')
|
||||
.e('pid', person.pid).up()
|
||||
.e('screen_name', person.screen_name).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
|
||||
static People(posts: HydratedPostDocument[], options: XMLResponseGeneratorOptions): string {
|
||||
const expirationDate = moment().add(1, 'days');
|
||||
|
||||
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
|
||||
.e('has_error', '0').up()
|
||||
.e('version', '1').up()
|
||||
.e('expire', expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
|
||||
.e('request_name', options.name).up()
|
||||
.e('people');
|
||||
|
||||
for (const post of posts) {
|
||||
xml = xml.e('person').e('posts');
|
||||
|
||||
postObj(xml, post, options, null);
|
||||
|
||||
xml = xml.up().up();
|
||||
}
|
||||
|
||||
return xml.up().end({ pretty: true, allowEmpty: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate xml for individual post
|
||||
* @param xml
|
||||
* @param post
|
||||
* @param options
|
||||
* @param community
|
||||
*/
|
||||
function postObj(xml, post, options, community) {
|
||||
xml = xml.e("post");
|
||||
if (post.app_data && options.app_data) {
|
||||
xml.e("app_data", post.app_data.replace(/[^A-Za-z0-9+/=]/g, "").replace(/[\n\r]+/gm, '').trim()).up();
|
||||
}
|
||||
xml.e("body", post.body ? post.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}+=,.<>/?;:'"\[\]]/g, "").replace(/[\n\r]+/gm, '') : "").up()
|
||||
.e("community_id", options.topics ? community.community_id : post.community_id).up()
|
||||
.e("country_id", post.country_id ? post.country_id : 254).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();
|
||||
if(options.with_mii) {
|
||||
xml.e("mii", post.mii.replace(/[^A-Za-z0-9+/=]/g, "").replace(/[\n\r]+/gm, '').trim()).up()
|
||||
.e("mii_face_url", post.mii_face_url).up()
|
||||
}
|
||||
xml.e("number", "0").up();
|
||||
if (post.painting) {
|
||||
xml.e("painting")
|
||||
.e("format", "tga").up()
|
||||
.e("content", post.painting.replace(/[\n\r]+/gm, '').trim()).up()
|
||||
.e("size", post.painting.length).up()
|
||||
.e("url", `https://pretendo-cdn.b-cdn.net/paintings/${post.pid}/${post.id}.png`).up()
|
||||
.up();
|
||||
}
|
||||
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();
|
||||
if (post.screenshot && post.screenshot_length) {
|
||||
xml.e("screenshot")
|
||||
.e("size", post.screenshot_length).up()
|
||||
.e("url", `https://pretendo-cdn.b-cdn.net/screenshots/${post.pid}/${post.id}.jpg`).up()
|
||||
.up();
|
||||
}
|
||||
if (post.topic_tag && options.topic_tag) {
|
||||
xml.e("topic_tag")
|
||||
.e("name", post.topic_tag).up()
|
||||
.e("title_id", post.title_id).up()
|
||||
.up();
|
||||
}
|
||||
xml.e("title_id", post.title_id).up().up()
|
||||
function postObj(xml: xmlbuilder.XMLElement, post: HydratedPostDocument, options: XMLResponseGeneratorOptions, community: HydratedCommunityDocument | null): void {
|
||||
xml = xml.e('post');
|
||||
|
||||
if (post.app_data && options.app_data) {
|
||||
xml.e('app_data', post.app_data.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim()).up();
|
||||
}
|
||||
|
||||
xml.e('body', post.body ? post.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}+=,.<>/?;:'"[\]]/g, '').replace(/[\n\r]+/gm, '') : '').up();
|
||||
|
||||
if (options.topics && community) {
|
||||
xml.e('community_id', community.community_id).up();
|
||||
} else {
|
||||
xml.e('community_id', post.community_id).up();
|
||||
}
|
||||
|
||||
xml.e('country_id', post.country_id ? post.country_id : 254).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();
|
||||
|
||||
if (options.with_mii) {
|
||||
xml.e('mii', post.mii.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim()).up()
|
||||
.e('mii_face_url', post.mii_face_url).up();
|
||||
}
|
||||
|
||||
xml.e('number', '0').up();
|
||||
|
||||
if (post.painting) {
|
||||
xml.e('painting')
|
||||
.e('format', 'tga').up()
|
||||
.e('content', post.painting.replace(/[\n\r]+/gm, '').trim()).up()
|
||||
.e('size', post.painting.length).up()
|
||||
.e('url', `https://pretendo-cdn.b-cdn.net/paintings/${post.pid}/${post.id}.png`).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (post.screenshot && post.screenshot_length) {
|
||||
xml.e('screenshot')
|
||||
.e('size', post.screenshot_length).up()
|
||||
.e('url', `https://pretendo-cdn.b-cdn.net/screenshots/${post.pid}/${post.id}.jpg`).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
if (post.topic_tag && options.topic_tag) {
|
||||
xml.e('topic_tag')
|
||||
.e('name', post.topic_tag).up()
|
||||
.e('title_id', post.title_id).up()
|
||||
.up();
|
||||
}
|
||||
|
||||
xml.e('title_id', post.title_id).up().up();
|
||||
}
|
||||
|
||||
export default XmlResponseGenerator;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user