通过 Metaweblog API 给 Hexo 接入客户端

Hexo 搭建静态博客的问题

Hexo + GitHub Pages 搭建博客的方式已经非常流行了。但是 Hexo 的写作几乎必须在电脑上进行。在 Travis 等 CI 工具的支持下,可以通过在 GitHub 页面上新建文件,编写内容,然后通过 Travis CI 持续集成,发布到 GitHub Pages 。但这个流程相当麻烦,而且在 iOS 上 GitHub 网页的那个编辑器不能直接输入中文。

解决方法有两种。

  • 搭建动态博客。给动态博客里面加入一个编辑器,这样在手机或平板上就可以进行编辑,没有 Hexo 环境也没关系。
  • 使用 Metaweblog API 。一些写作软件,如 MWeb 等都支持这个 API 。但是需要我们实现一套这样的 API,才能使用。

本文主要介绍如何使用 Express 实现这套接口。

依赖库

以下是主要用到的依赖库:

  • express
  • fs-extra
  • simple-git
  • hexo
  • md5

主要接口

MWeb 调用了两种接口,一种是 Metaweblog API ,一种是 Blogger API 。下面分别介绍参数

Metaweblog API

下面列出的是接口的参数:

接口说明参数类型参数说明
newPost新增一篇博客blogidString博客 ID ,适用于一个网站很多博客的那种(如 CSDN )
usernameString用户名
passwordString密码
postPostContent文章内容,具体类型详见下面的代码
getPost获取一篇博客postidString文章 ID ,由 newPost 接口返回
usernameString
passwordString
getCategories获取博客的分类blogidString
usernameString
passwordString
newMediaObject添加多媒体文件blogidString
usernameString
passwordString
mediaObjectMediaObject多媒体文件,具体类型详见下面的代码

