如何写一个给我打赏的Hexo插件

很多童鞋喜欢用Hexo写博客。Hexo也提供了大量丰富的主题和插件,满足了我们日常的需求。但是我们能找到的插件往往完成度较低,或者无法满足我们的需求,这个时候,我们就需要自己动手写插件了。

需求

有些时候,我们希望在文章的末尾附上打赏的二维码,来让观众更好地支持自己。比较简单的方法就是在写文章的末尾贴上打赏自己的二维码就是了。但是这毕竟是重复劳动,久而久之就会觉得麻烦了。我们可以动手写一个插件,自动在生成页面的时候附上我们的二维码。

实现

切换到博客源码的根目录下面之后,添加scripts文件夹。按文档说明,所有scripts下面的js文件,Hexo都会在执行的时候读取。
我们这里需要的是,在render我们写的markdown文件成html之前,在原文插入包含打赏二维码图片。

在文档中,有这么一段例子我们可以用来参考。

1
2
3
4
hexo.extend.filter.register('before_post_render', function(data){
data.title = data.title.toLowerCase();
return data;
});

上面的例子做的事情,是在渲染之前,将文章的标题换成大写然后返回。
而我们需要做的,其实类似,渲染之前,在data.content之后加上我们打赏的二维码图片。

1
2
3
4
5
6
7
8
9
10
11
// scripts/wechat.js
const config = hexo.config;
const bar_code = hexo.config.wechat_pay;

hexo.extend.filter.register('before_post_render', data => {
if (!bar_code || data.layout !== 'post') {
return data; //如果没有设置打赏,就直接返回原来结果。
}
let image = `<img src="${bar_code}" style="max-width: 200px;"/>`;
data.content += `\n\n 如果你觉得本文有用,请给我一点支持。\n\n${image}`;
});

之后,在_config.yml中添加打赏相关的设定就是了。

1
wechat_pay: /images/wechat_pay.png

注意,wechat_pay.png就是包含你二维码的图片,将这个图片存放到source/images下面即可。

效果,如下。

三分钟部署你自己的图床

有些时候,你会遇到比如hipchat这种不支持直接贴图的反人类工具。你的所有图必须先传图床,然后贴URL才行。
也有些时候,你用markdown写文档,苦于木有直接把身边的图上传然后获取url的便捷方式。
于是,何不自己去建一个支持api上传的图床呢?

我们先去github挑一张人家造好的床。
这个pictshare看起来不错,又有ui,又带简单的api。甚至还包装好了docker。
我们直接把docker拉过来用就是了。

1
2
3
mkdir /data/pictshareuploads
chown 1000 -R /data/pictshareuploads
docker run -d -v /data/pictshareuploads:/opt/pictshare/upload -p 8000:80 --name=pictshare hascheksolutions/pictshare

这样,在8000端口建好了张图床。我们可以用nginx做一下反向代理,加上https。至于肿么设置我就不废话了,证书可以用letsencrypt。

来试一下啦。

1
2
curl -s -F 'postimage=@test_image.png' -XPOST https://image.bocchi.tokyo/backend.php | jq -r .url
https://image.bocchi.tokyo/d3ipbmx30y.png

嗯,不错。连三分钟都不想折腾的就用我上面的endpoint好了。

浪费了两个小时的人参

因为一句话浪费了两个小时。

Ubuntu 16.04 安装 postgresql的时候,由于一开始的sudo apt-get install postgresql的最后步骤被默认的not
set locale 打断所以需要手动运行

1
sudo pg_createcluster --locale en_US.UTF-8 --start 9.5 main

要是直接没看到提示,错过了,下次删了再安装就永远不会有提示了。
直到你翻遍了半个google。

sort u is awesome

今天需要做一件比较蠢low的事情。从apache的access log中整理出包含某path的request的所有referer。
要找出referer不难。 cat + awk 基本搞定了。

比如对于类似192.168.0.101 - - [12/May/2014:20:41:48 +0900] "GET /index.html HTTP/1.1" 200 114 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0" 的log,我们只需要

1
cat access_log_* | awk '$7~/keywords/{print $11}' > referer.txt

但对于一个日PV几百万的服务来说,显然这样整理出来的referer会有几十万条。其中包含了无数的重复。那么我们肿么去掉这些重复呢?
用vim是正解。

command mode下,:sort u

世界瞬间变得清爽。
用了这么久的vim,现在还能感受到vim的博大精深啊。

Vim

将Rails Application 武装成 Progressive web Application

