Silence 发布的文章

钉钉的机器人群消息很好用,也不用申请特别的权限,免费的消息条数也很多,做个监控类的消息通知很合适。一般都是用 python、JAVA 等语言来编写调用代码,简单使用也可以直接命令行调 curl。
这几天想给一个用 delphi 编写的运维工具加上钉钉消息功能。尝试编写了一下,用自定义关键字方式发送消息很简单,一次就通过了;然而加签方式却死活通不过,总是返回加签错误。仔细阅读了N遍官方文档,就是个很常用的 HMAC-SHA256 + Base64 加签算法。蹊跷的是,我换了三四种不同的代码去实现,每一种算法得到的结果和网上的在线计算器的结果都一模一样,然而就是和官方的 Python 语言例程的结果不一样。
晚饭后出门散步时我继续思考这个问题:既然我的代码和在线计算器的一致,说明算法本身没有错误,那么只能是输入参数不一致;这时我突然领悟到,Python 和 JAVA 语言都会自动处理 \n 这样的转义字符串而 Delphi 不会,官方文档里要求在加签字符串中添加了一个 \n,肯定就是这里导致的错误。后来在代码中用 #10 来代替 \n,果然验签通过。
那么就分享一段完整的用 delphi 发送钉钉机器人消息的代码吧:

unit main;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  System.JSON, Vcl.StdCtrls, Hash, System.Net.URLClient, DateUtils,
  System.Net.HttpClient, System.Net.HttpClientComponent, NetEncoding;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    NetHTTPClient1: TNetHTTPClient;
    function dingtalk(content: string): boolean;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
  dingtalk('test,测试。[关键字]');
end;

function TForm1.dingtalk(content: string): boolean;
//钉钉机器人消息发送函数
//URL 格式:https://oapi.dingtalk.com/robot/send?access_token=XXXXXX&timestamp=XXX&sign=XXX
var
  url, keywords, token, secret, timestamp, sign, str: string;
  s: TStringStream;
  json: TJSONObject;
begin
  //result := false;
  begin
    NetHTTPClient1.ContentType := 'application/json';
    NetHTTPClient1.AcceptCharSet := 'utf-8';
    NetHTTPClient1.AcceptLanguage := 'zh-CN';
    token := 'xxxxxxxx';
    keywords := '';
    secret := 'SECxxxxxxxx';
    url := 'https://oapi.dingtalk.com/robot/send?access_token=' + token;
    (*
    发送消息有两种安全策略,自定义关键字和加签,至少选择一种,也可同时使用。
    secret 为加签密钥,不为空时表示需要加签。
    timestamp 为毫秒级时间戳。
    加签算法是常用的 HmacSHA256 + Base64:
    把 timestamp+"\n"+secret 当做签名字符串,使用HmacSHA256算法计算签名,
    然后进行 Base64 编码,将得到的结果再进行 urlEncode 编码,从而得到最终签名数据 sign。
    *)
    if secret <> '' then
    begin
      //获取毫秒级时间戳
      timestamp := (MilliSecondsBetween(Now, EncodeDateTime(1970, 1, 1, 0, 0, 0, 0)) - 8 * 60 * 60 * 1000).ToString;
      //加签
      sign := TNetEncoding.Base64.EncodeBytesToString(THashSHA2.GetHMACAsBytes(timestamp + #10 + secret, secret, SHA256));
      //URLEncode 编码
      sign := TNetEncoding.url.Encode(sign);
      //拼接 url
      url := url + '&timestamp=' + timestamp + '&sign=' + sign;
    end;
    //keywords 为自定义关键字,若为空时则必须加签,即 secret 不能为空。
    if keywords <> '' then
      keywords := keywords + '\n';
    //拼接 json 格式的消息内容字符串,如有自定义关键字,可插入到内容的任意位置。
    s := TStringStream.Create('{"msgtype":"text","text": {"content":"' + keywords + content + '"}}', TEncoding.UTF8);
    //发送 post 请求
    try
      str := NetHTTPClient1.Post(url, s).ContentAsString(nil);
    except
      on E: Exception do
        memo1.Lines.Add('发送消息时出错:' + e.Message);
    end;
    //解析返回结果
    try
      json := TJSONObject.ParseJSONValue(str, true, true) as TJSONObject;
      if json.GetValue<integer>('errcode') = 0 then
      begin
        result := true;
        memo1.Lines.Add('消息发送成功。');
      end
      else
      begin
        result := false;
        memo1.Lines.Add('消息发送失败,错误代码:' + json.GetValue<string>('errcode') + json.GetValue<string>('errmsg'));
      end;
      s.Free;
      json.Free;
    except
      on E: Exception do
      begin
        result := false;
        memo1.Lines.Add('发送消息时出错:' + e.Message);
      end;
    end;
  end;
end;

end.

大概是从 Windows 7 时代开始,很多声卡的立体声混音功能就消失了,据说是厂家迫于某些组织的压力为了维护音乐版权不得不屏蔽了这个功能。
既然是屏蔽,那么大概率是可以重新打开的。下面以 Thinkpad 某老款笔记本(Windows 10)自带的 Conexant 20671 声卡为例,讲一下处理方法:

  1. 运行注册表编辑器 regedit.exe,找到如下分支:

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4d36e96c-e325-11ce-bfc1-08002be10318}
  2. 在找到的分支下会有 0000、0001……等子分支,对应着各个音频设备。使用“Conexant”或“20671”关键字搜索到要修改的声卡分支。
  3. 假设上一步找到的分支是 0000,将如下内容保存为 conexant.reg 文件。如果你找到的分支不是 0000,自行修改内容。

    Windows Registry Editor Version 5.00
    
    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4d36e96c-e325-11ce-bfc1-08002be10318}\0000\Settings\EPattributes\EpSettings\StereoMixEnable]
    "MixAssocSeq"=hex:e0,e0
    "MuteGainSettings"=hex:00,00
    "Enable"=hex:01
  4. 双击 conexant.reg 文件导入注册表,然后重启操作系统即可。

