Skip to content

Commit

Permalink
feat: #130 支持发布到CSDN-支持专栏
Browse files Browse the repository at this point in the history
  • Loading branch information
terwer committed Sep 2, 2023
1 parent 9629fbc commit 6086047
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 48 deletions.
4 changes: 3 additions & 1 deletion docs/插件开发.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@
适配器 src/adaptors/index.ts getCfg
配置 src/adaptors/index.ts getAdaptor
YAML适配器(不一定有)

6. 开启配置页面
在 src/components/set/publish/singleplatform/SingleSettingIndex.vue 注册配置

59 changes: 59 additions & 0 deletions src/adaptors/web/base/webUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2023, Terwer . All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Terwer designates this
* particular file as subject to the "Classpath" exception as provided
* by Terwer in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com
* or visit www.terwer.space if you need additional information or have any
* questions.
*/

/**
* Web工具类,包含用于操作Cookie的方法
*
* @since 1.12.0
*/
class WebUtils {
/**
* 从Cookie字符串中读取指定键的值
*
* @param key - 要查找的Cookie键
* @param cookieString - 包含Cookie的字符串
* @returns 包含键对应值的Cookie值,如果未找到则返回空字符串
*/
public static readCookie(key: string, cookieString: string): string {
// 将Cookie字符串分割成Cookie键值对数组
const cookies = cookieString.split(";")

// 遍历Cookie数组,查找指定键的Cookie
for (const cookie of cookies) {
const [cookieKey, cookieValue] = cookie.split("=")

// 去除Cookie键的空白字符并比较是否与指定键相符
if (cookieKey.trim() === key) {
// 返回解码后的Cookie值,如果解码失败则返回空字符串
return decodeURIComponent(cookieValue) ?? ""
}
}

// 如果未找到指定键的Cookie则返回空字符串
return ""
}
}

