mirror of
https://github.com/alerta/alerta-contrib.git
synced 2025-03-17 05:52:38 +00:00
379 lines
14 KiB
Python
379 lines
14 KiB
Python
#!/usr/bin/env python
|
||
# _*_ coding:utf-8 _*_
|
||
# create time: 07/01/2018 11:35
|
||
__author__ = 'Devin -- http://zhangchuzhao.site'
|
||
|
||
import json
|
||
import time
|
||
import logging
|
||
import requests
|
||
try:
|
||
JSONDecodeError = json.decoder.JSONDecodeError
|
||
except AttributeError:
|
||
JSONDecodeError = ValueError
|
||
|
||
|
||
def is_not_null_and_blank_str(content):
|
||
"""
|
||
非空字符串
|
||
:param content: 字符串
|
||
:return: 非空 - True,空 - False
|
||
|
||
>>> is_not_null_and_blank_str('')
|
||
False
|
||
>>> is_not_null_and_blank_str(' ')
|
||
False
|
||
>>> is_not_null_and_blank_str(' ')
|
||
False
|
||
>>> is_not_null_and_blank_str('123')
|
||
True
|
||
"""
|
||
if content and content.strip():
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
|
||
class DingtalkChatbot(object):
|
||
"""
|
||
钉钉群自定义机器人(每个机器人每分钟最多发送20条),支持文本(text)、连接(link)、markdown三种消息类型!
|
||
"""
|
||
def __init__(self, webhook):
|
||
"""
|
||
机器人初始化
|
||
:param webhook: 钉钉群自定义机器人webhook地址
|
||
"""
|
||
super(DingtalkChatbot, self).__init__()
|
||
self.headers = {'Content-Type': 'application/json; charset=utf-8'}
|
||
self.webhook = webhook
|
||
self.times = 0
|
||
self.start_time = time.time()
|
||
|
||
def send_text(self, msg, is_at_all=False, at_mobiles=[], at_dingtalk_ids=[]):
|
||
"""
|
||
text类型
|
||
:param msg: 消息内容
|
||
:param is_at_all: @所有人时:true,否则为false(可选)
|
||
:param at_mobiles: 被@人的手机号(可选)
|
||
:param at_dingtalk_ids: 被@人的dingtalkId(可选)
|
||
:return: 返回消息发送结果
|
||
"""
|
||
data = {"msgtype": "text", "at": {}}
|
||
if is_not_null_and_blank_str(msg):
|
||
data["text"] = {"content": msg}
|
||
else:
|
||
logging.error("text类型,消息内容不能为空!")
|
||
raise ValueError("text类型,消息内容不能为空!")
|
||
|
||
if is_at_all:
|
||
data["at"]["isAtAll"] = is_at_all
|
||
|
||
if at_mobiles:
|
||
at_mobiles = list(map(str, at_mobiles))
|
||
data["at"]["atMobiles"] = at_mobiles
|
||
|
||
if at_dingtalk_ids:
|
||
at_dingtalk_ids = list(map(str, at_dingtalk_ids))
|
||
data["at"]["atDingtalkIds"] = at_dingtalk_ids
|
||
|
||
logging.debug('text类型:%s' % data)
|
||
return self.post(data)
|
||
|
||
def send_image(self, pic_url):
|
||
"""
|
||
image类型(表情)
|
||
:param pic_url: 图片表情链接
|
||
:return: 返回消息发送结果
|
||
"""
|
||
if is_not_null_and_blank_str(pic_url):
|
||
data = {
|
||
"msgtype": "image",
|
||
"image": {
|
||
"picURL": pic_url
|
||
}
|
||
}
|
||
logging.debug('image类型:%s' % data)
|
||
return self.post(data)
|
||
else:
|
||
logging.error("image类型中图片链接不能为空!")
|
||
raise ValueError("image类型中图片链接不能为空!")
|
||
|
||
def send_link(self, title, text, message_url, pic_url=''):
|
||
"""
|
||
link类型
|
||
:param title: 消息标题
|
||
:param text: 消息内容(如果太长自动省略显示)
|
||
:param message_url: 点击消息触发的URL
|
||
:param pic_url: 图片URL(可选)
|
||
:return: 返回消息发送结果
|
||
|
||
"""
|
||
if is_not_null_and_blank_str(title) and is_not_null_and_blank_str(text) and is_not_null_and_blank_str(message_url):
|
||
data = {
|
||
"msgtype": "link",
|
||
"link": {
|
||
"text": text,
|
||
"title": title,
|
||
"picUrl": pic_url,
|
||
"messageUrl": message_url
|
||
}
|
||
}
|
||
logging.debug('link类型:%s' % data)
|
||
return self.post(data)
|
||
else:
|
||
logging.error("link类型中消息标题或内容或链接不能为空!")
|
||
raise ValueError("link类型中消息标题或内容或链接不能为空!")
|
||
|
||
def send_markdown(self, title, text, is_at_all=False, at_mobiles=[], at_dingtalk_ids=[]):
|
||
"""
|
||
markdown类型
|
||
:param title: 首屏会话透出的展示内容
|
||
:param text: markdown格式的消息内容
|
||
:param is_at_all: 被@人的手机号(在text内容里要有@手机号,可选)
|
||
:param at_mobiles: @所有人时:true,否则为:false(可选)
|
||
:param at_dingtalk_ids: 被@人的dingtalkId(可选)
|
||
:return: 返回消息发送结果
|
||
"""
|
||
if is_not_null_and_blank_str(title) and is_not_null_and_blank_str(text):
|
||
data = {
|
||
"msgtype": "markdown",
|
||
"markdown": {
|
||
"title": title,
|
||
"text": text
|
||
},
|
||
"at": {}
|
||
}
|
||
if is_at_all:
|
||
data["at"]["isAtAll"] = is_at_all
|
||
|
||
if at_mobiles:
|
||
at_mobiles = list(map(str, at_mobiles))
|
||
data["at"]["atMobiles"] = at_mobiles
|
||
|
||
if at_dingtalk_ids:
|
||
at_dingtalk_ids = list(map(str, at_dingtalk_ids))
|
||
data["at"]["atDingtalkIds"] = at_dingtalk_ids
|
||
|
||
logging.debug("markdown类型:%s" % data)
|
||
return self.post(data)
|
||
else:
|
||
logging.error("markdown类型中消息标题或内容不能为空!")
|
||
raise ValueError("markdown类型中消息标题或内容不能为空!")
|
||
|
||
def send_action_card(self, action_card):
|
||
"""
|
||
ActionCard类型
|
||
:param action_card: 整体跳转ActionCard类型实例或独立跳转ActionCard类型实例
|
||
:return: 返回消息发送结果
|
||
"""
|
||
if isinstance(action_card, ActionCard):
|
||
data = action_card.get_data()
|
||
logging.debug("ActionCard类型:%s" % data)
|
||
return self.post(data)
|
||
else:
|
||
logging.error("ActionCard类型:传入的实例类型不正确!")
|
||
raise TypeError("ActionCard类型:传入的实例类型不正确!")
|
||
|
||
def send_feed_card(self, links):
|
||
"""
|
||
FeedCard类型
|
||
:param links: 信息集(FeedLink数组)
|
||
:return: 返回消息发送结果
|
||
"""
|
||
link_data_list = []
|
||
for link in links:
|
||
if isinstance(link, FeedLink) or isinstance(link, CardItem):
|
||
link_data_list.append(link.get_data())
|
||
if link_data_list:
|
||
# 兼容:1、传入FeedLink或CardItem实例列表;2、传入数据字典列表;
|
||
links = link_data_list
|
||
data = {"msgtype": "feedCard", "feedCard": {"links": links}}
|
||
logging.debug("FeedCard类型:%s" % data)
|
||
return self.post(data)
|
||
|
||
def post(self, data):
|
||
"""
|
||
发送消息(内容UTF-8编码)
|
||
:param data: 消息数据(字典)
|
||
:return: 返回发送结果
|
||
"""
|
||
self.times += 1
|
||
if self.times % 20 == 0:
|
||
if time.time() - self.start_time < 60:
|
||
logging.debug('钉钉官方限制每个机器人每分钟最多发送20条,当前消息发送频率已达到限制条件,休眠一分钟')
|
||
time.sleep(60)
|
||
self.start_time = time.time()
|
||
|
||
post_data = json.dumps(data)
|
||
try:
|
||
response = requests.post(self.webhook, headers=self.headers, data=post_data)
|
||
except requests.exceptions.HTTPError as exc:
|
||
logging.error("消息发送失败, HTTP error: %d, reason: %s" % (exc.response.status_code, exc.response.reason))
|
||
raise
|
||
except requests.exceptions.ConnectionError:
|
||
logging.error("消息发送失败,HTTP connection error!")
|
||
raise
|
||
except requests.exceptions.Timeout:
|
||
logging.error("消息发送失败,Timeout error!")
|
||
raise
|
||
except requests.exceptions.RequestException:
|
||
logging.error("消息发送失败, Request Exception!")
|
||
raise
|
||
else:
|
||
try:
|
||
result = response.json()
|
||
except JSONDecodeError:
|
||
logging.error("服务器响应异常,状态码:%s,响应内容:%s" % (response.status_code, response.text))
|
||
return {'errcode': 500, 'errmsg': '服务器响应异常'}
|
||
else:
|
||
logging.debug('发送结果:%s' % result)
|
||
if result['errcode']:
|
||
error_data = {"msgtype": "text", "text": {"content": "钉钉机器人消息发送失败,原因:%s" % result['errmsg']}, "at": {"isAtAll": True}}
|
||
logging.error("消息发送失败,自动通知:%s" % error_data)
|
||
requests.post(self.webhook, headers=self.headers, data=json.dumps(error_data))
|
||
return result
|
||
|
||
|
||
class ActionCard(object):
|
||
"""
|
||
ActionCard类型消息格式(整体跳转、独立跳转)
|
||
"""
|
||
def __init__(self, title, text, btns, btn_orientation=0, hide_avatar=0):
|
||
"""
|
||
ActionCard初始化
|
||
:param title: 首屏会话透出的展示内容
|
||
:param text: markdown格式的消息
|
||
:param btns: 按钮列表:(1)按钮数量为1时,整体跳转ActionCard类型;(2)按钮数量大于1时,独立跳转ActionCard类型;
|
||
:param btn_orientation: 0:按钮竖直排列,1:按钮横向排列(可选)
|
||
:param hide_avatar: 0:正常发消息者头像,1:隐藏发消息者头像(可选)
|
||
"""
|
||
super(ActionCard, self).__init__()
|
||
self.title = title
|
||
self.text = text
|
||
self.btn_orientation = btn_orientation
|
||
self.hide_avatar = hide_avatar
|
||
btn_list = []
|
||
for btn in btns:
|
||
if isinstance(btn, CardItem):
|
||
btn_list.append(btn.get_data())
|
||
if btn_list:
|
||
btns = btn_list # 兼容:1、传入CardItem示例列表;2、传入数据字典列表
|
||
self.btns = btns
|
||
|
||
def get_data(self):
|
||
"""
|
||
获取ActionCard类型消息数据(字典)
|
||
:return: 返回ActionCard数据
|
||
"""
|
||
if is_not_null_and_blank_str(self.title) and is_not_null_and_blank_str(self.text) and len(self.btns):
|
||
if len(self.btns) == 1:
|
||
# 整体跳转ActionCard类型
|
||
data = {
|
||
"msgtype": "actionCard",
|
||
"actionCard": {
|
||
"title": self.title,
|
||
"text": self.text,
|
||
"hideAvatar": self.hide_avatar,
|
||
"btnOrientation": self.btn_orientation,
|
||
"singleTitle": self.btns[0]["title"],
|
||
"singleURL": self.btns[0]["actionURL"]
|
||
}
|
||
}
|
||
return data
|
||
else:
|
||
# 独立跳转ActionCard类型
|
||
data = {
|
||
"msgtype": "actionCard",
|
||
"actionCard": {
|
||
"title": self.title,
|
||
"text": self.text,
|
||
"hideAvatar": self.hide_avatar,
|
||
"btnOrientation": self.btn_orientation,
|
||
"btns": self.btns
|
||
}
|
||
}
|
||
return data
|
||
else:
|
||
logging.error("ActionCard类型,消息标题或内容或按钮数量不能为空!")
|
||
raise ValueError("ActionCard类型,消息标题或内容或按钮数量不能为空!")
|
||
|
||
|
||
class FeedLink(object):
|
||
"""
|
||
FeedCard类型单条消息格式
|
||
"""
|
||
def __init__(self, title, message_url, pic_url):
|
||
"""
|
||
初始化单条消息文本
|
||
:param title: 单条消息文本
|
||
:param message_url: 点击单条信息后触发的URL
|
||
:param pic_url: 点击单条消息后面图片触发的URL
|
||
"""
|
||
super(FeedLink, self).__init__()
|
||
self.title = title
|
||
self.message_url = message_url
|
||
self.pic_url = pic_url
|
||
|
||
def get_data(self):
|
||
"""
|
||
获取FeedLink消息数据(字典)
|
||
:return: 本FeedLink消息的数据
|
||
"""
|
||
if is_not_null_and_blank_str(self.title) and is_not_null_and_blank_str(self.message_url) and is_not_null_and_blank_str(self.pic_url):
|
||
data = {
|
||
"title": self.title,
|
||
"messageURL": self.message_url,
|
||
"picURL": self.pic_url
|
||
}
|
||
return data
|
||
else:
|
||
logging.error("FeedCard类型单条消息文本、消息链接、图片链接不能为空!")
|
||
raise ValueError("FeedCard类型单条消息文本、消息链接、图片链接不能为空!")
|
||
|
||
|
||
class CardItem(object):
|
||
"""
|
||
ActionCard和FeedCard消息类型中的子控件
|
||
"""
|
||
|
||
def __init__(self, title, url, pic_url=None):
|
||
"""
|
||
CardItem初始化
|
||
@param title: 子控件名称
|
||
@param url: 点击子控件时触发的URL
|
||
@param pic_url: FeedCard的图片地址,ActionCard时不需要,故默认为None
|
||
"""
|
||
super(CardItem, self).__init__()
|
||
self.title = title
|
||
self.url = url
|
||
self.pic_url = pic_url
|
||
|
||
def get_data(self):
|
||
"""
|
||
获取CardItem子控件数据(字典)
|
||
@return: 子控件的数据
|
||
"""
|
||
if is_not_null_and_blank_str(self.pic_url) and is_not_null_and_blank_str(self.title) and is_not_null_and_blank_str(self.url):
|
||
# FeedCard类型
|
||
data = {
|
||
"title": self.title,
|
||
"messageURL": self.url,
|
||
"picURL": self.pic_url
|
||
}
|
||
return data
|
||
elif is_not_null_and_blank_str(self.title) and is_not_null_and_blank_str(self.url):
|
||
# ActionCard类型
|
||
data = {
|
||
"title": self.title,
|
||
"actionURL": self.url
|
||
}
|
||
return data
|
||
else:
|
||
logging.error("CardItem是ActionCard的子控件时,title、url不能为空;是FeedCard的子控件时,title、url、pic_url不能为空!")
|
||
raise ValueError("CardItem是ActionCard的子控件时,title、url不能为空;是FeedCard的子控件时,title、url、pic_url不能为空!")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
import doctest
|
||
doctest.testmod()
|
||
|