Gitalk 自动初始化评论

前言

Gitalk 非常好用,但是一直有个问题困扰着我,评论不能自动初始化。我在网上看了一些文章,都是Hexo这个博客框架,然后什么sitemap,网站文件地图,看的我云里雾里。

前几天突然灵光一闪,我其实只需要把 Gitalk 自身自带的初始化评论功能,在我的项目里复刻一遍就可以了,为啥还想的这么复杂。

环境

node: 18.16.0

Gitalk: 1.7.2

方案

读取本地所有 md 文件 -> 解析内容提取 title -> 获取 issue -> 没有 issue -> 新建 issue

实现

读取本地 md 文件

用正则更好,只是我不太会。

1// 获取md文件路径 2const getMdFilesPath = (path, fileArr = []) => { 3 const files = fs.readdirSync(path); 4 files.forEach((file) => { 5 const filePath = `${path}/${file}`; 6 stat = fs.statSync(filePath); 7 // 判断是文件还是文件夹,如果是文件夹继续递归 8 if (stat.isDirectory()) { 9 fileArr.concat(getMdFilesPath(filePath, fileArr)); 10 } else { 11 fileArr.push(filePath); 12 } 13 }); 14 return fileArr.filter((i) => i.split(".").pop() === "md"); 15};

解析本地 md 文件

这里我用的marked这个包,可以把md语法解析成html语法,我再通过截取<h1>标签内的文字,作为这篇文章的标题存起来。

1// 获取md文件标题 2const getMdFileTitle = (path, fn) => { 3 const fileContent = marked.marked(fs.readFileSync(path, "utf-8")).toString(); 4 const startIndex = 5 fileContent.indexOf("<h1>") === -1 ? 0 : fileContent.indexOf("<h1>") + 4; 6 const endIndex = 7 fileContent.indexOf("</h1>") === -1 ? 0 : fileContent.indexOf("</h1>"); 8 const title = fileContent.substring(startIndex, endIndex); 9 return title; 10};

获取 Github 授权

Github在 2022 年就禁止了账号密码直接登录,所以要通过Oath2来实现授权获取。

我这使用的最简单的方法,直接调起浏览器打开Github授权页面,手动进行授权,拿到code后再关闭授权回调页面,毕竟实现无感授权需要的时间有点多。

我是用open这个包来打开授权网址,这个包很简单,就是适配了不同系统下的打开网址命令。

发起请求我用的axios这个包,中国人民的老朋友了。

当你网页点击授权以后,Github会回调一个地址,我这边使用的是koa来接受这个回调带来的code值,然后再发起获取token的请求。

1// 打开网址 2const openUrl = async (param = new ParamGithub()) => { 3 const { clientID } = param; 4 const domain = "https://github.com/login/oauth/authorize"; 5 const query = { 6 client_id: clientID, 7 redirect_uri: `http://localhost:${port}/`, // 回调地址 8 scope: "public_repo", // 用户组 9 }; 10 const url = `${domain}?${Object.keys(query) 11 .map((key) => `${key}=${query[key]}`) 12 .join("&")}`; 13 await open(url); 14}; 15 16// 监听code获取 17const startupKoa = () => { 18 const app = new Koa(); 19 // 启动服务,监听端口 20 const _server = app.listen(port); 21 openUrl(); 22 app.use((ctx) => { 23 const urlArr = ctx.originalUrl.split("="); 24 if (urlArr[0].indexOf("code") > -1) { 25 accessCode = urlArr[1]; 26 createIssues(); 27 configMap.set("accessCode", accessCode); 28 writeConfigFile(); 29 _server.close(); 30 } 31 // 拿到code后关闭回调页面 32 ctx.response.body = `<script> 33 (function () { 34 window.close() 35 })(this) 36 </script>` 37 }); 38}; 39 40// 获取token 41const getAccessToken = (param = new ParamGithub()) => { 42 const { clientID, clientSecret } = param; 43 return axiosGithub 44 .post("/login/oauth/access_token", { 45 code: accessCode, 46 client_id: clientID, 47 client_secret: clientSecret, 48 }) 49 .then((res) => { 50 return Promise.resolve( 51 res.data.error === "bad_verification_code" 52 ? null 53 : res.data.access_token 54 ); 55 }) 56 .catch((err) => { 57 appendErrorFile("获取token", err.response.data.message); 58 }); 59};