export default WebUtils
8 changes: 4 additions & 4 deletions src/adaptors/web/csdn/csdnConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ import { CategoryTypeEnum, PageTypeEnum, PasswordType } from "zhi-blog-api"
*/
export class CsdnConfig extends CommonWebConfig {
constructor(username: string, password: string, middlewareUrl?: string) {
super("https://blog.csdn.net", "https://blog.csdn.net/api", username, password, middlewareUrl)
super("https://blog.csdn.net", "https://bizapi.csdn.net", username, password, middlewareUrl)
this.previewUrl = "[userid]/article/details/[postid]"
this.pageType = PageTypeEnum.Html
this.usernameEnabled = true
this.pageType = PageTypeEnum.Markdown
this.usernameEnabled = false
this.passwordType = PasswordType.PasswordType_Cookie
this.cateEnabled = false
this.knowledgeSpaceEnabled = true
this.knowledgeSpaceTitle = "分类"
this.knowledgeSpaceTitle = "专栏"
this.knowledgeSpaceType = CategoryTypeEnum.CategoryType_Single
this.allowKnowledgeSpaceChange = false
this.placeholder.knowledgeSpaceReadonlyModeTip =
Expand Down
3 changes: 2 additions & 1 deletion src/adaptors/web/csdn/csdnUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ describe("test csdnUtils", () => {
const url = "https://bizapi.csdn.net/blog/phoenix/console/v1/column/list?type=all"
const method = "GET"
const accept = "*/*"
const contentType = "application/json"

const xCaNonce = CsdnUtils.generateXCaNonce()
const xCaSignature = CsdnUtils.generateXCaSignature(url, method, accept, xCaNonce)
const xCaSignature = CsdnUtils.generateXCaSignature(url, method, accept, xCaNonce, contentType)

console.log("x-ca-nonce:", xCaNonce)
console.log("x-ca-signature:", xCaSignature)
Expand Down
4 changes: 2 additions & 2 deletions src/adaptors/web/csdn/csdnUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class CsdnUtils {
method: string,
accept: string,
uuid: string,
content_type?: string | null
content_type: string
): string {
// https://github.com/brix/crypto-js/issues/189
// https://www.npmjs.com/package/crypto-js
Expand All @@ -71,7 +71,7 @@ class CsdnUtils {
let toEnc: string
if (method === "GET") {
const path = s.pathname + s.search
toEnc = `GET\n${accept}\n\n\n\nx-ca-key:${CsdnUtils.X_CA_KEY}\nx-ca-nonce:${uuid}\n${path}`
toEnc = `GET\n${accept}\n\n${content_type}\n\nx-ca-key:${CsdnUtils.X_CA_KEY}\nx-ca-nonce:${uuid}\n${path}`
} else {
const path = s.pathname
toEnc = `POST\n${accept}\n\n${content_type}\n\nx-ca-key:${CsdnUtils.X_CA_KEY}\nx-ca-nonce:${uuid}\n${path}`
Expand Down
136 changes: 101 additions & 35 deletions src/adaptors/web/csdn/csdnWebAdaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,9 @@

import { BaseWebApi } from "~/src/adaptors/web/base/baseWebApi.ts"
import CsdnUtils from "~/src/adaptors/web/csdn/csdnUtils.ts"
import { CsdnConfig } from "~/src/adaptors/web/csdn/csdnConfig.ts"
import { PublisherAppInstance } from "~/src/publisherAppInstance.ts"
import { createAppLogger } from "~/src/utils/appLogger.ts"
import { CommonFetchClient } from "zhi-fetch-middleware"
import { isDev } from "~/src/utils/constants.ts"
import { UserBlog } from "zhi-blog-api"
import { CategoryInfo, UserBlog } from "zhi-blog-api"
import { StrUtil } from "zhi-common"
import WebUtils from "~/src/adaptors/web/base/webUtils.ts"

/**
* CSDN网页授权适配器
Expand All @@ -41,22 +38,18 @@ import { UserBlog } from "zhi-blog-api"
* @since 0.9.0
*/
class CsdnWebAdaptor extends BaseWebApi {
private readonly commonFetchClient: any

/**
* 初始化知乎 API 适配器
*
* @param appInstance 应用实例
* @param cfg 配置项
*/
constructor(appInstance: PublisherAppInstance, cfg: CsdnConfig) {
super(appInstance, cfg)
this.cfg = cfg

const middlewareUrl = this.cfg.middlewareUrl ?? "https://api.terwer.space/api/middleware"
this.commonFetchClient = new CommonFetchClient(appInstance, "", middlewareUrl, isDev)
this.logger = createAppLogger("zhihu-web-adaptor")
}
// /**
// * 初始化CSDN API 适配器
// *
// * @param appInstance 应用实例
// * @param cfg 配置项
// */
// constructor(appInstance: PublisherAppInstance, cfg: CsdnConfig) {
// super(appInstance, cfg)
// this.cfg = cfg
//
// this.logger = createAppLogger("csdn-web-adaptor")
// }

public async getMetaData(): Promise<any> {
const res = await this.csdnFetch("https://bizapi.csdn.net/blog-console-api/v1/user/info")
Expand All @@ -78,10 +71,74 @@ class CsdnWebAdaptor extends BaseWebApi {
public async getUsersBlogs(): Promise<Array<UserBlog>> {
let result: UserBlog[] = []

const res = await this.csdnFetch("https://bizapi.csdn.net/blog/phoenix/console/v1/column/list?type=all")
this.logger.debug("get csdn columns =>", res)
if (res?.code === 200) {
const columnList = res?.data?.list
const column = columnList?.column ?? []
const payCcolumn = columnList?.pay_column ?? []

// 普通专栏
column.forEach((item: any) => {
const userblog: UserBlog = new UserBlog()
userblog.blogid = item.id
userblog.blogName = item.edit_title
userblog.url = item.column_url
// userblog.imgUrl = item.img_url
result.push(userblog)
})

// 付费专栏
payCcolumn.forEach((item: any) => {
const userblog: UserBlog = new UserBlog()
userblog.blogid = item.id
userblog.blogName = `[付费]${item.edit_title}`
userblog.url = item.column_url
// userblog.imgUrl = item.img_url
result.push(userblog)
})
}

this.logger.debug("getUsersBlogs=>", result)
return result
}

public async getCategories(): Promise<CategoryInfo[]> {
const cats = [] as CategoryInfo[]

const res = await this.csdnFetch("https://bizapi.csdn.net/blog/phoenix/console/v1/column/list?type=all")
this.logger.debug("get csdn columns =>", res)
if (res?.code === 200) {
const columnList = res?.data?.list
const column = columnList?.column ?? []
const payCcolumn = columnList?.pay_column ?? []

// 普通专栏
column.forEach((item: any) => {
const cat = new CategoryInfo()

cat.categoryId = item.id
cat.categoryName = item.edit_title
cat.description = item.column_url
cat.categoryDescription = item.desc
cats.push(cat)
})

// 付费专栏
payCcolumn.forEach((item: any) => {
const cat = new CategoryInfo()

cat.categoryId = item.id
cat.categoryName = `[付费]${item.edit_title}`
cat.description = item.column_url
cat.categoryDescription = item.desc
cats.push(cat)
})
}

return cats
}

// ================
// private methods
// ================
Expand All @@ -90,41 +147,50 @@ class CsdnWebAdaptor extends BaseWebApi {
headers: any = {},
params: any = undefined,
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET",
contentType?: string | null
contentType: string = "application/json"
) {
// 设置请求头
const accept = "*/*"
const APPLICATION_JSON = "application/json"
const xcakey = CsdnUtils.X_CA_KEY
const xCaNonce = CsdnUtils.generateXCaNonce()
const xCaSignature = CsdnUtils.generateXCaSignature(url, method, accept, xCaNonce, contentType)

const reqHeader = {
accept,
...(contentType ? { "content-type": contentType } : {}),
"x-ca-key": xcakey,
"x-ca-nonce": xCaNonce,
"x-ca-signature": xCaSignature,
"x-ca-signature-headers": "x-ca-key,x-ca-nonce",
const reqHeaderMap = new Map<string, string>()
reqHeaderMap.set("accept", accept)
reqHeaderMap.set("content-type", contentType)
reqHeaderMap.set("x-ca-key", xcakey)
reqHeaderMap.set("x-ca-nonce", xCaNonce)
reqHeaderMap.set("x-ca-signature", xCaSignature)
reqHeaderMap.set("x-ca-signature-headers", "x-ca-key,x-ca-nonce")

const mergedHeaders = {
...Object.fromEntries(reqHeaderMap),
...headers,
Cookie: this.cfg.password,
}

// 构建请求选项
const requestOptions: RequestInit = {
method: method,
headers: reqHeader,
headers: mergedHeaders,
body: params,
redirect: "follow",
}

// 发送请求并返回响应
const res = await this.commonFetchClient.fetchCall(url, requestOptions)
this.logger.debug("csdn url =>", url)
this.logger.debug("csdn requestOptions =>", requestOptions)
const res = await this.webProxyFetch(url, [mergedHeaders], params, method, contentType)
if (res?.code !== 200) {
throw new Error(res?.body?.message)
}
return res
}

public async getPreviewUrl(postid: string): Promise<string> {
const token = this.cfg.password
const userid = WebUtils.readCookie("UserName", token)
const previewUrl = this.cfg.previewUrl.replace(/\[userid\]/g, userid).replace(/\[postid\]/g, postid)
return StrUtil.pathJoin(this.cfg.home ?? "", previewUrl)
}
}

export { CsdnWebAdaptor }
2 changes: 1 addition & 1 deletion src/components/publish/form/PublishCategories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const fetchCate = async () => {
/>
</div>
<div v-else></div>
<div v-if="formData.useAi">
<div v-if="formData.useAi && formData.categoryConfig.cateEnabled">
<el-form-item v-if="formData.recommCates.length > 0" class="recomm-show">
推荐的分类:
<el-tag class="ml-2 recomm-cate" type="success" v-for="rtag in formData.recommCates">{{ rtag }}</el-tag>
Expand Down
2 changes: 1 addition & 1 deletion src/components/set/publish/PlatformAddForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ initPage()
</el-form-item>
<!-- 是否启用 -->
<el-form-item label="是否启用">
<el-switch v-model="formData.dynCfg.isEnabled" :disabled="formData.isPre" />
<el-switch v-model="formData.dynCfg.isEnabled" />
</el-form-item>

<el-form-item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const subtype = getSubPlatformTypeByKey(apiType)
<cnblogs-setting v-else-if="subtype === SubPlatformType.Metaweblog_Cnblogs" :api-type="apiType" />
<typecho-setting v-else-if="subtype === SubPlatformType.Metaweblog_Typecho" :api-type="apiType" />
<wordpress-setting v-else-if="subtype === SubPlatformType.Wordpress_Wordpress" :api-type="apiType" />
<zhihu-setting v-else-if="subtype === SubPlatformType.Custom_Zhihu" :api-type="apiType"></zhihu-setting>
<zhihu-setting v-else-if="subtype === SubPlatformType.Custom_Zhihu" :api-type="apiType" />
<csdn-setting v-else-if="subtype === SubPlatformType.Custom_CSDN" :api-type="apiType" />
<span v-else>
<el-alert :closable="false" :title="t('setting.entry.not.supported')" class="top-tip" type="error" />
</span>
Expand Down
54 changes: 54 additions & 0 deletions src/components/set/publish/singleplatform/web/CsdnSetting.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!--
- Copyright (c) 2023, Terwer . All rights reserved.
- DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
-
- This code is free software; you can redistribute it and/or modify it
- under the terms of the GNU General Public License version 2 only, as
- published by the Free Software Foundation. Terwer designates this
- particular file as subject to the "Classpath" exception as provided
- by Terwer in the LICENSE file that accompanied this code.
-
- This code is distributed in the hope that it will be useful, but WITHOUT
- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- version 2 for more details (a copy is included in the LICENSE file that
- accompanied this code).
-
- You should have received a copy of the GNU General Public License version
- 2 along with this work; if not, write to the Free Software Foundation,
- Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
-
- Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com
- or visit www.terwer.space if you need additional information or have any
- questions.
-->

<script setup lang="ts">
import CustomWebSetting from "~/src/components/set/publish/singleplatform/base/impl/CustomWebSetting.vue"
import { useVueI18n } from "~/src/composables/useVueI18n.ts"
import { CsdnConfig } from "~/src/adaptors/web/csdn/csdnConfig.ts"
import { CsdnPlaceholder } from "~/src/adaptors/web/csdn/csdnPlaceholder.ts"
import { useCsdnWeb } from "~/src/adaptors/web/csdn/useCsdnWeb.ts"
const props = defineProps({
apiType: {
type: String,
default: "",
},
})
const { t } = useVueI18n()
const { cfg } = await useCsdnWeb(props.apiType)
const csdnCfg = cfg as CsdnConfig
const csdnPlaceholder = new CsdnPlaceholder()
csdnPlaceholder.homePlaceholder = t("setting.csdn.home.tip")
csdnPlaceholder.apiUrlPlaceholder = t("setting.csdn.apiUrl.tip")
csdnPlaceholder.passwordPlaceholder = t("setting.csdn.password.tip")
csdnPlaceholder.previewUrlPlaceholder = t("setting.csdn.previewUrl.tip")
csdnCfg.placeholder = csdnPlaceholder
</script>

<template>
<custom-web-setting :api-type="props.apiType" :cfg="csdnCfg" />
</template>

0 comments on commit 6086047

Please sign in to comment.