feishu.py 11 KB

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