Barebones move to TypeScript

This commit is contained in:
Jonathan Barrow
2023-04-22 10:17:33 -04:00
parent defdd35f4b
commit f70cd34406
56 changed files with 2834 additions and 7601 deletions

7088
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "./dist/server.js",
"scripts": {
"lint": "npx eslint .",
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias",
"build": "npm run clean && npx tsc && npx tsc-alias",
"clean": "rm -rf ./dist",
"start": "node .",
"start:dev": "NODE_ENV=development node ."
@@ -22,7 +22,6 @@
"express-session": "^1.17.0",
"express-subdomain": "^1.0.5",
"fs-extra": "^9.0.0",
"grpc": "github:pretendonetwork/grpc-js",
"memoizee": "^0.4.15",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
@@ -35,12 +34,15 @@
"node-snowflake": "0.0.1",
"pako": "^1.0.11",
"pngjs": "^5.0.0",
"pretendo-grpc-ts": "github:PretendoNetwork/grpc-ts",
"sanitize": "^2.1.0",
"tga": "^1.0.3",
"xmlbuilder": "^15.1.1",
"xmlbuilder2": "0.0.4"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/morgan": "^1.9.4",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"eslint": "^8.38.0",

View File

@@ -1,44 +0,0 @@
const mongoose = require('mongoose');
const { account_db: mongooseConfig } = require('../config.json');
const { uri, database, options } = mongooseConfig;
const logger = require('./logger');
let pnidConnection;
function connect() {
if(!pnidConnection)
pnidConnection = makeNewConnection(`${uri}/${database}`, options);
}
function verifyConnected() {
if (!pnidConnection) {
throw new Error('Cannot make database requests without being connected');
}
}
function makeNewConnection(uri) {
pnidConnection = mongoose.createConnection(uri, options);
pnidConnection.on('error', function (error) {
logger.error(`MongoDB connection ${this.name} ${JSON.stringify(error)}`);
pnidConnection.close().catch(() => logger.error(`MongoDB failed to close connection ${this.name}`));
});
pnidConnection.on('connected', function () {
logger.info(`MongoDB connected ${this.name} / ${uri}`);
});
pnidConnection.on('disconnected', function () {
logger.info(`MongoDB disconnected ${this.name}`);
});
return pnidConnection;
}
pnidConnection = makeNewConnection(`${uri}/${database}`, options);
module.exports = {
pnidConnection,
connect,
verifyConnected
};

39
src/accountdb.ts Normal file
View File

@@ -0,0 +1,39 @@
import mongoose from 'mongoose';
import { LOG_INFO, LOG_ERROR } from '@/logger';
import { account_db as mongooseConfig } from '../config.json';
const { uri, database, options } = mongooseConfig;
export let pnidConnection: mongoose.Connection;
export function connect() {
if(!pnidConnection)
pnidConnection = makeNewConnection(`${uri}/${database}`);
}
export function verifyConnected() {
if (!pnidConnection) {
throw new Error('Cannot make database requests without being connected');
}
}
export function makeNewConnection(uri) {
pnidConnection = mongoose.createConnection(uri, options as mongoose.ConnectOptions);
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('connected', function () {
LOG_INFO(`MongoDB connected ${this.name} / ${uri}`);
});
pnidConnection.on('disconnected', function () {
LOG_INFO(`MongoDB disconnected ${this.name}`);
});
return pnidConnection;
}
pnidConnection = makeNewConnection(`${uri}/${database}`);

View File

@@ -1,547 +0,0 @@
const mongoose = require('mongoose');
const { mongoose: mongooseConfig } = require('../config.json');
const { COMMUNITY } = require('./models/communities');
const { CONTENT } = require('./models/content');
const { CONVERSATION } = require('./models/conversation');
const { ENDPOINT } = require('./models/endpoint');
const { NOTIFICATIONS } = require('./models/notifications');
const { PNID } = require('./models/pnid');
const { POST } = require('./models/post');
const { SETTINGS } = require('./models/settings');
const { uri, database, options } = mongooseConfig;
const logger = require('./logger');
const accountDB = require('./accountdb');
let connection;
async function connect() {
await mongoose.connect(`${uri}/${database}`, options);
connection = mongoose.connection;
connection.on('connected', function () {
logger.info(`MongoDB connected ${this.name}`);
});
connection.on('error', console.error.bind(console, 'connection error:'));
connection.on('close', () => {
connection.removeAllListeners();
});
}
function verifyConnected() {
if (!connection) {
connect();
}
}
async function getCommunities(numberOfCommunities) {
verifyConnected();
if(numberOfCommunities === -1)
return COMMUNITY.find({ parent: null, type: 0 });
else
return COMMUNITY.find({ parent: null, type: 0 }).limit(numberOfCommunities);
}
async function getMostPopularCommunities(numberOfCommunities) {
verifyConnected();
return COMMUNITY.find({ parent: null, type: 0 }).sort({followers: -1}).limit(numberOfCommunities);
}
async function getNewCommunities(numberOfCommunities) {
verifyConnected();
return COMMUNITY.find({ parent: null, type: 0 }).sort([['created_at', -1]]).limit(numberOfCommunities);
}
async function getSubCommunities(communityID) {
verifyConnected();
return COMMUNITY.find({
parent: communityID
});
}
async function getCommunityByTitleID(title_id) {
verifyConnected();
return COMMUNITY.findOne({
title_id: title_id
});
}
async function getCommunityByTitleIDs(title_ids) {
verifyConnected();
return COMMUNITY.findOne({
title_ids: {$in: title_ids}
});
}
async function getCommunityByID(community_id) {
verifyConnected();
return COMMUNITY.findOne({
community_id: community_id
});
}
async function getTotalPostsByCommunity(community) {
verifyConnected();
return POST.find({
title_id: community.title_id,
parent: null,
removed: false
}).countDocuments();
}
async function getPostByID(postID) {
verifyConnected();
return POST.findOne({
id: postID
});
}
async function getPostsByUserID(userID) {
verifyConnected();
return POST.find({
pid: userID,
parent: null,
removed: false,
app_data: { $ne: null }
});
}
async function getPostReplies(postID, number) {
verifyConnected();
return POST.find({
parent: postID,
removed: false,
app_data: { $ne: null }
}).limit(number);
}
async function getDuplicatePosts(pid, post) {
verifyConnected();
return POST.findOne({
pid: pid,
body: post.body,
painting: post.painting,
screenshot: post.screenshot,
parent: null,
removed: false
});
}
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);
}
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);
}
async function getTotalPostsByUserID(userID) {
verifyConnected();
return POST.find({
pid: userID,
parent: null,
message_to_pid: null,
removed: false
}).countDocuments();
}
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);
}
async function getNumberNewCommunityPostsByID(community, number) {
verifyConnected();
return POST.find({
title_id: community.title_id,
parent: null,
removed: false
}).sort({ created_at: -1}).limit(number);
}
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);
}
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);
}
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);
}
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);
}
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);
}
async function getAllUserPosts(pid) {
verifyConnected();
return POST.find({
pid: pid,
message_to_pid: null,
app_data: { $ne: null }
});
}
async function getRemovedUserPosts(pid) {
verifyConnected();
return POST.find({
pid: pid,
message_to_pid: null,
removed: true
});
}
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);
}
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});
}
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);
}
async function getEndpoints() {
verifyConnected();
return ENDPOINT.find({});
}
async function getEndPoint(accessLevel) {
verifyConnected();
return ENDPOINT.findOne({
server_access_level: accessLevel
})
}
async function getUsersSettings(numberOfUsers) {
verifyConnected();
if(numberOfUsers === -1)
return SETTINGS.find({});
else
return SETTINGS.find({}).limit(numberOfUsers);
}
async function getUsersContent(numberOfUsers) {
verifyConnected();
if(numberOfUsers === -1)
return SETTINGS.find({});
else
return SETTINGS.find({}).limit(numberOfUsers);
}
async function getUserSettings(pid) {
verifyConnected();
return SETTINGS.findOne({pid: pid});
}
async function getUserContent(pid) {
verifyConnected();
return CONTENT.findOne({pid: pid});
}
async function getFollowingUsers(content) {
verifyConnected();
return SETTINGS.find({
pid: content.following_users
});
}
async function getFollowedUsers(content) {
verifyConnected();
return SETTINGS.find({
pid: content.followed_users
});
}
async function getUserByUsername(user_id) {
verifyConnected();
return PNID.findOne({
"username": new RegExp(`^${user_id}$`, 'i')
});
}
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});
}
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});
}
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});
}
async function getConversations(pid) {
verifyConnected();
return CONVERSATION.find({
"users.pid": pid
}).sort({ last_updated: -1});
}
async function getUnreadConversationCount(pid) {
verifyConnected();
return CONVERSATION.find({
"users": { $elemMatch: {
'pid': pid,
'read': false
}}
}).countDocuments();
}
async function getConversationByID(community_id) {
verifyConnected();
return CONVERSATION.findOne({
type: 3,
id: community_id
});
}
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);
}
async function getConversationByUsers(pids) {
verifyConnected();
return CONVERSATION.findOne({
$and: [
{'users.pid': pids[0]},
{'users.pid': pids[1]}
]
});
}
async function getLatestMessage(pid, pid2) {
verifyConnected();
return POST.findOne({
$or: [
{pid: pid, message_to_pid: pid2},
{pid: pid2, message_to_pid: pid}
],
removed: false
})
}
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);
}
async function getPNIDS() {
accountDB.verifyConnected();
return PNID.find({});
}
async function getPNID(pid) {
accountDB.verifyConnected();
return PNID.findOne({
pid: pid
});
}
async function getNotifications(pid, limit, offset) {
verifyConnected();
return NOTIFICATIONS.find({
pid: pid,
}).sort({created_at: 1}).skip(offset).limit(limit);
}
async function getNotification(pid, type, reference_id) {
verifyConnected();
return NOTIFICATIONS.findOne({
pid: pid,
type: type,
reference_id: reference_id
})
}
async function getLastNotification(pid) {
verifyConnected();
return NOTIFICATIONS.findOne({
pid: pid
}).sort({created_at: -1}).limit(1);
}
async function getUnreadNotificationCount(pid) {
verifyConnected();
return NOTIFICATIONS.find({
pid: pid,
read: false
}).countDocuments();
}
module.exports = {
connect,
getCommunities,
getMostPopularCommunities,
getNewCommunities,
getSubCommunities,
getCommunityByTitleID,
getCommunityByTitleIDs,
getCommunityByID,
getTotalPostsByCommunity,
getPostsByCommunity,
getHotPostsByCommunity,
getNumberNewCommunityPostsByID,
getNumberPopularCommunityPostsByID,
getNumberVerifiedCommunityPostsByID,
getNewPostsByCommunity,
getPostsByCommunityKey,
getPostsByUserID,
getPostReplies,
getUserPostRepliesAfterTimestamp,
getNumberUserPostsByID,
getTotalPostsByUserID,
getPostByID,
getDuplicatePosts,
getEndpoints,
getEndPoint,
getUserByUsername,
getUserPostsAfterTimestamp,
getUserPostsOffset,
getCommunityPostsAfterTimestamp,
getNewsFeed,
getNewsFeedAfterTimestamp,
getNewsFeedOffset,
getFollowingUsers,
getFollowedUsers,
getConversations,
getConversationByID,
getConversationByUsers,
getConversationMessages,
getUnreadConversationCount,
getLatestMessage,
getPNID,
getPNIDS,
getUsersSettings,
getUsersContent,
getUserSettings,
getUserContent,
getNotifications,
getUnreadNotificationCount,
getNotification,
getLastNotification,
getAllUserPosts,
getRemovedUserPosts,
getFriendMessages
};

