官方帮助文档:https://open.dingtalk.com/document/robots/custom-robot-access

一、获取自定义机器人webhook

可以通过如下步骤设置钉钉机器人:

  1. 首先建立或者进入某个群聊
  2. 在群聊内部点击“设置>机器人>添加机器人”

java 钉钉发送 钉钉发送代码_钉钉

  1. 添加一个自定义机器人, 机器人名称自定义,安全设置勾选“加签”

java 钉钉发送 钉钉发送代码_python_02

java 钉钉发送 钉钉发送代码_钉钉_03

  1. 添加完成后,复制机器人的webhook地址以及加签的密钥。(可以重新进去机器人管理页面查看得到)

安全设置

安全设置目前有3种方式:

1)方式一:自定义关键词
最多可以设置10个关键词,消息中至少包含其中1个关键词才可以发送成功。

例如:添加了一个自定义关键词:监控报警

则这个机器人所发送的消息,必须包含 监控报警 这个词,才能发送成功。

2)方式二:加签
第一步,把timestamp+“\n”+密钥当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,最后再把签名参数再进行urlEncode,得到最终的签名(需要使用UTF-8字符集)。

3)方式三:IP地址
仅指定的IP地址支持钉钉机器人,最多添加10个IP地址。

常见问题

当出现以下错误时,表示消息校验未通过,请查看机器人的安全设置。

// 消息内容中不包含任何关键词
{
  "errcode":310000,
  "errmsg":"keywords not in content"
}

// timestamp 无效
{
  "errcode":310000,
  "errmsg":"invalid timestamp"
}

// 签名不匹配
{
  "errcode":310000,
  "errmsg":"sign not match"
}

// IP地址不在白名单
{
  "errcode":310000,
  "errmsg":"ip X.X.X.X not in whitelist"
}

二、python封装脚本

# -*- coding: utf-8 -*-
# @Time    : 2023/5/11 9:47
# @Author  : chenyinhua
# @File    : dingding_handle.py
# @Software: PyCharm
# @Desc: 钉钉通知封装

import hmac
import hashlib
import base64
import urllib.parse
import time
import json
import urllib.request
from loguru import logger
from requests import request


