Gitalk 自动初始化评论

LibraHeresy
• 阅读 175

前言

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

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

环境

node: 18.16.0

Gitalk: 1.7.2

方案

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

实现

读取本地 md 文件

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

// 获取md文件路径
const getMdFilesPath = (path, fileArr = []) => {
  const files = fs.readdirSync(path);
  files.forEach((file) => {
    const filePath = `${path}/${file}`;
    stat = fs.statSync(filePath);
    // 判断是文件还是文件夹,如果是文件夹继续递归
    if (stat.isDirectory()) {
      fileArr.concat(getMdFilesPath(filePath, fileArr));
    } else {
      fileArr.push(filePath);
    }
  });
  return fileArr.filter((i) => i.split(".").pop() === "md");
};

解析本地 md 文件

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

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

获取 Github 授权

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

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

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

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

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

// 打开网址
const openUrl = async (param = new ParamGithub()) => {
  const { clientID } = param;
  const domain = "https://github.com/login/oauth/authorize";
  const query = {
    client_id: clientID,
    redirect_uri: `http://localhost:${port}/`, // 回调地址
    scope: "public_repo", // 用户组
  };
  const url = `${domain}?${Object.keys(query)
    .map((key) => `${key}=${query[key]}`)
    .join("&")}`;
  await open(url);
};

// 监听code获取
const startupKoa = () => {
  const app = new Koa();
  // 启动服务,监听端口
  const _server = app.listen(port);
  openUrl();
  app.use((ctx) => {
    const urlArr = ctx.originalUrl.split("=");
    if (urlArr[0].indexOf("code") > -1) {
      accessCode = urlArr[1];
      createIssues();
      configMap.set("accessCode", accessCode);
      writeConfigFile();
      _server.close();
    }
    // 拿到code后关闭回调页面
    ctx.response.body = `<script>
      (function () {
        window.close()
      })(this)
    </script>`
  });
};

// 获取token
const getAccessToken = (param = new ParamGithub()) => {
  const { clientID, clientSecret } = param;
  return axiosGithub
    .post("/login/oauth/access_token", {
      code: accessCode,
      client_id: clientID,
      client_secret: clientSecret,
    })
    .then((res) => {
      return Promise.resolve(
        res.data.error === "bad_verification_code"
          ? null
          : res.data.access_token
      );
    })
    .catch((err) => {
      appendErrorFile("获取token", err.response.data.message);
    });
};

创建 issue

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

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

// 获取issues
const getIssues = (param) => {
  const { owner, repo, clientID, clientSecret, labels, title } = param || {};
  axiosApiGithub
    .get(`/repos/${owner}/${repo}/issues`, {
      auth: {
        username: clientID,
        password: clientSecret,
      },
      params: {
        labels: labels
          .concat(title)
          .map((label) => (typeof label === "string" ? label : label.name))
          .join(","),
        t: Date.now(),
      },
    })
    .then((res) => {
      if (!(res && res.data && res.data.length)) {
        createIssue(param);
      }
    })
    .catch((err) => {
      console.log(err);
      appendErrorFile("获取issues", err?.response?.data?.message || "网络问题");
    });
};

// 创建issues
const createIssue = (param) => {
  const { owner, repo, labels, title } = param || {};
  axiosApiGithub
    .post(
      `/repos/${owner}/${repo}/issues`,
      {
        title: `${title} | 天秤的异端`,
        labels: labels.concat(title).map((label) =>
          typeof label === "string"
            ? {
                name: label,
              }
            : label
        ),
        body: "我的博客 https://libraheresy.github.io/libraheresy-blog",
      },
      {
        headers: {
          authorization: `Bearer ${accessToken}`,
        },
      }
    )
    .then(() => {
      console.log(`创建成功:${title}`);
    })
    .catch((err) => {
      appendErrorFile("创建issues", err.response.data.message);
      if (
        ["Not Found", "Bad credentials"].includes(err.response.data.message)
      ) {
        getAccessToken();
      }
    });
};

修改 package.json

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

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

问题

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

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

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

代码

const fs = require('fs') // 操作文件
const path = require('path') // 获取路径
const marked = require('marked') // 解析md文件
const axios = require('axios') // 请求
const Koa = require('koa') // 本地服务
const open = require('open') // 打开网址
const moment = require('moment') // 日期

// Github配置参数
class ParamGithub {
  title = ''
  owner = "LibraHeresy" // GitHub repository 所有者
  repo = "libraheresy-blog" // GitHub repository
  clientID = "87071bc8d1c9295cc650" // 自己的clientID
  clientSecret = "c831d96750a203e63abe55d13426e824b2b2aaef" // 自己的clientSecret
  admin = ["LibraHeresy"] // GitHub repository 所有者
  labels = ["Gitalk"] // GitHub issue 的标签