493
src/database.ts Normal file
View File

@@ -0,0 +1,493 @@
import mongoose from 'mongoose';
import { verifyConnected as accountDBVerifyConnected } from '@/accountdb';
import { LOG_INFO } from '@/logger';
import { Community } from '@/models/community';
import { Content } from '@/models/content';
import { Conversation } from '@/models/conversation';
import { Endpoint } from '@/models/endpoint';
import { Notification } from '@/models/notification';
import { PNID } from '@/models/pnid';
import { Post } from '@/models/post';
import { Settings } from '@/models/settings';
import { mongoose as mongooseConfig } from '../config.json';
const { uri, database, options } = mongooseConfig;
let connection;
export async function connect() {
await mongoose.connect(`${uri}/${database}`, options as mongoose.ConnectOptions || {});
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();
});
}
function verifyConnected() {
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(numberOfCommunities) {
verifyConnected();
return Community.find({ parent: null, type: 0 }).sort({ followers: -1 }).limit(numberOfCommunities);
}
export async function getNewCommunities(numberOfCommunities) {
verifyConnected();
return Community.find({ parent: null, type: 0 }).sort([['created_at', -1]]).limit(numberOfCommunities);
}
export async function getSubCommunities(communityID) {
verifyConnected();
return Community.find({
parent: communityID
});
}
export async function getCommunityByTitleID(title_id) {
verifyConnected();
return Community.findOne({
title_id: title_id
});
}
export async function getCommunityByTitleIDs(title_ids) {
verifyConnected();
return Community.findOne({
title_ids: { $in: title_ids }
});
}
export async function getCommunityByID(community_id) {
verifyConnected();
return Community.findOne({
community_id: community_id
});
}
export async function getTotalPostsByCommunity(community) {
verifyConnected();
return Post.find({
title_id: community.title_id,
parent: null,
removed: false
}).countDocuments();
}
export async function getPostByID(postID) {
verifyConnected();
return Post.findOne({
id: postID
});
}
export async function getPostsByUserID(userID) {
verifyConnected();
return Post.find({
pid: userID,
parent: null,
removed: false,
app_data: { $ne: null }
});
}
export async function getPostReplies(postID, number) {
verifyConnected();
return Post.find({
parent: postID,
removed: false,
app_data: { $ne: null }
}).limit(number);
}
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 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 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 getTotalPostsByUserID(userID) {
verifyConnected();
return Post.find({
pid: userID,
parent: null,
message_to_pid: null,
removed: false
}).countDocuments();
}
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 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 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 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();
}

View File

@@ -1,5 +1,7 @@
const fs = require('fs-extra');
require('colors');
import fs from 'fs-extra';
import colors from 'colors';
colors.enable();
const root = __dirname;
fs.ensureDirSync(`${root}/logs`);
@@ -12,7 +14,7 @@ const streams = {
info: fs.createWriteStream(`${root}/logs/info.log`)
};
function success(input) {
export function LOG_SUCCESS(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
@@ -20,7 +22,7 @@ function success(input) {
console.log(`${input}`.green.bold);
}
function error(input) {
export function LOG_ERROR(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
@@ -28,7 +30,7 @@ function error(input) {
console.log(`${input}`.red.bold);
}
function warn(input) {
export function LOG_WARN(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
@@ -36,17 +38,10 @@ function warn(input) {
console.log(`${input}`.yellow.bold);
}
function info(input) {
export function LOG_INFO(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
module.exports = {
success,
error,
warn,
info
};
}

View File

@@ -1,7 +1,6 @@
const config = require('../../config.json');
const util = require('../util/util');
const xml = require("object-to-xml");
const db = require("../database");
import xml from 'object-to-xml';
import { getPNID, getEndPoint } from '@/database';
import { decodeParamPack, processServiceToken } from '@/util';
async function auth(req, res, next) {
if(/*req.path.includes('/topics') || */req.path.includes('/v1/status'))
@@ -10,7 +9,7 @@ async function auth(req, res, next) {
let paramPackData = req.headers["x-nintendo-parampack"];
if(paramPackData)
paramPackData = paramPackData = util.decodeParamPack(paramPackData);
paramPackData = paramPackData = decodeParamPack(paramPackData);
else if(req.path.includes('/users/'))
return next();
@@ -20,16 +19,16 @@ async function auth(req, res, next) {
if(!token || !paramPackData)
badAuth(res);
else {
const pid = util.processServiceToken(token);
const pid = processServiceToken(token);
if(pid === null)
badAuth(res);
else {
let user = await db.getPNID(pid), discovery;
let user = await getPNID(pid), discovery;
if(user)
discovery = await db.getEndPoint(user.server_access_level);
discovery = await getEndPoint(user.server_access_level);
else
discovery = await db.getEndPoint('prod');
discovery = await getEndPoint('prod');
if(discovery.status !== 0) return serverError(res, discovery);
@@ -122,4 +121,4 @@ function serverError(res, discovery) {
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
}
module.exports = auth;
export default auth;

View File

@@ -1,38 +0,0 @@
const xmlbuilder = require('xmlbuilder');
const VALID_CLIENT_ID_SECRET_PAIRS = {
// 'Key' is the client ID, 'Value' is the client secret
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive?
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive?
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive?
};
function nintendoClientHeaderCheck(request, response, next) {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const {headers} = request;
if (
!headers['x-nintendo-client-id'] ||
!headers['x-nintendo-client-secret'] ||
!VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
) {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
}).end());
}
return next();
}
module.exports = nintendoClientHeaderCheck;

View File

@@ -0,0 +1,43 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { getValueFromHeaders } from '@/util';
const VALID_CLIENT_ID_SECRET_PAIRS: { [key: string]: string } = {
// * 'Key' is the client ID, 'Value' is the client secret
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // * Possibly WiiU exclusive?
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // * Possibly 3DS exclusive?
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // * Possibly 3DS exclusive?
};
function nintendoClientHeaderCheck(request: express.Request, response: express.Response, next: express.NextFunction): void {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime().toString());
const clientId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-id');
const clientSecret: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-client-secret');
if (
!clientId ||
!clientSecret ||
!VALID_CLIENT_ID_SECRET_PAIRS[clientId] ||
clientSecret !== VALID_CLIENT_ID_SECRET_PAIRS[clientId]
) {
response.send(xmlbuilder.create({
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
}).end());
return;
}
return next();
}
export default nintendoClientHeaderCheck;

View File

@@ -1,50 +0,0 @@
const xmlbuilder = require('xmlbuilder');
const database = require('../database');
async function PNIDMiddleware(request, response, next) {
const { headers } = request;
if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) {
return next();
}
const [type, token] = headers.authorization.split(' ');
let user;
if (type === 'Basic') {
user = await database.getUserBasic(token);
} else {
user = await database.getUserBearer(token);
}
if (!user) {
response.status(401);
if (type === 'Bearer') {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'access_token',
code: '0005',
message: 'Invalid access token'
}
}
}).end());
}
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1105',
message: 'Email address, username, or password, is not valid'
}
}
}).end());
}
request.pnid = user;
return next();
}
module.exports = PNIDMiddleware;

View File

@@ -8,7 +8,7 @@ const sessionStore = {};
function sessionMiddlware(request, response, next) {
const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!sessionStore[ip]) {
sessionStore[ip] = {};
}
@@ -20,4 +20,4 @@ function sessionMiddlware(request, response, next) {
return next();
}
module.exports = sessionMiddlware;
export default sessionMiddlware;

View File

@@ -1,35 +0,0 @@
const { document: xmlParser } = require('xmlbuilder2');
function XMLMiddleware(request, response, next) {
if (request.method == 'POST' || request.method == 'PUT') {
const headers = request.headers;
let body = '';
if (
!headers['content-type'] ||
!headers['content-type'].toLowerCase().includes('xml')
) {
return next();
}
request.setEncoding('utf-8');
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
try {
request.body = xmlParser(body);
request.body = request.body.toObject();
} catch (error) {
return next();
}
next();
});
} else {
next();
}
}
module.exports = XMLMiddleware;

View File

@@ -0,0 +1,57 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import { document as xmlParser } from 'xmlbuilder2';
import { getValueFromHeaders, mapToObject } from '@/util';
function XMLMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): void {
if (request.method == 'POST' || request.method == 'PUT') {
const contentType: string | undefined = getValueFromHeaders(request.headers, 'content-type');
const contentLength: string | undefined = getValueFromHeaders(request.headers, 'content-length');
let body: string = '';
if (
!contentType ||
!contentType.toLowerCase().includes('xml')
) {
return next();
}
if (
!contentLength ||
parseInt(contentLength) === 0
) {
return next();
}
request.setEncoding('utf-8');
request.on('data', (chunk: string) => {
body += chunk;
});
request.on('end', () => {
try {
request.body = xmlParser(body);
request.body = request.body.toObject();
request.body = mapToObject(request.body);
} catch (error) {
response.status(401);
// TODO: This is not a real error code, check to see if better one exists
return response.send(xmlbuilder.create({
errors: {
error: {
code: '0004',
message: 'XML parse error'
}
}
}).end());
}
next();
});
} else {
next();
}
}
export default XMLMiddleware;

View File