上面两个结构体的定义( Typescript 描述法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface PostContent {
categories: string[]; // 分类
dateCreated: Date; // 创建日期
description: string; // 文章内容
title: string; // 文章标题
mt_keywords: string; // 标签
wp_slug: string; // 自定义 Web 链接
}

interface MediaObject {
overwrite: boolean;
bits: Buffer; // 多媒体二进制数据
name: string; // 文件名
type: string; // MIME 类型
}

interface Category {
categoryid: string; // 分类ID
description: string; // 分类描述
title: string; // 分类名称
}

interface NewMediaObjectReturnType {
url: string; // 媒体文件访问地址
}

下面是这几个接口的返回类型:

接口返回类型说明
newPostString返回的是文章的 PostID
getPostPostContent返回文章的结构体
getCategoriesCategory[]返回的是分类的结构体数组
newMediaObjectNewMediaObjectReturnType返回的是表示媒体文件访问信息的结构体

Blogger API

这里只用到了一个接口

名称说明参数参数类型参数说明返回类型
getUserBlogs获取博客信息keyString(不用)GetUserBlogsReturnType
usernameString用户名
passwordString密码

接口返回信息

1
2
3
4
interface GetUserBlogsReturnType {
blogid: string // 博客 ID
blogName: string // 博客名称
}

基于 express 和 xrpc 库的实现方式

XML-RPC 协议的解析可以利用 xrpc 库进行实现。app.ts 的写法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import * as express from 'express';
import * as path from 'path';
import * as favicon from 'serve-favicon';
import * as logger from 'morgan';
import * as cookieParser from 'cookie-parser';
import * as bodyParser from 'body-parser';
import * as xrpc from "xrpc";
import { newPost, getPost, editPost, getCategories, newMediaObject } from './metaweblog';
import { getUserBlogs } from './blogger';

var app = express();

class ExpressError extends Error {
status: number;
}

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

/**
* 增加的 xrpc 部分
*/
app.use(xrpc.xmlRpc);
app.post('/RPC', xrpc.route({
metaWeblog: {
newPost: newPost,
getPost: getPost,
getCategories: getCategories,
newMediaObject: newMediaObject
},
blogger: {
getUsersBlogs: getUserBlogs,
}
}));
/**
* RPC
*/

// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new ExpressError('Not Found');
err.status = 404;
next(err);
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;

metaweblog.ts 的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import * as md5 from "md5";
import * as fs from "fs-extra";
import * as simplegit from "simple-git/promise";
import * as path from "path";
import * as moment from "moment";
import * as Hexo from "hexo";
import { Stream } from "stream";
const config = require("./package.json");
const blog = config.meta.blog

class ExpressError extends Error {
status: number;

constructor(msg, code) {
super(msg);
this.status = code;
}
}

const hexo = new Hexo(blog, {
silent: true,
safe: true
});

interface PostContent {
categories: string[];
dateCreated: Date;
description: string;
title: string;
mt_keywords: string;
wp_slug: string;
}

interface MediaObject {
overwrite: boolean;
bits: Buffer;
name: string;
type: string;
}

export async function newPost(blogid: string, username: string, password: string, post: PostContent, publish: boolean, callback: Function) {
if (blogid) {
if (username.toLowerCase() === "hpdell" && md5(password) === "2b759a6996d878c41bb7d56ce530d031") {
const git = simplegit(blog);
if (await git.checkIsRepo()) {
try {
let postInfo = (await fs.readFile(path.resolve(path.join(blog, "scaffolds", "post.md")))).toString();
postInfo = postInfo.replace("{{ title }}", post.title);
postInfo = postInfo.replace("{{ date }}", moment(post.dateCreated).format("YYYY-MM-DD HH:mm:ss"));
if (post.categories && post.categories.length) {
postInfo = postInfo.replace("categories:", `categories: ${post.categories[0]}`);
}
if (post.mt_keywords) {
let tags = post.mt_keywords.split(",");
let tagInfo = tags.map(item => ` - ${item}`).join("\n");
postInfo = postInfo.replace("tags:", `tags:\n${tagInfo}`);
}
let content = postInfo + post.description;
let postPath = path.resolve(path.join(blog, "source", "_posts", `${post.wp_slug}.md`));
try {
await fs.writeFile(postPath, content);
try {
await publishToGitHub(git, path.join("source", "_posts", `${post.wp_slug}.md`));
callback(null, post.wp_slug);
} catch (error) {
callback(new ExpressError((error as Error).message, 500));
}
} catch (error) {
callback(new ExpressError("Internal Server Error: Cannot write post file.", 500));
}
} catch (error) {
callback(new ExpressError("Internal Server Error: Cannot read scaffolds.", 500));
}
} else {
callback(new ExpressError("Inernal Server Error: Not a hexo repo.", 500));
}
} else {
callback(new ExpressError("Username or Password is wrong.", 500));
}
} else {
callback(new ExpressError("Blog id is required.", 500));
}
}

export async function getPost(postid: string, username: string, password: string, callback: Function) {
try {
await hexo.load()
let posts = hexo.locals.get("posts").filter((v, i) => v.title === postid).toArray();
if (posts && posts.length) {
let post = posts[0];
let postStructure: PostContent = {
categories: post.categories,
dateCreated: post.date.toDate(),
description: post.content,
title: post.title,
mt_keywords: post.tags.join(","),
wp_slug: path.basename(post.path)
};
callback(null, postStructure)
}
} catch (error) {
callback(new ExpressError("Post not found", 500));
}
}

export function getCategories(blogid: string, username: string, password: string, callback: Function) {
callback(null, config.meta.categories.map((item: string) => {
return {
categoryid: item,
description: item,
title: item
}
}));
}

export async function newMediaObject(blogid: string, username: string, password: string, mediaObject: MediaObject, callback: Function) {
if (blogid) {
if (username.toLowerCase() === "hpdell" && md5(password) === "2b759a6996d878c41bb7d56ce530d031") {
let imgPath = path.join(blog, "source", "assets", "img", mediaObject.name);
try {
await fs.writeFile(imgPath, mediaObject.bits);
callback(null, {
url: "/" + ["assets", "img", mediaObject.name].join("/")
})
} catch (error) {
callback(new ExpressError("Writefile Wrong.", 500));
}
} else {
callback(new ExpressError("Username or Password is wrong.", 500));
}
} else {
callback(new ExpressError("Blog id is required.", 500));
}
}

async function publishToGitHub(git: simplegit.SimpleGit, postPath) {
try {
let imgDirPath = path.join("source", "assets", "img", "*");
await git.add([postPath, imgDirPath]);
await git.commit(`Add Post: ${path.basename(postPath)}`);
await git.pull("origin", "master", {
"--rebase": "true"
});
await git.push();
} catch (error) {
throw new Error("git error");
}
}

这个实现还比较粗糙,为了简单起见, getPost 还是调用了 Hexo 的接口。分类部分还是用的 package.json 文件中确定的几种类型。对 blogid 也没有做判断。

blogger.ts 的实现方式

1
2
3
4
export function getUserBlogs(key: string, username: string, password: string, callback: Function) {
console.log('getuserblogs called with key:', key, 'username:', username, 'and password:', password);
callback(null, [{ blogid: 'hpdell-hexo', blogName: 'HPDell 的 Hexo 博客', }]);
}

这里这个实现也是为了简单起见,直接返回了固定的名称和 ID 。

至此,这个接口就算是基本实现了。

效果

查看 我的博客 里面有两篇博文

  • 测试发布到 GitHub
  • 测试通过 Metaweblog API 发图片

即可查看效果。

感谢您的阅读,本文由 HPDell 的个人博客 版权所有。如若转载,请注明出处:HPDell 的个人博客(http://hpdell.github.io/编程/hexo-metaweblog-guide/
测试发布到 GitHub
Microsoft Machine Learning Server 使用调研