class DingTalkBot:
    """
    钉钉机器人
    """

   # 适配钉钉机器人的加签模式和关键字模式/白名单IP模式
        if secret:
            timestamp = str(round(time.time() * 1000))
            sign = self.get_sign(secret, timestamp)
            self.webhook_url = webhook_url + f'×tamp={timestamp}&sign={sign}'  # 最终url,url+时间戳+签名
        else:
            self.webhook_url = webhook_url
        self.headers = {
            "Content-Type": "application/json",
            "Charset": "UTF-8"
        }

    def get_sign(self, secret, timestamp):
        """
        根据时间戳 + "sign" 生成密钥
        把timestamp+"\n"+密钥当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,最后再把签名参数再进行urlEncode,得到最终的签名(需要使用UTF-8字符集)。
        :return:
        """
        string_to_sign = f'{timestamp}\n{secret}'.encode('utf-8')
        hmac_code = hmac.new(
            secret.encode('utf-8'),
            string_to_sign,
            digestmod=hashlib.sha256).digest()

        sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
        return sign


    def send_text(self, content, mobiles=None, is_at_all=False):
        """
        发送文本消息
        :param content: 发送的内容
        :param mobiles: 被艾特的用户的手机号码,格式是列表,注意需要在content里面添加@人的手机号码
        :param is_at_all: 是否艾特所有人,布尔类型,true为艾特所有人,false为不艾特
        """
        if mobiles:
            if isinstance(mobiles, list):
                payload = {
                    "msgtype": "text",
                    "text": {
                        "content": content
                    },
                    "at": {
                        "atMobiles": mobiles,
                        "isAtAll": False
                    }
                }
                for mobile in mobiles:
                    payload["text"]["content"] += f"@{mobile}"
            else:
                raise TypeError("mobiles类型错误 不是list类型.")
        else:
            payload = {
                "msgtype": "text",
                "text": {
                    "content": content
                },
                "at": {
                    "atMobiles": "",
                    "isAtAll": is_at_all
                }
            }
        response = request(url=self.webhook_url, json=payload, headers=self.headers, method="POST")
        if response.json().get("errcode") == 0:
            logger.debug(f"send_text发送钉钉消息成功:{response.json()}")
            return True
        else:
            logger.debug(f"send_text发送钉钉消息失败:{response.text}")
            return False

    def send_link(self, title, text, message_url, pic_url=None):
        """
        发送链接消息
        :param title: 消息标题
        :param text: 消息内容,如果太长只会部分展示
        :param message_url: 点击消息跳转的url地址
        :param pic_url: 图片url
        """
        payload = {
            "msgtype": "link",
            "link": {
                "title": title,
                "text": text,
                "picUrl": pic_url,
                "messageUrl": message_url
            }
        }
        response = request(url=self.webhook_url, json=payload, headers=self.headers, method="POST")
        if response.json().get("errcode") == 0:
            logger.debug(f"send_link发送钉钉消息成功:{response.json()}")
            return True
        else:
            logger.debug(f"send_link发送钉钉消息失败:{response.text}")
            return False

    def send_markdown(self, title, text, mobiles=None, is_at_all=False):
        """
        发送markdown消息
        目前仅支持md语法的子集,如标题,引用,文字加粗,文字斜体,链接,图片,无序列表,有序列表
        :param title: 消息标题,首屏回话透出的展示内容
        :param text: 消息内容,markdown格式
        :param mobiles: 被艾特的用户的手机号码,格式是列表,注意需要在text里面添加@人的手机号码
        :param is_at_all: 是否艾特所有人,布尔类型,true为艾特所有人,false为不艾特
        """
        if mobiles:
            if isinstance(mobiles, list):
                payload = {
                    "msgtype": "markdown",
                    "markdown": {
                        "title": title,
                        "text": text
                    },
                    "at": {
                        "atMobiles": mobiles,
                        "isAtAll": False
                    }
                }
                for mobile in mobiles:
                    payload["markdown"]["text"] += f" @{mobile}"
            else:
                raise TypeError("mobiles类型错误 不是list类型.")
        else:
            payload = {
                "msgtype": "markdown",
                "markdown": {
                    "title": title,
                    "text": text
                },
                "at": {
                    "atMobiles": "",
                    "isAtAll": is_at_all
                }
            }
        response = request(url=self.webhook_url, json=payload, headers=self.headers, method="POST")
        if response.json().get("errcode") == 0:
            logger.debug(f"send_markdown发送钉钉消息成功:{response.json()}")
            return True
        else:
            logger.debug(f"send_markdown发送钉钉消息失败:{response.text}")
            return False

    def send_action_card_single(self, title, text, single_title, single_url, btn_orientation=0):
        """
        发送消息卡片(整体跳转ActionCard类型)
        :param title: 消息标题
        :param text: 消息内容,md格式消息
        :param single_title: 单个按钮的标题
        :param single_url: 点击singleTitle按钮后触发的URL
        :param btn_orientation: 0-按钮竖直排列,1-按钮横向排列
        """
        payload = {
            "msgtype": "actionCard",
            "actionCard": {
                "title": title,
                "text": text,
                "singleTitle": single_title,
                "singleURL": single_url,
                "btnOrientation": btn_orientation,
            }

        }
        response = request(url=self.webhook_url, json=payload, headers=self.headers, method="POST")
        if response.json().get("errcode") == 0:
            logger.debug(f"send_action_card_single发送钉钉消息成功:{response.json()}")
            return True
        else:
            logger.debug(f"send_action_card_single发送钉钉消息失败:{response.text}")
            return False

    def send_action_card_split(self, title, text, btns, btn_orientation=0):
        """
        发送消息卡片(独立跳转ActionCard类型)
        :param title: 消息标题
        :param text: 消息内容,md格式消息
        :param btns: 列表嵌套字典类型,"btns": [{"title": "内容不错", "actionURL": "https://www.dingtalk.com/"}, ......]
        :param btn_orientation: 0-按钮竖直排列,1-按钮横向排列
        """
        payload = {
            "msgtype": "actionCard",
            "actionCard": {
                "title": title,
                "text": text,
                "btns": [],
                "btnOrientation": btn_orientation,
            }

        }
        for btn in btns:
            payload["actionCard"]["btns"].append({
                "title": btn.get("title"),
                "actionURL": btn.get("action_url")
            })
        response = request(url=self.webhook_url, json=payload, headers=self.headers, method="POST")
        if response.json().get("errcode") == 0:
            logger.debug(f"send_action_card_split发送钉钉消息成功:{response.json()}")
            return True
        else:
            logger.debug(f"send_action_card_split发送钉钉消息失败:{response.text}")
            return False

    def send_feed_card(self, links_msg):
        """
        发送多组消息卡片(FeedCard类型)
        :param links_msg: 列表嵌套字典类型,每一个字段包括如下参数:title(单条信息文本), messageURL(点击单条信息后的跳转链接), picURL(单条信息后面图片的url)
        """
        payload = {
            "msgtype": "feedCard",
            "feedCard": {
                "links": []
            }
        }
        for link in links_msg:
            payload["feedCard"]["links"].append(
                {
                    "title": link.get("title"),
                    "messageURL": link.get("messageURL"),
                    "picURL": link.get("picURL")
                }
            )
        response = request(url=self.webhook_url, json=payload, headers=self.headers, method="POST")
        if response.json().get("errcode") == 0:
            logger.debug(f"send_feed_card发送钉钉消息成功:{response.json()}")
            return True
        else:
            logger.debug(f"send_feed_card发送钉钉消息失败:{response.text}")
            return False


