好难过,用blessed-contrib 画张图

五月花粉应该是消散了,终于摘掉了戴了将近两个月的口罩。
就像突然热起来的天气一般,谈了一场不到一个月就被发了朋友卡的恋爱。

好好学习吧。暂时还会难过一阵就是了。
伤心的时候,想想钱也许能好过一点。

blessed-contrib 是用来在命令行画表的工具。
所以,下面捉摸着如何用这个工具来及时显示日元以及美元汇率的走势。

如何随时获取日美汇率

我用的是下面的API。

https://finance.yahoo.com/webservice/v1/symbols/allcurrencies/quote/?format=json

用blessed-contrib画折线图

例子也是有了。

不过例子中的图是静态的,我们需要动态更新这个图。最简单的用setInterval刷新就行了,注意看文档的注意点。

一个坑

blessed-contrib默认当你会将Y轴的最大坐标设置成max(初始值中最大值的1.44倍,你设置的maxY),这个用来表示汇率的话就特么一条直线了。
所以按照这个PRPR改一下源码。

完成版

gist了,随意参考。

Oauth2 Password Grant Type

oauth2 里面最简单的password grant type。使用doorkeepr实现的话基本只需要用户名密码然后post获取用户token的api即可。

在config里面加入password的grant type。

1
2
3
#config/initializer/doorkeeper.rb

Doorkeeper.configuration.token_grant_types << "password"

调用API

1
2
3
4
5
6
7
post /oauth/token

params: {
"grant_type" : "password",
"username": 用户名,
"password": 密码,
}

注意参数重需要grant_type就是了。

Electron 处理文件路径时候的注意点

很多时候我们很自然而然喜欢类似filepath.split('/').sllice(0,-1).join("logfile")这种形式去在某个文件的同一目录下添加一个文件。
这个在*uix开发环境下并不会有什么问题,但是别忘了我们要做的是跨平台的东西,windows里面的文件路径是反斜杠。
所以正确做法是老实const path = require('path'),然后用path的API来进行文件的操作。

Laravel 5.4 下面Custom Auth的一些备忘

Laravel 提供了强大的用户登录功能。建立起Application之后直接 php artisan make:auth 就可以建立起一个最基本的基于用户邮箱密码登录的MiddleWare。
但是有时候,当我们需要进行二次开发,当数据库并不在本地,用户信息都需要通过API获取的时候,我们就需要按照laravel的接口自己来写用户的登录了。
这次我们需要从一个由Yar 的rpc api提供的接口中去获取用户信息。

配置自定义的用户登录Provider

1
2
3
4
5
//config/auth.php
'providers' => [
'users' => [
'driver' => 'yar_provider',
],

注册该Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace App\Auth;
use Auth;
use App\Auth\YarUserProvider;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* @return void
*/
public function boot()
{
Auth::provider('yar_provider', function($app, array $config) {
return new YarUserProvider();
});
}

}

可以看到我们需要返回一个YarUserProvider的object。在这个Provider中,我们需要完成的任务就是调用API获取用户信息并完成用户的验证。

实现custom的Provider

首先看一下我们需要实现的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace Illuminate\Contracts\Auth;

interface UserProvider {

public function retrieveById($identifier);
public function retrieveByToken($identifier, $token); //用于没有session的情况,比如API的http的header。这次无视。
public function updateRememberToken(Authenticatable $user, $token); // 暂时不管
public function retrieveByCredentials(array $credentials);
public function validateCredentials(Authenticatable $user, array $credentials);

}

retrieveById($identifier)一般用户当我们刷新页面的时候,我们的app从session中获取用户id,然后通过我们的实现,用这个id来获取用户信息。
这次我们需要做的仅仅是调用Yar的API(传递用户id,返回用户信息的API)。

retrieveByCredentials(array $credentials);用户用户登录时候提供用户名(或者邮箱),我们来通过这个用户名获取用户信息,以供validateCredentials(Authenticatable $user, array $credentials);来进行用户的验证。

