ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

基于Erlang的企业微信机器人

2021-11-15 18:05:58  阅读:236  来源: 互联网

标签:ok 微信 机器人 Msg jiffy json msgtype encode Erlang


在工作的微信群发现可以添加群机器人,本着好奇和探索的心态,用Erlang开发了一个能推送消息的群机器人

环境

企业微信+Erlang+Jiffy框架

关于Jiffy框架的使用我在这篇有过介绍 Erlang解析JSON之Jiffy篇 ,下面是关于机器人代码的添加过程

前置任务

当然是在企业微信的某个群先创建一个群机器人,需要拉两个人才能创群哦,别手误把老大拉进来了~

image-20211115141622750

拉完以后就可以直接添加机器人了,创建完以后会给我们一个机器人的webhook地址,复制下来,然后也有一个简略的API可以查看,

image-20211115141712974

可以发现直接通过该webhook发送post请求,且附带指定json参数,就可以对机器人进行操作,下面我们可以开始尝试用机器人发出第一条消息

搭建demo

发送文字

新建一个gen_server服务,这样能保证在后台持续运行,你也可以添加对应的监控树也好,重启策略也好

我们将发送代码写进其中一个handle_call/2,然后导出一个接口即可

可以把刚才的WebHook新建一个宏定义,等下直接调用定义名即可

-define(WebHook, "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=7fcbd8ef-644c-4473-aaaaaaaaaa").
%% 后面的具体内容替换成你的机器人链接哦

发送hello world

下面我们测试第一种,发送一个hello world的内容

API中测试的例子为

curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693axxx6-7aoc-4bc4-97a0-aaaaaaaaaa' \
   -H 'Content-Type: application/json' \
   -d '
   {
        "msgtype": "text",
        "text": {
            "content": "hello world"
        }
   }'

可以发现请求头为Content-Type: application/json,Data数据部分为json字符

在Erlang中我们可以调用httpc:request来实现该需求

handle_call(test, _From, State) ->
    Msg = jiffy:encode({[{msgtype, <<"text">>},{text, {[{content, <<"hello world">>}]}}]}),
    {ok, _Result}=httpc:request(post, {?WebHook, [], "applcation/json", Msg},[],[]),
    {reply, ok, State};

这里jiffy:encode内的内容对应的就是上面的json内容,是不是觉得很阴间?其实也有第二种写法,可以传入Map的结构,也就是 #{}结构,稍微阳间一点