注:此方法只适用于 conexant 声卡的部分型号,其它型号的声卡可能设置方法有所不同。

AlmaLinux 在安装完成重启后,在图形界面下会出现无法关闭的向导,强制让你创建用户,否则无法进入系统。有时候弄个测试用的系统,只用一个 root 账户就可以了,所以要想办法把它关掉。
注意:因为这个向导是在系统登录前出现的,以前的那种在用户的 .config 目录下创建一个内容为“yes”的 gnome-initial-setup-done 文件的做法不会生效。
用 root 账户 ssh 登录系统后:

  • 方法一:
    第一步:
    vim /etc/gdm/custom.conf
    在 [daemon] 小节下增加一行:
    InitialSetupEnable=false
    如果只做这一步,重启后可以登录系统,但仍会出现向导。
    第二步:
    vim /etc/xdg/autostart/gnome-initial-setup-first-login.desktop
    增加一行:
    X-GNOME-Autostart-enabled=false
    重启系统即可。
  • 方法二:
    直接卸载 gnome-initial-setup:
    dnf erase gnome-initial-setup

第一个 get_datetime 脚本是规范日期格式的写法:

:global DateTime
:local Date [/system clock get date]
:local Time [/system clock get time]
:local Month [:tostr ([:find [:toarray "jan,feb,mar,apr,may,jun,jul,ago,sep,oct,nov,dec"] [:pick $Date 0 3]]+1)]
#if MM
#:if ([:len $Month]<2) do={:set Month "0$Month"}
# Format YYYY-M-D H:MM:SS
#\E5\B9\B4 \E6\9C\88 \E6\97\A5
:set DateTime ([:pick $Date 7 11]."-".$Month."-".[:tonum [:pick $Date 4 6]]." ".[:tonum [:pick $Time 0 2]].[:pick $Time 2 8])

第二个 Check_WAN_IP 脚本是检测到路由器的公网地址变化时自动发送钉钉机器人消息:
(此脚本使用的是钉钉消息的自定义关键字模式,指定的关键字是[路由器]即[\E8\B7\AF\E7\94\B1\E5\99\A8]这几个字符串,可自行修改。)

:global currentIP;
:global DateTime;
:execute "get_datetime"
:local newIP [/ip address get [find interface="pppoe-out1"] address];
:if ($newIP != $currentIP) do={
#    :put "ip address $currentIP changed to $newIP";
    :local header "Content-Type:application/json";
    :local dingtalk "https://oapi.dingtalk.com/robot/send?access_token=xxxxxx";
    :local data "{\"msgtype\":\"text\",\"text\": {\"content\":\"$DateTime \E5\AE\BD\E5\B8\A6\E5\85\AC\E7\BD\91 IP \E5\8F\98\E6\9B\B4\E4\B8\BA: $newIP, [\E8\B7\AF\E7\94\B1\E5\99\A8]\"}}";
    :log info [/tool/fetch http-method=post mode=https http-header-field="$header" http-data="$data" url="$dingtalk"];
    :log warning "公网地址由 $currentIP 变为 $newIP";
    :set currentIP $newIP;
};

说明:此脚本中宽带拨号的接口是 pppoe-out1,用法是打开 /ppp/Profiles 下拨号使用的 profile 条目,在 script 标签页下的 On Up 对话框中输入如下内容(延迟三秒执行):

delay 3s
:execute "Check_WAN_IP"