我们看到我们都需要返回一个Authenticatable $user的东西。 这个接口要求我们去实现很多user model的功能,比如获取密码之类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace Illuminate\Contracts\Auth;

interface Authenticatable {

public function getAuthIdentifierName();
public function getAuthIdentifier();
public function getAuthPassword();
public function getRememberToken();
public function setRememberToken($value);
public function getRememberTokenName();

}

自己统统去实现自然也是可以的,不过Laravel已经提供了一个叫GenericUser的类来帮助我们啦。我们直接把从API获取的用户信息来生成一个GenericUser的对象就行了。

想这样。 当然由于GenericUser需要有些特殊的attribute,所以当没有的时候我们可以找适当的项目不上。比如下面的例子,我们需要提供一个name的attribute。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function validateCredentials(\Illuminate\Contracts\Auth\Authenticatable $user, array $credentials)
{
// we'll assume if a user was retrieved, it's good
$attributes = $this->client->Auth(['username' => $credentials["username"], 'passwd'=>$credentials["password"]]);
if($attributes["status"] != 1)
{
return null;
}
else
{
$attributes["data"]["name"] = $attributes["data"]["firstname"].$attributes["data"]["lastname"];
return new GenericUser($attributes["data"]);
}
}

具体实现就不贴了,由于API本身不是很优雅,上层实现也不是那么优雅就是了。

How Mastodon was implemented part1

This is just a memo and I will modify it later for better understanding.

What happened when we follow a remote user

code

first find local, otherwise find remote in the current instance db

code

not found, than use webfinger

using username@host can help find out the user information provided by other mastondon instances

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
$ curl https://client.webfinger.net/lookup?resource=gyorou_bocchi%40mustodon.bocchi.tokyo

# response
{
"subject": "acct:gyorou_bocchi@mustodon.bocchi.tokyo",
"aliases": [
"https://mustodon.bocchi.tokyo/@gyorou_bocchi"
],
"links": [
{
"rel": "https://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://mustodon.bocchi.tokyo/@gyorou_bocchi"
},
{
"rel": "https://schemas.google.com/g/2010#updates-from",
"type": "application/atom+xml",
"href": "https://mustodon.bocchi.tokyo/users/gyorou_bocchi.atom"
},
{
"rel": "salmon",
"href": "https://mustodon.bocchi.tokyo/api/salmon/3"
},
{
"rel": "magic-public-key",
"href": "data:application/magic-public-key,RSA.uSUCVT8zv3GRmdR9mMFvPTif_9geHjYQkYPIgHnyzIhOJcagtZUCWh0fAd_zXYjxoLE7NVYnA5IocvUIHLqZblPWujjaGbtCWRW54GpDGaXBiJPwienneBrGhSwkWEWwFjsNqPzvZifXKKSnsXE_Ryf6acfNP-Xmi5mRGod75j1cCWC291PbXfoIT0Xt8wYB9VMGCOgUiVvKZx1vhS39C7jPuV37WNr-Y0y7mGI4zZtSrRAbM9GHo1B32CyhkgXgXnEJw2miomeYQcWMYeQXg4eDLCq-JNqv7ppIQZwBumlj1Zn0zTe0ZrDOWxyRiU_6JZDbcduUjY0PJ-O5gtnd1Q==.AQAB"
},
{
"rel": "https://ostatus.org/schema/1.0/subscribe"
}
]
}

mustodon will create an account object for each of these fields.

1
2
irb(main):001:0> Account
=> Account(id: integer, username: string, domain: string, secret: string, private_key: text, public_key: text, remote_url: string, salmon_url: string, hub_url: string, created_at: datetime, updated_at: datetime, note: text, display_name: string, uri: string, url: string, avatar_file_name: string, avatar_content_type: string, avatar_file_size: integer, avatar_updated_at: datetime, header_file_name: string, header_content_type: string, header_file_size: integer, header_updated_at: datetime, avatar_remote_url: string, subscription_expires_at: datetime, silenced: boolean, suspended: boolean, locked: boolean, header_remote_url: string, statuses_count: integer, followers_count: integer, following_count: integer)

