Removed XMLResponseGenerator class in favor of Mongoose Schema methods

This commit is contained in:
Jonathan Barrow
2023-04-24 16:29:04 -04:00
parent c869b3ae36
commit 4664d9fe05
11 changed files with 272 additions and 326 deletions

View File

@@ -93,4 +93,17 @@ CommunitySchema.method('downFollower', async function downFollower(): Promise<vo
await this.save();
});
CommunitySchema.method('json', function json(): Record<string, any> {
return {
community_id: this.community_id,
name: this.name,
description: this.description,
icon: '',
icon_3ds: '',
pid: '',
app_data: this.app_data,
is_user_community: '0'
};
});
export const Community: CommunityModel = model<ICommunity, CommunityModel>('Community', CommunitySchema);

View File

@@ -1,6 +1,9 @@
import crypto from 'node:crypto';
import moment from 'moment';
import { Schema, model } from 'mongoose';
import { HydratedPostDocument, IPost, IPostMethods, PostModel } from '@/types/mongoose/post';
import { HydratedCommunityDocument } from '@/types/mongoose/community';
import { PostToJSONOptions } from '@/types/mongoose/post-to-json-options';
const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
id: String,
@@ -129,6 +132,69 @@ PostSchema.method('generatePostUID', async function generatePostUID(length: numb
}
});
PostSchema.method('json', function json(options: PostToJSONOptions, community?: HydratedCommunityDocument): Record<string, any> {
const json: Record<string, any> = {
body: this.body ? this.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}+=,.<>/?;:'"[\]]/g, '').replace(/[\n\r]+/gm, '') : '',
country_id: this.country_id ? this.country_id : 254,
created_at: moment(this.created_at).format('YYYY-MM-DD HH:MM:SS'),
feeling_id: this.feeling_id,
id: this.id,
is_autopost: this.is_autopost,
is_community_private_autopost: this.is_community_private_autopost,
is_spoiler: this.is_spoiler,
is_app_jumpable: this.is_app_jumpable,
empathy_count: this.empathy_count,
language_id: this.language_id,
number: '0',
pid: this.pid,
platform_id: this.platform_id,
region_id: this.region_id,
reply_count: this.reply_count,
screen_name: this.screen_name,
title_id: this.title_id
};
if (this.app_data && options.app_data) {
json.app_data = this.app_data.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
}
if (options.topics && community) {
json.community_id = community.community_id;
} else {
json.community_id = this.community_id;
}
if (options.with_mii) {
json.mii = this.mii.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
json.mii_face_url = this.mii_face_url;
}
if (this.painting) {
json.painting = {
format: 'tga',
content: this.painting.replace(/[\n\r]+/gm, '').trim(),
size: this.painting.length,
url: `https://pretendo-cdn.b-cdn.net/paintings/${this.pid}/${this.id}.png`
};
}
if (this.screenshot && this.screenshot_length) {
json.screenshot = {
size: this.screenshot_length,
url: `https://pretendo-cdn.b-cdn.net/screenshots/${this.pid}/${this.id}.jpg`
};
}
if (this.topic_tag && options.topic_tag) {
json.topic_tag = {
name: this.topic_tag,
title_id: this.title_id
};
}
return json;
});
PostSchema.pre('save', async function(next) {
if (!this.id) {
await this.generatePostUID(21);

View File

@@ -1,4 +1,5 @@
import express from 'express';
import xmlbuilder from 'xmlbuilder';
import multer from 'multer';
import { z } from 'zod';
import {
@@ -9,12 +10,10 @@ import {
getUserContent,
getCommunityByTitleIDs
} from '@/database';
import comPostGen from '@/util/xmlResponseGenerator';
import { getValueFromQueryString } from '@/util';
import { LOG_WARN } from '@/logger';
import { Community } from '@/models/community';
import { Post } from '@/models/post';
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
import { CreateNewCommunityBody } from '@/types/common/create-new-community-body';
import { HydratedCommunityDocument } from '@/types/mongoose/community';
import { CommunityPostsQuery } from '@/types/mongoose/community-posts-query';
@@ -34,18 +33,31 @@ const router: express.Router = express.Router();
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
response.type('application/xml');
const community: HydratedCommunityDocument | null = await getCommunityByTitleID(request.paramPack.title_id);
if (!community) {
const parentCommunity: HydratedCommunityDocument | null = await getCommunityByTitleID(request.paramPack.title_id);
if (!parentCommunity) {
response.sendStatus(404);
return;
}
const subCommunities: HydratedCommunityDocument[] = await getSubCommunities(community.olive_community_id);
subCommunities.unshift(community);
const communities: HydratedCommunityDocument[] = await getSubCommunities(parentCommunity.olive_community_id);
communities.unshift(parentCommunity);
const communities: string = await comPostGen.Communities(subCommunities);
const json: Record<string, any> = {
result: {
has_error: '0',
version: '1',
request_name: 'communities',
communities: []
}
};
response.send(communities);
for (const community of communities) {
json.result.communities.push({
community: community.json()
});
}
response.send(xmlbuilder.create(json).end({ pretty: true, allowEmpty: true }));
});
router.get('/popular', async function (_request: express.Request, response: express.Response): Promise<void> {
@@ -142,15 +154,27 @@ router.get('/:appID/posts', async function (request: express.Request, response:
posts = await Post.find(query).sort({ created_at: -1}).limit(limit);
}
/* Build formatted response and send it off. */
const options: XMLResponseGeneratorOptions = {
name: 'posts',
with_mii: withMii === '1',
app_data: true,
topic_tag: true
const json: Record<string, any> = {
has_error: 0,
version: 1,
request_name: 'posts',
topic: {
community_id: community.community_id
},
posts: []
};
response.send(await comPostGen.PostsResponse(posts, community, options));
for (const post of posts) {
json.result.posts.push({
post: post.json({
with_mii: withMii === '1',
app_data: true,
topic_tag: true
})
});
}
response.send(xmlbuilder.create(json).end({ pretty: true, allowEmpty: true }));
});
// Handler for POST on '/v1/communities'
@@ -188,7 +212,14 @@ router.post('/', multer().none(), async function (request: express.Request, resp
app_data: request.body.app_data.replace(/[^A-Za-z0-9+/=\s]/g, ''),
});
response.send(await comPostGen.Community(community));
response.send(xmlbuilder.create({
result: {
has_error: '0',
version: '1',
request_name: 'community',
community: community.json()
}
}).end({ pretty: true, allowEmpty: true }));
});
export default router;

View File

@@ -1,9 +1,9 @@
import express from 'express';
import xmlGenerator from '@/util/xmlResponseGenerator';
import xmlbuilder from 'xmlbuilder';
import moment from 'moment';
import { getUserContent, getFollowedUsers } from '@/database';
import { getValueFromQueryString, getUserFriendPIDs } from '@/util';
import { Post } from '@/models/post';
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
import { HydratedContentDocument } from '@/types/mongoose/content';
import { CommunityPostsQuery } from '@/types/mongoose/community-posts-query';
import { HydratedPostDocument } from '@/types/mongoose/post';
@@ -69,14 +69,30 @@ router.get('/', async function (request: express.Request, response: express.Resp
posts = await Post.find(query).sort({ created_at: -1}).limit(limit);
}
/* Build formatted response and send it off. */
const options: XMLResponseGeneratorOptions = {
name: 'posts',
with_mii: withMii === '1',
topic_tag: true
const json: Record<string, any> = {
has_error: 0,
version: 1,
expire: moment().add(1, 'days').format('YYYY-MM-DD HH:MM:SS'),
request_name: 'posts',
people: []
};
response.send(await xmlGenerator.People(posts, options));
for (const post of posts) {
json.result.people.push({
person: {
posts: [
{
post: post.json({
with_mii: withMii === '1',
topic_tag: true
})
}
]
}
});
}
response.send(xmlbuilder.create(json).end({ pretty: true, allowEmpty: true }));
});
router.get('/:pid/following', async function (request: express.Request, response: express.Response): Promise<void> {
@@ -98,7 +114,22 @@ router.get('/:pid/following', async function (request: express.Request, response
const people: HydratedSettingsDocument[] = await getFollowedUsers(userContent);
response.send(await xmlGenerator.Following(people));
const json: Record<string, any> = {
result: {
has_error: '0',
version: '1',
request_name: 'user_infos',
people: []
}
};
for (const person of people) {
json.result.people.push({
person: person.json()
});
}
response.send(xmlbuilder.create(json).end({ pretty: true, allowEmpty: true }));
});
export default router;

View File

@@ -1,7 +1,7 @@
import express from 'express';
import multer from 'multer';
import xml from 'object-to-xml';
import communityPostGen from '@/util/xmlResponseGenerator';
import xmlbuilder from 'xmlbuilder';
import { z } from 'zod';
import { processPainting, uploadCDNAsset, getValueFromQueryString } from '@/util';
import {
@@ -17,7 +17,6 @@ import {
import { LOG_WARN } from '@/logger';
import { Post } from '@/models/post';
import { Community } from '@/models/community';
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
import { HydratedPostDocument, IPost } from '@/types/mongoose/post';
import { HydratedContentDocument } from '@/types/mongoose/content';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
@@ -138,13 +137,23 @@ router.get('/:post_id/replies', async function (request: express.Request, respon
return;
}
const options: XMLResponseGeneratorOptions = {
name: 'replies',
with_mii: request.query.with_mii as string === '1',
topic_tag: true
const json: Record<string, any> = {
has_error: 0,
version: 1,
request_name: 'replies',
posts: []
};
response.send(await communityPostGen.RepliesResponse(posts, options));
for (const post of posts) {
json.result.posts.push({
post: post.json({
with_mii: request.query.with_mii as string === '1',
topic_tag: true
})
});
}
response.send(xmlbuilder.create(json).end({ pretty: true, allowEmpty: true }));
});
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
@@ -178,9 +187,19 @@ router.get('/', async function (request: express.Request, response: express.Resp
message: 'Not Found'
}
}));
} else {
response.send(await communityPostGen.QueryResponse(post));
return;
}
response.send(xmlbuilder.create({
result: {
has_error: '0',
version: '1',
request_name: 'posts.search',
posts: {
post: post.json({ with_mii: true })
}
}
}).end({ pretty: true, allowEmpty: true }));
});
async function newPost(request: express.Request, response: express.Response): Promise<void> {
@@ -364,7 +383,15 @@ async function newPost(request: express.Request, response: express.Response): Pr
parentPost.save();
}
response.send(await communityPostGen.SinglePostResponse(post));
response.send(xmlbuilder.create({
result: {
has_error: '0',
version: '1',
post: {
post: post.json({ with_mii: true })
}
}
}).end({ pretty: true, allowEmpty: true }));
}
export default router;

View File

@@ -1,9 +1,10 @@
import express from 'express';
import memoize from 'memoizee';
import { getPNID, getEndpoint } from '@/database';
import moment from 'moment';
import xmlbuilder from 'xmlbuilder';
import { getPNID, getEndpoint, getPostsBytitleID } from '@/database';
import { Post } from '@/models/post';
import { Community } from '@/models/community';
import comPostGen from '@/util/xmlResponseGenerator';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
import { HydratedCommunityDocument } from '@/types/mongoose/community';
@@ -12,7 +13,10 @@ import { HydratedPostDocument } from '@/types/mongoose/post';
const router: express.Router = express.Router();
// TODO - Need to add types to memoize in @/types/memoize.d.ts
const memoized = memoize(comPostGen.topics, { async: true, maxAge: 1000 * 60 * 60 });
const memoizedGenerateTopicsXML = memoize(generateTopicsXML, {
async: true,
maxAge: 1000 * 60 * 60 // * cache for 1 hour
});
/* GET post titles. */
router.get('/', async function (request: express.Request, response: express.Response): Promise<void> {
@@ -39,9 +43,60 @@ router.get('/', async function (request: express.Request, response: express.Resp
return;
}
response.send(await memoized(communities));
response.send(await memoizedGenerateTopicsXML(communities));
});
async function generateTopicsXML(communities: HydratedCommunityDocument[]): Promise<string> {
const json: Record<string, any> = {
has_error: 0,
version: 1,
expire: moment().add(1, 'days').format('YYYY-MM-DD HH:MM:SS'),
request_name: 'topics',
topics: []
};
for (const community of communities) {
const topic: Record<string, any> = {
topic: {
empathy_count: community.empathy_count,
has_shop_page: community.has_shop_page,
icon: community.icon,
title_ids: [],
title_id: community.title_id[0],
community_id: community.community_id,
is_recommended: community.is_recommended,
name: community.name,
people: []
}
};
community.title_id.forEach(function (title_id: string) {
if (title_id !== '') {
topic.topic.title_ids.push({ title_id });
}
});
const posts: HydratedPostDocument[] = await getPostsBytitleID(community.title_id, 30);
for (const post of posts) {
topic.topic.people.push({
person: {
posts: [
{
post: post.json({
with_mii: true,
topics: true
})
}
]
}
});
}
}
return xmlbuilder.create(json).end({ pretty: true, allowEmpty: true });
}
async function calculateMostPopularCommunities(hours: number, limit: number): Promise<HydratedCommunityDocument[]> {
const now: Date = new Date();
const last24Hours: Date = new Date(now.getTime() - hours * 60 * 60 * 1000);

View File

@@ -30,10 +30,11 @@ export interface ICommunity {
}
export interface ICommunityMethods {
upEmpathy(): Promise<void>
downEmpathy(): Promise<void>
upFollower(): Promise<void>
downFollower(): Promise<void>
upEmpathy(): Promise<void>;
downEmpathy(): Promise<void>;
upFollower(): Promise<void>;
downFollower(): Promise<void>;
json(): Record<string, any>;
}
interface ICommunityQueryHelpers {}

View File

@@ -1,4 +1,4 @@
export interface XMLResponseGeneratorOptions {
export interface PostToJSONOptions {
name?: string;
with_mii: boolean;
app_data?: boolean;

View File

@@ -1,4 +1,6 @@
import { Model, Types, HydratedDocument } from 'mongoose';
import { HydratedCommunityDocument } from '@/types/mongoose/community';
import { PostToJSONOptions } from '@/types/mongoose/post-to-json-options';
export interface IPost {
id: string;
@@ -42,6 +44,7 @@ export interface IPostMethods {
remove(reason: string): Promise<void>;
unRemove(reason: string): Promise<void>;
generatePostUID(length: number): Promise<void>;
json(options: PostToJSONOptions, community?: HydratedCommunityDocument): Record<string, any>;
}
interface IPostQueryHelpers {}

View File

@@ -26,6 +26,7 @@ export interface ISettingsMethods {
relationshipVisible(active: boolean): Promise<void>;
countryVisible(active: boolean): Promise<void>;
favCommunityVisible(active: boolean): Promise<void>;
json(): Record<string, any>;
}
interface ISettingsQueryHelpers {}

View File

@@ -1,282 +0,0 @@
import xmlbuilder from 'xmlbuilder';
import moment from 'moment';
import { getPostsBytitleID } from '@/database';
import { XMLResponseGeneratorOptions } from '@/types/common/xml-response-generator-options';
import { HydratedPostDocument } from '@/types/mongoose/post';
import { HydratedCommunityDocument } from '@/types/mongoose/community';
import { HydratedSettingsDocument } from '@/types/mongoose/settings';
class XmlResponseGenerator {
static RepliesResponse(posts: HydratedPostDocument[], options: XMLResponseGeneratorOptions): string {
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', 'replies').up()
.e('posts');
for (const post of posts) {
postObj(xml, post, options, null);
}
return xml.up().end({ pretty: true, allowEmpty: true });
}
static PostsResponse(posts: HydratedPostDocument[], community: HydratedCommunityDocument, options: XMLResponseGeneratorOptions): string {
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', options.name).up()
.e('topic')
.e('community_id', community.community_id).up()
.up()
.e('posts');
for (const post of posts) {
postObj(xml, post, options, null);
}
return xml.up().end({ pretty: true, allowEmpty: true });
}
static EmptyResponse(): string {
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up();
return xml.end({ pretty: true, allowEmpty: true });
}
static Communities(communities: HydratedCommunityDocument[]): string {
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', 'communities').up()
.e('communities');
for (const community of communities) {
xml = xml.e('community')
.e('community_id', community.community_id).up()
.e('name', community.name).up()
.e('description', community.description).up()
.e('icon').up()
.e('icon_3ds').up()
.e('pid').up()
.e('app_data', community.app_data).up()
.e('is_user_community', 0).up()
.up();
}
return xml.up().end({ pretty: true, allowEmpty: true });
}
static Community(community: HydratedCommunityDocument): string {
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', 'community').up()
.e('community')
.e('community_id', community.community_id).up()
.e('name', community.name).up()
.e('description', community.description).up()
.e('icon').up()
.e('icon_3ds').up()
.e('pid').up()
.e('app_data', community.app_data).up()
.e('is_user_community', 0)
.up();
return xml.up().end({ pretty: true, allowEmpty: true });
}
static SinglePostResponse(post: HydratedPostDocument): string {
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('post');
const options: XMLResponseGeneratorOptions = {
with_mii: true
};
postObj(xml, post, options, null);
return xml.up().end({ pretty: true, allowEmpty: true });
}
static QueryResponse(post: HydratedPostDocument): string {
const xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', 'posts.search').up()
.e('posts');
const options: XMLResponseGeneratorOptions = {
with_mii: true
};
postObj(xml, post, options, null);
return xml.up().end({ pretty: true, allowEmpty: true });
}
/**
* Generate response to /v1/topics
* @param communities
* @returns xml
*/
static async topics(communities: HydratedCommunityDocument[]): Promise<string> {
const expirationDate = moment().add(1, 'days');
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', 'topics').up()
.e('expire', expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
.e('topics');
for (const community of communities) {
const posts = await getPostsBytitleID(community.title_id, 30);
xml = xml.e('topic')
.e('empathy_count', community.empathy_count).up()
.e('has_shop_page', community.has_shop_page).up()
.e('icon', community.icon).up()
.e('title_ids');
community.title_id.forEach(function (title_id: string) {
if (title_id !== '') {
xml = xml.e('title_id', title_id).up();
}
});
xml = xml.up()
.e('title_id', community.title_id[0]).up()
.e('community_id', community.community_id).up()
.e('is_recommended', community.is_recommended).up()
.e('name', community.name).up()
.e('people');
for (const post of posts) {
xml = xml.e('person').e('posts');
const options: XMLResponseGeneratorOptions = { with_mii: true,
app_data: false,
topic_tag: false,
topics: true
};
postObj(xml, post, options, community);
xml = xml.up().up();
}
xml = xml.up().up();
}
return xml.end({ pretty: false, allowEmpty: true });
}
static Following(people: HydratedSettingsDocument[]): string {
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('request_name', 'user_infos').up()
.e('people');
for (const person of people) {
xml = xml.e('person')
.e('pid', person.pid).up()
.e('screen_name', person.screen_name).up()
.up();
}
return xml.up().end({ pretty: true, allowEmpty: true });
}
static People(posts: HydratedPostDocument[], options: XMLResponseGeneratorOptions): string {
const expirationDate = moment().add(1, 'days');
let xml: xmlbuilder.XMLElement = xmlbuilder.create('result', { encoding: 'UTF-8' })
.e('has_error', '0').up()
.e('version', '1').up()
.e('expire', expirationDate.format('YYYY-MM-DD HH:MM:SS')).up()
.e('request_name', options.name).up()
.e('people');
for (const post of posts) {
xml = xml.e('person').e('posts');
postObj(xml, post, options, null);
xml = xml.up().up();
}
return xml.up().end({ pretty: true, allowEmpty: true });
}
}
function postObj(xml: xmlbuilder.XMLElement, post: HydratedPostDocument, options: XMLResponseGeneratorOptions, community: HydratedCommunityDocument | null): void {
xml = xml.e('post');
if (post.app_data && options.app_data) {
xml.e('app_data', post.app_data.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim()).up();
}
xml.e('body', post.body ? post.body.replace(/[^A-Za-z\d\s-_!@#$%^&*(){}+=,.<>/?;:'"[\]]/g, '').replace(/[\n\r]+/gm, '') : '').up();
if (options.topics && community) {
xml.e('community_id', community.community_id).up();
} else {
xml.e('community_id', post.community_id).up();
}
xml.e('country_id', post.country_id ? post.country_id : 254).up()
.e('created_at', moment(post.created_at).format('YYYY-MM-DD HH:MM:SS')).up()
.e('feeling_id', post.feeling_id).up()
.e('id', post.id).up()
.e('is_autopost', post.is_autopost).up()
.e('is_community_private_autopost', post.is_community_private_autopost).up()
.e('is_spoiler', post.is_spoiler).up()
.e('is_app_jumpable', post.is_app_jumpable).up()
.e('empathy_count', post.empathy_count).up()
.e('language_id', post.language_id).up();
if (options.with_mii) {
xml.e('mii', post.mii.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim()).up()
.e('mii_face_url', post.mii_face_url).up();
}
xml.e('number', '0').up();
if (post.painting) {
xml.e('painting')
.e('format', 'tga').up()
.e('content', post.painting.replace(/[\n\r]+/gm, '').trim()).up()
.e('size', post.painting.length).up()
.e('url', `https://pretendo-cdn.b-cdn.net/paintings/${post.pid}/${post.id}.png`).up()
.up();
}
xml.e('pid', post.pid).up()
.e('platform_id', post.platform_id).up()
.e('region_id', post.region_id).up()
.e('reply_count', post.reply_count).up()
.e('screen_name', post.screen_name).up();
if (post.screenshot && post.screenshot_length) {
xml.e('screenshot')
.e('size', post.screenshot_length).up()
.e('url', `https://pretendo-cdn.b-cdn.net/screenshots/${post.pid}/${post.id}.jpg`).up()
.up();
}
if (post.topic_tag && options.topic_tag) {
xml.e('topic_tag')
.e('name', post.topic_tag).up()
.e('title_id', post.title_id).up()
.up();
}
xml.e('title_id', post.title_id).up().up();
}
export default XmlResponseGenerator;