From 4cfe257dcf452b1b6e441f311acda213ded6a5b3 Mon Sep 17 00:00:00 2001 From: terwer Date: Tue, 23 Aug 2022 20:14:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:#27=20=E9=80=82=E9=85=8Djvue=E3=80=81cnblo?= =?UTF-8?q?gs=E6=96=87=E7=AB=A0=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/themes/default/css/post.module.css | 2 +- components/themes/default/defaultHeader.tsx | 4 +- .../themes/default/defaultHomePostList.tsx | 25 +- components/themes/default/defaultLayout.tsx | 9 +- components/themes/default/defaultNavbar.tsx | 14 +- lib/common/post.ts | 8 +- lib/constants/metaweblogMethodConstants.ts | 15 + lib/constants/postStatusConstants.ts | 9 + lib/htmlUtil.ts | 28 +- lib/logUtil.ts | 13 +- lib/metaweblog/README.md | 2 +- lib/metaweblog/cnblogsApiAdaptor.ts | 6 +- lib/metaweblog/confApiAdaptor.ts | 6 +- lib/metaweblog/customXmlrpc.ts | 45 +++ lib/metaweblog/jvueApiAdaptor.ts | 6 +- lib/metaweblog/metaWeblogApi.ts | 270 ++++++++++++++++++ lib/metaweblog/metaWeblogApiAdaptor.ts | 19 +- lib/metaweblog/nodeXmlrpc.ts | 56 ++++ lib/metaweblog/xmlrpc.ts | 60 ++++ lib/siyuan/siYuanApi.ts | 8 +- lib/siyuan/siYuanApiAdaptor.ts | 16 +- lib/util.ts | 64 +++++ package.json | 5 +- pages/index.tsx | 106 +++---- pages/post/[slug].tsx | 33 ++- yarn.lock | 31 +- 26 files changed, 732 insertions(+), 128 deletions(-) create mode 100644 lib/constants/metaweblogMethodConstants.ts create mode 100644 lib/constants/postStatusConstants.ts create mode 100644 lib/metaweblog/customXmlrpc.ts create mode 100644 lib/metaweblog/metaWeblogApi.ts create mode 100644 lib/metaweblog/nodeXmlrpc.ts create mode 100644 lib/metaweblog/xmlrpc.ts create mode 100644 lib/util.ts diff --git a/components/themes/default/css/post.module.css b/components/themes/default/css/post.module.css index 249ea8a1..35cfb228 100644 --- a/components/themes/default/css/post.module.css +++ b/components/themes/default/css/post.module.css @@ -63,7 +63,7 @@ } .postPublish{ - + margin-bottom: 10px; } .postPublish a{ diff --git a/components/themes/default/defaultHeader.tsx b/components/themes/default/defaultHeader.tsx index b4a65927..1a080cd8 100644 --- a/components/themes/default/defaultHeader.tsx +++ b/components/themes/default/defaultHeader.tsx @@ -4,7 +4,7 @@ import SiteConfig from "../../../lib/common/siteconfig"; import Image from "next/image"; import headerStyles from "./css/header.module.css" -export default function DefaultHeader({props,keyword}: { props: SiteConfig,keyword?:string }) { +export default function DefaultHeader({props, keyword, type}: { props: SiteConfig, keyword?: string, type: string }) { return ( <> @@ -21,7 +21,7 @@ export default function DefaultHeader({props,keyword}: { props: SiteConfig,keywo data-recalc-dims="1"/> - + ) diff --git a/components/themes/default/defaultHomePostList.tsx b/components/themes/default/defaultHomePostList.tsx index 7a3b0edf..ced8cd47 100644 --- a/components/themes/default/defaultHomePostList.tsx +++ b/components/themes/default/defaultHomePostList.tsx @@ -1,16 +1,31 @@ import {Post} from "../../../lib/common/post"; import {Card, ListGroup} from "react-bootstrap"; +import {getQueryString, isEmptyString} from "../../../lib/util"; +import {API_TYPE_CONSTANTS} from "../../../lib/constants"; -export default function DefaultHomePostList({posts}: { posts: Post[] }) { +const getPermalink = function (postid: string, type: string) { + let postUrl = "/post/" + postid + ".html" + if (!isEmptyString(type) && type != API_TYPE_CONSTANTS.API_TYPE_SIYUAN) { + postUrl = "/post/" + postid + ".html?t=" + type + } + return postUrl +} + +export default function DefaultHomePostList({posts, type}: { posts: Post[], type: string }) { return ( 最近更新 - {posts.map((post) => ( - - {post.title} + {posts.length > 0 ? + posts.map((post) => ( + + {post.title} + + )) : + + ~未查询到内容~ - ))} + } ) diff --git a/components/themes/default/defaultLayout.tsx b/components/themes/default/defaultLayout.tsx index 4679a240..9337880d 100644 --- a/components/themes/default/defaultLayout.tsx +++ b/components/themes/default/defaultLayout.tsx @@ -11,12 +11,17 @@ import SiteConfig from "../../../lib/common/siteconfig"; * @param children * @constructor */ -export default function DefaultLayout({props,keyword,children}: { props:SiteConfig,keyword?:string,children: any }) { +export default function DefaultLayout({ + props, + keyword, + children, + type + }: { props: SiteConfig, keyword?: string, children: any, type: string }) { return ( <> - + diff --git a/components/themes/default/defaultNavbar.tsx b/components/themes/default/defaultNavbar.tsx index 82402e68..9ba6ac33 100644 --- a/components/themes/default/defaultNavbar.tsx +++ b/components/themes/default/defaultNavbar.tsx @@ -6,8 +6,10 @@ import {faBook, faDownload, faFile, faFileText, faHome, faPieChart} from '@forta import SiteConfig from "../../../lib/common/siteconfig"; import {useState} from "react"; import Image from "next/image"; +import {isEmptyString} from "../../../lib/util"; +import {API_TYPE_CONSTANTS} from "../../../lib/constants"; -export default function DefaultNavbar({props, keyword}: { props: SiteConfig, keyword?: string }) { +export default function DefaultNavbar({props, keyword, type}: { props: SiteConfig, keyword?: string, type: string }) { let [value, setValue] = useState("") @@ -16,10 +18,18 @@ export default function DefaultNavbar({props, keyword}: { props: SiteConfig, key window.location.href = "/s/" + value } + const getHomelink = function (type: string) { + let homeLink = "/" + if (!isEmptyString(type) && type != API_TYPE_CONSTANTS.API_TYPE_SIYUAN) { + homeLink = "/?t=" + type + } + return homeLink + } + return ( - + {props.webname}/ { diff --git a/lib/common/post.ts b/lib/common/post.ts index f0005eeb..0e0e0bb6 100644 --- a/lib/common/post.ts +++ b/lib/common/post.ts @@ -1,6 +1,8 @@ /** * 通用文章模型定义 */ +import {POST_STATUS_CONSTANTS} from "../constants/postStatusConstants"; + export class Post { postid: string title: string @@ -17,8 +19,9 @@ export class Post { dateCreated: Date categories: Array mt_text_more?: string + post_status?:string isPublished: boolean - postPassword: string + wp_password: string constructor() { this.postid = "" @@ -30,6 +33,7 @@ export class Post { this.dateCreated = new Date() this.categories = [] this.isPublished = true - this.postPassword = "" + this.post_status = POST_STATUS_CONSTANTS.POST_STATUS_PUBLISH + this.wp_password = "" } } \ No newline at end of file diff --git a/lib/constants/metaweblogMethodConstants.ts b/lib/constants/metaweblogMethodConstants.ts new file mode 100644 index 00000000..72bb6e21 --- /dev/null +++ b/lib/constants/metaweblogMethodConstants.ts @@ -0,0 +1,15 @@ +const GET_USERS_BLOGS = "blogger.getUsersBlogs" +const NEW_POST = "metaWeblog.newPost" +const EDIT_POST = "metaWeblog.editPost" +const DELETE_POST = "blogger.deletePost" +const GET_RECENT_POSTS = "metaWeblog.getRecentPosts" +const GET_POST = "metaWeblog.getPost" + +export const METAWEBLOG_METHOD_CONSTANTS = { + GET_USERS_BLOGS, + NEW_POST, + EDIT_POST, + DELETE_POST, + GET_RECENT_POSTS, + GET_POST +} \ No newline at end of file diff --git a/lib/constants/postStatusConstants.ts b/lib/constants/postStatusConstants.ts new file mode 100644 index 00000000..084cd50e --- /dev/null +++ b/lib/constants/postStatusConstants.ts @@ -0,0 +1,9 @@ +const POST_STATUS_PUBLISH = "publish" +const POST_TYPE_DRAFT = "draft" +const POST_TYPE_INHERIT = "inherit" + +export const POST_STATUS_CONSTANTS = { + POST_STATUS_PUBLISH, + POST_TYPE_DRAFT, + POST_TYPE_INHERIT +} \ No newline at end of file diff --git a/lib/htmlUtil.ts b/lib/htmlUtil.ts index b9d22f3e..dfadbe7a 100644 --- a/lib/htmlUtil.ts +++ b/lib/htmlUtil.ts @@ -14,24 +14,44 @@ import {render} from "./markdownUtil"; +/** + * 移除标题数字 + * @param str + */ +export function removeTitleNumber(str: string) { + let newstr = str + + // 移除序号 + const publisherRegex = /([0-9]*)\./g; + newstr = newstr.replace(publisherRegex, "") + + return newstr +} + /** * 删除挂件的HTML * @param str 原字符 * @returns {*|string} 删除后的字符 */ export function removeWidgetTag(str: string) { + let newstr = str + // 旧版发布挂件 const publisherRegex = //g; - str = str.replaceAll(publisherRegex, "") + newstr = newstr.replace(publisherRegex, "") // 新版发布挂件 const syPublisherRegex = //g; - str = str.replaceAll(syPublisherRegex, "") + newstr = newstr.replace(syPublisherRegex, "") // 文章属性挂件 const noteAttrRegex = //g - str = str.replaceAll(noteAttrRegex, "") - return str + newstr = newstr.replace(noteAttrRegex, "") + + const h1Regex = //g + newstr = newstr.replace(h1Regex, "") + + return newstr } /** diff --git a/lib/logUtil.ts b/lib/logUtil.ts index cffc0b2a..0ee9d309 100644 --- a/lib/logUtil.ts +++ b/lib/logUtil.ts @@ -13,7 +13,8 @@ const LOG_ERROR_ENABLED = true const logInfo = (msg: any, param?: any) => { if (LOG_INFO_ENABLED) { if (param) { - console.log(msg, param) + console.log(msg) + console.log(param) } else { console.log(msg) } @@ -27,7 +28,8 @@ const logInfo = (msg: any, param?: any) => { const logWarn = (msg: any, param?: any) => { if (LOG_WARN_ENABLED) { if (param) { - console.warn(msg, param) + console.warn(msg) + console.warn(param) } else { console.warn(msg) } @@ -41,7 +43,8 @@ const logWarn = (msg: any, param?: any) => { const logError = (msg: any, param?: any) => { if (LOG_ERROR_ENABLED) { if (param) { - console.error(msg, param) + console.error(msg) + console.error(param) } else { console.error(msg) } @@ -51,10 +54,10 @@ const logError = (msg: any, param?: any) => { /** * 日志记录 */ -const log = { +const logUtil = { logInfo, logWarn, logError } -export default log \ No newline at end of file +export default logUtil \ No newline at end of file diff --git a/lib/metaweblog/README.md b/lib/metaweblog/README.md index 806c5feb..846477ed 100644 --- a/lib/metaweblog/README.md +++ b/lib/metaweblog/README.md @@ -5,7 +5,7 @@ ```json { "dependencies": { - "metaweblog-api": "^1.2.0" + "xmlrpc": "^1.3.2" } } ``` \ No newline at end of file diff --git a/lib/metaweblog/cnblogsApiAdaptor.ts b/lib/metaweblog/cnblogsApiAdaptor.ts index baa2c8f7..b70938cc 100644 --- a/lib/metaweblog/cnblogsApiAdaptor.ts +++ b/lib/metaweblog/cnblogsApiAdaptor.ts @@ -1,7 +1,7 @@ import {IApi} from "../api"; import {API_TYPE_CONSTANTS} from "../constants"; import {MetaWeblogApiAdaptor} from "./metaWeblogApiAdaptor"; -import MetaWeblog from "metaweblog-api"; +import {MetaWeblogApi} from "./metaWeblogApi"; /** * 博客园的API适配器 @@ -10,9 +10,11 @@ export class CnblogsApiAdaptor extends MetaWeblogApiAdaptor implements IApi { constructor() { super(); - this.metaWeblog = new MetaWeblog(process.env.CNBLOGS_API_URL || ""); + this.apiUrl = process.env.CNBLOGS_API_URL || "" this.username = process.env.CNBLOGS_USERNAME || "" this.password = process.env.CNBLOGS_PASSWORD || "" this.appkey = API_TYPE_CONSTANTS.API_TYPE_CNBLOGS + + this.metaWeblog = new MetaWeblogApi(this.appkey, this.apiUrl, this.username, this.password); } } \ No newline at end of file diff --git a/lib/metaweblog/confApiAdaptor.ts b/lib/metaweblog/confApiAdaptor.ts index 1a4a62d1..2b49bb1e 100644 --- a/lib/metaweblog/confApiAdaptor.ts +++ b/lib/metaweblog/confApiAdaptor.ts @@ -1,7 +1,7 @@ import {IApi} from "../api"; import {MetaWeblogApiAdaptor} from "./metaWeblogApiAdaptor"; import {API_TYPE_CONSTANTS} from "../constants"; -import MetaWeblog from "metaweblog-api"; +import {MetaWeblogApi} from "./metaWeblogApi"; /** * Confluence的API适配器 @@ -11,9 +11,11 @@ export class ConfApiAdaptor extends MetaWeblogApiAdaptor implements IApi { constructor() { super(); - this.metaWeblog = new MetaWeblog(process.env.CONF_API_URL || ""); + this.apiUrl = process.env.CONF_API_URL || "" this.username = process.env.CONF_USERNAME || "" this.password = process.env.CONF_PASSWORD || "" this.appkey = API_TYPE_CONSTANTS.API_TYPE_CONF + + this.metaWeblog = new MetaWeblogApi(this.appkey, this.apiUrl, this.username, this.password); } } \ No newline at end of file diff --git a/lib/metaweblog/customXmlrpc.ts b/lib/metaweblog/customXmlrpc.ts new file mode 100644 index 00000000..8c474d7e --- /dev/null +++ b/lib/metaweblog/customXmlrpc.ts @@ -0,0 +1,45 @@ +import logUtil from "../logUtil"; + +const Serializer = require('xmlrpc/lib/serializer') +// const xmlParser = require('xml2json'); +const {XMLParser} = require('fast-xml-parser'); +const options = { + ignoreAttributes : false +}; +const xmlParser = new XMLParser(options); + +/** + * 自定义xmlrpc的请求与解析,解决apache xmlrpc的扩展问题 + * @param apiUrl + * @param reqMethod + * @param reqParams + */ +export async function fetchCustom(apiUrl: string, reqMethod: string, reqParams: Array) { + let ret + + try { + const methodBodyXml = Serializer.serializeMethodCall(reqMethod, reqParams, "utf8") + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "content-type": "text/xml" + }, + body: methodBodyXml + }) + const resXml = await response.text() + logUtil.logInfo("resXml=>", resXml) + + const parseResult: any = xmlParser.parse(resXml) + logUtil.logInfo("parseResult=>", parseResult) + + const resJson = parseResult.methodResponse || {} + logUtil.logInfo("resJson=>", JSON.stringify(resJson)) + + return resJson + } catch (e: any) { + throw new Error(e) + } + + return ret +} \ No newline at end of file diff --git a/lib/metaweblog/jvueApiAdaptor.ts b/lib/metaweblog/jvueApiAdaptor.ts index 374f02c2..caf56abe 100644 --- a/lib/metaweblog/jvueApiAdaptor.ts +++ b/lib/metaweblog/jvueApiAdaptor.ts @@ -1,7 +1,7 @@ import {IApi} from "../api"; import {MetaWeblogApiAdaptor} from "./metaWeblogApiAdaptor"; -import MetaWeblog from "metaweblog-api"; import {API_TYPE_CONSTANTS} from "../constants"; +import {MetaWeblogApi} from "./metaWeblogApi"; /** * JVue的API适配器 @@ -10,9 +10,11 @@ export class JvueApiAdaptor extends MetaWeblogApiAdaptor implements IApi { constructor() { super(); - this.metaWeblog = new MetaWeblog(process.env.JVUE_API_URL || ""); + this.apiUrl = process.env.JVUE_API_URL || "" this.username = process.env.JVUE_USERNAME || "" this.password = process.env.JVUE_PASSWORD || "" this.appkey = API_TYPE_CONSTANTS.API_TYPE_JVUE + + this.metaWeblog = new MetaWeblogApi(this.appkey, this.apiUrl, this.username, this.password); } } \ No newline at end of file diff --git a/lib/metaweblog/metaWeblogApi.ts b/lib/metaweblog/metaWeblogApi.ts new file mode 100644 index 00000000..cef5f469 --- /dev/null +++ b/lib/metaweblog/metaWeblogApi.ts @@ -0,0 +1,270 @@ +import {XmlrpcClient} from "./xmlrpc"; +import {UserBlog} from "../common/userBlog"; +import {Post} from "../common/post"; +import logUtil from "../logUtil"; +import {METAWEBLOG_METHOD_CONSTANTS} from "../constants/metaweblogMethodConstants"; +import {POST_STATUS_CONSTANTS} from "../constants/postStatusConstants"; +import {inBrowser, isEmptyString} from "../util"; +import {render} from "../markdownUtil"; + +export class MetaWeblogApi { + private readonly apiType: string + private readonly apiUrl: string + private readonly username: string + private readonly password: string + + private readonly xmlrpcClient: any + + constructor(apiType: string, apiUrl: string, username: string, password: string) { + this.apiType = apiType + this.apiUrl = apiUrl + this.username = username + this.password = password + + this.xmlrpcClient = new XmlrpcClient(this.apiType, this.apiUrl, this.username, this.password) + } + + /** + * 错误处理 + * @param ret + * @private + */ + private doFault(ret: any) { + const faultObj = ret.fault + if (faultObj) { + const fault = faultObj.value.struct.member + const faultCode = this.parseFieldValue(fault, "faultCode") + const faultString = this.parseFieldValue(fault, "faultString") + throw new Error("发生异常,错误码=>" + faultCode + ",错误信息=>" + faultString) + } + } + + public async getUsersBlogs(appkey: string, username: string, password: string): Promise> { + const usersBlogs: Array = [] + let ret = await this.xmlrpcClient.methodCallEntry(METAWEBLOG_METHOD_CONSTANTS.GET_USERS_BLOGS, + [this.apiType, username, password]) + logUtil.logInfo("getUsersBlogs ret=>") + logUtil.logInfo(ret) + + // 错误处理 + this.doFault(ret) + + // 数据适配 + const dataArr = ret.params.param.value.array.data.value.struct.member || [] + + const userBlog = new UserBlog() + userBlog.blogid = this.parseFieldValue(dataArr, "blogid") + userBlog.url = this.parseFieldValue(dataArr, "url") + userBlog.blogName = this.parseFieldValue(dataArr, "blogName") + + usersBlogs.push(userBlog) + + return usersBlogs + } + + /** + * 解析字段 + * @param dataArr + * @param key + * @private + */ + private parseFieldValue(dataArr: [], key: string) { + let val + + for (let i = 0; i < dataArr.length; i++) { + const obj: any = dataArr[i] + + if (obj.name == key) { + if (typeof obj.value == "string") { + val = obj.value + break + } else { + val = obj.value.string || obj.value.int || obj.value.i4 + break + } + } + + } + + return val + } + + public async getRecentPosts(appkey: string, username: string, password: string, numOfPosts: number): Promise> { + let result = >[] + + const ret = await this.xmlrpcClient.methodCallEntry(METAWEBLOG_METHOD_CONSTANTS.GET_RECENT_POSTS, + [this.apiType, username, password, numOfPosts]) + logUtil.logInfo("getRecentPosts ret=>") + logUtil.logInfo(ret) + + // 错误处理 + this.doFault(ret) + + const postArray = ret.params.param.value.array.data.value || [] + postArray.forEach((item: any) => { + const post = this.parsePost(item) + result.push(post) + }) + + logUtil.logInfo("result=>", result) + return result + } + + private parsePost(postStruct: any): Post { + const post = new Post() + const postObj = postStruct.struct.member + post.mt_keywords = this.parseFieldValue(postObj, "mt_keywords") + post.title = this.parseFieldValue(postObj, "title") + post.link = this.parseFieldValue(postObj, "link") + post.permalink = this.parseFieldValue(postObj, "permalink") + post.postid = this.parseFieldValue(postObj, "postid") + post.description = this.parseFieldValue(postObj, "description") + post.mt_excerpt = this.parseFieldValue(postObj, "mt_excerpt") + post.wp_slug = this.parseFieldValue(postObj, "wp_slug") + post.dateCreated = this.parseFieldValue(postObj, "dateCreated") + post.categories = this.parseFieldValue(postObj, "categories") + post.mt_text_more = this.parseFieldValue(postObj, "mt_text_more") + + return post + } + + public async getPost(postid: string, username: string, password: string): Promise { + let result = >[] + + const ret = await this.xmlrpcClient.methodCallEntry(METAWEBLOG_METHOD_CONSTANTS.GET_POST, + [postid, username, password]) + logUtil.logInfo("getPost ret=>") + logUtil.logInfo(ret) + + // 错误处理 + this.doFault(ret) + + const postStruct = ret.params.param.value || [] + const post = this.parsePost(postStruct) + + post.description = render(post.description) + + return Promise.resolve(post) + } + + /** + * 新建文章 + * @param blogid + * @param username + * @param password + * @param post + * @param publish + */ + public async newPost(blogid: string, username: string, password: string, post: Post, publish: boolean): Promise { + // 草稿 + if (!publish) { + post.post_status = POST_STATUS_CONSTANTS.POST_TYPE_DRAFT + } + + const postStruct = this.createPostStruct(post) + logUtil.logWarn("postStruct=>") + logUtil.logWarn(postStruct) + let ret = await this.xmlrpcClient.methodCallEntry(METAWEBLOG_METHOD_CONSTANTS.NEW_POST, + [this.apiType, username, password, postStruct, publish]) + ret = ret.replace(/"/g, "") + logUtil.logInfo("newPost ret=>") + logUtil.logInfo(ret) + + return ret; + } + + public async editPost(postid: string, username: string, password: string, post: Post, publish: boolean): Promise { + // 草稿 + if (!publish) { + post.post_status = POST_STATUS_CONSTANTS.POST_TYPE_DRAFT + } + + const postStruct = this.createPostStruct(post) + logUtil.logWarn("postStruct=>") + logUtil.logWarn(postStruct) + const ret = await this.xmlrpcClient.methodCallEntry(METAWEBLOG_METHOD_CONSTANTS.EDIT_POST, + [postid, username, password, postStruct, publish]) + logUtil.logInfo("editPost ret=>") + logUtil.logInfo(ret) + + return ret; + } + + public async deletePost(appKey: string, postid: string, username: string, password: string, publish: boolean) { + const ret = await this.xmlrpcClient.methodCallEntry(METAWEBLOG_METHOD_CONSTANTS.DELETE_POST, + [appKey, postid, username, password, publish]) + logUtil.logInfo("deletePost ret=>") + logUtil.logInfo(ret) + + return ret; + }; + + /** + * 适配文章字段 + * @param post 原始文章 + * @private + */ + private createPostStruct(post: Post): object { + let postObj = {} + + if (!isEmptyString(post.title)) { + Object.assign(postObj, { + title: post.title + }) + } + + if (!isEmptyString(post.mt_keywords)) { + Object.assign(postObj, { + mt_keywords: post.mt_keywords + }) + } + + if (!isEmptyString(post.description)) { + Object.assign(postObj, { + description: post.description + }) + } + + if (!isEmptyString(post.wp_slug)) { + Object.assign(postObj, { + wp_slug: post.wp_slug + }) + } + + // 浏览器端的date转换有问题 + if (!inBrowser()) { + Object.assign(postObj, { + // 这里要注意时间格式 + // http://www.ab-weblog.com/en/create-new-posts-with-publishing-date-in-wordpress-using-xml-rpc-and-php/ + // dateCreated: post.dateCreated.toISOString() || new Date().toISOString() + dateCreated: post.dateCreated || new Date() + }) + } + + Object.assign(postObj, { + categories: post.categories || [], + }) + + Object.assign(postObj, { + post_status: post.post_status || POST_STATUS_CONSTANTS.POST_STATUS_PUBLISH, + }) + + if (!isEmptyString(post.wp_password)) { + Object.assign(postObj, { + wp_password: post.wp_password + }) + } + + return postObj; + // return { + // title: post.title || '', + // mt_keywords: post.mt_keywords || '', + // description: post.description || '', + // wp_slug: post.wp_slug || '', + // dateCreated: post.dateCreated.toISOString() || new Date().toISOString(), + // categories: post.categories || [], + // post_status: post.post_status || POST_STATUS_CONSTANTS.POST_STATUS_PUBLISH, + // wp_password: post.wp_password || '' + // } + } +} \ No newline at end of file diff --git a/lib/metaweblog/metaWeblogApiAdaptor.ts b/lib/metaweblog/metaWeblogApiAdaptor.ts index ace9338e..ab09b70c 100644 --- a/lib/metaweblog/metaWeblogApiAdaptor.ts +++ b/lib/metaweblog/metaWeblogApiAdaptor.ts @@ -1,21 +1,26 @@ import {IApi} from "../api"; import {Post} from "../common/post"; import {UserBlog} from "../common/userBlog"; +import logUtil from "../logUtil"; /** * 博客园的API适配器 */ export class MetaWeblogApiAdaptor implements IApi { - protected metaWeblog: any + protected apiUrl: string protected username: string protected password: string protected appkey: string + protected metaWeblog: any + constructor() { - this.metaWeblog = null; + this.apiUrl = "" this.username = "" this.password = "" this.appkey = "" + + this.metaWeblog = null } /** @@ -26,7 +31,15 @@ export class MetaWeblogApiAdaptor implements IApi { public async getUsersBlogs(): Promise> { let result: Array = [] const data = await this.metaWeblog.getUsersBlogs(this.appkey, this.username, this.password); - // TODO + // logUtil.logInfo("data=>", data) + + data.forEach((item:any)=>{ + const userBlog = new UserBlog(); + userBlog.blogid = item.blogid + userBlog.url = item.url + userBlog.blogName = item.blogName + result.push(userBlog) + }) return result; } diff --git a/lib/metaweblog/nodeXmlrpc.ts b/lib/metaweblog/nodeXmlrpc.ts new file mode 100644 index 00000000..337effe2 --- /dev/null +++ b/lib/metaweblog/nodeXmlrpc.ts @@ -0,0 +1,56 @@ +import logUtil from "../logUtil"; + +const xmlrpc = require('xmlrpc'); + +/** + * 已废弃,不支持apache xmlrpc扩展返回值的解析 + * @deprecated + * @param apiUrl + * @param reqMethod + * @param reqParams + */ +export async function fetchNode(apiUrl: string, reqMethod: string, reqParams: Array) { + let client + + const secure = apiUrl.indexOf('https:') > -1; + if (secure) { + client = xmlrpc.createSecureClient(apiUrl); + } else { + client = xmlrpc.createClient(apiUrl); + } + + try { + logUtil.logWarn("methodCallDirectNode开始") + logUtil.logWarn("xmlrpcNodeParams.reqMethod=>") + logUtil.logWarn(reqMethod) + logUtil.logWarn("xmlrpcNodeParams.reqParams=>") + logUtil.logWarn(reqParams) + const data = await methodCallDirectNode(client, reqMethod, reqParams) + const dataJson = JSON.stringify(data) + return dataJson + } catch (e) { + logUtil.logError(e) + throw new Error("请求处理异常") + } +} + +// xmlrpc +/* + * Makes an XML-RPC call to the server and returns a Promise. + * @param {String} methodName - The method name. + * @param {Array} params - Params to send in the call. + * @return {Promise} + */ +async function methodCallDirectNode(client: any, methodName: string, params: any): Promise { + return new Promise(function (resolve, reject) { + client.methodCall(methodName, params, function (error: any, data: any) { + if (!error) { + logUtil.logInfo("resolve=>") + logUtil.logInfo(data) + resolve(data); + } else { + reject(error); + } + }); + }); +} \ No newline at end of file diff --git a/lib/metaweblog/xmlrpc.ts b/lib/metaweblog/xmlrpc.ts new file mode 100644 index 00000000..61af4a16 --- /dev/null +++ b/lib/metaweblog/xmlrpc.ts @@ -0,0 +1,60 @@ +import {fetchNode} from "./nodeXmlrpc"; +import logUtil from "../logUtil"; +import {fetchCustom} from "./customXmlrpc"; + +/** + * Xmlrpc客户端封装类 + */ +export class XmlrpcClient { + private readonly apiType: string + private readonly apiUrl: string + private readonly username: string + private readonly password: string + + constructor(apiType: string, apiUrl: string, username: string, password: string) { + this.apiType = apiType + this.apiUrl = apiUrl + this.username = username + this.password = password + } + + /** + * 同时兼容浏览器和思源宿主环境的xmlrpc API + * @param apiUrl 端点 + * @param reqMethod 方法 + * @param reqParams 参数数组 + */ + private async fetchXmlrpc(apiUrl: string, reqMethod: string, reqParams: Array) { + let result + + logUtil.logWarn("开始使用node来fetch获取数据") + // result = await fetchNode(apiUrl, reqMethod, reqParams) + result = await fetchCustom(apiUrl, reqMethod, reqParams) + if (!result || result == "") { + throw new Error("请求错误或者返回结果为空") + } + + logUtil.logInfo("最终返回给前端的数据=>", result) + + return result + } + + /** + * xmlrpc统一调用入口 + * @param reqMethod 方法 + * @param reqMarams 参数 + */ + public async methodCallEntry(reqMethod: string, reqMarams: Array) { + const result = await this.fetchXmlrpc(this.apiUrl, reqMethod, reqMarams) + logUtil.logInfo("请求结果,result=>") + logUtil.logInfo(result) + return result + } +} + +/** + * Xmlrpc服务器封装类 + */ +export class XmlrpcServer { + +} diff --git a/lib/siyuan/siYuanApi.ts b/lib/siyuan/siYuanApi.ts index 123dab32..5047fbc6 100644 --- a/lib/siyuan/siYuanApi.ts +++ b/lib/siyuan/siYuanApi.ts @@ -1,5 +1,5 @@ import {config} from "./siYuanConfig" -import log from "../logUtil"; +import logUtil from "../logUtil"; /** * 思源API v2.0.27 @@ -109,11 +109,11 @@ async function request(url: string, data: any, method?: string, useToken?: boole }) } - log.logInfo("向思源请求数据,url=>", url) - log.logInfo("向思源请求数据,fetchOps=>", fetchOps) + logUtil.logInfo("向思源请求数据,url=>", url) + logUtil.logInfo("向思源请求数据,fetchOps=>", fetchOps) await fetch(url, fetchOps).then(function (response) { resData = response.json() - log.logInfo("向思源请求数据,resData=>", resData) + logUtil.logInfo("向思源请求数据,resData=>", resData) }) return resData } diff --git a/lib/siyuan/siYuanApiAdaptor.ts b/lib/siyuan/siYuanApiAdaptor.ts index 422612f2..b14ff3e6 100644 --- a/lib/siyuan/siYuanApiAdaptor.ts +++ b/lib/siyuan/siYuanApiAdaptor.ts @@ -3,9 +3,9 @@ import {exportMdContent, getBlockAttrs, getBlockByID, getBlockBySlug, getDoc, ge import {Post} from "../common/post"; import {UserBlog} from "../common/userBlog"; import {API_TYPE_CONSTANTS} from "../constants"; -import log from "../logUtil"; +import logUtil from "../logUtil"; import {render} from "../markdownUtil"; -import {removeWidgetTag} from "../htmlUtil"; +import {removeTitleNumber, removeWidgetTag} from "../htmlUtil"; /** * 思源笔记API适配器 @@ -53,10 +53,13 @@ export class SiYuanApiAdaptor implements IApi { // 文章别名 const customSlug = attrs["custom-slug"] || "" + let title = siyuanPost.content || "" + title = removeTitleNumber(title) + // 适配公共属性 let commonPost = new Post() commonPost.postid = siyuanPost.root_id - commonPost.title = siyuanPost.content + commonPost.title = title commonPost.permalink = customSlug == "" ? "/post/" + siyuanPost.root_id : "/post/" + customSlug + ".html" // commonPost.isPublished = isPublished // commonPost.mt_keywords = attrs.tags || "" @@ -100,15 +103,18 @@ export class SiYuanApiAdaptor implements IApi { // 移除挂件html html = removeWidgetTag(html) + let title = siyuanPost.content || "" + title = removeTitleNumber(title) + // 适配公共属性 let commonPost = new Post() commonPost.postid = siyuanPost.root_id || "" - commonPost.title = siyuanPost.content || "" + commonPost.title = title commonPost.description = html || "" commonPost.shortDesc = shortDesc || "" commonPost.mt_keywords = attrs.tags || "" commonPost.isPublished = isPublished - commonPost.postPassword = postPassword + commonPost.wp_password = postPassword // commonPost.dateCreated return commonPost diff --git a/lib/util.ts b/lib/util.ts new file mode 100644 index 00000000..8e876fe7 --- /dev/null +++ b/lib/util.ts @@ -0,0 +1,64 @@ +/** + * 是否在浏览器 + */ +export function inBrowser() { + return typeof window !== 'undefined'; +} + +/** + * 获取url参数 + * @param sParam 参数 + */ +export function getQueryString(sParam: string) { + if (!inBrowser()) { + return "" + } + const sPageURL = window.location.search.substring(1); + const sURLVariables = sPageURL.split('&'); + + for (let i = 0; i < sURLVariables.length; i++) { + const sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == sParam) { + return sParameterName[1]; + } + } +} + +/** + * 设置url参数 + * @param urlstring + * @param key + * @param value + */ +export function setUrlParameter(urlstring: string, key: string, value: string) { + if (!inBrowser()) { + return "" + } + // 已经有参数了,不重复添加 + if (urlstring.indexOf(key) > -1) { + return urlstring + } + urlstring += (urlstring.match(/[?]/g) ? '&' : '?') + key + '=' + value; + return urlstring +} + +export function isEmptyObject(obj: any) { + if (!obj) { + return true; + } + return ( + Object.getPrototypeOf(obj) === Object.prototype && + Object.getOwnPropertyNames(obj).length === 0 && + Object.getOwnPropertySymbols(obj).length === 0 + ); +} + +export function isEmptyString(str: any) { + if (!str) { + return true; + } + if (!(typeof str === 'string')) { + return true + } + return str.trim().length == 0 +} \ No newline at end of file diff --git a/package.json b/package.json index 5efe408d..e558d0df 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,13 @@ "@fortawesome/react-fontawesome": "^0.2.0", "bootstrap": "^5.2.0", "clsx": "^1.2.1", + "fast-xml-parser": "^4.0.9", "highlight.js": "^11.6.0", - "metaweblog-api": "^1.2.0", "next": "12.2.3", "react": "18.2.0", "react-bootstrap": "^2.4.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "xmlrpc": "^1.3.2" }, "devDependencies": { "@types/node": "18.6.3", diff --git a/pages/index.tsx b/pages/index.tsx index 71dc0b1f..0fb548c6 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -14,8 +14,8 @@ type Props = { const Home: NextPage = (props, context) => { return ( - - + + ) } @@ -24,58 +24,25 @@ export default Home // https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md#%E9%89%B4%E6%9D%83 // https://github.com/vercel/next.js/blob/canary/examples/cms-wordpress/pages/index.js -// export const getServerSideProps: GetServerSideProps = async (context) => { -// // Add whatever `Cache-Control` value you want here -// // 生产环境进行请求缓存 -// if(process.env.NODE_ENV=="production"){ -// context.res.setHeader( -// 'Cache-Control', -// 'public, s-maxage=1, stale-while-revalidate=59' -// ) -// } -// -// const query = context.query || {} -// if (query.t instanceof Array) { -// throw new Error("参数类型错误") -// } -// -// let cfg: SiteConfig = new SiteConfig() -// let result: Array = [] -// const type = query.t || API_TYPE_CONSTANTS.API_TYPE_SIYUAN -// const pageno = query.p -// -// const api = new API(type) -// -// // 配置 -// const cfgs = await api.getUsersBlogs() || [] -// if (cfgs.length > 0) { -// cfg.userBlog = cfgs[0] -// } -// // 文章 -// if (pageno) { -// let num = 1 -// if (typeof pageno === "string") { -// num = parseInt(pageno) || 1 -// } -// result = await api.getRecentPosts(10, num - 1) -// } else { -// result = await api.getRecentPosts(10) -// } -// -// return { -// props: { -// type: type, -// layoutCfg: JSON.parse(JSON.stringify(cfg)), -// posts: JSON.parse(JSON.stringify(result)) -// } -// } -// } +export const getServerSideProps: GetServerSideProps = async (context) => { + // Add whatever `Cache-Control` value you want here + // 生产环境进行请求缓存 + // if(process.env.NODE_ENV=="production"){ + // context.res.setHeader( + // 'Cache-Control', + // 'public, s-maxage=1, stale-while-revalidate=59' + // ) + // } + + const query = context.query || {} + if (query.t instanceof Array) { + throw new Error("参数类型错误") + } -// 鉴于性能考虑,首页采取缓存模式 -export const getStaticProps: GetStaticProps = async (context) => { let cfg: SiteConfig = new SiteConfig() let result: Array = [] - const type = API_TYPE_CONSTANTS.API_TYPE_SIYUAN + const type = query.t || API_TYPE_CONSTANTS.API_TYPE_SIYUAN + const pageno = query.p const api = new API(type) @@ -85,7 +52,15 @@ export const getStaticProps: GetStaticProps = async (context) => { cfg.userBlog = cfgs[0] } // 文章 - result = await api.getRecentPosts(10) + if (pageno) { + let num = 1 + if (typeof pageno === "string") { + num = parseInt(pageno) || 1 + } + result = await api.getRecentPosts(10, num - 1) + } else { + result = await api.getRecentPosts(10) + } return { props: { @@ -94,4 +69,29 @@ export const getStaticProps: GetStaticProps = async (context) => { posts: JSON.parse(JSON.stringify(result)) } } -} \ No newline at end of file +} + +// 鉴于性能考虑,首页采取缓存模式 +// export const getStaticProps: GetStaticProps = async (context) => { +// let cfg: SiteConfig = new SiteConfig() +// let result: Array = [] +// const type = API_TYPE_CONSTANTS.API_TYPE_SIYUAN +// +// const api = new API(type) +// +// // 配置 +// const cfgs = await api.getUsersBlogs() || [] +// if (cfgs.length > 0) { +// cfg.userBlog = cfgs[0] +// } +// // 文章 +// result = await api.getRecentPosts(10) +// +// return { +// props: { +// type: type, +// layoutCfg: JSON.parse(JSON.stringify(cfg)), +// posts: JSON.parse(JSON.stringify(result)) +// } +// } +// } \ No newline at end of file diff --git a/pages/post/[slug].tsx b/pages/post/[slug].tsx index 951958d1..1934411a 100644 --- a/pages/post/[slug].tsx +++ b/pages/post/[slug].tsx @@ -6,18 +6,25 @@ import styles from "../../components/themes/default/css/layout.module.css"; import postStyles from "../../components/themes/default/css/post.module.css" import SiteConfig from "../../lib/common/siteconfig"; import DefaultLayout from "../../components/themes/default/defaultLayout"; -import {useEffect} from "react"; +import {useEffect, useState} from "react"; import hljs from 'highlight.js' import 'highlight.js/styles/vs.css' import {Alert, Button} from "react-bootstrap"; -import DefaultPostTags from "../../components/themes/default/defaultPostTags"; +import dynamic from "next/dynamic"; +// import DefaultPostTags from "../../components/themes/default/defaultPostTags"; type Props = { type: string, propCfg: any post: Post + publishLink: string } +const DefaultPostTags = dynamic( + () => import('../../components/themes/default/defaultPostTags'), + {ssr: false} +); + const PostDetail: NextPage = (props, context) => { useEffect(() => { @@ -38,22 +45,18 @@ const PostDetail: NextPage = (props, context) => { return {__html: props.post?.description}; } - function getPublishLink() { - const pubSiteUrl = process.env.PUBLISH_SITE_URL || "" - return pubSiteUrl + "/index.html?id=" + props.post.postid - } - return ( - +
{ props.post.isPublished ?
- {props.post && props.post.title && + {(props.type == API_TYPE_CONSTANTS.API_TYPE_SIYUAN) ? + :
} {props.post && props.post.mt_keywords && @@ -87,6 +90,11 @@ const PostDetail: NextPage = (props, context) => { export default PostDetail +function getPublishLink(postid: string) { + const pubSiteUrl = process.env.PUBLISH_SITE_URL || "" + return pubSiteUrl + "/index.html?id=" + postid +} + export const getServerSideProps: GetServerSideProps = async (context) => { // 生产环境进行请求缓存 if (process.env.NODE_ENV == "production") { @@ -129,7 +137,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => // 密码授权访问 const pwd = context.query.pwd || "" - if (pwd != "" && pwd == post.postPassword) { + if (pwd != "" && pwd == post.wp_password) { post.isPublished = true } @@ -147,7 +155,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => props: { type: type, propCfg: JSON.parse(JSON.stringify(cfg)), - post: JSON.parse(JSON.stringify(post)) + post: JSON.parse(JSON.stringify(post)), + publishLink: getPublishLink(post.postid) || "" } } } diff --git a/yarn.lock b/yarn.lock index 9142bc9b..7b0281a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,11 +221,6 @@ dependencies: tslib "^2.4.0" -"@types/es6-promise@0.0.28": - version "0.0.28" - resolved "https://registry.npmmirror.com/@types/es6-promise/-/es6-promise-0.0.28.tgz#1af00118ff7b88fe3e46be943add54edeb285b75" - integrity sha512-4GA06MWCWCmuyBf+ptxrVK4BMQBXUSSCmyydk18Fvn9huS5zJdi24mEwxTn5x7sKMmGpJwKS6f1i2oLN7yL4lw== - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -236,11 +231,6 @@ resolved "https://registry.npmmirror.com/@types/node/-/node-18.6.3.tgz#4e4a95b6fe44014563ceb514b2598b3e623d1c98" integrity sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg== -"@types/node@^6.0.31": - version "6.14.13" - resolved "https://registry.npmmirror.com/@types/node/-/node-6.14.13.tgz#b6649578fc0b5dac88c4ef48a46cab33c50a6c72" - integrity sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw== - "@types/prop-types@*": version "15.7.5" resolved "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -878,6 +868,13 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-xml-parser@^4.0.9: + version "4.0.9" + resolved "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.0.9.tgz#3a81dab7b4952b8d38f0136d28bd055b80ed6512" + integrity sha512-4G8EzDg2Nb1Qurs3f7BpFV4+jpMVsdgLVuG1Uv8O2OHJfVCg7gcA53obuKbmVqzd4Y7YXVBK05oJG7hzGIdyzg== + dependencies: + strnum "^1.0.5" + fastq@^1.6.0: version "1.13.0" resolved "https://registry.npmmirror.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" @@ -1324,15 +1321,6 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -metaweblog-api@^1.2.0: - version "1.2.0" - resolved "https://registry.npmmirror.com/metaweblog-api/-/metaweblog-api-1.2.0.tgz#572981d0f91d28ecfd8f429f6552e283a6d62d6c" - integrity sha512-lbN5Ge/WSM810k26Z2AtO6iLwhni0Q9qIwy2X3tEsjLNN/gP1TIs2MV5VGltVxbTpvs2nO7F86wr7AhW5TXHpQ== - dependencies: - "@types/es6-promise" "0.0.28" - "@types/node" "^6.0.31" - xmlrpc "^1.3.2" - micromatch@^4.0.4: version "4.0.5" resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -1822,6 +1810,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.npmmirror.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + styled-jsx@5.0.2: version "5.0.2" resolved "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729"