In the above example, "https://mustodon.bocchi.tokyo/users/gyorou_bocchi.atom" will be the remote_url, which we can fetch the latest status of the remote user, and salmon_url will be https://mustodon.bocchi.tokyo/api/salmon/3, which we use salmon protocol to reply the status of the remote user.

hub_url is the pubsubhubbub server endpoint, we post to it to register our webhook.

the webhook callback url will be saved in the Subscription model.

1
2
3
irb(main):003:0> Subscription.first
Subscription Load (15.5ms) SELECT "subscriptions".* FROM "subscriptions" ORDER BY "subscriptions"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<Subscription id: 1, callback_url: "https://abysswalking.net/api/subscriptions/2", secret: nil, expires_at: "2017-04-14 18:16:29", confirmed: false, account_id: 3, created_at: "2017-04-07 18:16:29", updated_at: "2017-04-07 18:16:29", last_successful_delivery_at: nil>

whenever we push a new status. we post the callback_url for each of our followers to notify them the update.
And the callback_url related action will save the new status received into a status object in the instance db.

1
2
3
4
irb(main):005:0> Status.first
Status Load (5.0ms) SELECT "statuses".* FROM "statuses" ORDER BY id desc LIMIT $1 [["LIMIT", 1]]
=> #<Status id: 4668, uri: "tag:oransns.com,2017-04-18:objectId=144679:objectT...", account_id: 211, text: "<p><span class=\"h-card\"><a href=\"https://mastodon....", created_at: "2017-04-18 15:14:14", updated_at: "2017-04-18 15:14:17", in_reply_to_id: 4661, reblog_of_id: nil, url: "https://oransns.com/users/okome/updates/8182", sensitive: false, visibility: "public", in_reply_to_account_id: 238, application_id: nil, spoiler_text: "", reply: true, favourites_count: 0, reblogs_count: 0>
irb(main):006:0>

试着做一下微信的词云

之前用Itchat写了抓取微信群聊撤销内容的脚本,作为副产物,所有的群聊记录都在我的Redis里面静静地躺着。
今天终于我怀着百无聊赖地心情把所有聊天的正文都抽取了出来看看能做出些什么可视化的东西。

第一个想到的肯定是类似词云的东西啦。
抽取很简单,我就不说了,分词使用三年前自制的Ruby gem Kurumi
停用词比较头疼,拉来了这里的内容,不过漏网之鱼还是很多。
也许考虑一下如何转换一下tfidf来适配这些群聊记录的比重提取比较重要。

这次就先不考虑这么多啦。分词之后直接用空格隔开,把生成的文本随便网上找个在线词云工具扔上去就是了。比如这个

结果在这里。

分析

由于没有方法过滤用户名出现在聊天内容里的情况,所以,被提及很多次的人的Id就出现了。

“中国,日本, 学校,公司”等词频繁出现也较合情合理。

“特么” 果然不是只有我经常说。

“一起,喜欢,需要”,可见大家应该都比较寂寞吧。

哈哈哈, 其他自己慢慢欣赏吧。

开始Mastodon吧

最近两天如果关注github trend的话会发现有个叫Mastodon的Rails项目的人气正在飞速上升中。

引用ReadMe的介绍, 可以发现这个是一个去中心化的微博系统。
话说我们可能对去中心化相关听得最多的应该就是blockchain相关了。
这里所谓的去中心化最重大的意义就是不会有服务商来投广告,塞抹布,黄赌毒各种不良信息随便发。

Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.

那么用户如何创建一个Mastodon的账号呢?

很简单,从这里的节点中选择一个节点,点进去注册即可。注册之后用户名就相当于username@host的形式。用这个用户名就可以和任何instance上的任何用户发生关系(follow, retweet之类的)啦。

如何找到感兴趣的用户?

mastodon提供了从twitter好友中发现mastodon用户的方式(这里)。 经试用悲剧地发现并没有好友在使用mastodon。当然还有其他方法,比如等别人把自己的用户名贴出来什么的。