  constructor(title) {
    this.title = title
  }
}

const writeConfigFile = () => {
  fs.writeFileSync(path.join(__dirname, './config.txt'), Array.from(configMap).map(arr => arr.join('=')).join(';'))
}

const appendErrorFile = (opera, message) => {
  const filePath = path.join(__dirname, './error.txt')
  if(!fs.existsSync(filePath)) fs.writeFileSync(filePath, '')
  const time = moment().format('YYYY-MM-DD hh:mm:ss')
  fs.appendFileSync(path.join(__dirname, './error.txt'), `${opera}报错 ${time})}\n ${message}\n`)
  console.log(`${opera}报错`, time)
}

// 本地配置
let config = ''
let configMap = new Map()
if(!fs.existsSync(path.join(__dirname, './config.txt'))) {
  writeConfigFile()
}
config = fs.readFileSync(path.join(__dirname, './config.txt'), 'utf-8')
configMap = new Map(config.split(';').map(text => text.split('=')))
let accessCode = configMap.get('accessCode') || ''
let accessToken = configMap.get('accessToken') || ''
let port = 3000

const axiosGithub = axios.create({
  baseURL: 'https://github.com',
  headers: {
    'accept': 'application/json'
  }
})
const axiosApiGithub = axios.create({
  baseURL: 'https://api.github.com',
  headers: {
    'accept': 'application/json',
  }
})

// 规避控制台警告
marked.setOptions({
  mangle: false,
  headerIds: false,
})

// 获取md文件路径
const getMdFilesPath = (path, fileArr = []) => {
  const files = fs.readdirSync(path)
  files.forEach((file) => {
    const filePath = `${path}/${file}`
    stat = fs.statSync(filePath)
    if (stat.isDirectory()) {
      fileArr.concat(getMdFilesPath(filePath, fileArr))
    } else {
      fileArr.push(filePath)
    }
  })
  return fileArr.filter(i => i.split('.').pop() === 'md')
}

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

// 打开网址
const openUrl = async (param = new ParamGithub()) => {
  const {
    clientID
  } = param
  const domain = 'https://github.com/login/oauth/authorize'
  const query = {
    client_id: clientID,
    redirect_uri: `http://localhost:${port}/`, // 回调地址
    scope: 'public_repo', // 用户组
  }
  const url = `${domain}?${Object.keys(query).map(key => `${key}=${query[key]}`).join('&')}`
  await open(url)
}

// 监听code获取
const startupKoa = () => {
  const app = new Koa()
  const _server = app.listen(port)
  openUrl()
  app.use(ctx => {
    const urlArr = ctx.originalUrl.split("=")
    if (urlArr[0].indexOf("code") > -1) {
      accessCode = urlArr[1]
      createIssues()
      configMap.set('accessCode', accessCode)
      writeConfigFile()
      _server.close()
    }
    // 拿到code后关闭回调页面
    ctx.response.body = `<script>
      (function () {
        window.close()
      })(this)
    </script>`
  })
}

// 获取token
const getAccessToken = (param = new ParamGithub()) => {
  const {
    clientID,
    clientSecret
  } = param
  return axiosGithub
    .post('/login/oauth/access_token', {
      code: accessCode,
      client_id: clientID,
      client_secret: clientSecret
    }).then(res => {
      return Promise.resolve(res.data.error === 'bad_verification_code' ? null : res.data.access_token)
    }).catch(err => {
      appendErrorFile('获取token', err.response.data.message)
    })
}

// 获取授权
const getAuth = () => {
  return getAccessToken()
    .then(res => {
      configMap.set('accessToken', res)
      writeConfigFile()
      return res
    })
}

// 批量创建issue
const createIssues = async () => {
  if (accessCode) {
    const token = await getAuth()
    if(token) {
      accessToken = token;
      mdFileTitleArr.forEach(title => {
        getIssues(new ParamGithub(title))
      })
    } else {
      accessCode  = ''
      createIssues()
    }
  } else {
    startupKoa()
  }
}

// 获取issues
const getIssues = (param) => {
  const {
    owner,
    repo,
    clientID,
    clientSecret,
    labels,
    title
  } = param || {}
  axiosApiGithub
    .get(`/repos/${owner}/${repo}/issues`, {
      auth: {
        username: clientID,
        password: clientSecret
      },
      params: {
        labels: labels.concat(title).map(label => typeof label === 'string' ? label : label.name).join(','),
        t: Date.now()
      }
    }).then((res) => {
      if (!(res && res.data && res.data.length)) {
        createIssue(param);
      }
    }).catch(err => {
      console.log(err)
      appendErrorFile('获取issues', err?.response?.data?.message || '网络问题')
    });
}

