Full conversion to TypeScript

This commit is contained in:
Jonathan Barrow 2023-04-23 19:32:42 -04:00
parent c13383812e
commit 106b581fe3
No known key found for this signature in database
GPG Key ID: E86E9FE9049C741F
31 changed files with 2023 additions and 1849 deletions

View File

@ -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
View File

@ -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"
}
}
}
}

View File

@ -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"

View File

@ -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);

View File

@ -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
});
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,6 @@
export interface CreateNewCommunityBody {
name: string;
description: string;
icon: string;
app_data: string;
}

View File

@ -0,0 +1,4 @@
export interface CryptoOptions {
private_key: Buffer
hmac_secret: string
}

View 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;
};
}

View File

@ -0,0 +1,7 @@
export interface SendMessageBody {
message_to_pid: number;
body: string;
painting?: string;
screenshot?: string;
app_data?: string;
}

View 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
View File

@ -0,0 +1,3 @@
declare module 'express-subdomain';
// TODO - Add proper types

View File

@ -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
}
}
}

View 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
};
}

View File

@ -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
View File

@ -0,0 +1,3 @@
declare module 'node-snowflake';
// TODO - Add proper types

View File

@ -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;
}

View File

@ -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;