如何自己host一个Mastodon的实例?

按照ReadMe的方法用Docker-compose是最快的方法。

clone项目之后docker-compose build && docker-compose up -d 就好。

当然要公开你的instance别忘了加上反向代理和SSL。
SSL的话用Letsencrypt即可。

当然实际部署上去之后可能会有各种问题,比如我遇到了邮箱不能发送验证邮件的问题。解决方法是
去掉了这一行。https://github.com/tootsuite/mastodon/blob/master/config/environments/production.rb#L101

最后

这个是我的Instance。

我的id是 `gyorou_bocchi@mustodon.bocchi.tokyo`

欢迎大家和我搞基以及聊三俗话题。

SNS

用Rails实现带token验证的微信小程序服务端

用微信文档中的一张图解释。

服务器端要做的就是在客户端通过微信的api获得临时的code之后,调用获取用户openidsession_key的api并存入数据库。由于openid对应唯一用户,所以就能保证一个用户在服务器端有唯一一个user object与其对应。我们再为这个用户生成在服务器端可以代表这个用户的access_token,返回给客户端,客户端带上这个access_token就可以很方便待用需要验证用户的api了。

自己实现生成access_token其实也不复杂,不过用knock这个gem可以节省一些时间。

卧槽我不想写了,今天本来说好去赏樱结果下雨,说好去唱k结果特么没凑齐人,结果就是郁闷写了一天代码啊。
具体怎么实现自己看下面吧。

源码

微信小程序试水

微信小程序用于开放了个人开发。可蛋疼的事情是,首先注册需要国内的手机号接受验证码以及通过绑定国内银行卡进行身份验证,对于国内身份证已经过期的我来说,简直是个噩耗。
好在朋友帮忙搞定把我加到了开发号中,终于可以尝鲜体验一把。
但是注意应用上线还是需要通过备案的域名以及服务器,海外党不亲自回趟国办理备案基本无能为力,这种繁琐的流程极大打击了开发者的积极性。

总体来说微信小程序是一个封闭的H5框架,使用微信提供的api以及dom元素构建vc。每个页面对应一个js文件,一个wxsml文件(view),以及一个wxss、
文件(style). js文件作为controller,在这个js中完成渲染view需要的变量的定义,以及定义用户交互时间发生之后的处理函数。wxsml中用微信定义的view渲染并嵌入变量。

整个小程序有一个总的app.js作为小程序的入口。app.json中定义总的页面元素,window元素,其对应的路径,图表之类。app.wxss中则是总的style。

比如我例子中的小程序有两个tab,一个是index,一个是logs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
☁ 10:59PM tokyo_dev  tree
.
├── app.js
├── app.json
├── app.wxss
├── image
│   ├── avatar.png
│   ├── girl.png
│   └── me.png
├── pages
│   ├── index
│   │   ├── index.js
│   │   ├── index.wxml
│   │   └── index.wxss
│   └── logs
│   ├── logs.js
│   ├── logs.json
│   ├── logs.wxml
│   └── logs.wxss
└── utils
└── util.js

在app.json中可以定义tab的路径和图表。

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
{
"pages":[
"pages/index/index",
"pages/logs/logs"
],
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "东京健康外卖",
"navigationBarTextStyle":"black"
},
"tabBar": {
"color": "#dddddd",
"selectedColor": "#3cc51f",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "image/girl.png",
"selectedIconPath": "image/girl.png",
"text": "菜单"
}, {
"pagePath": "pages/logs/logs",
"iconPath": "image/me.png",
"selectedIconPath": "image/me.png",
"text": "我"
}]
}
}

很容易理解,我也不多废话了。

再来看page/index/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//index.js
//获取应用实例
var app = getApp()

