feishu.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import json
  2. from app.infra.shared import AsyncHttpClient
  3. class Feishu:
  4. # 服务号分组群发监测机器人
  5. server_account_publish_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/380fdecf-402e-4426-85b6-7d9dbd2a9f59"
  6. # 外部服务号投流监测机器人
  7. outside_gzh_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/0899d43d-9f65-48ce-a419-f83ac935bf59"
  8. # 长文 daily 报警机器人
  9. long_articles_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/b44333f2-16c0-4cb1-af01-d135f8704410"
  10. # 测试环境报警机器人
  11. long_articles_bot_dev = "https://open.feishu.cn/open-apis/bot/v2/hook/f32c0456-847f-41f3-97db-33fcc1616bcd"
  12. # 长文任务报警群
  13. long_articles_task_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/223b3d72-f2e8-40e0-9b53-6956e0ae7158"
  14. # cookie 监测机器人
  15. cookie_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/51b9c83a-f50d-44dd-939f-bcd10ac6c55a"
  16. # rank_bot
  17. rank_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/f9dae7ba-decf-436b-b438-41994c35af1e"
  18. # 用户名与手机号映射
  19. name_phone_dict = {
  20. "卓异": "18624010360",
  21. "范军": "15200827642",
  22. "林强": "15810236123",
  23. }
  24. def __init__(self):
  25. self.token = None
  26. self.headers = {"Content-Type": "application/json"}
  27. self.mention_all = {
  28. "content": "<at id=all></at>\n",
  29. "tag": "lark_md",
  30. }
  31. self.not_mention = {}
  32. async def fetch_token(self):
  33. url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
  34. post_data = {
  35. "app_id": "cli_a51114cf8bf8d00c",
  36. "app_secret": "cNoTAqMpsAm7mPBcpCAXFfvOzCNL27fe",
  37. }
  38. async with AsyncHttpClient(default_headers=self.headers) as client:
  39. response = await client.post(url=url, json=post_data)
  40. tenant_access_token = response["tenant_access_token"]
  41. self.token = tenant_access_token
  42. async def get_user_open_id(self, username):
  43. """
  44. 根据用户名获取飞书 open_id
  45. :param username: 用户名
  46. :return: open_id
  47. """
  48. if not self.token:
  49. await self.fetch_token()
  50. mobile = self.name_phone_dict.get(username)
  51. if not mobile:
  52. return None
  53. url = "https://open.feishu.cn/open-apis/user/v1/batch_get_id"
  54. headers = {
  55. "Authorization": f"Bearer {self.token}",
  56. "Content-Type": "application/json; charset=utf-8",
  57. }
  58. params = {"mobiles": [mobile]}
  59. async with AsyncHttpClient() as client:
  60. response = await client.get(url=url, params=params, headers=headers)
  61. try:
  62. open_id = response["data"]["mobile_users"][mobile][0]["open_id"]
  63. return open_id
  64. except (KeyError, IndexError):
  65. return None
  66. class FeishuSheetApi(Feishu):
  67. async def prepend_value(self, sheet_token, sheet_id, ranges, values):
  68. insert_value_url = "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{}/values_prepend".format(
  69. sheet_token
  70. )
  71. headers = {
  72. "Authorization": "Bearer " + self.token,
  73. "contentType": "application/json; charset=utf-8",
  74. }
  75. body = {
  76. "valueRange": {"range": "{}!{}".format(sheet_id, ranges), "values": values}
  77. }
  78. async with AsyncHttpClient() as client:
  79. response = await client.post(
  80. url=insert_value_url, json=body, headers=headers
  81. )
  82. print(response)
  83. async def insert_value(self, sheet_token, sheet_id, ranges, values):
  84. insert_value_url = (
  85. "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{}/values".format(
  86. sheet_token
  87. )
  88. )
  89. # self.token = 't-g104bpfHNZN45BVJWFSQEM6WD45AAI4FNRWXCZVK'
  90. headers = {
  91. "Authorization": "Bearer " + self.token,
  92. "contentType": "application/json; charset=utf-8",
  93. }
  94. body = {
  95. "valueRange": {"range": "{}!{}".format(sheet_id, ranges), "values": values}
  96. }
  97. async with AsyncHttpClient() as client:
  98. response = await client.put(
  99. url=insert_value_url, json=body, headers=headers
  100. )
  101. class FeishuBotApi(Feishu):
  102. async def create_mention_text(self, usernames):
  103. """
  104. 创建 @ 用户的文本
  105. :param usernames: 用户名列表
  106. :return: @ 用户的 markdown 文本
  107. """
  108. if not usernames:
  109. return ""
  110. mention_parts = []
  111. for username in usernames:
  112. open_id = await self.get_user_open_id(username)
  113. if open_id:
  114. mention_parts.append(f"<at id={open_id}></at>")
  115. return " ".join(mention_parts) + "\n" if mention_parts else ""
  116. @classmethod
  117. def create_feishu_columns_sheet(
  118. cls,
  119. sheet_type,
  120. sheet_name,
  121. display_name,
  122. width="auto",
  123. vertical_align="top",
  124. horizontal_align="left",
  125. number_format=None,
  126. ):
  127. match sheet_type:
  128. case "plain_text":
  129. return {
  130. "name": sheet_name,
  131. "display_name": display_name,
  132. "width": width,
  133. "data_type": "text",
  134. "vertical_align": vertical_align,
  135. "horizontal_align": horizontal_align,
  136. }
  137. case "lark_md":
  138. return {
  139. "name": sheet_name,
  140. "display_name": display_name,
  141. "data_type": "lark_md",
  142. }
  143. case "number":
  144. return {
  145. "name": sheet_name,
  146. "display_name": display_name,
  147. "data_type": "number",
  148. "format": number_format,
  149. "width": width,
  150. }
  151. case "date":
  152. return {
  153. "name": sheet_name,
  154. "display_name": display_name,
  155. "data_type": "date",
  156. "date_format": "YYYY/MM/DD",
  157. }
  158. case "options":
  159. return {
  160. "name": sheet_name,
  161. "display_name": display_name,
  162. "data_type": "options",
  163. }
  164. case _:
  165. return {
  166. "name": sheet_name,
  167. "display_name": display_name,
  168. "width": width,
  169. "data_type": "text",
  170. "vertical_align": vertical_align,
  171. "horizontal_align": horizontal_align,
  172. }
  173. # 表格形式
  174. async def create_feishu_table(self, title, columns, rows, mention, mention_users=None):
  175. """
  176. 创建飞书表格消息
  177. :param title: 标题
  178. :param columns: 列定义
  179. :param rows: 行数据
  180. :param mention: 是否 @ 所有人
  181. :param mention_users: 要 @ 的具体用户列表,例如 ["mark"]
  182. """
  183. mention_element = self.not_mention
  184. if mention:
  185. mention_element = self.mention_all
  186. elif mention_users:
  187. mention_text = await self.create_mention_text(mention_users)
  188. mention_element = {"content": mention_text, "tag": "lark_md"}
  189. table_base = {
  190. "header": {
  191. "template": "blue",
  192. "title": {"content": title, "tag": "plain_text"},
  193. },
  194. "elements": [
  195. mention_element,
  196. {
  197. "tag": "table",
  198. "page_size": len(rows) + 1,
  199. "row_height": "low",
  200. "header_style": {
  201. "text_align": "left",
  202. "text_size": "normal",
  203. "background_style": "grey",
  204. "text_color": "default",
  205. "bold": True,
  206. "lines": 1,
  207. },
  208. "columns": columns,
  209. "rows": rows,
  210. },
  211. ],
  212. }
  213. return table_base
  214. async def create_feishu_bot_obj(self, title, mention, detail, mention_users=None):
  215. """
  216. create feishu bot object
  217. :param title: 标题
  218. :param mention: 是否 @ 所有人
  219. :param detail: 详细内容
  220. :param mention_users: 要 @ 的具体用户列表,例如 ["luojunhui"]
  221. """
  222. mention_element = self.not_mention
  223. if mention:
  224. mention_element = self.mention_all
  225. elif mention_users:
  226. mention_text = await self.create_mention_text(mention_users)
  227. mention_element = {"content": mention_text, "tag": "lark_md"}
  228. return {
  229. "elements": [
  230. {
  231. "tag": "div",
  232. "text": mention_element,
  233. },
  234. {
  235. "tag": "div",
  236. "text": {
  237. "content": json.dumps(detail, ensure_ascii=False, indent=4),
  238. "tag": "lark_md",
  239. },
  240. },
  241. ],
  242. "header": {"title": {"content": title, "tag": "plain_text"}},
  243. }
  244. # bot
  245. async def bot(
  246. self,
  247. title,
  248. detail,
  249. mention=True,
  250. table=False,
  251. env="long_articles_task",
  252. mention_users=None,
  253. ):
  254. """
  255. 发送飞书机器人消息
  256. :param title: 标题
  257. :param detail: 详细内容
  258. :param mention: 是否 @ 所有人
  259. :param table: 是否为表格形式
  260. :param env: 环境,决定发送到哪个机器人
  261. :param mention_users: 要 @ 的具体用户列表,例如 ["luojunhui"]
  262. """
  263. match env:
  264. case "dev":
  265. url = self.long_articles_bot_dev
  266. case "prod":
  267. url = self.long_articles_bot
  268. case "outside_gzh_monitor":
  269. url = self.outside_gzh_monitor_bot
  270. case "server_account_publish_monitor":
  271. url = self.server_account_publish_monitor_bot
  272. case "long_articles_task":
  273. url = self.long_articles_task_bot
  274. case "cookie_monitor":
  275. url = self.cookie_monitor_bot
  276. case "rank_bot":
  277. url = self.rank_monitor_bot
  278. case _:
  279. url = self.long_articles_bot_dev
  280. headers = {"Content-Type": "application/json"}
  281. if table:
  282. card = await self.create_feishu_table(
  283. title=title,
  284. columns=detail["columns"],
  285. rows=detail["rows"],
  286. mention=mention,
  287. mention_users=mention_users,
  288. )
  289. else:
  290. card = await self.create_feishu_bot_obj(
  291. title=title, mention=mention, detail=detail, mention_users=mention_users
  292. )
  293. data = {"msg_type": "interactive", "card": card}
  294. async with AsyncHttpClient() as client:
  295. res = await client.post(url=url, headers=headers, json=data)
  296. return res