handle_call(test, _From, State) ->
    Msg = jiffy:encode(#{msgtype => <<"text">>,text=>{[{content,<<"干饭人"/utf8>>}]}}),
    {ok, _Result}=httpc:request(post, {?WebHook, [], "applcation/json", Msg},[],[]),
    {reply, ok, State};

注意每一项的层级对应关系,不放心的话可以先用jiffy:encode导出看看得到的是不是对应的json结构

测试导出

21> jiffy:encode({[{msgtype, <<"text">>},{text, {[{content, <<"hello world">>}]}}]}).
<<"{\"msgtype\":\"text\",\"text\":{\"content\":\"hello world\"}}">>
22> io:format("~ts",[jiffy:encode(#{msgtype => <<"text">>,text=>{[{content,<<"hello world">>}]}})]).
{"text":{"content":"hello world"},"msgtype":"text"}ok

可以发现两种结构导出都没什么问题,都能得到Erlang对应的Json数据流,下面我们测试一下发送

哦对了,启动服务之前,记得启动Erlang中inets和ssl的模块

init([]) ->
    inets:start(),
    ssl:start(),
    {ok, #state{}}.

测试发送

Eshell V10.3  (abort with ^G)
1> robot_demo:start().
{ok,<0.78.0>}
2> robot_demo:test().
ok
3> 

image-20211115143519935

可以发现我们的第一步完成了,hello world成功了那自然是成功了一半,但也不要高兴的太早,下面我们测试一下Erlang发送中文的情况,且以下的json转码前结构我们均采用Map的形式来存储

发送你好我是机器人

我们简单的把content改成中文试试

handle_call(test1, _From, State) ->
    Msg = jiffy:encode(#{msgtype => <<"text">>,text=>{[{content,<<"你好我是机器人">>}]}}),
    {ok, _Result} = httpc:request(post, {?WebHook, [], "applcation/json", Msg}, [], []),
    {reply, ok, State};

编译以后发现直接崩了

gen_server:call(wechat_robot, {send, jiffy:encode(#{msgtype => <<"text">>,text=>{[{content,<<"你好我是机器人">>}]}})}).
** exception error: {invalid_string,<<"�ɷ���">>}
     in function  jiffy:encode/2 (src/ip_set_address/jiffy.erl, line 99)

咋回事呢?既然问题是出现在Jiffy,那就回忆一下Jiffy框架的特性,查看ReadMe和报错的源码

image-20211115144028721

发现问题可能是我传进去的中文问题,将对应编码改为utf8即可,回头看看API发现也是有提示我们这么做的

image-20211115144451937

在Erlang将中文转化为utf8也有两种方案

141> unicode:characters_to_binary("干饭人").
<<229,185,178,233,165,173,228,186,186>>
142> unicode:characters_to_binary("干饭人",utf8).
<<229,185,178,233,165,173,228,186,186>>
143> <<"干饭人"/utf8>>.
<<229,185,178,233,165,173,228,186,186>>

这里关于unicode和utf8的关系大家可以自己去查阅一下,还是挺有意思的,之前我在两种编码上翻过车,记录在Unicode和UTF8的区别

可以发现都没问题,我们修改一下handle_call代码

handle_call(test1, _From, State) ->
%%    Msg = jiffy:encode(#{msgtype => <<"text">>,text=>{[{content,<<"你好我是机器人"/utf8>>}]}}),
    Msg = jiffy:encode(#{msgtype => <<"text">>,text=>{[{content,unicode:characters_to_binary("你好我是机器人")}]}}),
    {ok, _Result} = httpc:request(post, {?WebHook, [], "applcation/json", Msg}, [], []),
    {reply, ok, State};

测试发送

image-20211115145302213

下面我们编写一个更复杂的功能,找一个能返回天气数据的API网站,我们将查找到的天气数据传给这个机器人,让机器人每天早上定时发送天气数据

发送天气数据

首先我们找一个接口网站,这里使用了搞得开放平台的API接口,具体页面如下链接

https://lbs.amap.com/api/webservice/guide/api/weatherinfo#weatherinfo

Tips:高德开放平台注册个人开发账后以后比较稳定,且有很多其他的功能可以玩

可以看到这里返回了一个json数据,我们又要用到Jiffy的解码了

{"status":"1","count":"1","info":"OK","infocode":"10000","lives":[{"province":"广东","city":"广州市","adcode":"440100","weather":"多云","temperature":"23","winddirection":"北","windpower":"≤3","humidity":"39","reporttime":"2021-11-15 15:30:43"}]}

可以发现返回的东西还是有点多了,我们肯定是挑需要的拿,且这里的请求我们还是使用到httpc:request的方法,只不过参数改为get,

请求逻辑

WeatherUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=这里写你注册的高德的key&city=440100(这里是城市的adcode,我这里写了广州)",
    case catch httpc:request(get, {WeatherUrl, []}, [], [{full_result, false}]) of
        {ok, {200, Json}} ->
            case jiffy:decode(Json) of
                {_Data} ->
                    %% 具体操作
                    ok;
                _ ->
                    ok
                    %%返回错误信息
            end;
        _Err ->
            io:format("请求获取天气信息失败:~w", [_Err])
    end,

这里我们简单写一个解码过程,大概是这个样子,然后我们来解析返回给我们的Json数据

解码过程

我们用httpc请求返回的数据长什么样可以先尝试一下

2>{ok, {200, Json}} = httpc:request(get, {WeatherUrl, []}, [], [{full_result, false}]).
{ok,{200,
     [123,34,115,116,97,116,117,115,34,58,34,49,34,44,34,99,111,
      117,110,116,34,58,34,49,34|...]}}
4> {Data} = jiffy:decode(Json).
{[{<<"status">>,<<"1">>},
  {<<"count">>,<<"1">>},
  {<<"info">>,<<"OK">>},
  {<<"infocode">>,<<"10000">>},
  {<<"lives">>,
   [{[{<<"province">>,<<229,185,191,228,184,156>>},
      {<<"city">>,<<229,185,191,229,183,158,229,184,130>>},
      {<<"adcode">>,<<"440100">>},
      {<<"weather">>,<<229,164,154,228,186,145>>},
      {<<"temperature">>,<<"23">>},
      {<<"winddirection">>,<<229,140,151>>},
      {<<"windpower">>,<<226,137,164,51>>},
      {<<"humidity">>,<<"39">>},
      {<<"reporttime">>,<<"2021-11-15 15:30:43">>}]}]}]}

可以发现返回的参数还可以,我们如果取weather,可以先把Data的外衣{}匹配掉,发现剩下的是一个list,怎么取呢,直接lists:keyfind强行拿出来吧hhh,就有了以下代码

handle_call(get_weather, _From, State) ->
    WeatherUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=这里写你注册的高德的key&city=440100(这里是城市的adcode,我这里写了广州)",
    WeatherInfo =
        case catch httpc:request(get, {WeatherUrl, []}, [], [{full_result, false}]) of
            {ok, {200, Json}} ->
                case jiffy:decode(Json) of
                    {Data} ->
                        {_, [{LivesData}]} = lists:keyfind(<<"lives">>, 1, Data),
                        {_, Weather} = lists:keyfind(<<"weather">>, 1, LivesData),
                        Weather;
                    _ ->
                        404
                    %%返回错误信息
                end;
            _Err ->
                io:format("请求获取天气信息失败:~w", [_Err]),
                404
        end,
    WeatherMsg = list_to_binary([unicode:characters_to_binary("广州当前天气为:"),
        WeatherInfo]),
    Msg = jiffy:encode(#{msgtype => <<"text">>,
        text=>{[{content, WeatherMsg}]}}),
    {ok, _Result} = httpc:request(post, {?WebHook, [], "applcation/json", Msg}, [], []),
    {reply, ok, State};

关于上面的Json解析,其实也可以用map返回,也更好处理,仁者见仁智者见智吧

handle_call(get_weather, _From, State) ->
    WeatherUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&city=440100",
    WeatherInfo =
        case catch httpc:request(get, {WeatherUrl, []}, [], [{full_result, false}]) of
            {ok, {200, Json}} ->
                case jiffy:decode(Json, [return_maps]) of
                    MapData ->
                        [LivesData] = maps:get(<<"lives">>, MapData),
                        #{<<"weather">> := Weather,<<"city">> := City, <<"humidity">> := Humidity,
                            <<"province">> := Province, <<"reporttime">> := ReportTime,
                            <<"temperature">> := Temperature, <<"winddirection">> := WindDirection,
                            <<"windpower">> := WindPower} = LivesData,
                        io:format("现在是~ts,~ts~ts当前气温为~ts℃,空气湿度为~ts,风力为~ts,风向为~ts",
                            [ReportTime,Province, City, Temperature, Humidity, WindPower, WindDirection]),
                        Weather;
                    _ ->
                        404
                    %%返回错误信息
                end;
            _Err ->
                io:format("请求获取天气信息失败:~w", [_Err]),
                404
        end,
    WeatherMsg = list_to_binary([unicode:characters_to_binary("广州当前天气为:"), WeatherInfo]),
    Msg = jiffy:encode(#{msgtype => <<"text">>,
        text=>{[{content, WeatherMsg}]}}),
    {ok, _Result} = httpc:request(post, {?WebHook, [], "applcation/json", Msg}, [], []),
    {reply, ok, State};

测试发送

拼接是有点丑,但是无伤大雅

image-20211115155442810

返回Map的情况:

1> robot_demo:start().
{ok,<0.78.0>}
2> robot_demo:get_weather().
现在是2021-11-15 16:00:55,广东广州市当前气温为23℃,空气湿度为39,风力为<=3,风向为北ok
3>

当然了,天气API有很多,如果你的API接口多种多样则可以不同信息的解析,下面我们测试一下发送图片

发送图片

这个相对复杂一些,但也都可以解决

首先我们查看一下API,需要些什么,

{
    "msgtype": "image",
    "image": {
        "base64": "DATA",
        "md5": "MD5"
    }
}

乍一看挺简单的,就三个参数,问题来了,Erlang怎么获取这些参数呢?

参数是否必填说明
msgtype消息类型,此时固定为image
base64图片内容的base64编码
md5图片内容(base64编码前)的md5值

一步一步来,我们先用Erlang获取图片的base64编码

Erlang获取图片的base64编码

先准备一张图片,我这里准备了一张喝水的表情包,放在我们项目下的目录中

image-20211115174630960

注意大小不要超过2M(表情包会有多大呢?)

image-20211115174654893

核心代码为base64:encode/1

处理图片代码

handle_call(send_pic, _From, State) ->
    Md5 =
        case os:type() of
            {win32, _} ->
%%                DD = os:cmd("certutil -hashfile " ++ Path ++" MD5"),
                [_, MatchMd5Win, _] = string:tokens(os:cmd("certutil -hashfile ./pic/drink.jpg MD5"), "\r\n"),
                MatchMd5Win;
            _ ->
%%                [H|_] = string:tokens(os:cmd("md5sum " ++ Path)," "),
                [H | _] = string:tokens(os:cmd("md5sum \"drink.jpg\""), " "),
                H
        end,
    {ok, BinStream} = file:read_file("./pic/drink.jpg"),
    Base64 = base64:encode(BinStream),
    Msg = jiffy:encode(#{msgtype => <<"image">>, image =>
    {[{base64, unicode:characters_to_binary(Base64)}, {md5, unicode:characters_to_binary(Md5)}]}}),
    {ok, _Result} = httpc:request(post, {?WebHook, [], "applcation/json", list_to_binary(Msg)}, [], []),
    {reply, ok, State};

注意这里的jiffy:encode返回的是一个list(在这里卡了很久,不知道为什么这里返回一个list,是因为太长了自动拼接了?),所以我们要把list转回binary,再推送给我们的机器人接口,同时这里写了两种win和linux下 获得md5加密值的方式,感觉还是挺有意思的,两种环境下的md5好像是不一样的,这里都调用了系统的cmd md5接口,没有使用erlang:md5,那个返回的东西要自己转码,也不是不能用,但是转出来的不太一致,建议不用

win下调用erlang:md5的值(建议不用)

5> list_to_binary([io_lib:format("~2.16.0b", [N]) || N <- binary_to_list(erlang:md5("drink.jpg"))]).
<<"921824ad7b62bfd94f5f7525cced39be">>

测试发送

image-20211115175301752

到这里教程就告一段落了,之所以写在gen_server中也是方便服务一直运行,你可以编写特定的时间函数,让机器人在指定的时间推送指定的消息,如早上八点报天气,饭点发个干饭表情包,每两个小时来一张提醒喝水表情包等,甚至可以把这个模块放进项目中(别git push了哦),在项目报错的时候,监控Err信息转发给机器人,将对应的错误发送到群聊中

最后祭出喝水表情包原图

drink

标签:ok,微信,机器人,Msg,jiffy,json,msgtype,encode,Erlang
来源: https://blog.csdn.net/weixin_43876186/article/details/121339867

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有