// 创建issues
const createIssue = (param) => {
  const {
    owner,
    repo,
    labels,
    title
  } = param || {}
  axiosApiGithub
    .post(`/repos/${owner}/${repo}/issues`, {
      title: `${title} | 天秤的异端`,
      labels: labels.concat(title).map(label => typeof label === 'string' ? {
        name: label
      } : label),
      body: '我的博客 https://libraheresy.github.io/libraheresy-blog'
    }, {
      headers: {
        authorization: `Bearer ${accessToken}`
      }
    }).then(() => {
      console.log(`创建成功:${title}`)
    }).catch((err) => {
      appendErrorFile('创建issues', err.response.data.message)
      if(['Not Found', 'Bad credentials'].includes(err.response.data.message)) {
        getAccessToken()
      }
    });
}

// 读取本地文件
const mdFilePathArr = getMdFilesPath(path.join(__dirname, '../docs'))
const mdFileTitleArr = mdFilePathArr.map(path => getMdFileTitle(path)).filter(i => i)

// 调用授权函数
createIssues()
点赞
收藏
评论区
推荐文章
冴羽 冴羽
2年前
VuePress 博客优化之增加 Vssue 评论功能
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何使用Vssue快速的实现评论功能。主题内置因为我用的是vuepressthemereco主题,主题内置评论插件@vuepressreco/vuepressplugincomments,可以根据自己的喜好选择Valine或者Vssue。那我们来介绍下Vss
冴羽 冴羽
2年前
VuePress 博客优化之增加 Valine 评论功能
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何使用Valine快速的实现评论功能。主题内置因为我用的是vuepressthemereco主题,主题内置评论插件@vuepressreco/vuepressplugincomments,可以根据自己的喜好选择Valine或者Vssue。本篇讲讲使用Val
Jacquelyn38 Jacquelyn38
2年前
手把手教你用前端实现短视频App(滑动切换)
前言平常在玩短视频App时,喜欢看的视频可以多看一会,不喜欢看的视频直接往上一划,这个功能一直深受用户喜爱。今天我们不妨实现一个这样功能的App。功能1.上下滑动切换视频2.可查看对应视频下的评论示例下面我挑出了几张代表性的图片,供大家参考。1.2.3.下载链接以上是我自己做的一款App
CuterCorley CuterCorley
3年前
Python 不用selenium 带你高效爬取京东商品评论
一、项目说明1.项目背景一天,一朋友扔给我一个链接,让我看看这个歌商品的所有评论怎么抓取,我打开一看,好家伙,竟然有近300万条评论,不是一个小数目啊。但是仔细一看,原来有234万的评论是默认好评,还是有少部分是有价值的评价的。经过进一步观察,可以看到显然,网页中显示的只有100页数据,每页显示10条,通常可以用selenium点击每一页然后获取
Stella981 Stella981
2年前
Hexo NexT 主题添加评论和文章阅读量
前言折腾了畅言、gitalk、disqus这些评论API,最后都以失败告终,最终试到valine的时候终于成功,顺便把文章阅读量统计也搞定了。下面把我的经验写下来分享给大家,欢迎评论。Valine(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fvalin
Stella981 Stella981
2年前
HashMap 初始化时赋值
一般初始化一个map并且给map赋值的写法:HashMap<String,StringmapnewHashMap<String,String();map.put("name","test");map.put("age","20");但是我想在初始化的时候就直接给map中set值。
Stella981 Stella981
2年前
Gitalk
Gitalk是一个基于GithubIssue和Preact开发的评论组件。特性使用Github登录支持多语言\en,zhCN,zhTW\支持个人或组织项目无干扰模式(设置distractionFreeMode为true开启)
Stella981 Stella981
2年前
SEO优化:Sitemap插件生成WordPress网站地图
!image(https://www.cztcms.cn/wpcontent/uploads/2020/05/sitemapseo.png)▣前言什么是sitemap网站地图?wordpress如何自动生成sitemap地图文件呢?    对于网站建设来说,网站地图是非常关键的,因为这对搜索引擎收录是很有利的。网
Wesley13 Wesley13
2年前
Valine评论系统邮件提醒
这几天想到,别人给我发的评论,我还要到后台去看,实在是太麻烦了,于是发现了一个好项目valineadmin可以帮我发送邮件评论提醒,这样我就可以实时收到别人给我发的评论。!image(https://oscimg.oschina.net/oscnet/5940a6f5b07c26046b2c2fa0e9b40e56249.png)<btnce
LibraHeresy LibraHeresy
11个月前
VitePress 使用 Gitalk 添加评论功能
前言一个优质的博客怎么能没有评论功能呢,没有评论怎么和同志们的思想激情♂碰撞,获得新的收获。Ah,That'sgood.那么用什么方案呢,看来看去,不用自己搭建后端服务的方案都大同小异,都是利用Github的issues模块实现的。其中的佼佼者是Gitme