var items = [{"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/katou_natumi2.jpg", "oto": "katou_natumi2", "yomi": "\u304b\u3068\u3046\u306a\u3064\u307f2", "gyou": "ka", "id": 1000617, "name": "\u52a0\u85e4\u306a\u3064\u307f"}, {"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/kanou_momoka.jpg", "oto": "kanou_momoka", "yomi": "\u304b\u306e\u3046\u3082\u3082\u304b", "gyou": "ka", "id": 1000734, "name": "\u53f6\u6843\u82b1"}, {"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/kasuga_momo.jpg", "oto": "kasuga_momo", "yomi": "\u304b\u3059\u304c\u3082\u3082", "gyou": "ka", "id": 1000904, "name": "\u6625\u65e5\u3082\u3082"}, {"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/kaduki_manaka.jpg", "oto": "kaduki_manaka", "yomi": "\u304b\u3065\u304d\u307e\u306a\u304b", "gyou": "ka", "id": 1000956, "name": "\u83ef\u6708\u307e\u306a\u304b"}, {"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/kaduki_aika.jpg", "oto": "kaduki_aika", "yomi": "\u304b\u3065\u304d\u3042\u3044\u304b", "gyou": "ka", "id": 1001037, "name": "\u6a3a\u6708\u611b\u83ef"}, {"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/kaede_himeki.jpg", "oto": "kaede_himeki", "yomi": "\u304b\u3048\u3067\u3072\u3081\u304d", "gyou": "ka", "id": 1001357, "name": "\u6953\u59eb\u8f1d"}, {"thumb": "https://pics.dmm.co.jp/mono/actjpgs/thumbnail/kazami_nagisa.jpg", "oto": "kazami_nagisa", "yomi": "\u304b\u3056\u307f\u306a\u304e\u3055", "gyou": "ka", "id": 1001630, "name": "\u98a8\u898b\u6e1a"} ]
Page({
data: {
items: []
},
onLoad: function () {
var that = this;
wx.setNavigationBarTitle({
title: '外卖'
});
that.setData({items: items});
}
})

这里使用setData来定义要嵌入view的变量。 这个data可以来自外部api。这个外部api只能用wechat封装好的wx.request调用。
wx.request(err,res)是个典型的callback函数,我们也可以用第三方的裤将其改写成promise形式。这里也不废话了。

在看view。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--index.wxml-->
<view class="container">
<block wx:for-items="{{items}}">
<view class="flex item">
<view class="item_left">
<image src="{{item.thumb}}"/>
</view>
<view class="flex_auto item_middle">
<view><text class="title">{{item.name}}</text></view>
<view><text class="sub_title">{{item.yomi}}</text></view>
</view>
<view class="item_right">
<view><text class="action">出张中</text></view>
</view>
</view>
</block>
</view>

dom怎么摆看文档就行了。

最后效果图一张。

源代码

用huginn 实现health check 并通知Slack

huginn是一款功能强大的agent工具。其功能大抵和IFTTT相当,但前者的优势是开源,可定制。
接下来试着用huginn实现网站health check的功能,如果网站挂了,则通过webhook通知slack。

直接用docker的话基本上打开即用了。用默认用户名密码登录之后别忘了改密码和用户名。
然后开始创建Agent。

所谓的Agent就是完成某种任务的一个代理。huginn中,Agent可以用receiver,用来接受其发出的Event。
比如我们基于HttpStatusAgent建立一个我们自己的forum_health_check Agent.

这个agent作用很简单,就是去获取一下目标的页面,然后返回一些信息。这些信息被保存在叫做Event的一个hash之中传给下一个Agent,也就是这里的receiver。
我们可以点击一下dry run看一下具体的返回内容。

接下来我们基于TriggerAgent建立forum_health_check的receiver。在source里面填上forum_health_check 表示我们这个agent接收从forum_health_check传来的消息。

在option里面,我们看右边的说明,可以在rules数组中包含符合我们要求的rule。比如这里,当我们关心的Event中的filed(path)不符合我们预期的值(value)的时候(也就是传来的Event中status的值不是200的时候),这个触发器会被触发,然后继续产生Event传送给下一个receiver。

我们接着建立下一个receiver。这次很简单,我们选择基于SlackAgent,填上webhook的一些内容,然后就ok了。

最后看一下我们的这个三个Agent的关系图。

嗯以上。