import json from app.infra.shared import AsyncHttpClient class Feishu: # 服务号分组群发监测机器人 server_account_publish_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/380fdecf-402e-4426-85b6-7d9dbd2a9f59" # 外部服务号投流监测机器人 outside_gzh_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/0899d43d-9f65-48ce-a419-f83ac935bf59" # 长文 daily 报警机器人 long_articles_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/b44333f2-16c0-4cb1-af01-d135f8704410" # 测试环境报警机器人 long_articles_bot_dev = "https://open.feishu.cn/open-apis/bot/v2/hook/f32c0456-847f-41f3-97db-33fcc1616bcd" # 长文任务报警群 long_articles_task_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/223b3d72-f2e8-40e0-9b53-6956e0ae7158" # cookie 监测机器人 cookie_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/51b9c83a-f50d-44dd-939f-bcd10ac6c55a" # rank_bot rank_monitor_bot = "https://open.feishu.cn/open-apis/bot/v2/hook/f9dae7ba-decf-436b-b438-41994c35af1e" # 用户名与手机号映射 name_phone_dict = { "卓异": "18624010360", "俊辉": "18801281360", "范军": "15200827642", "林强": "15810236123", } def __init__(self): self.token = None self.headers = {"Content-Type": "application/json"} self.mention_all = { "content": "\n", "tag": "lark_md", } self.not_mention = {} async def fetch_token(self): url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/" post_data = { "app_id": "cli_a51114cf8bf8d00c", "app_secret": "cNoTAqMpsAm7mPBcpCAXFfvOzCNL27fe", } async with AsyncHttpClient(default_headers=self.headers) as client: response = await client.post(url=url, json=post_data) tenant_access_token = response["tenant_access_token"] self.token = tenant_access_token async def get_user_open_id(self, username): """ 根据用户名获取飞书 open_id :param username: 用户名 :return: open_id """ if not self.token: await self.fetch_token() mobile = self.name_phone_dict.get(username) if not mobile: return None url = "https://open.feishu.cn/open-apis/user/v1/batch_get_id" headers = { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json; charset=utf-8", } params = {"mobiles": [mobile]} async with AsyncHttpClient() as client: response = await client.get(url=url, params=params, headers=headers) try: open_id = response["data"]["mobile_users"][mobile][0]["open_id"] return open_id except (KeyError, IndexError): return None class FeishuSheetApi(Feishu): async def prepend_value(self, sheet_token, sheet_id, ranges, values): insert_value_url = "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{}/values_prepend".format( sheet_token ) headers = { "Authorization": "Bearer " + self.token, "contentType": "application/json; charset=utf-8", } body = { "valueRange": {"range": "{}!{}".format(sheet_id, ranges), "values": values} } async with AsyncHttpClient() as client: response = await client.post( url=insert_value_url, json=body, headers=headers ) print(response) async def insert_value(self, sheet_token, sheet_id, ranges, values): insert_value_url = ( "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{}/values".format( sheet_token ) ) # self.token = 't-g104bpfHNZN45BVJWFSQEM6WD45AAI4FNRWXCZVK' headers = { "Authorization": "Bearer " + self.token, "contentType": "application/json; charset=utf-8", } body = { "valueRange": {"range": "{}!{}".format(sheet_id, ranges), "values": values} } async with AsyncHttpClient() as client: response = await client.put( url=insert_value_url, json=body, headers=headers ) class FeishuBotApi(Feishu): async def create_mention_text(self, usernames): """ 创建 @ 用户的文本 :param usernames: 用户名列表 :return: @ 用户的 markdown 文本 """ if not usernames: return "" mention_parts = [] for username in usernames: open_id = await self.get_user_open_id(username) if open_id: mention_parts.append(f"") return " ".join(mention_parts) + "\n" if mention_parts else "" @classmethod def create_feishu_columns_sheet( cls, sheet_type, sheet_name, display_name, width="auto", vertical_align="top", horizontal_align="left", number_format=None, ): match sheet_type: case "plain_text": return { "name": sheet_name, "display_name": display_name, "width": width, "data_type": "text", "vertical_align": vertical_align, "horizontal_align": horizontal_align, } case "lark_md": return { "name": sheet_name, "display_name": display_name, "data_type": "lark_md", } case "number": return { "name": sheet_name, "display_name": display_name, "data_type": "number", "format": number_format, "width": width, } case "date": return { "name": sheet_name, "display_name": display_name, "data_type": "date", "date_format": "YYYY/MM/DD", } case "options": return { "name": sheet_name, "display_name": display_name, "data_type": "options", } case _: return { "name": sheet_name, "display_name": display_name, "width": width, "data_type": "text", "vertical_align": vertical_align, "horizontal_align": horizontal_align, } # 表格形式 async def create_feishu_table(self, title, columns, rows, mention, mention_users=None): """ 创建飞书表格消息 :param title: 标题 :param columns: 列定义 :param rows: 行数据 :param mention: 是否 @ 所有人 :param mention_users: 要 @ 的具体用户列表,例如 ["mark"] """ mention_element = self.not_mention if mention: mention_element = self.mention_all elif mention_users: mention_text = await self.create_mention_text(mention_users) mention_element = {"content": mention_text, "tag": "lark_md"} table_base = { "header": { "template": "blue", "title": {"content": title, "tag": "plain_text"}, }, "elements": [ mention_element, { "tag": "table", "page_size": len(rows) + 1, "row_height": "low", "header_style": { "text_align": "left", "text_size": "normal", "background_style": "grey", "text_color": "default", "bold": True, "lines": 1, }, "columns": columns, "rows": rows, }, ], } return table_base async def create_feishu_bot_obj(self, title, mention, detail, mention_users=None): """ create feishu bot object :param title: 标题 :param mention: 是否 @ 所有人 :param detail: 详细内容 :param mention_users: 要 @ 的具体用户列表,例如 ["luojunhui"] """ mention_element = self.not_mention if mention: mention_element = self.mention_all elif mention_users: mention_text = await self.create_mention_text(mention_users) mention_element = {"content": mention_text, "tag": "lark_md"} return { "elements": [ { "tag": "div", "text": mention_element, }, { "tag": "div", "text": { "content": json.dumps(detail, ensure_ascii=False, indent=4), "tag": "lark_md", }, }, ], "header": {"title": {"content": title, "tag": "plain_text"}}, } # bot async def bot( self, title, detail, mention=True, table=False, env="long_articles_task", mention_users=None, ): """ 发送飞书机器人消息 :param title: 标题 :param detail: 详细内容 :param mention: 是否 @ 所有人 :param table: 是否为表格形式 :param env: 环境,决定发送到哪个机器人 :param mention_users: 要 @ 的具体用户列表,例如 ["luojunhui"] """ match env: case "dev": url = self.long_articles_bot_dev case "prod": url = self.long_articles_bot case "outside_gzh_monitor": url = self.outside_gzh_monitor_bot case "server_account_publish_monitor": url = self.server_account_publish_monitor_bot case "long_articles_task": url = self.long_articles_task_bot case "cookie_monitor": url = self.cookie_monitor_bot case "rank_bot": url = self.rank_monitor_bot case _: url = self.long_articles_bot_dev headers = {"Content-Type": "application/json"} if table: card = await self.create_feishu_table( title=title, columns=detail["columns"], rows=detail["rows"], mention=mention, mention_users=mention_users, ) else: card = await self.create_feishu_bot_obj( title=title, mention=mention, detail=detail, mention_users=mention_users ) data = {"msg_type": "interactive", "card": card} async with AsyncHttpClient() as client: res = await client.post(url=url, headers=headers, json=data) return res