if __name__ == '__main__':
    my_secret = '**********'
    my_url = 'https://oapi.dingtalk.com/robot/send?access_token=**********'

    dingding = DingTalkBot(secret=my_secret, webhook_url=my_url)
    dingding.send_text(content="发送钉钉消息的响应数据12", mobiles=['1816398****', "1326332****"], is_at_all=True)
    dingding.send_link(title="chytest", text="时代的长河", message_url="https://www.gitlink.org.cn/chenyh")
    dingding.send_markdown(title="test markdown",
                           text="# 一级标题 \n## 二级标题 \n> 引用文本  \n**加粗**  \n*斜体*  \n[百度链接](https://www.baidu.com)\n\n\n\n",
                           mobiles=['1877497****'])
    dingding.send_action_card_single(title="测试消息的标题",
                              text="### 乔布斯 20 年前想打造的苹果咖啡厅 Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划",
                              single_title="阅读全文", single_url="https://www.gitlink.org.cn/chenyh", btn_orientation=0)
    dingding.send_action_card_split(title="测试消息的标题", text="### 乔布斯 20 年前想打造的苹果咖啡厅 Apple Store",
                                    btns=[{"title": "内容不错", "actionURL": "https://www.dingtalk.com/"},
                                          {"title": "不感兴趣", "actionURL": "https://www.dingtalk.com/"}],
                                    btn_orientation=1)
    links = [
        {
            "title": "时代的火车向前开",
            "messageURL": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI",
            "picURL": "https://tse1-mm.cn.bing.net/th/id/OIP-C.nelCIKYD30NJW6W68-ZHxAHaJA?pid=ImgDet&rs=1"
        },
        {
            "title": "工作厌倦了怎么办?",
            "messageURL": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI",
            "picURL": "https://tse1-mm.cn.bing.net/th/id/OIP-C.nelCIKYD30NJW6W68-ZHxAHaJA?pid=ImgDet&rs=1"
        },
        {
            "title": "也许你对于选用的字体的选择应该更慎重一点",
            "messageURL": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI",
            "picURL": "https://tse1-mm.cn.bing.net/th/id/OIP-C.nelCIKYD30NJW6W68-ZHxAHaJA?pid=ImgDet&rs=1"
        },

    ]
    dingding.send_feed_card(links)