Comment plugin for hexo, Why is Twikoo


Formerly I employed Giscus as blog comment plugin, which worked perfectly expect the poor expansibility. So I keep digging alternatives to get rid of third party authentication and bring back website input box. Finally, Twikoo comes into my vision.

Gisus possesses many highlight features, such as serverless and easy to embed, which brings incomparable stability and feasibility. Besides, the UI appearance lives up to my aesthetic. But, I stick to the point that NICKNAME/EMAIL/WEBSITE suite is essential for blog comment implementation. Only given the options to show identity and exchange blog site, the visiter will have desire to comment. Obviously, it’s irrealizable with Gisus.

Although there are many free comment service, which can be grouped into two types: uncontrollable closed system and open source system need self-hosted server. Among the 1st ones, Disqus is a best choice. Unfortunately, it’s not available in China mainland. Compromised solutions such as SukkaW/DisqusJS is readonly. As to 2nd ones, artalk and Twikoo is the most popular and active projects.

Unlike artalk, Twikoo is designed for Tencent CloudBase. A cloudflare compatible version twikoojs/twikoo-cloudflare is also available. Because the free plan limits the bundle size of 1 Mb, and incomplete support of node, we have to drop some features. Email notification which depend on nodemailer is disabled and indispensable. The default makeshift is Sendgrid, which is also not applicable in China mainland. After digging for long time, I realized that free mail sender service is either unstable or unreachable for China. So I choose Aliyun Direct Mail for notification purpose. We have 2,000 free posts after first launch, and the subsequent rate is ¥2 per 2,000 or ¥20 per 10,000 within 6 month. Refer to dyslab/vue-neuron, I successfully porting the function to Twikoo-cloudflare.

To keep small bundle size as far as possible, I took advantage of build-in Web Crypto and reduced function count and size. The main idea is to override fetch scope within sendMail function. First construct unsigned url params:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const unsignedParams = new Map([
['AccessKeyId', config.SMTP_USER],
['AccountName', config.SENDER_EMAIL],
['Action', 'SingleSendMail'],
['AddressType', '1'],
['Format', 'json'],
['FromAlias', config.SENDER_NAME],
['HtmlBody', html],
['RegionId', 'cn-hangzhou'],
['ReplyToAddress', 'false'],
['SignatureMethod', 'HMAC-SHA1'],
['SignatureNonce', crypto.randomUUID()],
['SignatureVersion', '1.0'],
['Subject', subject],
['TagName', 'blog'],
['Timestamp', new Date().toISOString().slice(0,-5)+"Z"],
['ToAddress', to],
['Version', '2015-11-23'],
])

Please note param keys should be sort by alphabetic, and Timestamp is millisecond-dropped ISOString. and the fetch function with HMAC-SHA1 signed authentication:

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
const realFetch = async (secret, params) => {
let canonicalizedQueryString = ''
let index = 0
for (const [key, value] of params) {
if (index > 0) canonicalizedQueryString += '&'
canonicalizedQueryString += `${fixedEncodeURIComponent(key)}=${fixedEncodeURIComponent(value)}`
index ++
}
const signed_string = `POST&${fixedEncodeURIComponent("/")}&${fixedEncodeURIComponent(canonicalizedQueryString)}`
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret+'&'),
{ name: "HMAC", hash: "SHA-1" },
true,
["sign", "verify"]
)
const mac = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode(signed_string)
)
let queryString = `Signature=${encodeURIComponent(Buffer.from(mac).toString("base64"))}`
for (const [key, value] of params) {
queryString += `&${key}=${encodeURIComponent(value)}`
}
return fetch('https://dm.aliyuncs.com/', {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: queryString
})
}

There are some easily overlooked point, such as:

  • crypto secret is tailed with &
  • final request url query string should be encoded, include signature
  • If mail tracking is enabled in aliyun account, TagName field is required
  • from argument contains both sender_name and sender_mail like "Tim"<tim@mail.com>

After some playing around, I figure out some bugs relate to telegram noticing and image uploading, as well as disqus importing. Some can be fixed, and others become limitations. The telegram notice is limit by axio method used by Pushoo.js. It’s not hard to make Pushoo compatible with fetch method that cloudflare supported, or patch twikoo-cloudflare just like email notice, maybe make it happen later or after suffering comment spam.

So far, Twikoo is working fine in my blog. It can self-adapted to dark theme and own configuration to match internationalization. I prefer to leave other features like spam and code highlight to the future explore. Besides, I made a switch button for native disqus, so visitors can do the choice.

UPDATE TODO LIST:

  • Front: able to upload images
  • Front: show commenter’s region
  • Backend: able to notice through telegram issue
  • Front: improve theme to fit Mist
  • Front: able to send private message