我们经常用websocket来实现后台消息推送。比如提醒用户新的留言,发帖,评论之类。
但是这种方式的局限性也很明显,就是当我们在浏览其他网页的时候,我们并不能知道在另一个网页上发生的这些通知。
为了在我们浏览其他网站的时候,也能在浏览器上收到我们在其他网站上的通知消息,就有了 webpush 这个东西。
有了webpush这种东西,我们就可以说,我们的web application是渐进式的为application了。

浏览器支持

主流的pc chrome 和firefox,android chrome。

什么,你不用这些浏览器? 食💩吧。

Rails的场合

下面我们就来给Rails Application添加webpush。

首先我们要生成和push service用于通信的公钥和私钥。

安装webpush这个gem。

1
2
3
4
5
6
7
# gem install webpush
# One-time, on the server
vapid_key = Webpush.generate_key

# Save these in your application server settings
vapid_key.public_key
vapid_key.private_key

把生成的public keyprivate key 作为环境变量保存。

接下来,将webpushserviceworker-rails 这两个gem添加到Gemfile中。

1
2
gem 'webpush'
gem 'serviceworker-rails'

bundle install,运行generator

1
rails g serviceworker:install

这样就会生成以下文件。

1
2
3
4
5
config/initializers/serviceworker.rb - 配置service worker的文件
app/assets/javascripts/serviceworker.js.erb - 一个带有一些例子的serviceworker文件
app/assets/javascripts/serviceworker-companion.js - 用来把你的serviceworker注册到浏览器中
app/assets/javascripts/manifest.json.erb - 上面提到的manifest
public/offline.html - 这个暂时我也没管是啥

因为要嵌入我们之前的公钥,所以先把app/assets/javascripts/serviceworker-companion.js 重命名成 app/assets/javascripts/serviceworker-companion.js.erb

添加如下内容。

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
if ('serviceWorker' in navigator) {
console.log('Service Worker is supported');
var vapidPublicKey = new Uint8Array(<%= Base64.urlsafe_decode64(ENV["WEB_PUSH_VAPID_PUBLIC_KEY"]).bytes %>);
navigator.serviceWorker.register('/serviceworker.js')
.then(function(registration) {
console.log('Successfully registered!', ':^)', registration);
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey
})
.then(function(subscription) {
$.ajax({
url: "/sessions/subscribe",
type: "post",
headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')},
data: {
subscription: subscription.toJSON()
},
success: function(data){
}
});
console.log('endpoint:', subscription.endpoint);
});
}).catch(function(error) {
console.log('Registration failed', ':^(', error);
});
}

这里我们注册我们的serviceworker,注册成功之后,我们得把浏览器返回给我们的关于push 服务器的一些信息持久化保存起来,利用我们保存的信息,我们就可以主动推送信息了。
这里先看一下serviceworker.js的内容。

打开 serviceworker.js添加如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
function onPush(event) {
var json = event.data ? event.data.json() : {"title" : "DEBUG TITLE", "body" : "DEBUG BODY"};
event.waitUntil(
self.registration.showNotification(json.title, {
body: json.body,
icon: json.icon,
data: {
target_url: json.target_url
}
})
)
}
self.addEventListener("push", onPush);

这样我们就会在收到push消息的时候在浏览器的右上角显示消息框。至于传过来的event里面的内容是什么,现在暂时无视好了。

接下来我们再看一下持久化subscription的部分。
也就是这个js的具体实现。

1
2
3
4
5
6
7
8
9
10

