├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── setup.py ├── tea.yaml └── wechat_work ├── __init__.py ├── __version__.py └── wechat_work.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 sheep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 企业微信工具 2 | 3 | 这是一个基于企业微信 API 开发的用于消息通知的 Python 库 4 | 5 | ## Guide 6 | 7 | 开始之前,请先跟随一下步骤创建一个企业微信应用 8 | 9 | - [注册/登录企业微信管理后台](https://work.weixin.qq.com/) 10 | - [创建/查看/管理应用](https://work.weixin.qq.com/wework_admin/frame#apps) 11 | - [查看企业ID](https://work.weixin.qq.com/wework_admin/frame#profile) 12 | - [查看企业微信成员信息](https://work.weixin.qq.com/wework_admin/frame#contacts) 13 | - [添加插件以支持推送到个人微信](https://work.weixin.qq.com/wework_admin/frame#profile/wxPlugin) 14 | 15 | ## Installation 16 | 17 | - 通过 `pip` 安装 18 | 19 | ```bash 20 | pip install wechat_work 21 | ``` 22 | 23 | - 通过 `git` 和 `pip` 安装 24 | 25 | ```bash 26 | pip install git+https://github.com/Micro-sheep/wechat_work.git 27 | ``` 28 | 29 | ## Examples 30 | 31 | ```python 32 | from wechat_work import WechatWork 33 | corpid = '企业 ID' 34 | appid = '企业应用 ID' 35 | corpsecret = '企业应用 Secret' 36 | users = ['企业微信的用户账号1', '企业微信的用户账号2'] 37 | w = WechatWork(corpid=corpid, 38 | appid=appid, 39 | corpsecret=corpsecret) 40 | # 发送文本 41 | w.send_text('Hello World!', users) 42 | # 发送 Markdown 43 | w.send_markdown('# Hello World', users) 44 | # 发送图片 45 | w.send_image('./hello.jpg', users) 46 | # 发送文件 47 | w.send_file('./hello.txt', users) 48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | requests_toolbelt 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | here = pathlib.Path(__file__).parent 4 | # require = (here / "requirements.txt").read_text(encoding='utf-8').split() 5 | require = ['requests', 'requests_toolbelt'] 6 | readme = (here / "README.md").read_text(encoding='utf-8') 7 | about = {} 8 | exec((here/'wechat_work'/'__version__.py').read_text(encoding='utf-8'), about) 9 | setup( 10 | name=about['__title__'], 11 | version=about['__version__'], 12 | description=about['__description__'], 13 | long_description=readme, 14 | long_description_content_type="text/markdown", 15 | url=about['__url__'], 16 | author=about['__author__'], 17 | author_email=about['__author_email__'], 18 | license="MIT", 19 | platforms=['any'], 20 | keywords=about['__keywords__'], 21 | classifiers=[ 22 | "License :: OSI Approved :: MIT License", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | ], 28 | packages=find_packages(), 29 | install_requires=require, 30 | project_urls=about['__project_urls__'] 31 | ) 32 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x6A9695a318514529397d53f3C4cFeA6127568F43' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /wechat_work/__init__.py: -------------------------------------------------------------------------------- 1 | from .wechat_work import WechatWork 2 | -------------------------------------------------------------------------------- /wechat_work/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'wechat_work' 2 | __version__ = '0.0.3' 3 | __author__ = 'micro sheep' 4 | __url__ = 'https://github.com/Micro-sheep/wechat_work' 5 | __author_email__ = 'micro-sheep@outlook.com' 6 | __keywords__ = ['wechat', 'wechat work', 'message', 'notification'] 7 | __description__ = 'A tool for Wechat Work' 8 | __project_urls__ = { 9 | 'Documentation': 'https://github.com/Micro-sheep/wechat_work', 10 | 'Source': 'https://github.com/Micro-sheep/wechat_work', 11 | } 12 | -------------------------------------------------------------------------------- /wechat_work/wechat_work.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | import requests 4 | from requests_toolbelt import MultipartEncoder 5 | import datetime 6 | 7 | UPLOAD_URL = 'https://qyapi.weixin.qq.com/cgi-bin/media/upload' 8 | SEND_URL = 'https://qyapi.weixin.qq.com/cgi-bin/message/send' 9 | TOKEN_URL = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' 10 | 11 | 12 | class WechatWork: 13 | """ 14 | 企业微信消息推送 15 | """ 16 | 17 | access_token: str = None 18 | access_token_expires_time: datetime.datetime = None 19 | 20 | def __init__(self, 21 | corpid: str, 22 | appid: str, 23 | corpsecret: str) -> None: 24 | """ 25 | 初始化消息通知应用 26 | 27 | Parameters 28 | ---------- 29 | corpid : str 30 | 企业 ID 31 | appid : str 32 | 应用 ID (企业微信网页后台应用管理界面的 AgentId) 33 | corpsecret : str 34 | 应用 Secret (企业微信网页后台应用管理界面的 Secret) 35 | """ 36 | self.corpid = corpid 37 | self.appid = appid 38 | self.corpsecret = corpsecret 39 | self.access_token = self.get_access_token() 40 | 41 | def upload_file(self, 42 | filepath: str, 43 | filename: str) -> str: 44 | """ 45 | 上传文件 46 | 47 | Parameters 48 | ---------- 49 | filepath : str 50 | 本地文件路径 51 | filename : str 52 | 云端存储的文件名 53 | 54 | Returns 55 | ------- 56 | str 57 | 上传的文件的 ID 58 | """ 59 | access_token = self.get_access_token() 60 | params = { 61 | 'access_token': access_token, 62 | 'type': 'file' 63 | } 64 | with open(filepath, 'rb') as f: 65 | m = MultipartEncoder( 66 | fields={'file': (filename, f, 'multipart/form-data')}) 67 | 68 | response = requests.post(url=UPLOAD_URL, params=params, data=m, headers={ 69 | 'Content-Type': m.content_type}) 70 | js = response.json() 71 | if js['errmsg'] != 'ok': 72 | return '' 73 | return js['media_id'] 74 | 75 | def send(self, 76 | msg_type: str, 77 | users: List[str], 78 | content: str = None, 79 | media_id: str = None) -> bool: 80 | """ 81 | 发送消息 82 | 83 | Parameters 84 | ---------- 85 | msg_type : str 86 | 消息类型 87 | 88 | 部分可选值及示例如下 89 | - ``'text'`` 纯文本 90 | - ``'markdown'`` Markdown 文本 91 | - ``'image'`` 图片 92 | - ``'file'`` 文件 93 | 94 | users : List[str] 95 | 接受消息的的用户账号列表 96 | 例如 ``['ZhangSan','LiSi']`` 97 | content : str, optional 98 | 消息内容, 默认为 ``None`` 99 | media_id : str, optional 100 | 文件 ID, 默认为 ``None`` 101 | 102 | Returns 103 | ------- 104 | bool 105 | 是否发送成功 106 | """ 107 | userid_str = '|'.join(users) 108 | access_token = self.get_access_token() 109 | data = { 110 | 'touser': userid_str, 111 | 'msgtype': msg_type, 112 | 'agentid': self.appid, 113 | msg_type: { 114 | 'content': content, 115 | 'media_id': media_id 116 | }, 117 | 'safe': 0, 118 | 'enable_id_trans': 1, 119 | 'enable_duplicate_check': 0, 120 | 'duplicate_check_interval': 1800 121 | } 122 | 123 | params = { 124 | 'access_token': access_token 125 | } 126 | 127 | response = requests.post( 128 | SEND_URL, 129 | params=params, 130 | json=data) 131 | return response.json()['errmsg'] == 'ok' 132 | 133 | def get_access_token(self) -> str: 134 | """ 135 | 获取企业微信应用 token 136 | 137 | Returns 138 | ------- 139 | str 140 | 企业微信程序 token 141 | 142 | Raises 143 | ------ 144 | Exception 145 | 当无法获取 token 时 146 | 147 | """ 148 | if self.access_token_expires_time and self.access_token and datetime.datetime.now() < self.access_token_expires_time: 149 | return self.access_token 150 | 151 | params = { 152 | 'corpid': self.corpid, 153 | 'corpsecret': self.corpsecret 154 | 155 | } 156 | response = requests.get( 157 | TOKEN_URL, params=params) 158 | js: dict = response.json() 159 | access_token = js.get('access_token') 160 | if access_token is None: 161 | raise Exception('获取 token 失败 请确保相关信息填写的正确性') 162 | self.access_token = access_token 163 | self.access_token_expires_time = datetime.datetime.now( 164 | ) + datetime.timedelta(seconds=js.get('expires_in') - 60) 165 | return access_token 166 | 167 | def send_image(self, 168 | image_path: str, 169 | users: List[str]) -> bool: 170 | """ 171 | 发送图片给多个用户 172 | 173 | Parameters 174 | ---------- 175 | image_path : str 176 | 本地图片路径 177 | users : List[str] 178 | 接受消息的的用户账号列表 179 | """ 180 | media_id = self.upload_file(image_path, Path(image_path).name) 181 | return self.send(msg_type='image', 182 | users=users, 183 | media_id=media_id) 184 | 185 | def send_file(self, 186 | file_path: str, 187 | users: List[str]) -> bool: 188 | """ 189 | 发送文件给多个用户 190 | 191 | Parameters 192 | ---------- 193 | file_path : str 194 | 本地文件路径 195 | users : List[str] 196 | 接受消息的用户账号列表 197 | """ 198 | media_id = self.upload_file(file_path, Path(file_path).name) 199 | return self.send(msg_type='file', 200 | users=users, 201 | media_id=media_id) 202 | 203 | def send_text(self, 204 | content: str, 205 | users: List[str]) -> bool: 206 | """ 207 | 发送文本消息给多个用户 208 | 209 | Parameters 210 | ---------- 211 | content : str 212 | 文本内容 213 | users : List[str] 214 | 接受消息的用户账号列表 215 | """ 216 | return self.send(msg_type='text', 217 | users=users, 218 | content=content) 219 | 220 | def send_markdown(self, 221 | content: str, 222 | users: List[str]) -> bool: 223 | """ 224 | 发送 Markdown 消息给多个用户 225 | 226 | Parameters 227 | ---------- 228 | content : str 229 | Markdown 内容 230 | users : List[str] 231 | 接受消息的用户账号列表 232 | """ 233 | return self.send(msg_type='markdown', 234 | users=users, 235 | content=content) 236 | --------------------------------------------------------------------------------