This was too long in the making, and I've already made too many changes to list here. The Great rewrite begins!

This commit is contained in:
CaramelKat 2022-08-28 20:45:09 -05:00 committed by Jemma Poffinbarger
parent fcb375fe75
commit 035c4e0a23
19 changed files with 3962 additions and 540 deletions

View File

@ -2,19 +2,30 @@
"http": {
"port": 8081
},
"account_server": "account.miiverse.cc:8080",
"secret": "806590af0a2bf0cf9ce54b0b5c1493a1",
"X-Nintendo-Client-ID": "a2efa818a34fa16b8afbc8a74eba3eda",
"X-Nintendo-Client-Secret": "c91cdb5658bd4954ade78533a339cf9a",
"account_server": "",
"secret": "",
"X-Nintendo-Client-ID": "",
"X-Nintendo-Client-Secret": "",
"mongoose": {
"uri": "mongodb://localhost:27017",
"database": "Miiverse",
"uri": "",
"database": "",
"options": {
"useNewUrlParser": true,
"useUnifiedTopology": true
}
},
"authorized_PNIDs" : [
1420636409, 1657414049, 1587572630
]
"account_db": {
"uri": "",
"database": "",
"options": {
"useNewUrlParser": true,
"useUnifiedTopology": true
}
},
"aws": {
"spaces": {
"key": "",
"secret": ""
}
}
}

3040
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@
"author": "Jemma",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.1204.0",
"bmp-js": "^0.1.0",
"body-parser": "^1.19.0",
"colors": "^1.4.0",
"express": "^4.17.1",

44
src/accountdb.js Normal file
View File

@ -0,0 +1,44 @@
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
};

View File