@@ -1,6 +1,7 @@
const { Schema, model } = require('mongoose');
import { Schema, model } from 'mongoose';
import { ICommunity, ICommunityMethods, CommunityModel } from '@/types/mongoose/community';
const CommunitySchema = new Schema({
const CommunitySchema = new Schema<ICommunity, CommunityModel, ICommunityMethods>({
platform_id: Number,
name: String,
description: String,
@@ -19,8 +20,8 @@ const CommunitySchema = new Schema({
* 3: Private Community
*/
type: {
type: Number,
default: 0
type: Number,
default: 0
},
parent: {
type: String,
@@ -31,7 +32,7 @@ const CommunitySchema = new Schema({
default: undefined
},
created_at: {
type: Date,
type: Date,
default: new Date(),
},
empathy_count: {
@@ -64,37 +65,32 @@ const CommunitySchema = new Schema({
app_data: String,
});
CommunitySchema.methods.upEmpathy = async function() {
const empathy = this.get('empathy_count');
CommunitySchema.method('upEmpathy', async function upEmpathy(): Promise<void> {
const empathy = this.get('empathy_count');
this.set('empathy_count', empathy + 1);
await this.save();
};
});
CommunitySchema.methods.downEmpathy = async function() {
const empathy = this.get('empathy_count');
CommunitySchema.method('downEmpathy', async function downEmpathy(): Promise<void> {
const empathy = this.get('empathy_count');
this.set('empathy_count', empathy - 1);
await this.save();
};
});
CommunitySchema.methods.upFollower = async function() {
const followers = this.get('followers');
CommunitySchema.method('upFollower', async function upFollower(): Promise<void> {
const followers = this.get('followers');
this.set('followers', followers + 1);
await this.save();
};
});
CommunitySchema.methods.downFollower = async function() {
const followers = this.get('followers');
CommunitySchema.method('downFollower', async function downFollower(): Promise<void> {
const followers = this.get('followers');
this.set('followers', followers - 1);
await this.save();
};
});
const COMMUNITY = model('COMMUNITY', CommunitySchema);
module.exports = {
CommunitySchema,
COMMUNITY
};
export const Community: CommunityModel = model<ICommunity, CommunityModel>('Community', CommunitySchema);

View File

@@ -1,6 +1,7 @@
const { Schema, model } = require('mongoose');
import { Schema, model } from 'mongoose';
import { IContent, IContentMethods, ContentModel } from '@/types/mongoose/content';
const ContentSchema = new Schema({
const ContentSchema = new Schema<IContent, ContentModel, IContentMethods>({
pid: Number,
followed_communities: {
type: [String],
@@ -13,48 +14,43 @@ const ContentSchema = new Schema({
following_users: {
type: [Number],
default: [0]
},
}
});
ContentSchema.methods.addToCommunities = async function(postID) {
ContentSchema.method('addToCommunities', async function addToCommunities(postID) {
const communities = this.get('followed_communities');
communities.addToSet(postID);
await this.save();
}
});
ContentSchema.methods.removeFromCommunities = async function(postID) {
ContentSchema.method('removeFromCommunities', async function removeFromCommunities(postID) {
const communities = this.get('followed_communities');
communities.pull(postID);
await this.save();
}
});
ContentSchema.methods.addToUsers = async function(postID) {
ContentSchema.method('addToUsers', async function addToUsers(postID) {
const users = this.get('followed_users');
users.addToSet(postID);
await this.save();
}
});
ContentSchema.methods.removeFromUsers = async function(postID) {
ContentSchema.method('removeFromUsers', async function removeFromUsers(postID) {
const users = this.get('followed_users');
users.pull(postID);
await this.save();
}
});
ContentSchema.methods.addToFollowers = async function(postID) {
ContentSchema.method('addToFollowers', async function addToFollowers(postID) {
const users = this.get('following_users');
users.addToSet(postID);
await this.save();
}
});
ContentSchema.methods.removeFromFollowers = async function(postID) {
ContentSchema.method('removeFromFollowers', async function removeFromFollowers(postID) {
const users = this.get('following_users');
users.pull(postID);
await this.save();
}
});
const CONTENT = model('CONTENT', ContentSchema);
module.exports = {
ContentSchema,
CONTENT
};
export const Content: ContentModel = model<IContent, ContentModel>('Content', ContentSchema);

View File

@@ -1,67 +0,0 @@
const { Schema, model } = require('mongoose');
const moment = require("moment");
const snowflake = require('node-snowflake').Snowflake;
const user = new Schema({
pid: Number,
official: {
type: Boolean,
default: false
},
read: {
type: Boolean,
default: true
}
});
const ConversationSchema = new Schema({
id: {
type: String,
default: snowflake.nextId()
},
created_at: {
type: Date,
default: new Date(),
},
last_updated: {
type: Date,
default: new Date(),
},
message_preview: {
type: String,
default: ""
},
users: [user]
});
ConversationSchema.methods.newMessage = async function(message, fromPid) {
if(this.users[0].pid === fromPid) {
this.users[1].read = false;
this.markModified('users[1].read');
}
else {
this.users[0].read = false;
this.markModified('users[0].read');
}
this.set('last_updated', moment(new Date()));
this.set('message_preview', message);
await this.save();
}
ConversationSchema.methods.markAsRead = async function(pid) {
let users = this.get('users');
if(users[0].pid === pid)
users[0].read = true;
else if(users[1].pid === pid)
users[1].read = true;
this.set('users', users)
this.markModified('users');
await this.save();
}
const CONVERSATION = model('CONVERSATION', ConversationSchema);
module.exports = {
ConversationSchema: ConversationSchema,
CONVERSATION: CONVERSATION
};

View File

@@ -0,0 +1,61 @@
import { Schema, model } from 'mongoose';
import moment from 'moment';
import { Snowflake } from 'node-snowflake';
import { IConversation, IConversationMethods, ConversationModel } from '@/types/mongoose/conversation';
const ConversationSchema = new Schema<IConversation, ConversationModel, IConversationMethods>({
id: {
type: String,
default: Snowflake.nextId()
},
created_at: {
type: Date,
default: new Date(),
},
last_updated: {
type: Date,
default: new Date(),
},
message_preview: {
type: String,
default: ''
},
users: [{
pid: Number,
official: {
type: Boolean,
default: false
},
read: {
type: Boolean,
default: true
}
}]
});
ConversationSchema.method('newMessage', async function newMessage(message, senderPID) {
if(this.users[0].pid === senderPID) {
this.users[1].read = false;
this.markModified('users[1].read');
}
else {
this.users[0].read = false;
this.markModified('users[0].read');
}
this.set('last_updated', moment(new Date()));
this.set('message_preview', message);
await this.save();
});
ConversationSchema.method('markAsRead', async function markAsRead(pid) {
let users = this.get('users');
if(users[0].pid === pid)
users[0].read = true;
else if(users[1].pid === pid)
users[1].read = true;
this.set('users', users)
this.markModified('users');
await this.save();
});
export const Conversation: ConversationModel = model<IConversation, ConversationModel>('Conversation', ConversationSchema);

View File

@@ -1,19 +0,0 @@
const { Schema, model } = require('mongoose');
const endpointSchema = new Schema({
status: Number,
server_access_level: String,
topics: Boolean,
guest_access: Boolean,
host: String,
api_host: String,
portal_host: String,
n3ds_host: String
});
const ENDPOINT = model('ENDPOINT', endpointSchema);
module.exports = {
endpointSchema,
ENDPOINT
};

15
src/models/endpoint.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Schema, model } from 'mongoose';
import { IEndpoint, IEndpointMethods, EndpointModel } from '@/types/mongoose/endpoint';
const endpointSchema = new Schema<IEndpoint, EndpointModel, IEndpointMethods>({
status: Number,
server_access_level: String,
topics: Boolean,
guest_access: Boolean,
host: String,
api_host: String,
portal_host: String,
n3ds_host: String
});
export const Endpoint: EndpointModel = model<IEndpoint, EndpointModel>('Endpoint', endpointSchema);

View File

@@ -0,0 +1,22 @@
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
});
NotificationSchema.method('markRead', async function markRead() {
this.set('read', true);
await this.save();
});
export const Notification: NotificationModel = model<INotification, NotificationModel>('Notification', NotificationSchema);

View File

@@ -1,26 +0,0 @@
const { Schema, model } = require('mongoose');
const NotificationSchema = new Schema({
pid: String,
type: String,
link: String,
objectID: String,
users: [{
user: String,
timestamp: Date
}],
read: Boolean,
lastUpdated: Date
});
NotificationSchema.methods.markRead = async function() {
this.set('read', true);
await this.save();
};
const NOTIFICATION = model('NOTIFICATION', NotificationSchema);
module.exports = {
NotificationSchema,
NOTIFICATION
};

View File

@@ -1,7 +1,8 @@
const mongoose = require('mongoose');
const {pnidConnection} = require('../accountdb');
import mongoose from 'mongoose';
import { pnidConnection } from '@/accountdb';
import { IPNID, IPNIDMethods, PNIDModel } from '@/types/mongoose/pnid';
const PNIDSchema = new mongoose.Schema({
const PNIDSchema = new mongoose.Schema<IPNID, PNIDModel, IPNIDMethods>({
access_level: {
type: Number,
default: 0 // -1: banned, 0: standard, 1: tester, 2: mod, 3: dev
@@ -31,12 +32,7 @@ const PNIDSchema = new mongoose.Schema({
latest_webhook_timestamp: Number
}
}
});
const PNID = pnidConnection.model('PNID', PNIDSchema);
module.exports = {
PNID,
};
export const PNID: PNIDModel = pnidConnection.model<IPNID, PNIDModel>('PNID', PNIDSchema);

View File

@@ -1,6 +1,7 @@
const { Schema, model } = require('mongoose');
import { Schema, model } from 'mongoose';
import { IPost, IPostMethods, PostModel } from '@/types/mongoose/post';
const PostSchema = new Schema({
const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
id: String,
title_id: String,
screen_name: String,
@@ -75,10 +76,11 @@ const PostSchema = new Schema({
default: false
},
removed_reason: String,
yeahs: [Number]
yeahs: [Number],
number: Number
});
PostSchema.methods.upReply = async function() {
PostSchema.method('upReply', async function upReply() {
const replyCount = this.get('reply_count');
if(replyCount + 1 < 0)
this.set('reply_count', 0);
@@ -86,9 +88,9 @@ PostSchema.methods.upReply = async function() {
this.set('reply_count', replyCount + 1);
await this.save();
};
});
PostSchema.methods.downReply = async function() {
PostSchema.method('downReply', async function downReply() {
const replyCount = this.get('reply_count');
if(replyCount - 1 < 0)
this.set('reply_count', 0);
@@ -96,23 +98,18 @@ PostSchema.methods.downReply = async function() {
this.set('reply_count', replyCount - 1);
await this.save();
};
});
PostSchema.methods.remove = async function(reason) {
PostSchema.method('remove', async function remove(reason) {
this.set('remove', true);
this.set('removed_reason', reason)
await this.save();
};
});
PostSchema.methods.unRemove = async function(reason) {
PostSchema.method('unRemove', async function unRemove(reason) {
this.set('remove', false);
this.set('removed_reason', reason)
await this.save();
};
});
const POST = model('POST', PostSchema);
module.exports = {
PostSchema,
POST
};
export const Post: PostModel = model<IPost, PostModel>('Post', PostSchema);

View File

@@ -1,18 +0,0 @@
const { Schema, model } = require('mongoose');
const ReportSchema = new Schema({
pid: String,
post_id: String,
reason: Number,
created_at: {
type: Date,
default: new Date()
}
});
const REPORT = model('REPORT', ReportSchema);
module.exports = {
ReportSchema,
REPORT
};

14
src/models/report.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Schema, model } from 'mongoose';
import { IReport, IReportMethods, ReportModel } from '@/types/mongoose/report';
const ReportSchema = new Schema<IReport, ReportModel, IReportMethods>({
pid: String,
post_id: String,
reason: Number,
created_at: {
type: Date,
default: new Date()
}
});
export const Report: ReportModel = model<IReport, ReportModel>('Report', ReportSchema);

View File

@@ -1,6 +1,7 @@
const { Schema, model } = require('mongoose');
import { Schema, model } from 'mongoose';
import { ISettings, ISettingsMethods, SettingsModel } from '@/types/mongoose/settings';
const SettingsSchema = new Schema({
const SettingsSchema = new Schema<ISettings, SettingsModel, ISettingsMethods>({
pid: Number,
screen_name: String,
account_status: {
@@ -47,49 +48,44 @@ const SettingsSchema = new Schema({
}
});
SettingsSchema.methods.updateComment = async function(comment) {
SettingsSchema.method('updateComment', async function updateComment(comment) {
this.set('profile_comment', comment);
await this.save();
};
});
SettingsSchema.methods.updateSkill = async function(skill) {
SettingsSchema.method('updateSkill', async function updateSkill(skill) {
this.set('game_skill', skill);
await this.save();
};
});
SettingsSchema.methods.commentVisible = async function(active) {
SettingsSchema.method('commentVisible', async function commentVisible(active) {
this.set('profile_comment_visibility', active);
await this.save();
};
});
SettingsSchema.methods.skillVisible = async function(active) {
SettingsSchema.method('skillVisible', async function skillVisible(active) {
this.set('game_skill_visibility', active);
await this.save();
};
});
SettingsSchema.methods.birthdayVisible = async function(active) {
SettingsSchema.method('birthdayVisible', async function birthdayVisible(active) {
this.set('birthday_visibility', active);
await this.save();
};
});
SettingsSchema.methods.relationshipVisible = async function(active) {
SettingsSchema.method('relationshipVisible', async function relationshipVisible(active) {
this.set('relationship_visibility', active);
await this.save();
};
});
SettingsSchema.methods.countryVisible = async function(active) {
SettingsSchema.method('countryVisible', async function countryVisible(active) {
this.set('country_visibility', active);
await this.save();
};
});
SettingsSchema.methods.favCommunityVisible = async function(active) {
SettingsSchema.method('favCommunityVisible', async function favCommunityVisible(active) {
this.set('profile_favorite_community_visibility', active);
await this.save();
};
});
const SETTINGS = model('SETTINGS', SettingsSchema);
module.exports = {
SettingsSchema,
SETTINGS
};
export const Settings: SettingsModel = model<ISettings, SettingsModel>('Settings', SettingsSchema);

View File

@@ -1,76 +0,0 @@
process.title = 'Pretendo - Miiverse';
const express = require('express');
const morgan = require('morgan');
const xmlparser = require('./middleware/xml-parser');
const database = require('./database');
const logger = require('./logger');
const config = require('../config.json');
const auth = require('./middleware/auth');
const { http: { port } } = config;
const app = express();
const miiverse = require('./services/miiverse-api');
const xml = require("object-to-xml");
app.set('etag', false);
app.disable('x-powered-by');
// Create router
logger.info('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true,
limit: '5mb',
parameterLimit: 100000
}));
app.use(xmlparser);
app.use(auth);
// import the servers into one
app.use(miiverse);
// 404 handler
logger.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;
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));
});
// non-404 error handler
logger.info('Creating non-404 status handler');
app.use((error, req, res) => {
const status = error.status || 500;
res.set("Content-Type", "application/xml");
res.statusCode = 404;
let response = {
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
logger.info('Starting server');
database.connect().then(() => {
app.listen(port, () => {
logger.success(`Server started on port ${port}`);
});
});

76
src/server.ts Normal file
View File

@@ -0,0 +1,76 @@
process.title = 'Pretendo - Miiverse';
import express from 'express';
import morgan from 'morgan';
import xml from 'object-to-xml';
import { connect as connectDatabase } from '@/database';
import { LOG_INFO, LOG_SUCCESS } from '@/logger';
import config from '../config.json';
import xmlparser from '@/middleware/xml-parser';
import auth from '@/middleware/auth';
import miiverse from '@/services/miiverse-api';
const { http: { port } } = config;
const app = express();
app.set('etag', false);
app.disable('x-powered-by');
// Create router
LOG_INFO('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true,
limit: '5mb',
parameterLimit: 100000
}));
app.use(xmlparser);
app.use(auth);
// import the servers into one
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 = {
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 = {
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
LOG_INFO('Starting server');
connectDatabase().then(() => {
app.listen(port, () => {
LOG_SUCCESS(`Server started on port ${port}`);
});
});

View File

@@ -1,39 +0,0 @@
const express = require('express');
const subdomain = require('express-subdomain');
const sessionMiddleware = require('../../middleware/session');
const pnidMiddleware = require('../../middleware/pnid');
const logger = require('../../logger');
const routes = require('./routes');
// Main router for endpointsindex.js
const router = express.Router();
// Router to handle the subdomain restriction
const discovery = express.Router();
const api = express.Router();
// Create subdomains
logger.info('[MIIVERSE] Creating \'discovery\' subdomain');
router.use(subdomain('discovery.olv', discovery));
logger.info('[MIIVERSE] Creating \'api\' subdomain');
router.use(subdomain('api.olv', api));
router.use(subdomain('api-test.olv', api));
router.use(subdomain('api-dev.olv', api));
logger.info('[MIIVERSE] Importing middleware');
discovery.use(sessionMiddleware);
discovery.use(pnidMiddleware);
// Setup routes
discovery.use('/v1/endpoint', routes.DISCOVERY);
api.use('/v1/posts', routes.POST);
api.use('/v1/posts.search', routes.POST);
api.use('/v1/friend_messages', routes.MESSAGE);
api.use('/v1/communities/', routes.COMMUNITY);
api.use('/v1/people/', routes.PEOPLE);
api.use('/v1/topics/', routes.TOPICS);
api.use('/v1/users/', routes.USERS);
api.use('/v1/status/', routes.PING);
module.exports = router;

View File

@@ -0,0 +1,44 @@
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';
import POST from '@/services/miiverse-api/routes/post';
import MESSAGE from '@/services/miiverse-api/routes/message';
import COMMUNITY from '@/services/miiverse-api/routes/communities';
import PEOPLE from '@/services/miiverse-api/routes/people';
import TOPICS from '@/services/miiverse-api/routes/topics';
import USERS from '@/services/miiverse-api/routes/users';
import PING from '@/services/miiverse-api/routes/ping';
// Main router for endpointsindex.js
const router = express.Router();
// Router to handle the subdomain restriction
const discovery = express.Router();
const api = express.Router();
// Create subdomains
LOG_INFO('[MIIVERSE] Creating \'discovery\' subdomain');
router.use(subdomain('discovery.olv', discovery));
LOG_INFO('[MIIVERSE] Creating \'api\' subdomain');
router.use(subdomain('api.olv', api));
router.use(subdomain('api-test.olv', api));
router.use(subdomain('api-dev.olv', api));
LOG_INFO('[MIIVERSE] Importing middleware');
discovery.use(sessionMiddleware);
// Setup routes
discovery.use('/v1/endpoint', DISCOVERY);
api.use('/v1/posts', POST);
api.use('/v1/posts.search', POST);
api.use('/v1/friend_messages', MESSAGE);
api.use('/v1/communities/', COMMUNITY);
api.use('/v1/people/', PEOPLE);
api.use('/v1/topics/', TOPICS);
api.use('/v1/users/', USERS);
api.use('/v1/status/', PING);
export default router;

View File

@@ -1,19 +1,27 @@
const express = require('express');
import express from 'express';
import multer from 'multer';
import {
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";
const router = express.Router();
const database = require('../../../database');
const comPostGen = require('../../../util/xmlResponseGenerator');
const util = require('../../../util/util');
const {COMMUNITY} = require("../../../models/communities");
const {POST} = require("../../../models/post");
const multer = require('multer')
/* GET post titles. */
router.get('/', async function (req, res) {
const paramPack = util.decodeParamPack(req.headers["x-nintendo-parampack"]);
let community = await database.getCommunityByTitleID(paramPack.title_id);
const paramPack = decodeParamPack(req.headers["x-nintendo-parampack"]);
let community = await getCommunityByTitleID(paramPack.title_id);
if(!community) res.sendStatus(404);
let communities = await database.getSubCommunities(community.olive_community_id);
let communities = await getSubCommunities(community.olive_community_id);
if(!communities) res.sendStatus(404);
communities.unshift(community);
let response = await comPostGen.Communities(communities);
@@ -22,7 +30,7 @@ router.get('/', async function (req, res) {
});
router.get('/popular', async function (req, res) {
let community = await database.getMostPopularCommunities(100);
let community = await getMostPopularCommunities(100);
if (community != null) {
res.contentType("application/json");
res.send(community);
@@ -30,7 +38,7 @@ router.get('/popular', async function (req, res) {
});
router.get('/new', async function (req, res) {
let community = await database.getNewCommunities(100);
let community = await getNewCommunities(100);
if (community != null) {
res.contentType("application/json");
res.send(community);
@@ -38,17 +46,21 @@ router.get('/new', async function (req, res) {
});
router.get('/:appID/posts', async function (req, res) {
const paramPack = util.decodeParamPack(req.headers["x-nintendo-parampack"]);
let community = await COMMUNITY.findOne({ community_id: req.params.appID });
const paramPack = decodeParamPack(req.headers["x-nintendo-parampack"]);
let community = await Community.findOne({ community_id: req.params.appID });
if(!community)
community = await database.getCommunityByTitleID(paramPack.title_id);
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 }
message_to_pid: { $eq: null },
search_key: null,
is_spoiler: null,
painting: null,
pid: null
}
if(req.query.search_key)
@@ -59,7 +71,7 @@ router.get('/:appID/posts', async function (req, res) {
if(req.query.type === 'memo')
query.painting = { $ne: null };
if(req.query.by === 'followings') {
let userContent = await database.getUserContent(req.pid);
let userContent = await getUserContent(req.pid);
query.pid = userContent.following_users;
}
else if(req.query.by === 'self')
@@ -67,7 +79,7 @@ router.get('/:appID/posts', async function (req, res) {
let posts;
if(req.query.distinct_pid === '1')
posts = await POST.aggregate([
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
@@ -75,7 +87,7 @@ router.get('/:appID/posts', async function (req, res) {
{ $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));
posts = await Post.find(query).sort({ created_at: -1}).limit(parseInt(req.query.limit as string));
/* Build formatted response and send it off. */
let options = {
@@ -90,12 +102,12 @@ router.get('/:appID/posts', async function (req, res) {
// Handler for POST on '/v1/communities'
router.post('/', multer().none(), async function (req, res) {
const paramPack = util.decodeParamPack(req.headers["x-nintendo-parampack"]);
let parent_community = await database.getCommunityByTitleIDs(paramPack.title_id);
const paramPack = decodeParamPack(req.headers["x-nintendo-parampack"]);
let parent_community = await getCommunityByTitleIDs(paramPack.title_id);
if(!parent_community) res.sendStatus(404);
let num_communities = await COMMUNITY.count();
let new_community = new COMMUNITY({
let num_communities = await Community.count();
let new_community = new Community({
platform_id: 0, // WiiU
name: req.body.name,
description: req.body.description,
@@ -118,4 +130,4 @@ router.post('/', multer().none(), async function (req, res) {
res.send(response);
});
module.exports = router;
export default router;

View File

@@ -1,17 +1,18 @@
const express = require('express');
const xml = require('object-to-xml');
const database = require('../../../database');
import express from 'express';
import xml from 'object-to-xml';
import { getPNID, getEndPoint } from '@/database';
const router = express.Router();
/* GET discovery server. */
router.get('/', async function (req, res) {
let user = await database.getPNID(req.pid);
let user = await getPNID(req.pid);
let discovery;
if(user)
discovery = await database.getEndPoint(user.server_access_level);
discovery = await getEndPoint(user.server_access_level);
else
discovery = await database.getEndPoint('prod');
discovery = await getEndPoint('prod');
let message = '', error = 0;
switch(discovery.status) {
@@ -79,4 +80,4 @@ router.get('/', async function (req, res) {
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
});
module.exports = router;
export default router;

View File

@@ -1,10 +0,0 @@
module.exports = {
DISCOVERY: require('./discovery'),
POST: require('./post'),
MESSAGE: require('./message'),
COMMUNITY: require('./communities'),
PEOPLE: require('./people'),
TOPICS: require('./topics'),
USERS: require('./users'),
PING: require('./ping')
};

View File

@@ -1,27 +1,28 @@
const express = require('express');
import crypto from "node:crypto";
import express from 'express';
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 { Post } from '@/models/post';
import { Conversation } from '@/models/conversation';
const router = express.Router();
const moment = require('moment');
const xml = require('object-to-xml');
const { POST } = require('../../../models/post');
const { CONVERSATION } = require('../../../models/conversation');
const util = require('../../../util/util');
const database = require('../../../database');
const multer = require('multer');
const crypto = require("crypto");
const snowflake = require('node-snowflake').Snowflake;
const upload = multer();
router.post('/', upload.none(), async function (req, res) {
let user = await database.getPNID(req.pid);
let user2 = await database.getPNID(req.body.message_to_pid);
let conversation = await database.getConversationByUsers([user.pid, user2.pid]);
let userSettings = await database.getUserSettings(req.pid), user2Settings = await database.getUserSettings(user2.pid), postID = await generatePostUID(21);
let friends = await util.data.getFriends(user2.pid);
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(),
id: Snowflake.nextId(),
users: [
{
pid: user.pid,
@@ -35,31 +36,31 @@ router.post('/', upload.none(), async function (req, res) {
},
]
};
const newConversations = new CONVERSATION(document);
const newConversations = new Conversation(document);
await newConversations.save();
conversation = await database.getConversationByID(document.id);
conversation = await getConversationByID(document.id);
}
if(!conversation)
return res.sendStatus(404);
if(!friends || friends.indexOf(req.pid) === -1)
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 = util.decodeParamPack(req.headers["x-nintendo-parampack"]);
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 util.processPainting(painting, true);
await util.uploadCDNAsset('pn-cdn', `paintings/${req.pid}/${postID}.png`, paintingURI, 'public-read');
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 util.uploadCDNAsset('pn-cdn', `screenshots/${req.pid}/${postID}.jpg`, Buffer.from(screenshot, 'base64'), 'public-read');
await uploadCDNAsset('pn-cdn', `screenshots/${req.pid}/${postID}.jpg`, Buffer.from(screenshot, 'base64'), 'public-read');
}
let miiFace;
@@ -90,7 +91,7 @@ router.post('/', upload.none(), async function (req, res) {
body = body.substring(0,280);
const document = {
title_id: paramPackData.title_id,
community_id: community.olive_community_id,
community_id: conversation.id,
screen_name: user.mii.name,
body: body,
app_data: appData,
@@ -107,17 +108,17 @@ router.post('/', upload.none(), async function (req, res) {
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}`,
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: (PNID.access_level === 2 || PNID.access_level === 3),
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);
const newPost = new Post(document);
newPost.save();
res.sendStatus(200);
let postPreviewText;
@@ -131,9 +132,9 @@ router.post('/', upload.none(), async function (req, res) {
});
router.get('/', async function(req, res) {
let limit = parseInt(req.query.limit), search_key = req.query.search_key;
let posts = await database.getFriendMessages(req.pid, search_key, limit);
posts = posts.length === 0 ? " " : posts
let limit = parseInt(req.query.limit as string), search_key = req.query.search_key;
let posts = await getFriendMessages(req.pid, search_key, limit);
let postBody = [];
for(let post of posts) {
console.log(post)
@@ -179,13 +180,15 @@ router.get('/', async function(req, res) {
});
router.post('/:post_id/empathies', upload.none(), async function (req, res) {
let pid = util.processServiceToken(req.headers["x-nintendo-servicetoken"]);
const post = await database.getPostByID(req.params.post_id);
// 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 database.getUserByPID(pid);
let user = await getUserByPID(pid);
if(user.likes.indexOf(post.id) === -1 && user.id !== post.pid)
{
post.upEmpathy();
@@ -194,13 +197,14 @@ router.post('/:post_id/empathies', upload.none(), async function (req, res) {
}
else
res.sendStatus(403);
*/
});
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 });
const inuse = await Post.findOne({ id });
id = (inuse ? await generatePostUID(length) : id);
return id;
}
module.exports = router;
export default router;

View File

@@ -1,24 +1,26 @@
const express = require('express');
import express from 'express';
import xmlGenerator from '@/util/xmlResponseGenerator';
import { getUserContent, getFollowedUsers } from '@/database';
import { getFriends } from '@/util';
import { Post } from "@/models/post";
const router = express.Router();
const database = require('../../../database');
const xmlGenerator = require('../../../util/xmlResponseGenerator');
const {POST} = require("../../../models/post");
const util = require('../../../util/util')
/* GET post titles. */
router.get('/', async function (req, res) {
let userContent = await database.getUserContent(req.pid);
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 }
message_to_pid: { $eq: null },
pid: null
}
if(req.query.relation === 'friend') {
let friends = await util.getFriends(req.pid);
let friends = await getFriends(req.pid);
if(!friends) return res.sendStatus(204);
query.pid = { $in: friends.pids };
}
@@ -26,11 +28,11 @@ router.get('/', async function (req, res) {
query.pid = { $in: userContent.followed_users.map(i=>Number(i)) };
}
else if(req.query.pid) {
query.pid = { $in: req.query.pid.map(i=>Number(i)) }
query.pid = { $in: (req.query.pid as string[]).map(i=>Number(i)) }
}
let posts;
if(req.query.distinct_pid === '1')
posts = await POST.aggregate([
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
@@ -38,9 +40,9 @@ router.get('/', async function (req, res) {
{ $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));
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));
posts = await Post.find(query).sort({ created_at: -1}).limit(parseInt(req.query.limit as string));
/* Build formatted response and send it off. */
let options = {
@@ -53,11 +55,11 @@ router.get('/', async function (req, res) {
});
router.get('/:pid/following', async function (req, res) {
let user = await database.getUserContent(req.params.pid);
let user = await getUserContent(req.params.pid);
if(!user) res.sendStatus(404);
let people = await database.getFollowedUsers(user);
let people = await getFollowedUsers(user);
if(!people) res.sendStatus(404);
res.send(await xmlGenerator.Following(people));
});
module.exports = router;
export default router;

View File

@@ -1,15 +1,16 @@
const express = require('express');
import express from 'express';
import { getEndpoints } from '@/database';
const router = express.Router();
const database = require('../../../database');
router.get('/', async function(req, res) {
res.send('Pong!');
});
router.get('/database', async function(req, res) {
let document = await database.getEndpoints();
let document = await getEndpoints();
if(document)
res.send('DB Connection Working! :D');
});
module.exports = router;
export default router;

View File

@@ -1,16 +1,25 @@
const express = require('express');
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 {
getPostByID,
getUserContent,
getPostReplies,
getPNID,
getUserSettings,
getCommunityByID,
getCommunityByTitleID,
getDuplicatePosts
} from '@/database';
import { Post } from '@/models/post';
const { Community } = require("@/models/community");
const router = express.Router();
const xml = require('object-to-xml');
const { POST } = require('../../../models/post');
const util = require('../../../util/util');
const database = require('../../../database');
const multer = require('multer');
const snowflake = require('node-snowflake').Snowflake;
const communityPostGen = require('../../../util/xmlResponseGenerator');
const {COMMUNITY} = require("../../../models/communities");
const processHeaders = require("../../../util/util");
const comPostGen = require("../../../util/xmlResponseGenerator");
const crypto = require("crypto");
const upload = multer();
/* GET post titles. */
@@ -19,8 +28,8 @@ router.post('/', upload.none(), async function (req, res) { await newPost(req, r
router.post('/:post_id/replies', upload.none(), async function (req, res) { await newPost(req, res)});
router.post('/:post_id.delete', async function (req, res) {
const post = await database.getPostByID(req.params.post_id);
let user = await database.getUserContent(req.pid);
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) {
@@ -32,10 +41,10 @@ router.post('/:post_id.delete', async function (req, res) {
});
router.post('/:post_id/empathies', upload.none(), async function (req, res) {
const post = await database.getPostByID(req.params.post_id);
const post = await getPostByID(req.params.post_id);
if(!post) res.sendStatus(404);
if(post.yeahs.indexOf(req.pid) === -1) {
await POST.updateOne({
await Post.updateOne({
id: post.id,
yeahs: {
$ne: req.pid
@@ -51,7 +60,7 @@ router.post('/:post_id/empathies', upload.none(), async function (req, res) {
});
}
else if(post.yeahs.indexOf(req.pid) !== -1) {
await POST.updateOne({
await Post.updateOne({
id: post.id,
yeahs: {
$eq: req.pid
@@ -70,16 +79,16 @@ router.post('/:post_id/empathies', upload.none(), async function (req, res) {
});
router.get('/:post_id/replies', async function (req, res) {
let pid = util.processServiceToken(req.headers["x-nintendo-servicetoken"]);
const post = await database.getPostByID(req.params.post_id);
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 database.getPostReplies(post.id, req.query.limit)
if(!posts.length === 0)
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 === 1,
with_mii: req.query.with_mii as string === '1',
topic_tag: true
}
/* Build formatted response and send it off. */
@@ -89,7 +98,7 @@ router.get('/:post_id/replies', async function (req, res) {
});
router.get('', async function (req, res) {
const post = await database.getPostByID(req.query.post_id);
const post = await getPostByID(req.query.post_id);
if(!post) {
res.set("Content-Type", "application/xml");
res.statusCode = 404;
@@ -106,23 +115,21 @@ router.get('', async function (req, res) {
else res.send(await communityPostGen.QueryResponse(post));
});
module.exports = router;
async function newPost(req, res) {
let PNID = await database.getPNID(req.pid), userSettings = await database.getUserSettings(req.pid), postID = await generatePostUID(21), parentPost = null;
let paramPackData = util.decodeParamPack(req.headers["x-nintendo-parampack"]);
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;
let community = await database.getCommunityByID(community_id)
let community = await getCommunityByID(community_id)
if(!community)
community = await COMMUNITY.findOne({olive_community_id: community_id});
community = await Community.findOne({olive_community_id: community_id});
if(!community)
community = await database.getCommunityByTitleID(paramPackData.title_id);
community = await getCommunityByTitleID(paramPackData.title_id);
if(!community || userSettings.account_status !== 0 || community.community_id === 'announcements')
return res.sendStatus(403);
if(req.params.post_id) {
parentPost = await database.getPostByID(req.params.post_id.toString());
parentPost = await getPostByID(req.params.post_id.toString());
if(!parentPost)
return res.sendStatus(403);
}
@@ -137,12 +144,12 @@ async function newPost(req, res) {
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 util.processPainting(painting, true);
await util.uploadCDNAsset('pn-cdn', `paintings/${req.pid}/${postID}.png`, paintingURI, 'public-read');
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 util.uploadCDNAsset('pn-cdn', `screenshots/${req.pid}/${postID}.jpg`, Buffer.from(screenshot, 'base64'), 'public-read');
await uploadCDNAsset('pn-cdn', `screenshots/${req.pid}/${postID}.jpg`, Buffer.from(screenshot, 'base64'), 'public-read');
}
let miiFace;
@@ -201,7 +208,7 @@ async function newPost(req, res) {
parent: parentPost ? parentPost.id : null,
removed: false
};
let duplicatePost = await database.getDuplicatePosts(req.pid, document);
let duplicatePost = await getDuplicatePosts(req.pid, document);
if(duplicatePost || document.body === '' && document.painting === '' && document.screenshot === '') {
res.set("Content-Type", "application/xml");
res.statusCode = 400;
@@ -216,7 +223,7 @@ async function newPost(req, res) {
};
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
}
const newPost = new POST(document);
const newPost = new Post(document);
newPost.save();
if(parentPost) {
parentPost.reply_count = parentPost.reply_count + 1;
@@ -227,7 +234,10 @@ async function newPost(req, res) {
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 });
const inuse = await Post.findOne({ id });
id = (inuse ? await generatePostUID(length) : id);
return id;
}
export default router;

View File

@@ -1,19 +1,21 @@
const express = require('express');
import express from 'express';
import memoize from 'memoize';
import { getPNID, getEndPoint } from '@/database';
import { Post } from '@/models/post';
import { Community } from '@/models/community';
import comPostGen from '@/util/xmlResponseGenerator';
const router = express.Router();
const database = require('../../../database');
const {POST} = require('../../../models/post');
const {COMMUNITY} = require('../../../models/communities');
const comPostGen = require('../../../util/xmlResponseGenerator');
let memoize = require("memoizee");
memoized = memoize(comPostGen.topics, { async: true, maxAge: 1000 * 60 * 60 });
const memoized = memoize(comPostGen.topics, { async: true, maxAge: 1000 * 60 * 60 });
/* GET post titles. */
router.get('/', async function (req, res) {
let user = await database.getPNID(req.pid), discovery;
let user = await getPNID(req.pid), discovery;
if(user)
discovery = await database.getEndPoint(user.server_access_level);
discovery = await getEndPoint(user.server_access_level);
else
discovery = await database.getEndPoint('prod');
discovery = await getEndPoint('prod');
if(!discovery.topics) return res.sendStatus(404);
let communities = await calculateMostPopularCommunities(24, 10);
@@ -27,7 +29,7 @@ router.get('/', async function (req, res) {
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 });
const posts = await Post.find({ created_at: { $gte: last24Hours }, message_to_pid: null });
if(!posts) return;
const communityIds = {};
for (const post of posts) {
@@ -35,12 +37,12 @@ async function calculateMostPopularCommunities(hours, limit) {
communityIds[communityId] = (communityIds[communityId] || 0) + 1;
}
const communities = Object.entries(communityIds)
.sort((a, b) => b[1] - a[1])
.sort((a, b) => (b[1] as number) - (a[1] as number))
.map((entry) => entry[0]);
if(communities.size < limit)
return COMMUNITY.find().limit(limit).sort({followers: -1});
if(communities.length < limit)
return Community.find().limit(limit).sort({followers: -1});
let response = await COMMUNITY.aggregate([
let response = await Community.aggregate([
{ $match: { olive_community_id: { $in: communities }, parent: null } },
{$addFields: {
index: { $indexOfArray: [ communities, "$olive_community_id" ] }
@@ -54,4 +56,4 @@ async function calculateMostPopularCommunities(hours, limit) {
else return response;
}
module.exports = router;
export default router;

View File

@@ -1,6 +1,7 @@
const express = require('express');
import express from 'express';
import xml from 'object-to-xml';
const router = express.Router();
const xml = require('object-to-xml');
router.get('/:pid/notifications', async function(req, res) {
let type = req.query.type, title_id = req.query.title_id;
@@ -19,4 +20,4 @@ router.get('/:pid/notifications', async function(req, res) {
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
});
module.exports = router;
export default router;

View File

@@ -0,0 +1,16 @@
export interface ParamPack {
title_id: string;
access_key: string;
platform_id: string;
region_id: string;
language_id: string;
country_id: string;
area_id: string;
network_restriction: string;
friend_restriction: string;
rating_restriction: string;
rating_organization: string;
transferable_id: string;
tz_name: string;
utc_offset: string;
}

View File

@@ -0,0 +1,3 @@
export interface SafeQs {
[key: string]: string | undefined
}

10
src/types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
// to make the file a module and avoid the TypeScript error
export {}
declare global {
namespace Express {
interface Request {
pid: number;
}
}
}

3
src/types/memoize.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,43 @@
import { Model, Types, HydratedDocument } from 'mongoose';
enum COMMUNITY_TYPE {
Main = 0,
Sub = 1,
Announcement = 2,
Private = 3
}
export interface ICommunity {
platform_id: number;
name: string;
description: string;
open: boolean;
allows_comments: boolean;
type: COMMUNITY_TYPE;
parent: string;
admins: Types.Array<number>;
created_at: Date;
empathy_count: number;
followers: number;
has_shop_page: number;
icon: string;
title_ids: Types.Array<string>;
title_id: Types.Array<string>;
community_id: string;
olive_community_id: string;
is_recommended: number;
app_data: string;
}
export interface ICommunityMethods {
upEmpathy(): Promise<void>
downEmpathy(): Promise<void>
upFollower(): Promise<void>
downFollower(): Promise<void>
}
interface ICommunityQueryHelpers {}
export interface CommunityModel extends Model<ICommunity, ICommunityQueryHelpers, ICommunityMethods> {}
export type HydratedCommunityDocument = HydratedDocument<ICommunity, ICommunityMethods>

View File

@@ -0,0 +1,23 @@
import { Model, Types, HydratedDocument } from 'mongoose';
export interface IContent {
pid: number;
followed_communities: Types.Array<string>;
followed_users: Types.Array<number>;
following_users: Types.Array<number>;
}
export interface IContentMethods {
addToCommunities(): Promise<void>
removeFromCommunities(): Promise<void>
addToUsers(): Promise<void>
removeFromUsers(): Promise<void>
addToFollowers(): Promise<void>
removeFromFollowers(): Promise<void>
}
interface IContentQueryHelpers {}
export interface ContentModel extends Model<IContent, IContentQueryHelpers, IContentMethods> {}
export type HydratedContentDocument = HydratedDocument<IContent, IContentMethods>

View File

@@ -0,0 +1,26 @@
import { Model, Types, HydratedDocument } from 'mongoose';
export type ConversationUser = {
pid: number;
official: boolean;
read: boolean;
}
export interface IConversation {
id: string;
created_at: Date;
last_updated: Date;
message_preview: string,
users: Types.Array<ConversationUser>;
}
export interface IConversationMethods {
newMessage(message: string, senderPID: number): Promise<void>
markAsRead(pid: number): Promise<void>
}
interface IConversationQueryHelpers {}
export interface ConversationModel extends Model<IConversation, IConversationQueryHelpers, IConversationMethods> {}
export type HydratedConversationDocument = HydratedDocument<IConversation, IConversationMethods>

View File

@@ -0,0 +1,20 @@
import { Model, HydratedDocument } from 'mongoose';
export interface IEndpoint {
status: number;
server_access_level: string;
topics: boolean;
guest_access: boolean;
host: string;
api_host: string;
portal_host: string;
n3ds_host: string;
}
export interface IEndpointMethods {}
interface IEndpointQueryHelpers {}
export interface EndpointModel extends Model<IEndpoint, IEndpointQueryHelpers, IEndpointMethods> {}
export type HydratedEndpointDocument = HydratedDocument<IEndpoint, IEndpointMethods>

View File

@@ -0,0 +1,26 @@
import { Model, Types, HydratedDocument } from 'mongoose';
export type NotificationUser = {
user: string;
timestamp: number;
}
export interface INotification {
pid: string;
type: string;
link: string;
objectID: string;
users: Types.Array<NotificationUser>;
read: boolean;
lastUpdated: number;
}
export interface INotificationMethods {
markRead(): Promise<void>
}
interface INotificationQueryHelpers {}
export interface NotificationModel extends Model<INotification, INotificationQueryHelpers, INotificationMethods> {}
export type HydratedNotificationDocument = HydratedDocument<INotification, INotificationMethods>

View File

@@ -0,0 +1,42 @@
import { Model, HydratedDocument } from 'mongoose';
enum ACCESS_LEVEL {
Banned = -1,
Standard = 0,
Tester = 1,
Mod = 2,
Developer = 3
};
type SERVER_ACCESS_LEVEL = 'prod' | 'test' | 'dev';
export interface IPNID {
access_level: ACCESS_LEVEL;
server_access_level: SERVER_ACCESS_LEVEL;
pid: number;
username: string;
birthdate: string;
country: string;
mii: {
name: string;
data: string;
};
connections: {
stripe: {
customer_id: string;
subscription_id: string;
price_id: string;
tier_level: number;
tier_name: string;
latest_webhook_timestamp: number;
};
};
}
export interface IPNIDMethods {}
interface IPNIDQueryHelpers {}
export interface PNIDModel extends Model<IPNID, IPNIDQueryHelpers, IPNIDMethods> {}
export type HydratedPNIDDocument = HydratedDocument<IPNID, IPNIDMethods>

View File

@@ -0,0 +1,50 @@
import { Model, Types, HydratedDocument } from 'mongoose';
export interface IPost {
id: string;
title_id: string;
screen_name: string;
body: string;
app_data: string;
painting: string;
screenshot: string;
screenshot_length: number;
search_key: Types.Array<string>;
topic_tag: string;
community_id: string;
created_at: number;
feeling_id: number;
is_autopost: number;
is_community_private_autopost: number;
is_spoiler: number;
is_app_jumpable: number;
empathy_count: number;
country_id: number;
language_id: number;
mii: string;
mii_face_url: string;
pid: number;
platform_id: number;
region_id: number;
parent: string;
reply_count: number;
verified: boolean;
message_to_pid: string;
removed: boolean;
removed_reason: string;
yeahs: Types.Array<number>;
number: number;
}
export interface IPostMethods {
upReply(): Promise<void>;
downReply(): Promise<void>;
remove(reason: string): Promise<void>;
unRemove(reason: string): Promise<void>;
}
interface IPostQueryHelpers {}
export interface PostModel extends Model<IPost, IPostQueryHelpers, IPostMethods> {}
export type HydratedPostDocument = HydratedDocument<IPost, IPostMethods>

View File

@@ -0,0 +1,16 @@
import { Model, HydratedDocument } from 'mongoose';
export interface IReport {
pid: string;
post_id: string;
reason: number;
created_at: Date;
}
export interface IReportMethods {}
interface IReportQueryHelpers {}
export interface ReportModel extends Model<IReport, IReportQueryHelpers, IReportMethods> {}
export type HydratedReportDocument = HydratedDocument<IReport, IReportMethods>

View File

@@ -0,0 +1,35 @@
import { Model, HydratedDocument } from 'mongoose';
export interface ISettings {
pid: number;
screen_name: string;
account_status: number;
ban_lift_date: Date;
ban_reason: string;
profile_comment: string;
profile_comment_visibility: boolean;
game_skill: number;
game_skill_visibility: boolean;
birthday_visibility: boolean;
relationship_visibility: boolean;
country_visibility: boolean;
profile_favorite_community_visibility: boolean;
receive_notifications: boolean;
}
export interface ISettingsMethods {
updateComment(comment: string): Promise<void>;
updateSkill(skill: number): Promise<void>;
commentVisible(active: boolean): Promise<void>;
skillVisible(active: boolean): Promise<void>;
birthdayVisible(active: boolean): Promise<void>;
relationshipVisible(active: boolean): Promise<void>;
countryVisible(active: boolean): Promise<void>;
favCommunityVisible(active: boolean): Promise<void>;
}
interface ISettingsQueryHelpers {}
export interface SettingsModel extends Model<ISettings, ISettingsQueryHelpers, ISettingsMethods> {}
export type HydratedSettingsDocument = HydratedDocument<ISettings, ISettingsMethods>

310
src/util.ts Normal file
View File

@@ -0,0 +1,310 @@
import crypto from 'node:crypto';
import { IncomingHttpHeaders } from 'node:http';
import NodeRSA from 'node-rsa';
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 { FriendsDefinition } from 'pretendo-grpc-ts/src/friends/friends_service';
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';
const config = require('../../config.json');
const { ip, port, api_key } = config.grpc.friends;
const channel = createChannel(`${ip}:${port}`);
const client = createClient(FriendsDefinition, channel);
const spacesEndpoint = new aws.Endpoint('nyc3.digitaloceanspaces.com');
const s3 = new aws.S3({
endpoint: spacesEndpoint,
accessKeyId: config.aws.spaces.key,
secretAccessKey: config.aws.spaces.secret
});
export async function create_user(pid, experience, notifications, region) {
const pnid = await getPNID(pid);
if(!pnid)
return;
let newSettings = {
pid: pid,
screen_name: pnid.mii.name,
game_skill: experience,
receive_notifications: notifications,
}
let newContent = {
pid: pid
}
const newSettingsObj = new Settings(newSettings);
await newSettingsObj.save();
const newContentObj = new Content(newContent);
await newContentObj.save();
}
export function decodeParamPack(paramPack): ParamPack {
/* Decode base64 */
let 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) {
try
{
let B64token = Buffer.from(token, 'base64');
let decryptedToken = this.decryptToken(B64token);
return decryptedToken.readUInt32LE(0x2);
}
catch(e)
{
return null;
}
}
export function decryptToken(token) {
// Access and refresh tokens use a different format since they must be much smaller
// Assume a small length means access or refresh token
if (token.length <= 32) {
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const iv = Buffer.alloc(16);
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
let decryptedBody = decipher.update(token);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
return decryptedBody;
}
const cryptoPath = `${__dirname}/../certs/access`;
const cryptoOptions = {
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
hmac_secret: config.account_server_secret
};
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
const cryptoConfig = token.subarray(0, 0x82);
const signature = token.subarray(0x82, 0x96);
const encryptedBody = token.subarray(0x96);
const encryptedAESKey = cryptoConfig.subarray(0, 128);
const point1 = cryptoConfig.readInt8(0x80);
const point2 = cryptoConfig.readInt8(0x81);
const iv = Buffer.concat([
Buffer.from(encryptedAESKey.subarray(point1, point1 + 8)),
Buffer.from(encryptedAESKey.subarray(point2, point2 + 8))
]);
try {
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
let decryptedBody = decipher.update(encryptedBody);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
const calculatedSignature = hmac.digest();
if (Buffer.compare(calculatedSignature, signature) !== 0) {
LOG_ERROR('Token signature did not match');
return null;
}
return decryptedBody;
}
catch (e) {
LOG_ERROR('Failed to decrypt token. Probably a NNID from the topics request');
return null;
}
}
export async function processPainting(painting, isTGA) {
if (isTGA) {
let paintingBuffer = Buffer.from(painting, 'base64');
let output = '';
try {
output = pako.inflate(paintingBuffer);
} catch (err) {
console.error(err);
}
let tga = new TGA(Buffer.from(output));
let png = new PNG({
width: tga.width,
height: tga.height
});
png.data = tga.pixels;
return PNG.sync.write(png);
//return `data:image/png;base64,${pngBuffer.toString('base64')}`;
}
else {
let paintingBuffer = Buffer.from(painting, 'base64');
let bitmap = bmp.decode(paintingBuffer)
const tga = this.createBMPTgaBuffer(bitmap.width, bitmap.height, bitmap.data, false);
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) {
var 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);
var offset = 18;
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
var 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
}
}
return buffer;
}
export async function uploadCDNAsset(bucket, key, data, acl) {
const awsPutParams = {
Body: data,
Key: key,
Bucket: bucket,
ACL: acl
};
await s3.putObject(awsPutParams).promise();
}
export async function getFriends(pid) {
return await client.getUserFriendPIDs({
pid: pid
}, {
metadata: Metadata({
'X-API-Key': api_key
})
})
}
export async function getFriendRequests(pid) {
const requests = await client.getUserFriendRequestsIncoming({
pid: pid
}, {
metadata: Metadata({
'X-API-Key': api_key
})
});
return requests.friendRequests;
}
export function makeSafeQs(query: ParsedQs): SafeQs {
const entries = Object.entries(query);
const output: SafeQs = {};
for (const [key, value] of entries) {
if (typeof value !== 'string') {
// * ignore non-strings
continue;
}
output[key] = value;
}
return output;
}
export function getValueFromQueryString(qs: ParsedQs, key: string): string | undefined {
let property: string | ParsedQs | string[] | ParsedQs[] | SafeQs | undefined = qs[key];
let value: string | undefined;
if (property) {
if (Array.isArray(property)) {
property = property[0];
}
if (typeof property !== 'string') {
property = makeSafeQs(<ParsedQs>property);
value = (<SafeQs>property)[key];
} else {
value = <string>property;
}
}
return value;
}
export function getValueFromHeaders(headers: IncomingHttpHeaders, key: string): string | undefined {
let header: string | string[] | undefined = headers[key];
let value: string | undefined;
if (header) {
if (Array.isArray(header)) {
header = header[0];
}
value = header;
}
return value;
}
export function mapToObject(map: Map<any, any>): object {
return Object.fromEntries(Array.from(map.entries(), ([ k, v ]) => v instanceof Map ? [ k, mapToObject(v) ] : [ k, v ]));
}

View File

@@ -1,329 +0,0 @@
const crypto = require('crypto');
const NodeRSA = require('node-rsa');
const fs = require('fs-extra');
const database = require('../database');
const logger = require('../logger');
const grpc = require('nice-grpc');
const grpcServices = require('grpc');
const config = require('../../config.json');
const { SETTINGS } = require('../models/settings');
const { CONTENT } = require('../models/content');
const { NOTIFICATIONS } = require('../models/notifications');
const { FriendsDefinition } = grpcServices.friends.service;
const TGA = require('tga');
const pako = require('pako');
const PNG = require('pngjs').PNG;
const bmp = require("bmp-js");
const aws = require('aws-sdk');
const { ip, port, api_key } = config.grpc.friends;
const channel = grpc.createChannel(`${ip}:${port}`);
const client = grpc.createClient(FriendsDefinition, channel);
const spacesEndpoint = new aws.Endpoint('nyc3.digitaloceanspaces.com');
const s3 = new aws.S3({
endpoint: spacesEndpoint,
accessKeyId: config.aws.spaces.key,
secretAccessKey: config.aws.spaces.secret
});
async function saveNotification(pid, type, title, content, reference_id, link) {
let notification = {
pid: pid,
type: type,
title: title,
content: content,
reference_id: reference_id,
link: link,
}
let newNotification = new NOTIFICATIONS(notification);
return await newNotification.save();
}
let methods = {
create_user: async function(pid, experience, notifications, region) {
const pnid = await database.getPNID(pid);
if(!pnid)
return;
let newSettings = {
pid: pid,
screen_name: pnid.mii.name,
game_skill: experience,
receive_notifications: notifications,
}
let newContent = {
pid: pid
}
const newSettingsObj = new SETTINGS(newSettings);
await newSettingsObj.save();
const newContentObj = new CONTENT(newContent);
await newContentObj.save();
},
decodeParamPack: function (paramPack) {
/* Decode base64 */
let dec = Buffer.from(paramPack, "base64").toString("ascii");
/* Remove starting and ending '/', split into array */
dec = dec.slice(1, -1).split("\\");
/* Parameters are in the format [name, val, name, val]. Copy into out{}. */
const out = {};
for (let i = 0; i < dec.length; i += 2) {
out[dec[i].trim()] = dec[i + 1].trim();
}
return out;
},
processServiceToken: function(token) {
try
{
let B64token = Buffer.from(token, 'base64');
let decryptedToken = this.decryptToken(B64token);
return decryptedToken.readUInt32LE(0x2);
}
catch(e)
{
return null;
}
},
decryptToken: function(token) {
// Access and refresh tokens use a different format since they must be much smaller
// Assume a small length means access or refresh token
if (token.length <= 32) {
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const iv = Buffer.alloc(16);
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
let decryptedBody = decipher.update(token);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
return decryptedBody;
}
const cryptoPath = `${__dirname}/../certs/access`;
const cryptoOptions = {
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
hmac_secret: config.account_server_secret
};
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
const cryptoConfig = token.subarray(0, 0x82);
const signature = token.subarray(0x82, 0x96);
const encryptedBody = token.subarray(0x96);
const encryptedAESKey = cryptoConfig.subarray(0, 128);
const point1 = cryptoConfig.readInt8(0x80);
const point2 = cryptoConfig.readInt8(0x81);
const iv = Buffer.concat([
Buffer.from(encryptedAESKey.subarray(point1, point1 + 8)),
Buffer.from(encryptedAESKey.subarray(point2, point2 + 8))
]);
try {
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
let decryptedBody = decipher.update(encryptedBody);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
const calculatedSignature = hmac.digest();
if (Buffer.compare(calculatedSignature, signature) !== 0) {
logger.error('Token signature did not match');
return null;
}
return decryptedBody;
}
catch (e) {
logger.error('Failed to decrypt token. Probably a NNID from the topics request');
return null;
}
},
processPainting: async function (painting, isTGA) {
if (isTGA) {
let paintingBuffer = Buffer.from(painting, 'base64');
let output = '';
try {
output = pako.inflate(paintingBuffer);
} catch (err) {
console.error(err);
}
let tga = new TGA(Buffer.from(output));
let png = new PNG({
width: tga.width,
height: tga.height
});
png.data = tga.pixels;
return PNG.sync.write(png);
//return `data:image/png;base64,${pngBuffer.toString('base64')}`;
}
else {
let paintingBuffer = Buffer.from(painting, 'base64');
let bitmap = bmp.decode(paintingBuffer)
const tga = this.createBMPTgaBuffer(bitmap.width, bitmap.height, bitmap.data, false);
let output;
try
{
output = pako.deflate(tga, {level: 6});
}
catch (err)
{
console.error(err);
}
return new Buffer(output).toString('base64')
}
},
nintendoPasswordHash: function(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');
},
resizeImage: function (file, width, height) {
sharp(file)
.resize({ height: height, width: width })
.toBuffer()
.then(data => {
return data;
});
},
createBMPTgaBuffer: function(width, height, pixels, dontFlipY) {
var 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);
var offset = 18;
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
var 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
}
}
return buffer;
},
uploadCDNAsset: async function(bucket, key, data, acl) {
const awsPutParams = {
Body: data,
Key: key,
Bucket: bucket,
ACL: acl
};
await s3.putObject(awsPutParams).promise();
},
newNotification: async function(pid, type, reference_id, origin_pid, title, content) {
let user = await database.getUserSettings(origin_pid);
/**
* 0 like
* 1 reply
* 2 new follower
* 3 other
*/
if(type === 1)
return await saveNotification(pid, type, `${user.screen_name} Replied to your post.`, content, reference_id, `/posts/${reference_id}`);
else if(type === 2)
return await saveNotification(pid, type, `${user.screen_name} Followed you!`, '', reference_id, `/users/show?pid=${origin_pid}`);
let lastNotification = await database.getLastNotification(pid);
if(lastNotification && lastNotification.type === 0 && lastNotification.reference_id === reference_id) {
let post = await database.getPostByID(reference_id);
let newTitle = '';
switch (post.empathy_count) {
case 1:
newTitle = `${user.screen_name} Yeahed your post!`;
break;
case 2:
newTitle = `${user.screen_name} and 1 other Yeahed your post!`;
break;
default:
newTitle = `${user.screen_name} and ${post.empathy_count - 1} others Yeahed your post!`;
break;
}
lastNotification.title = newTitle;
await lastNotification.save();
}
else if(type === 0) {
let post = await database.getPostByID(reference_id);
let newTitle = '';
switch (post.empathy_count) {
case 1:
newTitle = `${user.screen_name} Yeahed your post!`;
break;
case 2:
newTitle = `${user.screen_name} and 1 other Yeahed your post!`;
break;
default:
newTitle = `${user.screen_name} and ${post.empathy_count - 1} others Yeahed your post!`;
break;
}
let newContent;
if(!post.body) {
if(post.screenshot)
newContent = 'Screenshot Post';
else if(post.painting)
newContent = 'Drawing Post';
}
else
newContent = post.body;
return await saveNotification(pid, type, newTitle, newContent, reference_id, `/posts/${post.id}`);
}
else
return await saveNotification(pid, type, title, content, reference_id, '');
},
getFriends: async function(pid) {
return await client.getUserFriendPIDs({
pid: pid
}, {
metadata: grpc.Metadata({
'X-API-Key': api_key
})
})
},
getFriendRequests: async function(pid) {
const requests = await client.getUserFriendRequestsIncoming({
pid: pid
}, {
metadata: grpc.Metadata({
'X-API-Key': api_key
})
});
return requests.friendRequests;
}
};
module.exports = methods;

View File

@@ -1,6 +1,6 @@
const xmlbuilder = require("xmlbuilder");
const moment = require("moment");
const database = require('../database');
import xmlbuilder from 'xmlbuilder';
import moment from 'moment';
import { getNumberNewCommunityPostsByID } from '@/database';
class XmlResponseGenerator {
/**
@@ -17,7 +17,7 @@ class XmlResponseGenerator {
.e("request_name", "replies").up()
.e("posts");
for (const post of posts) {
postObj(xml, post, options);
postObj(xml, post, options, {});
}
xml = xml.up();
return xml.end({ pretty: true, allowEmpty: true });
@@ -41,7 +41,7 @@ class XmlResponseGenerator {
.up()
.e("posts");
for (const post of posts) {
postObj(xml, post, options);
postObj(xml, post, options, {});
}
xml = xml.up();
return xml.end({ pretty: true, allowEmpty: true });
@@ -122,7 +122,7 @@ class XmlResponseGenerator {
.e("has_error", "0").up()
.e("version", "1").up()
.e("post");
postObj(xml, post, { with_mii: true });
postObj(xml, post, { with_mii: true }, {});
xml = xml.up();
return xml.end({ pretty: true, allowEmpty: true });
}
@@ -139,7 +139,7 @@ class XmlResponseGenerator {
.e("version", "1").up()
.e("request_name", "posts.search").up()
.e("posts");
postObj(xml, post, { with_mii: true });
postObj(xml, post, { with_mii: true }, {});
xml = xml.up();
return xml.end({ pretty: true, allowEmpty: true });
}
@@ -158,7 +158,7 @@ class XmlResponseGenerator {
.e("expire", expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
.e("topics");
for (const community of communities) {
let posts = await database.getNumberNewCommunityPostsByID(community, 30);
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()
@@ -224,7 +224,7 @@ class XmlResponseGenerator {
for (const post of posts) {
xml = xml.e("person")
.e("posts")
postObj(xml, post, options);
postObj(xml, post, options, {});
xml = xml.up().up();
}
xml = xml.up();
@@ -247,7 +247,7 @@ function postObj(xml, post, options, community) {
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", new moment(post.created_at).format('YYYY-MM-DD HH:MM:SS')).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()
@@ -289,6 +289,4 @@ function postObj(xml, post, options, community) {
xml.e("title_id", post.title_id).up().up()
}
if (typeof module !== "undefined") {
module.exports = XmlResponseGenerator;
}
export default XmlResponseGenerator;