'''
wechat.py
集合了本应用需要发送到微信的函数
调用提示
函数可以假设是异步IO,参数符合条件时不抛出异常
由于异步假设,函数只返回尝试状态,即是否设置了定时任务,不保证成功发送
'''
from datetime import timedelta
from extern.wechat import send_wechat, DEFAULT_URL
from app.extern.config import (
notification_wechat_config as CONFIG,
Levels as WechatMessageLevel,
Apps as WechatApp,
)
from app.models import NaturalPerson, Organization, Notification, Position
from app.utils import get_person_or_org
import utils.models.query as SQ
from utils.http.utils import build_full_url
from app.log import logger
__all__ = [
'WechatApp', 'WechatMessageLevel',
'publish_notification', 'publish_notifications',
]
def app2path(app: str) -> str:
'''将应用名转换为路径,可能是绝对路径'''
url = CONFIG.app2url.get(app)
if url is None:
url = CONFIG.app2url.get('default', '')
return url
def _get_default_level(typename, instance=None) -> int:
if typename == 'notification':
if (instance is not None and
instance.typename == Notification.Type.NEEDDO):
# 处理类通知默认等级较高
return WechatMessageLevel.IMPORTANT
return WechatMessageLevel.INFO
else:
return WechatMessageLevel.INFO
def _get_default_app(typename, instance=None) -> str:
if typename == 'activity':
return WechatApp._PROMOTE
elif typename == 'notification':
if (instance is not None and
instance.title == Notification.Title.ACTIVITY_INFORM):
return WechatApp._PROMOTE
return WechatApp._MESSAGE
else:
return WechatApp.NORMAL
def can_send(person, level=None):
'''获取个人接收人是否接收'''
if level is not None and level < person.wechat_receive_level:
return False
return True
def org2receivers(org, level=None, force=True):
'''获取组织接收人的学号列表'''
managers = Position.objects.activated().filter(org=org, is_admin=True)
# 提供等级时,不小于接收等级
if level is not None:
receivers = managers.filter(person__wechat_receive_level__lte=level)
if not receivers.exists() and force:
receivers = managers.filter(pos=0)
managers = receivers
return SQ.qsvlist(managers, Position.person, NaturalPerson.person_id, 'username')
def user2receivers(user, level=None, get_obj=False):
'''供发布级YPPF接口调用的函数,获取接收人的学号列表,以及可选的原始对象'''
receiver = get_person_or_org(user)
if isinstance(receiver, NaturalPerson):
wechat_receivers = [user.username] if can_send(receiver, level) else []
else:
wechat_receivers = org2receivers(receiver, level)
if get_obj:
return wechat_receivers, receiver
return wechat_receivers
def get_person_receivers(all_receiver_ids, level=None):
'''获取接收的个人的学号列表'''
receivers = NaturalPerson.objects.activated().filter(
SQ.sq([NaturalPerson.person_id, 'in'], all_receiver_ids))
# 提供等级时,不小于接收等级
if level is not None:
receivers = receivers.filter(wechat_receive_level__lte=level)
return SQ.qsvlist(receivers, NaturalPerson.person_id, 'username')
[文档]
@logger.secure_func(fail_value=False)
def publish_notification(notification_or_id,
show_source=True,
app=None, level=None):
"""
根据单个通知或id(实际是主键)向通知的receiver发送
别创建了好多通知然后循环调用这个,批量发送用publish_notifications
- show_source: bool, 显示消息来源 默认显示
- app: str | WechatApp宏, 确定发送的应用 请推广类消息务必注意
- level: int | WechatMessageLevel宏, 用于筛选用户 推广类消息可以不填
"""
try:
if isinstance(notification_or_id, Notification):
notification = notification_or_id
else:
notification = Notification.objects.get(pk=notification_or_id)
except:
raise ValueError("未找到该id的通知")
if app is None or app == WechatApp.DEFAULT:
app = _get_default_app('notification', notification)
check_block = app not in CONFIG.unblock_apps
url = notification.URL
if url and url[0] == "/": # 相对路径变为绝对路径
url = build_full_url(url)
title = notification.get_title_display()
messages = []
if len(notification.content) < 120:
# 卡片类型消息最多显示256字节
# 因留白等原因,内容120字左右就超出了
kws = {"card": True}
if show_source:
sender = get_person_or_org(notification.sender)
# 通知内容暂时也一起去除了
messages += [f'发送者:{str(sender)}', '通知内容:']
messages += [notification.content]
if url:
kws["url"] = url
kws["btntxt"] = "查看详情"
else:
# 超出卡片字数范围的消息使用文本格式发送
kws = {"card": False}
messages.append('')
if show_source:
sender = get_person_or_org(notification.sender)
# 通知内容暂时也一起去除了
messages += ['发送者:' + f'{str(sender)}', '通知内容:']
messages += [notification.content]
if url:
messages += ['', f'<a href="{url}">阅读原文</a>']
else:
messages += ['', f'<a href="{DEFAULT_URL}">查看详情</a>']
# 获取完整消息
message = '\n'.join(messages)
if check_block and (level is None or level == WechatMessageLevel.DEFAULT):
# 考虑屏蔽时,获得默认行为的消息等级
level = _get_default_level('notification', notification)
elif not check_block:
# 不屏蔽时,消息等级设置为空
level = None
# 获取接受者列表
wechat_receivers, receiver = user2receivers(notification.receiver, level,
get_obj=True)
if isinstance(receiver, Organization): # 小组
# 转发小组消息给其负责人
message += f'\n消息来源:{str(receiver)},请切换到该小组账号进行操作。'
if not wechat_receivers: # 没有人接收
return True
send_wechat(wechat_receivers, title, message, api_path=app2path(app), **kws)
return True
[文档]
@logger.secure_func(fail_value=False)
def publish_notifications(
notifications_or_ids=None, filter_kws=None, exclude_kws=None,
show_source=True,
app=None, level=None,
*, check=True
) -> bool:
"""
批量发送通知,选取筛选后范围内所有与最新通知发送者等相同、且内容结尾一致的通知
如果能保证这些通知全都一致,可以要求不检查
Argument
--------
- notifications_or_ids: QuerySet | List or Tuple[notification with id] |
Iter[id] | None, 通知基本范围, 别问参数类型为什么这么奇怪,问就是django不统一
- filter_kws: dict | None, 这些参数将被直接传递给filter函数
- exclude_kws: dict | None, 这些参数将被直接传递给exclude函数
- 以上参数不能都为空
- show_source: bool, 显示消息来源 默认显示
- app: str | WechatApp宏, 确定发送的应用 请推广类消息务必注意
- level: int | WechatMessageLevel宏, 用于筛选用户 推广类消息可以不填
Keyword-Only
------------
- check: bool, default=True, 是否检查最终筛选结果的相关性
Returns
-------
- success: bool, 是否尝试了发送,出错时返回False
"""
if notifications_or_ids is None and filter_kws is None and exclude_kws is None:
raise ValueError("必须至少传入一个有效参数才能发布通知到微信!")
try:
notifications = Notification.objects.all()
if notifications_or_ids is not None:
if (isinstance(notifications_or_ids, (list, tuple))
and notifications_or_ids
and isinstance(notifications_or_ids[0], Notification)
):
notifications_or_ids = [n.id for n in notifications_or_ids]
notifications = notifications.filter(id__in=notifications_or_ids)
if filter_kws is not None:
notifications = notifications.filter(**filter_kws)
if exclude_kws is not None:
notifications = notifications.exclude(**exclude_kws)
notifications = notifications.order_by("-start_time")
except:
raise ValueError("必须至少传入一个有效参数才能发布通知到微信!")
total_ct = len(notifications)
if total_ct == 0:
return True
try:
latest_notification = notifications[0]
sender = latest_notification.sender
typename = latest_notification.typename
title = latest_notification.title
content = latest_notification.content
content_start = content[:10]
content_end = content[-10:]
url = latest_notification.URL
if check:
send_time = latest_notification.start_time
before_5min = send_time - timedelta(minutes=5) # 最多差5分钟
notifications = notifications.filter(
sender=sender,
typename=typename,
title=title,
start_time__gte=before_5min,
content__startswith=content_start,
content__endswith=content_end,
URL=url,
)
except:
raise Exception("检查失败,发生了未知错误,这里不该发生异常")
if url and url[0] == "/": # 相对路径变为绝对路径
url = build_full_url(url)
title = latest_notification.get_title_display()
messages = []
if len(latest_notification.content) < 120:
# 卡片类型消息最多显示256字节
# 因留白等原因,内容120字左右就超出了
kws = {"card": True}
if show_source:
sender = get_person_or_org(latest_notification.sender)
# 通知内容暂时也一起去除了
messages += [f'发送者:{str(sender)}', '通知内容:']
messages += [latest_notification.content]
if url:
kws["url"] = url
kws["btntxt"] = "查看详情"
else:
# 超出卡片字数范围的消息使用文本格式发送
kws = {"card": False}
messages.append('')
if show_source:
sender = get_person_or_org(latest_notification.sender)
# 通知内容暂时也一起去除了
messages += ['发送者:' + f'{str(sender)}', '通知内容:']
messages += [latest_notification.content]
if url:
messages += ['', f'<a href="{url}">阅读原文</a>']
else:
messages += ['', f'<a href="{DEFAULT_URL}">查看详情</a>']
# 获取完整消息
message = '\n'.join(messages)
# 获得发送应用和消息发送等级
if app is None or app == WechatApp.DEFAULT:
app = _get_default_app('notification', latest_notification)
check_block = app not in CONFIG.unblock_apps
if check_block and (level is None or level == WechatMessageLevel.DEFAULT):
level = _get_default_level('notification', latest_notification)
if not check_block:
level = None
# 获取接收者列表,小组的接收者为其负责人,去重
receiver_ids = notifications.values_list("receiver_id", flat=True)
person_receivers = get_person_receivers(receiver_ids, level)
wechat_receivers = person_receivers
receiver_set = set(wechat_receivers)
# 接下来是发送给小组的部分
org_receivers = Organization.objects.activated().filter(
organization_id__in=receiver_ids)
for org in org_receivers:
managers = [
manager for manager in org2receivers(org, level, force=False)
if manager not in receiver_set
]
wechat_receivers.extend(managers)
receiver_set.update(managers)
if not wechat_receivers: # 可能都不接收此等级的消息
return True
send_wechat(wechat_receivers, title, message, api_path=app2path(app), **kws)
return True