@ -1,12 +1,17 @@
const mongoose = require('mongoose');
const { mongoose: mongooseConfig } = require('../config.json');
const { ENDPOINT } = require('./models/endpoint');
const { COMMUNITY } = require('./models/communities');
const { POST } = require('./models/post');
const { USER } = require('./models/user');
const { 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;
@ -31,19 +36,19 @@ function verifyConnected() {
async function getCommunities(numberOfCommunities) {
verifyConnected();
if(numberOfCommunities === -1)
return COMMUNITY.find({ parent: null });
return COMMUNITY.find({ parent: null, type: 0 });
else
return COMMUNITY.find({ parent: null }).limit(numberOfCommunities);
return COMMUNITY.find({ parent: null, type: 0 }).limit(numberOfCommunities);
}
async function getMostPopularCommunities(numberOfCommunities) {
verifyConnected();
return COMMUNITY.find({ parent: null }).sort({followers: -1}).limit(numberOfCommunities);
return COMMUNITY.find({ parent: null, type: 0 }).sort({followers: -1}).limit(numberOfCommunities);
}
async function getNewCommunities(numberOfCommunities) {
verifyConnected();
return COMMUNITY.find({ parent: null }).sort([['created_at', -1]]).limit(numberOfCommunities);
return COMMUNITY.find({ parent: null, type: 0 }).sort([['created_at', -1]]).limit(numberOfCommunities);
}
async function getSubCommunities(communityID) {
@ -77,7 +82,6 @@ async function getTotalPostsByCommunity(community) {
async function getPostByID(postID) {
verifyConnected();
return POST.findOne({
id: postID
});
@ -98,11 +102,23 @@ async function getPostReplies(postID, number) {
}).limit(number);
}
async function getDuplicatePosts(pid, post) {
verifyConnected();
return POST.findOne({
pid: pid,
body: post.body,
painting: post.painting,
screenshot: post.screenshot,
parent: null
});
}
async function getUserPostRepliesAfterTimestamp(post, numberOfPosts) {
verifyConnected();
return POST.find({
parent: post.pid,
created_at: { $lt: post.created_at }
created_at: { $lt: post.created_at },
message_to_pid: null
}).limit(numberOfPosts);
}
@ -110,7 +126,8 @@ async function getNumberUserPostsByID(userID, number) {
verifyConnected();
return POST.find({
pid: userID,
parent: null
parent: null,
message_to_pid: null
}).sort({ created_at: -1}).limit(number);
}
@ -118,7 +135,8 @@ async function getTotalPostsByUserID(userID) {
verifyConnected();
return POST.find({
pid: userID,
parent: null
parent: null,
message_to_pid: null
}).countDocuments();
}
@ -158,7 +176,7 @@ async function getNumberVerifiedCommunityPostsByID(community, limit, offset) {
async function getPostsByCommunity(community, numberOfPosts) {
verifyConnected();
return POST.find({
title_id: community.title_id,
community_id: community.id,
parent: null
}).limit(numberOfPosts);
}
@ -172,6 +190,15 @@ async function getPostsByCommunityKey(community, numberOfPosts, search_key) {
}).limit(numberOfPosts);
}
async function getFriendMessages(pid, search_key, limit) {
verifyConnected();
return POST.find({
message_to_pid: pid,
search_key: search_key,
parent: null
}).limit(limit);
}
async function getNewPostsByCommunity(community, limit, offset) {
verifyConnected();
return POST.find({
@ -185,7 +212,8 @@ async function getUserPostsAfterTimestamp(post, numberOfPosts) {
return POST.find({
pid: post.pid,
created_at: { $lt: post.created_at },
parent: null
parent: null,
message_to_pid: null,
}).limit(numberOfPosts);
}
@ -193,8 +221,9 @@ async function getUserPostsOffset(pid, limit, offset) {
verifyConnected();
return POST.find({
pid: pid,
parent: null
}).sort({ created_at: -1}).skip(offset).limit(limit);
parent: null,
message_to_pid: null
}).skip(offset).limit(limit).sort({ created_at: -1});
}
async function getCommunityPostsAfterTimestamp(post, numberOfPosts) {
@ -206,115 +235,146 @@ async function getCommunityPostsAfterTimestamp(post, numberOfPosts) {
}).limit(numberOfPosts);
}
async function pushNewNotificationByPID(PID, content, link) {
async function getEndpoints() {
verifyConnected();
return USER.update(
{ pid: PID }, { $push: { notification_list: { content: content, link: link, read: false, created_at: Date() }}});
return ENDPOINT.find({});
}
async function pushNewNotificationToAll(content, link) {
verifyConnected();
return USER.updateMany(
{}, { $push: { notification_list: { content: content, link: link, read: false, created_at: Date() }}});
}
async function getDiscoveryHosts() {
async function getEndPoint(accessLevel) {
verifyConnected();
return ENDPOINT.findOne({
version: 1
});
server_access_level: accessLevel
})
}
async function getUsers(numberOfUsers) {
async function getUsersSettings(numberOfUsers) {
verifyConnected();
if(numberOfUsers === -1)
return USER.find({});
return SETTINGS.find({});
else
return USER.find({}).limit(numberOfUsers);
return SETTINGS.find({}).limit(numberOfUsers);
}
async function getFollowingUsers(user) {
async function getUsersContent(numberOfUsers) {
verifyConnected();
return USER.find({
pid: user.following_users
});
if(numberOfUsers === -1)
return SETTINGS.find({});
else
return SETTINGS.find({}).limit(numberOfUsers);
}
async function getFollowedUsers(user) {
async function getUserSettings(pid) {
verifyConnected();
return USER.find({
pid: user.followed_users
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 getUserByPID(PID) {
async function getFollowedUsers(content) {
verifyConnected();
return USER.findOne({
pid: PID
return SETTINGS.find({
pid: content.followed_users
});
}
async function getUserByUsername(user_id) {
verifyConnected();
return USER.findOne({
pnid: new RegExp(`^${user_id}$`, 'i')
return PNID.findOne({
"username": new RegExp(`^${user_id}$`, 'i')
});
}
async function getServerConfig() {
verifyConnected();
return ENDPOINT.findOne();
}
async function getNewsFeed(user, numberOfPosts) {
async function getNewsFeed(content, numberOfPosts) {
verifyConnected();
return POST.find({
$or: [
{pid: user.followed_users},
{pid: user.pid}
{pid: content.followed_users},
{pid: content.pid},
{community_id: content.followed_communities},
],
parent: null
parent: null,
message_to_pid: null
}).limit(numberOfPosts).sort({ created_at: -1});
}
async function getNewsFeedAfterTimestamp(user, numberOfPosts, post) {
async function getNewsFeedAfterTimestamp(content, numberOfPosts, post) {
verifyConnected();
return POST.find({
$or: [
{pid: user.followed_users},
{pid: user.pid}
{pid: content.followed_users},
{pid: content.pid},
{community_id: content.followed_communities},
],
created_at: { $lt: post.created_at },
parent: null
parent: null,
message_to_pid: null
}).limit(numberOfPosts).sort({ created_at: -1});
}
async function getNewsFeedOffset(user, limit, offset) {
async function getNewsFeedOffset(content, limit, offset) {
verifyConnected();
return POST.find({
$or: [
{pid: user.followed_users},
{pid: user.pid}
{pid: content.followed_users},
{pid: content.pid},
{community_id: content.followed_communities},
],
parent: null
parent: null,
message_to_pid: null
}).skip(offset).limit(limit).sort({ created_at: -1});
}
async function getConversations(pid) {
verifyConnected();
return CONVERSATION.find({
pids: pid
"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 getConversation(pid, pid2) {
async function getConversationMessages(community_id, limit, offset) {
verifyConnected();
return CONVERSATION.find({
return POST.find({
community_id: community_id,
parent: null
}).sort({created_at: 1}).skip(offset).limit(limit);
}
async function getConversationByUsers(pids) {
verifyConnected();
return CONVERSATION.findOne({
$and: [
{pids: pid},
{pids: pid2}
],
{'users.pid': pids[0]},
{'users.pid': pids[1]}
]
});
}
@ -328,6 +388,50 @@ async function getLatestMessage(pid, pid2) {
})
}
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,
@ -337,7 +441,6 @@ module.exports = {
getCommunityByTitleID,
getCommunityByID,
getTotalPostsByCommunity,
getDiscoveryHosts,
getPostsByCommunity,
getHotPostsByCommunity,
getNumberNewCommunityPostsByID,
@ -351,21 +454,33 @@ module.exports = {
getNumberUserPostsByID,
getTotalPostsByUserID,
getPostByID,
getUsers,
getUserByPID,
getDuplicatePosts,
getEndpoints,
getEndPoint,
getUserByUsername,
getUserPostsAfterTimestamp,
getUserPostsOffset,
getCommunityPostsAfterTimestamp,
getServerConfig,
pushNewNotificationByPID,
pushNewNotificationToAll,
getNewsFeed,
getNewsFeedAfterTimestamp,
getNewsFeedOffset,
getFollowingUsers,
getFollowedUsers,
getConversations,
getConversation,
getConversationByID,
getConversationByUsers,
getConversationMessages,
getUnreadConversationCount,
getLatestMessage,
getFriendMessages,
getPNID,
getPNIDS,
getUsersSettings,
getUsersContent,
getUserSettings,
getUserContent,
getNotifications,
getUnreadNotificationCount,
getNotification,
getLastNotification
};

40
src/middleware/auth.js Normal file
View File

@ -0,0 +1,40 @@
const config = require('../../config.json');
const util = require('../util/util');
const xml = require("object-to-xml");
function auth(req, res, next) {
if(req.path.includes('/topics'))
return next();
let token = req.headers["x-nintendo-servicetoken"];
let paramPackData = util.data.decodeParamPack(req.headers["x-nintendo-parampack"]);
if(!token || !paramPackData)
badAuth(res);
else {
let pid = util.data.processServiceToken(token);
if(pid === null)
badAuth(res);
else {
req.pid = pid;
req.paramPackData = paramPackData;
return next();
}
}
}
function badAuth(res) {
res.set("Content-Type", "application/xml");
res.statusCode = 400;
let response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 7,
message: "POSTING_FROM_NNID"
}
};
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
}
module.exports = auth;

View File

@ -1,4 +1,5 @@
const { Schema, model } = require('mongoose');
const moment = require("moment");
const CommunitySchema = new Schema({
platform_id: Number,
@ -12,6 +13,7 @@ const CommunitySchema = new Schema({
* 0: Main Community
* 1: Sub-Community
* 2: Announcement Community
* 3: Private Community
*/
type: {
type: Number,
@ -62,7 +64,7 @@ const CommunitySchema = new Schema({
browser_icon: String,
browser_thumbnail: String,
CTR_browser_header: String,
WiiU_browser_header: String
WiiU_browser_header: String,
});
CommunitySchema.methods.upEmpathy = async function() {

76
src/models/content.js Normal file
View File

@ -0,0 +1,76 @@
const { Schema, model } = require('mongoose');
const ContentSchema = new Schema({
pid: String,
likes: {
type: [String],
default: [0]
},
followed_communities: {
type: [String],
default: [0]
},
followed_users: {
type: [String],
default: [0]
},
following_users: {
type: [String],
default: [0]
},
});
ContentSchema.methods.addToLikes = async function(postID) {
const likes = this.get('likes');
likes.addToSet(postID);
await this.save();
}
ContentSchema.methods.removeFromLike = async function(postID) {
const likes = this.get('likes');
likes.pull(postID);
await this.save();
}
ContentSchema.methods.addToCommunities = async function(postID) {
const communities = this.get('followed_communities');
communities.addToSet(postID);
await this.save();
}
ContentSchema.methods.removeFromCommunities = async function(postID) {
const communities = this.get('followed_communities');
communities.pull(postID);
await this.save();
}
ContentSchema.methods.addToUsers = async function(postID) {
const users = this.get('followed_users');
users.addToSet(postID);
await this.save();
}
ContentSchema.methods.removeFromUsers = async function(postID) {
const users = this.get('followed_users');
users.pull(postID);
await this.save();
}
ContentSchema.methods.addToFollowers = async function(postID) {
const users = this.get('following_users');
users.addToSet(postID);
await this.save();
}
ContentSchema.methods.removeFromFollowers = async function(postID) {
const users = this.get('following_users');
users.pull(postID);
await this.save();
}
const CONTENT = model('CONTENT', ContentSchema);
module.exports = {
ContentSchema,
CONTENT
};

View File

@ -1,6 +1,19 @@
const { Schema, model } = require('mongoose');
const moment = require("moment");
const snowflake = require('node-snowflake').Snowflake;
const user = new Schema({
pid: String,
official: {
type: Boolean,
default: false
},
read: {
type: Boolean,
default: true
}
});
const ConversationSchema = new Schema({
id: {
type: String,
@ -10,7 +23,7 @@ const ConversationSchema = new Schema({
type: Date,
default: new Date(),
},
last_message_sent: {
last_updated: {
type: Date,
default: new Date(),
},
@ -18,44 +31,35 @@ const ConversationSchema = new Schema({
type: String,
default: ""
},
pid1: {
pid: String,
official: {
type: Boolean,
default: false
},
screen_name: String,
read: Boolean
},
pid2: {
pid: String,
official: {
type: Boolean,
default: false
},
screen_name: String,
read: Boolean
}
users: [user]
});
ConversationSchema.methods.newMessage = async function(message, fromPid) {
const pid1 = this.get('pid1');
const pid2 = this.get('pid2');
if(pid1.pid === fromPid) {
pid1.read = true
pid2.read = false
const users = this.get('users');
console.log(fromPid)
if(users[0].pid.toString() === fromPid.toString()) {;
users[1].read = false;
}
else {
pid1.read = false
pid2.read = true
users[0].read = false;
}
this.set('pid1', pid1);
this.set('pid2', pid2);
this.set('last_message_sent', new Date());
this.set('users', users);
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.toString())
users[0].read = true;
else if(users[1].pid === pid.toString())
users[1].read = true;
this.set('users', users)
this.markModified('users');
await this.save();
}
const CONVERSATION = model('CONVERSATION', ConversationSchema);
module.exports = {

View File

@ -1,30 +1,15 @@
const { Schema, model } = require('mongoose');
const endpointSchema = new Schema({
has_error: Number,
version: Number,
endpoint: {
host: String,
api_host: String,
portal_host: String,
n3ds_host: String
},
guest: Boolean,
status: Number,
server_access_level: String,
guest_access: Boolean,
host: String,
api_host: String,
portal_host: String,
n3ds_host: String
});
endpointSchema.methods.updateHosts = async function({host, api_host, portal_host, n3ds_host}) {
this.set('endpoint.host', host);
this.set('endpoint.api_host', api_host);
this.set('endpoint.portal_host', portal_host);
this.set('endpoint.n3ds_host', n3ds_host);
await this.save();
};
endpointSchema.methods.updateGuest = async function(mode) {
this.set('guest', mode);
await this.save();
}
const ENDPOINT = model('ENDPOINT', endpointSchema);
module.exports = {

View File

@ -0,0 +1,37 @@
const { Schema, model } = require('mongoose');
const NotificationsSchema = new Schema({
pid: String,
/**
* 0 like
* 1 reply
* 2 new follower
* 3 other
*/
type: Number,
title: String,
content: String,
reference_id: String,
link: String,
created_at: {
type: Date,
default: new Date()
},
read: {
type: Boolean,
default: false
},
origin_pid: String,
});
NotificationsSchema.methods.markRead = async function() {
this.set('read', true);
await this.save();
};
const NOTIFICATIONS = model('NOTIFICATIONS', NotificationsSchema);
module.exports = {
NotificationsSchema,
NOTIFICATIONS
};

30
src/models/pnid.js Normal file
View File

@ -0,0 +1,30 @@
const mongoose = require('mongoose');
const {pnidConnection} = require('../accountdb');
const PNIDSchema = new mongoose.Schema({
access_level: {
type: Number,
default: 0 // -1: banned, 0: standard, 1: tester, 2: mod, 3: dev
},
server_access_level: {
type: String,
default: 'prod' // prod, test, dev
},
pid: {
type: Number,
unique: true
},
username: String,
birthdate: String,
country: String,
mii: {
name: String,
data: String,
},
});
const PNID = pnidConnection.model('PNID', PNIDSchema);
module.exports = {
PNID,
};

View File

@ -1,14 +1,13 @@
const { Schema, model } = require('mongoose');
const PostSchema = new Schema({
id: String,
title_id: String,
screen_name: String,
body: String,
app_data: String,
painting: String,
painting_uri: String,
screenshot: String,
url: String,
search_key: {
type: [String],
default: undefined
@ -17,11 +16,12 @@ const PostSchema = new Schema({
type: [String],
default: undefined
},
community_id: String,
country_id: Number,
community_id: {
type: String,
default: undefined
},
created_at: Date,
feeling_id: Number,
id: String,
is_autopost: {
type: Number,
default: 0
@ -48,14 +48,10 @@ const PostSchema = new Schema({
},
mii: String,
mii_face_url: String,
number: {
type: Number,
default: 1
},
pid: Number,
platform_id: Number,
region_id: Number,
parent_post: String,
parent: String,
reply_count: {
type: Number,
default: 0
@ -64,17 +60,9 @@ const PostSchema = new Schema({
type: Boolean,
default: false
},
parent: {
type: String,
default: null
},
message_to_pid: {
type: String,
default: null
},
conversation_id: {
type: String,
default: null
}
});

95
src/models/settings.js Normal file
View File

@ -0,0 +1,95 @@
const { Schema, model } = require('mongoose');
const SettingsSchema = new Schema({
pid: String,
screen_name: String,
account_status: {
type: Number,
default: 0
},
ban_lift_date: Date,
ban_reason: String,
profile_comment: {
type: String,
default: undefined
},
profile_comment_visibility: {
type: Boolean,
default: true
},
game_skill: {
type: Number,
default: 0
},
game_skill_visibility: {
type: Boolean,
default: true
},
birthday_visibility: {
type: Boolean,
default: false
},
relationship_visibility: {
type: Boolean,
default: false
},
country_visibility: {
type: Boolean,
default: false
},
profile_favorite_community_visibility: {
type: Boolean,
default: true
},
receive_notifications: {
type: Boolean,
default: true
}
});
SettingsSchema.methods.updateComment = async function(comment) {
this.set('profile_comment', comment);
await this.save();
};
SettingsSchema.methods.updateSkill = async function(skill) {
this.set('game_skill', skill);
await this.save();
};
SettingsSchema.methods.commentVisible = async function(active) {
this.set('profile_comment_visibility', active);
await this.save();
};
SettingsSchema.methods.skillVisible = async function(active) {
this.set('game_skill_visibility', active);
await this.save();
};
SettingsSchema.methods.birthdayVisible = async function(active) {
this.set('birthday_visibility', active);
await this.save();
};
SettingsSchema.methods.relationshipVisible = async function(active) {
this.set('relationship_visibility', active);
await this.save();
};
SettingsSchema.methods.countryVisible = async function(active) {
this.set('country_visibility', active);
await this.save();
};
SettingsSchema.methods.favCommunityVisible = async function(active) {
this.set('profile_favorite_community_visibility', active);
await this.save();
};
const SETTINGS = model('SETTINGS', SettingsSchema);
module.exports = {
SettingsSchema,
SETTINGS
};

View File

@ -5,6 +5,7 @@ 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();
@ -25,6 +26,7 @@ app.use(express.urlencoded({
parameterLimit: 100000
}));
app.use(xmlparser);
app.use(auth);
// import the servers into one
app.use(miiverse);

View File

@ -1,198 +1,93 @@
var express = require('express');
var xml = require('object-to-xml');
const database = require('../../../database');
const util = require('../../../util/util');
var router = express.Router();
/* GET discovery server. */
router.get('/', async function (req, res) {
const discovery = await database.getDiscoveryHosts();
try
{
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
let usrObj;
if(pid == null)
{
throw new Error('The User token was not valid');
}
else
{
usrObj = await util.data.processUser(pid);
if(!usrObj) {
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 2,
message: "SETUP_NOT_COMPLETE"
}
};
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
let user = await database.getPNID(req.pid);
console.log(user.mii)
if(!user) {
res.set("Content-Type", "application/xml");
res.statusCode = 400;
let response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 2,
message: "SETUP_NOT_COMPLETE"
}
switch (usrObj.account_status) {
case 0:
break;
case 1:
case 2:
case 3:
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 7,
message: "POSTING_FROM_NNID"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
}
}
};
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
}
catch (e)
{
console.error(e);
}
switch(discovery.has_error)
{
let discovery = await database.getEndPoint(user.server_access_level);
if(!discovery)
discovery = await database.getEndPoint('prod');
let message = '', error = 0;
switch(discovery.status) {
case 0 :
res.set("Content-Type", "application/xml");
response = {
let response = {
result: {
has_error: 0,
version: discovery.version,
version: 0,
endpoint: {
host: discovery.endpoint.host,
api_host: discovery.endpoint.api_host,
portal_host: discovery.endpoint.portal_host,
n3ds_host: discovery.endpoint.n3ds_host
host: discovery.host,
api_host: discovery.api_host,
portal_host: discovery.portal_host,
n3ds_host: discovery.n3ds_host
}
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
case 1 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 1,
message: "SYSTEM_UPDATE_REQUIRED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
message = 'SYSTEM_UPDATE_REQUIRED';
error = 1;
break;
case 2 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 2,
message: "SETUP_NOT_COMPLETE"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
message = 'SETUP_NOT_COMPLETE';
error = 2;
break;
case 3 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 3,
message: "SERVICE_MAINTENANCE"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
message = 'SERVICE_MAINTENANCE';
error = 3;
break;
case 4:
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 4,
message: "SERVICE_CLOSED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
message = 'SERVICE_CLOSED';
error = 4;
break;
case 5 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 5,
message: "PARENTAL_CONTROLS_ENABLED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
message = 'PARENTAL_CONTROLS_ENABLED';
error = 5;
break;
case 6 :
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 6,
message: "POSTING_LIMITED_PARENTAL_CONTROLS"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
message = 'POSTING_LIMITED_PARENTAL_CONTROLS';
error = 6;
break;
case 7 :
message = 'NNID_BANNED';
error = 7;
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 7,
message: "PNID_BANNED"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
default :
message = 'SERVER_ERROR';
error = 15;
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 15,
message: "SERVER_ERROR"
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
break;
}
});
router.post('/posts', function (req, res) {
res.sendStatus(200);
res.set("Content-Type", "application/xml");
res.statusCode = 400;
let response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: error,
message: message
}
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
});
module.exports = router;

View File

@ -10,147 +10,75 @@ var multer = require('multer');
const snowflake = require('node-snowflake').Snowflake;
var upload = multer();
/* GET post titles. */
router.post('/', upload.none(), async function (req, res, next) {
try
{
let paramPackData = util.data.decodeParamPack(req.headers["x-nintendo-parampack"]);
let pid = util.data.processServiceToken(req.headers["x-nintendo-servicetoken"]);
if(pid === null)
{
throw new Error('The User token was not valid');
}
else
{
let conversation = await database.getConversation(pid.toString(), req.body.message_to_pid.toString())
if(!conversation) {
let user = await database.getUserByPID(pid);
let user2 = await database.getUserByPID(req.body.message_to_pid);
if(!user || !user2)
return res.sendStatus(422)
let doc = {
message_preview: req.body.body,
pids: [
{
pid: user.pid.toString(),
official: user.official,
screen_name: user.user_id,
read: true
},
{
pid: user2.pid.toString(),
official: user2.official,
screen_name: user2.user_id,
read: false
}
]
}
const newConversation = new CONVERSATION(doc);
await newConversation.save();
}
else {
let messageType = '';
if(req.body.screenshot)
messageType = '(Screenshot)';
else if(req.body.painting)
messageType = '(Drawing)';
else
messageType = req.body.body;
await conversation.newMessage(messageType, req.pid.toString())
}
conversation = await database.getConversation(pid.toString(), req.body.message_to_pid.toString())
let user = await util.data.processUser(pid);
let appData = "";
if (req.body.app_data) {
appData = req.body.app_data.replace(/\0/g, "").trim();
}
let painting = "";
if (req.body.painting) {
painting = req.body.painting.replace(/\0/g, "").trim();
}
let paintingURI = "";
if (req.body.painting) {
paintingURI = await util.data.processPainting(painting);
}
let screenshot = "";
if (req.body.screenshot) {
screenshot = req.body.screenshot.replace(/\0/g, "").trim();
}
let miiFace;
console.log(parseInt(req.body.feeling_id))
switch (parseInt(req.body.feeling_id)) {
case 1:
miiFace = 'smile_open_mouth.png';
break;
case 2:
miiFace = 'wink_left.png';
break;
case 3:
miiFace = 'surprise_open_mouth.png';
break;
case 4:
miiFace = 'frustrated.png';
break;
case 5:
miiFace = 'sorrow.png';
break;
default:
miiFace = 'normal_face.png';
break;
}
const document = {
title_id: paramPackData.title_id,
screen_name: user.user_id,
body: req.body.body,
app_data: appData,
painting: painting,
painting_uri: paintingURI,
screenshot: screenshot,
url: req.body.url,
search_key: req.body.search_key,
topic_tag: req.body.topic_tag,
country_id: paramPackData.country_id,
created_at: new Date(),
feeling_id: req.body.feeling_id,
id: snowflake.nextId(),
is_autopost: req.body.is_autopost,
is_spoiler: req.body.is_spoiler,
is_app_jumpable: req.body.is_app_jumpable,
language_id: req.body.language_id,
mii: user.mii,
mii_face_url: `http://mii.olv.pretendo.cc/mii/${user.pid}/${miiFace}`,
pid: user.pid,
verified: user.official,
platform_id: paramPackData.platform_id,
region_id: paramPackData.region_id,
parent: null,
message_to_pid: req.body.message_to_pid,
conversation_id: conversation.id
};
const newPost = new POST(document);
newPost.save();
res.sendStatus(200);
}
}
catch (e)
{
console.error(e);
res.set("Content-Type", "application/xml");
res.statusCode = 400;
response = {
result: {
has_error: 1,
version: 1,
code: 400,
error_code: 7,
message: "POSTING_FROM_NNID"
}
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]);
if(!conversation) {
if(!user || !user2)
return res.sendStatus(422)
let document = {
id: snowflake.nextId(),
users: [
{
pid: user.pid,
official: (user.access_level === 2 || user.access_level === 3),
read: true
},
{
pid: user2.pid,
official: (user2.access_level === 2 || user2.access_level === 3),
read: false
},
]
};
res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
const newConversations = new CONVERSATION(document);
await newConversations.save();
conversation = await database.getConversationByID(document.id);
}
if(!conversation)
return res.sendStatus(404);
let document = {
screen_name: user.mii.name,
body: req.body.body,
painting: req.body.raw,
created_at: new Date(),
id: snowflake.nextId(),
mii: user.mii.data,
mii_face_url: `https://mii.olv.pretendo.cc/${user.pid}/normal_face.png`,
pid: user.pid,
verified: (user.access_level === 2 || user.access_level === 3),
parent: null,
search_key: req.body.search_key,
community_id: conversation.id,
message_to_pid: req.body.message_to_pid
};
const newPost = new POST(document);
newPost.save();
res.sendStatus(200);
let postPreviewText;
if(document.painting)
postPreviewText = 'sent a Drawing'
else if(document.body.length > 25)
postPreviewText = document.body.substring(0, 25) + '...';
else
postPreviewText = document.body;
await conversation.newMessage(postPreviewText, document.message_to_pid);
});
router.get('/', async function(req, res, next) {
let limit = parseInt(req.query.limit), type = req.query.type, search_key = req.query.search_key, by = req.query.by;
let posts = await database.getFriendMessages(req.pid, search_key, limit);
res.set("Content-Type", "application/xml");
let response = {
result: {
has_error: 0,
version: 1,
posts: posts.length === 0 ? " " : posts
}
};
return res.send("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xml(response));
});
router.post('/:post_id/empathies', upload.none(), async function (req, res, next) {

View File

@ -194,14 +194,17 @@ class CommunityPostGen {
.e("expire", expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
.e("topics");
for (const community of communities) {
let posts = await database.getPostsByCommunity(community, 30);
console.log(community.name)
let posts = await database.getNumberNewCommunityPostsByID(community, 30);
console.log(posts);
xml = xml.e('topic')
.e('empathy_count', community.empathy_count).up()
.e('has_shop_page', community.has_shop_page).up()
.e('icon', community.icon).up()
.e('title_ids');
community.title_ids.forEach(function (title_id) {
xml = xml.e('title_id', title_id).up()
if(title_id !== '')
xml = xml.e('title_id', title_id).up();
})
xml = xml.up()
.e('title_id', community.title_ids[0]).up()
@ -218,38 +221,38 @@ class CommunityPostGen {
.e("post")
.e("body", newBody).up()
.e("community_id", community.community_id).up()
.e("country_id", post.country_id).up()
.e("country_id", post.country_id || 0).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("id", '').up()
.e("is_autopost", post.is_autopost).up()
.e("is_community_private_autopost", post.is_community_private_autopost).up()
.e("is_spoiler", post.is_spoiler).up()
.e("is_app_jumpable", post.is_app_jumpable).up()
.e("empathy_count", post.empathy_count).up()
.e("language_id", post.language_id).up()
.e("mii", post.mii).up()
.e("mii_face_url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up();
.e("mii", post.mii.toString().replace(/[\n\r]+/gm, '')).up()
.e("mii_face_url", post.mii_face_url).up();
xml = xml.e("number", "0").up();
if (post.painting) {
xml = xml.e("painting")
.e("format", "tga").up()
.e("content", post.painting.replace( /[\r\n]+/gm, "" )).up()
.e("size", post.painting.length).up()
.e("url", "https://s3.amazonaws.com/olv-public/pap/WVW69koebmETvBVqm1").up()
.e("url", `https://cdn.pretendo.cc/paintings/${post.pid}/${post.id}.png`).up()
.up();
}
xml = xml.e("pid", post.pid).up()
.e("platform_id", post.platform_id).up()
.e("region_id", post.region_id).up()
.e("reply_count", post.reply_count).up()
.e("screen_name", post.screen_name).up()
.e("screen_name", 'placeholder').up()
.e("title_id", post.title_id).up()
.up().up().up();
}
xml = xml.up().up()
}
return xml.end({ pretty: true, allowEmpty: true });
return xml.end({ pretty: false, allowEmpty: true });
}
}

View File

@ -2,56 +2,56 @@ const crypto = require('crypto');
const NodeRSA = require('node-rsa');
const fs = require('fs-extra');
const database = require('../database');
const logger = require('../logger');
const config = require('../../config.json');
const xmlParser = require('xml2json');
const request = require("request");
const moment = require('moment');
const { USER } = require('../models/user');
const { SETTINGS } = require('../models/settings');
const { CONTENT } = require('../models/content');
const { NOTIFICATIONS } = require('../models/notifications');
let TGA = require('tga');
let pako = require('pako');
let PNG = require('pngjs').PNG;
const path = require('path')
let bmp = require("bmp-js");
const aws = require('aws-sdk');
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 = {
processUser: function(pid) {
return new Promise(async function(resolve, reject) {
let userObject = await database.getUserByPID(pid);
if(userObject != null)
resolve(userObject);
else
{
await request({
url: "http://" + config.account_server + "/v1/api/miis?pids=" + pid,
headers: {
'X-Nintendo-Client-ID': 'a2efa818a34fa16b8afbc8a74eba3eda',
'X-Nintendo-Client-Secret': 'c91cdb5658bd4954ade78533a339cf9a'
}
}, function (error, response, body) {
if (!error && response.statusCode === 200) {
let xml = xmlParser.toJson(body, {object: true});
const newUsr = {
pid: pid,
created_at: moment().format('YYYY-MM-DD HH:mm:SS'),
user_id: xml.miis.mii.user_id,
account_status: 0,
mii: xml.miis.mii.data,
official: false
};
const newUsrObj = new USER(newUsr);
newUsrObj.save();
resolve(newUsr);
}
else
{
console.log('fail');
reject();
}
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 */
@ -74,7 +74,7 @@ let methods = {
}
catch(e)
{
console.log(e)
//console.log(e)
return null;
}
@ -83,7 +83,7 @@ let methods = {
// Access and refresh tokens use a different format since they must be much smaller
// Assume a small length means access or refresh token
if (token.length <= 32) {
const cryptoPath = path.normalize(`${__dirname}/../certs/access`);
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const iv = Buffer.alloc(16);
@ -96,11 +96,11 @@ let methods = {
return decryptedBody;
}
const cryptoPath = path.normalize(`${__dirname}/../certs/access`);
const cryptoPath = `${__dirname}/../certs/access`;
const cryptoOptions = {
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
hmac_secret: config.secret
hmac_secret: config.account_server_secret
};
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
@ -123,42 +123,183 @@ let methods = {
Buffer.from(encryptedAESKey.subarray(point2, point2 + 8))
]);
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
try {
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
let decryptedBody = decipher.update(encryptedBody);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
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();
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
const calculatedSignature = hmac.digest();
if (Buffer.compare(calculatedSignature, signature) !== 0) {
console.log('Token signature did not match');
if (Buffer.compare(calculatedSignature, signature) !== 0) {
//console.log('Token signature did not match');
return null;
}
return decryptedBody;
}
catch (e) {
//console.log('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);
return decryptedBody;
},
processPainting: function (painting) {
let paintingBuffer = Buffer.from(painting, 'base64');
let output = '';
try
{
output = pako.inflate(paintingBuffer);
let output;
try
{
output = pako.deflate(tga, {level: 6});
}
catch (err)
{
console.error(err);
}
return new Buffer(output).toString('base64')
}
catch (err)
{
console.error(err);
}
let tga = new TGA(Buffer.from(output));
let png = new PNG({
width: tga.width,
height: tga.height
});
png.data = tga.pixels;
let pngBuffer = PNG.sync.write(png);
return `data:image/png;base64,${pngBuffer.toString('base64')}`;
},
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, '');
}
};
exports.data = methods;