创建 issue

授权拿到手以后,就要发起查询issue和创建issue的请求了。

这一部分没什么好说的,直接给大家看怎么调用,到这一步基本就算完成了。

1// 获取issues 2const getIssues = (param) => { 3 const { owner, repo, clientID, clientSecret, labels, title } = param || {}; 4 axiosApiGithub 5 .get(`/repos/${owner}/${repo}/issues`, { 6 auth: { 7 username: clientID, 8 password: clientSecret, 9 }, 10 params: { 11 labels: labels 12 .concat(title) 13 .map((label) => (typeof label === "string" ? label : label.name)) 14 .join(","), 15 t: Date.now(), 16 }, 17 }) 18 .then((res) => { 19 if (!(res && res.data && res.data.length)) { 20 createIssue(param); 21 } 22 }) 23 .catch((err) => { 24 console.log(err); 25 appendErrorFile("获取issues", err?.response?.data?.message || "网络问题"); 26 }); 27}; 28 29// 创建issues 30const createIssue = (param) => { 31 const { owner, repo, labels, title } = param || {}; 32 axiosApiGithub 33 .post( 34 `/repos/${owner}/${repo}/issues`, 35 { 36 title: `${title} | 天秤的异端`, 37 labels: labels.concat(title).map((label) => 38 typeof label === "string" 39 ? { 40 name: label, 41 } 42 : label 43 ), 44 body: "我的博客 https://libraheresy.github.io/libraheresy-blog", 45 }, 46 { 47 headers: { 48 authorization: `Bearer ${accessToken}`, 49 }, 50 } 51 ) 52 .then(() => { 53 console.log(`创建成功:${title}`); 54 }) 55 .catch((err) => { 56 appendErrorFile("创建issues", err.response.data.message); 57 if ( 58 ["Not Found", "Bad credentials"].includes(err.response.data.message) 59 ) { 60 getAccessToken(); 61 } 62 }); 63};

修改 package.json

加一个脚本命令不是美滋滋。

1"scripts": { 2 "init:comment": "node ./utils/auto-create-blog-issues.js" 3},

问题

获取 token 后,请求创建 issue,报 404

这里的404并不是找不到请求资源的意思,这里的404其实指的是你没有权限操作。这给我一顿好想,在翻看Gitalk源码的时候才发现打开授权页面时需要指明用户组,不然给你的就是最低权限,啥用没有。