$.ajax({
url: "/sessions/subscribe",
type: "post",
headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')},
data: {
subscription: subscription.toJSON()
},
success: function(data){
}

注意在Rails中,ajax要带上csrf的header。

如何持久化因需求而异,这里简单地塞到数据库的表中而已。如果没有用户登陆的情况下,我们则塞到session里面。

1
2
3
4
5
6
7
8
9
10
11
12
class SessionsController < ApplicationController

def subscribe
subscription = JSON.dump(params[:subscription].permit!.to_hash)
if current_user
current_user.update(subscription: subscription)
end
session[:subscription] = subscription
head :ok
end

end

接下来就是激动人心的发送部分了。在发送之前我们得做一些准备工作,比如先把我们要发送的subscription从数据库或者session里面取出来。

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
def fetch_subscription(user)
if user && user.subscription
encoded_subscription = user.subscription
else
raise "Cannot create notification: no :subscription for user"
end
JSON.parse(encoded_subscription).with_indifferent_access
end

## message format
#message: {
# icon: 'https://example.com/images/demos/icon-512x512.png',
# title: title,
# body: body,
# target_url: target_url
#}
def webpush_params(user,message)
subscription_params = fetch_subscription(user)
message = message.to_json
endpoint = subscription_params[:endpoint]
p256dh = subscription_params.dig(:keys, :p256dh)
auth = subscription_params.dig(:keys, :auth)
vapid = {
subject: 'gyorou@tjjtds.me',
public_key: ENV['WEB_PUSH_VAPID_PUBLIC_KEY'],
private_key: ENV['WEB_PUSH_VAPID_PRIVATE_KEY']
}
{ message: message, endpoint: endpoint, p256dh: p256dh, auth: auth, vapid: vapid }
end

{ message: message, endpoint: endpoint, p256dh: p256dh, auth: auth, vapid: vapid } 就是我们要喂给
webpush的全部内容了。

我们调用Webpush.payload_send webpush_params(user, message)就可以完成一次发送。

实装时候的一些困惑

  1. 我们肿么知道要发送的目标的endpoint,p256dh这些东西?

这些东西其实来自订阅成功之后浏览器返回给我们的结果,我们不需要自己指定,只需要把结果持久化,等到要发送时候取出来,填入发送方法的相关参数就好了。

暂时以上。

是谁强制我SSL?

今天准备用omniauth和omniauth-oauth2 这两个东西自己写一下使用forum.qilian.jp账号作为第三方登陆的策略。
发现了一个蛋疼的问题,尽管我配置的callback url是类似https://localhost:3000/callback这种形式,返回的callback居然自动切换成了https。
在local开发的是时候,自然不需要https,当然最好也不需要https,所以必须得把这个问题解决。
那么到底是什么造成了callback成了https呢?

Rails 的 force_ssl 选项

查看配置,force_ssl是false,这个锅应该是别人的。

DoorKeeper 的 force_ssl_in_redirect_uri 选项

同样查看配置,发现 force_ssl_in_redirect_uri 也是false。

???

Nginx

最后在Nginx的配置里找到了这么一行。

1
proxy_redirect https:// https://

proxy_redirect 会在重定向的时候根据提供的规则修改Location header。
原本的配置本意是强制所有重定都指向https,其实有点画蛇添足了。因为这么一改,把原本指向非本站点的重定向也强制成https了。
注释掉这么一句,终于拜托了讨厌的重定向,看一下时间,已经是快接近黄昏的时候了。

使用ELK来可视化微信群聊记录

自从有了itchat这个神器之后,一直在考虑能做些什么东西。
先后做了发送撤回消息的bot和推荐spotify音乐的bot,到头来,所有的聊天记录都静静地躺在我的redis里面,需要利用起来也十分麻烦。
于是最近,我突然想到,既然ELK能用来可视化log,那何尝不能用来可视化一下微信的聊天记录呢。

需要的东西

  1. ELK全家桶。

  2. Redis,用来通过在itchat中pub,在logstash中sub来获取微信聊天内容。

  3. Itchat。基于web微信的第三方SDK。

Step by Step

首先自然需要写个Itchat的脚本来获取群消息。例子看文档已经足够。

接下来假设我们获取到一个类似

1
message = {"msg_from": "gyorou", "msg_content": "fuck the world"}

的消息内容。我们需要把 message传递给logstash,再由logstash写入elasticsearch。

我想到的方法是使用Redis的pubsub方法。

先把message dump成 string的形式,交给redis publish出去。

1
2
3
4
5
6
7
8
import json
import redis
import itchat

# 省略
redis = redis.client(...)
message = json.dumps(message)
redis.publish('wechat_message', message)

为了接收publish的message,logstash应该有如下的输入配置。

1
2
3
4
5
6
7
input {
redis {
data_type => "channel"
codec => "json"
key => "wechat_message"
}
}

注意点是需要codec => "json"这一项,因为,我们publish的是一个string,我们要需要将其按照json的格式解析出来里面的内容。

接下来,配置logstash的输出。

1
2
3
4
5
6
output {
elasticsearch {
index => "from_my_wechat"
doucument_type => "chatlog"
}
}

以上两项分别对应elasticsearch的index和type。这样我们可以通过localhost:9200/from_my_wechat/chatlog的形式访问和操作被索引的聊天内容。

最后打开kibana,把我们的索引项目称from_my_wechat填进去就大功告成了。
随便点开图标一览,画两个图呗,比如统计一下谁特么发言最多这种。


To do

  • 对中文的field需要进行分词。
  • 对占用较大的无用field需要整理移除。

以上。

使用homebridge以及IRkit来声控电灯开关

需要的东西

  • 树莓派。作为用来跑homebridge的服务器。
  • IRkit。请上Amazon自行购买。
  • 带遥控的灯。

跳过的准备阶段

按照irkit的说明把初始化,连上家里的无线网。 树莓派自然也是要安装完系统这种。

开搞

  1. 树莓派安装node之后,需要安装 homebridge以及 homebridge的irkit plugin。
    我树莓派的系统是ubuntu-mate 16.04, 个人偏好使用anyenv这个工具安装管理所有常用编程语言。
1
2
3
4
$ sudo apt-get install libavahi-compat-libdnssd-dev

$ npm install -g --unsafe-perm homebridge
$ npm install -g homebridge-irkit
  1. 建立homebridge的config文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ~/.homebridge/config.json
{
"bridge": {
"name": "Homebridge",
"username": "CD:22:3D:E3:CE:30",
"port": 51826,
"pin": "031-45-156"
},

"description": "The SAMPLE",

"platforms": [],

"accessories": [
{
"accessory": "IRKit",
"name": "電気",
"irkit_host": "irkitxxxxx.local",
"on_form": {"format":"raw","freq":38,"data":[省略]},
"off_form": {"format":"raw","freq":38,"data":[省略]}
}
]
}

这里的 on_formoff_form 其实是我们要post给irkit的内容。irkit接收外界的http的request然后会根据我们post的内容发出信号控制开关。

那么我们肿么知道开关电灯要发出什么信号呢?

答案是,我们先把遥控器对准irkit射一下。然后利用irkit提供的api来获取刚刚irkit接收到的遥控器发来的信号。

1
2
3
4
5
6
7
% curl -i "https://irkitxxxxx.local/messages" -H "X-Requested-With: curl"
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Server: IRKit/2.1.3.13.gbe33d36
Content-Type: text/plain

{"format":"raw","freq":38,"data":[18031,8755,1190,1190,1190,3341,1190,3341,1190,3341,1190,1190,1190,3341,1190,3341,1190,3341,1190,3341,1190,3341,1190,3341,1190,1190,1190,1190,1190,1190,1190,1190,1190,3341,1190,3341,1190,1190,1190,3341,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,1190,3341,1190,3341,1190,3341,1190,3341,1190,3341,1190,65535,0,9379,18031,4400,1190]}

注意,我们get了一次之后,内容就清空了,只有我们再射一次,才能再get到内容。
总之,分别获取一下开和关的按钮的数据,填入相应的on_formoff_form就行了。

  1. 试一下

在树莓派上启动homebridge。然后打开iphone,找到home那个应用,连上家里wifi之后自动就能检索到我们的开灯关灯设备了。
把siri喊出来试一下呗。

效果如下

https://pi.bocchi.tokyo/index.php/s/wqW8wysOQEU84KS

简单调试API,jq 和 curl 足矣

以前调试API喜欢扔postman或者用irb各种开始写。
其实,使用curl 和 jq 就足够应付大多数需求了。

jq作为命令行下的json的parse工具,实在是略强大。

比如调用豆瓣的API获取其中的电影数组的第一个元素,可以用

1
curl https://api.douban.com/v2/movie/nowplaying\?apikey\=xxxxx |jq '.entries[0]'

嘛其他的看文档吧。以后别再傻乎乎开个irb开始各种require,各种写了。

当NAT内部的设备访问内网由路由的外界IP port forward过来的TCP端口的时候

已经想不到更短的标题了。问题简单描述就是兴冲冲把家里路由的外部IP绑定了动态dns,然后port forward到树莓派上。
装上了owncloud服务,挂上大硬盘,嗯不错。
可是一回家就傻眼了。

突然发现自己家里的电脑肿么都连不上树莓派上的owncloud。 手机切了wifi就又连上了。这是为什么呢?

为什么呀

答案纯引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 The problem here is, that your router does not NAT your internal client's address. Thus, the TCP handshake fails.

Let's assume following IPs

Client: 192.168.1.3
Server: 192.168.1.2
Router internal: 192.168.1
Router external: 123.123.123.1
Here is what is happening:

Client (192.168.1.3) sends TCP-SYN to your external IP, Port 80 (123.123.123.1:80)
Router sees port forwarding rule and forwards the packet to the server (192.168.1.2:80) without changing the source IP (192.168.1.3)
Client waits for a SYN-ACK from the external IP
Server send his answer back to the client directly, because it's on the same subnet. It does not send the packet to the router, which would reverse the NAT.
Client recieves a SYN-ACK from 192.168.1.2 instead of 123.123.123.1. And discards it.
Client still waits for a SYN-ACK from 123.123.123.1 and times out.

该肿么办

pc的话在 /etc/hosts里将绑定的host直接指定给内网ip。

手机该肿么办?谁来告诉我啊。

NAT