1const query = { 2 client_id: clientID, 3 redirect_uri: `http://localhost:${port}/`, // 回调地址 4 scope: "public_repo", // 用户组 5};

代码

1const fs = require('fs') // 操作文件 2const path = require('path') // 获取路径 3const marked = require('marked') // 解析md文件 4const axios = require('axios') // 请求 5const Koa = require('koa') // 本地服务 6const open = require('open') // 打开网址 7const moment = require('moment') // 日期 8 9// Github配置参数 10class ParamGithub { 11 title = '' 12 owner = "LibraHeresy" // GitHub repository 所有者 13 repo = "libraheresy-blog" // GitHub repository 14 clientID = "87071bc8d1c9295cc650" // 自己的clientID 15 clientSecret = "c831d96750a203e63abe55d13426e824b2b2aaef" // 自己的clientSecret 16 admin = ["LibraHeresy"] // GitHub repository 所有者 17 labels = ["Gitalk"] // GitHub issue 的标签 18 19 constructor(title) { 20 this.title = title 21 } 22} 23 24const writeConfigFile = () => { 25 fs.writeFileSync(path.join(__dirname, './config.txt'), Array.from(configMap).map(arr => arr.join('=')).join(';')) 26} 27 28const appendErrorFile = (opera, message) => { 29 const filePath = path.join(__dirname, './error.txt') 30 if(!fs.existsSync(filePath)) fs.writeFileSync(filePath, '') 31 const time = moment().format('YYYY-MM-DD hh:mm:ss') 32 fs.appendFileSync(path.join(__dirname, './error.txt'), `${opera}报错 ${time})}\n ${message}\n`) 33 console.log(`${opera}报错`, time) 34} 35 36// 本地配置 37let config = '' 38let configMap = new Map() 39if(!fs.existsSync(path.join(__dirname, './config.txt'))) { 40 writeConfigFile() 41} 42config = fs.readFileSync(path.join(__dirname, './config.txt'), 'utf-8') 43configMap = new Map(config.split(';').map(text => text.split('='))) 44let accessCode = configMap.get('accessCode') || '' 45let accessToken = configMap.get('accessToken') || '' 46let port = 3000 47 48const axiosGithub = axios.create({ 49 baseURL: 'https://github.com', 50 headers: { 51 'accept': 'application/json' 52 } 53}) 54const axiosApiGithub = axios.create({ 55 baseURL: 'https://api.github.com', 56 headers: { 57 'accept': 'application/json', 58 } 59}) 60 61// 规避控制台警告 62marked.setOptions({ 63 mangle: false, 64 headerIds: false, 65}) 66 67// 获取md文件路径 68const getMdFilesPath = (path, fileArr = []) => { 69 const files = fs.readdirSync(path) 70 files.forEach((file) => { 71 const filePath = `${path}/${file}` 72 stat = fs.statSync(filePath) 73 if (stat.isDirectory()) { 74 fileArr.concat(getMdFilesPath(filePath, fileArr)) 75 } else { 76 fileArr.push(filePath) 77 } 78 }) 79 return fileArr.filter(i => i.split('.').pop() === 'md') 80} 81 82// 获取md文件标题 83const getMdFileTitle = (path, fn) => { 84 const fileContent = (marked.marked(fs.readFileSync(path, 'utf-8'))).toString() 85 const startIndex = fileContent.indexOf('<h1>') === -1 ? 0 : fileContent.indexOf('<h1>') + 4 86 const endIndex = fileContent.indexOf('</h1>') === -1 ? 0 : fileContent.indexOf('</h1>') 87 const title = fileContent.substring(startIndex, endIndex) 88 return title 89} 90 91// 打开网址 92const openUrl = async (param = new ParamGithub()) => { 93 const { 94 clientID 95 } = param 96 const domain = 'https://github.com/login/oauth/authorize' 97 const query = { 98 client_id: clientID, 99 redirect_uri: `http://localhost:${port}/`, // 回调地址 100 scope: 'public_repo', // 用户组 101 } 102 const url = `${domain}?${Object.keys(query).map(key => `${key}=${query[key]}`).join('&')}` 103 await open(url) 104} 105 106// 监听code获取 107const startupKoa = () => { 108 const app = new Koa() 109 const _server = app.listen(port) 110 openUrl() 111 app.use(ctx => { 112 const urlArr = ctx.originalUrl.split("=") 113 if (urlArr[0].indexOf("code") > -1) { 114 accessCode = urlArr[1] 115 createIssues() 116 configMap.set('accessCode', accessCode) 117 writeConfigFile() 118 _server.close() 119 } 120 // 拿到code后关闭回调页面 121 ctx.response.body = `<script> 122 (function () { 123 window.close() 124 })(this) 125 </script>` 126 }) 127} 128 129// 获取token 130const getAccessToken = (param = new ParamGithub()) => { 131 const { 132 clientID, 133 clientSecret 134 } = param 135 return axiosGithub 136 .post('/login/oauth/access_token', { 137 code: accessCode, 138 client_id: clientID, 139 client_secret: clientSecret 140 }).then(res => { 141 return Promise.resolve(res.data.error === 'bad_verification_code' ? null : res.data.access_token) 142 }).catch(err => { 143 appendErrorFile('获取token', err.response.data.message) 144 }) 145} 146 147// 获取授权 148const getAuth = () => { 149 return getAccessToken() 150 .then(res => { 151 configMap.set('accessToken', res) 152 writeConfigFile() 153 return res 154 }) 155} 156 157// 批量创建issue 158const createIssues = async () => { 159 if (accessCode) { 160 const token = await getAuth() 161 if(token) { 162 accessToken = token; 163 mdFileTitleArr.forEach(title => { 164 getIssues(new ParamGithub(title)) 165 }) 166 } else { 167 accessCode = '' 168 createIssues() 169 } 170 } else { 171 startupKoa() 172 } 173} 174 175// 获取issues 176const getIssues = (param) => { 177 const { 178 owner, 179 repo, 180 clientID, 181 clientSecret, 182 labels, 183 title 184 } = param || {} 185 axiosApiGithub 186 .get(`/repos/${owner}/${repo}/issues`, { 187 auth: { 188 username: clientID, 189 password: clientSecret 190 }, 191 params: { 192 labels: labels.concat(title).map(label => typeof label === 'string' ? label : label.name).join(','), 193 t: Date.now() 194 } 195 }).then((res) => { 196 if (!(res && res.data && res.data.length)) { 197 createIssue(param); 198 } 199 }).catch(err => { 200 console.log(err) 201 appendErrorFile('获取issues', err?.response?.data?.message || '网络问题') 202 }); 203} 204 205// 创建issues 206const createIssue = (param) => { 207 const { 208 owner, 209 repo, 210 labels, 211 title 212 } = param || {} 213 axiosApiGithub 214 .post(`/repos/${owner}/${repo}/issues`, { 215 title: `${title} | 天秤的异端`, 216 labels: labels.concat(title).map(label => typeof label === 'string' ? { 217 name: label 218 } : label), 219 body: '我的博客 https://libraheresy.github.io/libraheresy-blog' 220 }, { 221 headers: { 222 authorization: `Bearer ${accessToken}` 223 } 224 }).then(() => { 225 console.log(`创建成功:${title}`) 226 }).catch((err) => { 227 appendErrorFile('创建issues', err.response.data.message) 228 if(['Not Found', 'Bad credentials'].includes(err.response.data.message)) { 229 getAccessToken() 230 } 231 }); 232} 233 234// 读取本地文件 235const mdFilePathArr = getMdFilesPath(path.join(__dirname, '../docs')) 236const mdFileTitleArr = mdFilePathArr.map(path => getMdFileTitle(path)).filter(i => i) 237 238// 调用授权函数 239createIssues()
点赞
收藏

评论区

加载中...

相关推荐

VuePress 博客优化之增加 Valine 评论功能

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何使用Valine快速的实现评论功能。主题内置因为我用的是vuepressthemereco主题,主题内置评论插件@vuepressreco/vuepressplugincomments,可以根据自己的喜好选择Valine或者Vssue。本篇讲讲使用Val

VuePress 博客优化之增加 Vssue 评论功能

前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何使用Vssue快速的实现评论功能。主题内置因为我用的是vuepressthemereco主题,主题内置评论插件@vuepressreco/vuepressplugincomments,可以根据自己的喜好选择Valine或者Vssue。那我们来介绍下Vss

Python 不用selenium 带你高效爬取京东商品评论

一、项目说明1.项目背景一天,一朋友扔给我一个链接,让我看看这个歌商品的所有评论怎么抓取,我打开一看,好家伙,竟然有近300万条评论,不是一个小数目啊。但是仔细一看,原来有234万的评论是默认好评,还是有少部分是有价值的评价的。经过进一步观察,可以看到显然,网页中显示的只有100页数据,每页显示10条,通常可以用selenium点击每一页然后获取

Hexo NexT 主题添加评论和文章阅读量

前言折腾了畅言、gitalk、disqus这些评论API,最后都以失败告终,最终试到valine的时候终于成功,顺便把文章阅读量统计也搞定了。下面把我的经验写下来分享给大家,欢迎评论。Valine(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fvalin

HashMap 初始化时赋值

一般初始化一个map并且给map赋值的写法:HashMap<String,StringmapnewHashMap<String,String();map.put("name","test");map.put("age","20");但是我想在初始化的时候就直接给map中set值。

Gitalk

Gitalk是一个基于GithubIssue和Preact开发的评论组件。特性使用Github登录支持多语言\en,zhCN,zhTW\支持个人或组织项目无干扰模式(设置distractionFreeMode为true开启)