diff --git a/utils/authenticator.py b/utils/authenticator.py index 1b3d8d0..973839e 100644 --- a/utils/authenticator.py +++ b/utils/authenticator.py @@ -11,6 +11,7 @@ from pathlib import Path import threading import time from typing import Optional, Tuple +from datetime import datetime from request import Request @@ -70,19 +71,20 @@ class Authenticator: ) # 指定服务商的访问令牌和失效时间戳 if time.time() > expired_timestamp: match servicer: - # 刷新深圳快瞳访问凭证cd C:\Python\.venv\Scripts + # 获取深圳快瞳访问凭证cd C:\Python\.venv\Scripts case "inspirvision": token, expired_timestamp = ( - self._refresh_inspirvision_certification() + self._get_inspirvision_certification() ) - # 刷新合力亿捷访问凭证 + # 获取合力亿捷访问凭证 case "hollycrm": - token, expired_timestamp = ( - self._refresh_hollycrm_certification() - ) - # 刷新飞书访问凭证 + token, expired_timestamp = self._get_hollycrm_certification() + # 获取飞书访问凭证 case "feishu": - token, expired_timestamp = self._refresh_feishu_certification() + token, expired_timestamp = self._get_feishu_certification() + # 获取 Cloudreve 访问凭证 + case "cloudreve": + token, expired_timestamp = self._get_cloudreve_certification() case _: raise RuntimeError(f"未知服务商:{servicer}") @@ -103,9 +105,9 @@ class Authenticator: ) return token - def _refresh_inspirvision_certification(self) -> Tuple[str, float]: + def _get_inspirvision_certification(self) -> Tuple[str, float]: """ - 刷新深圳快瞳访问凭证 + 获取深圳快瞳访问凭证 :return: 深圳快瞳访问令牌和失效时间戳 """ response = self.request.get( @@ -113,15 +115,15 @@ class Authenticator: ) # 若非响应成功则抛出异常 if not (response["status"] == 200 and response["code"] == 0): - raise RuntimeError("刷新深圳快瞳访问凭证发生异常") + raise RuntimeError("获取深圳快瞳访问凭证发生异常") return ( response["data"]["access_token"], time.time() + response["data"]["expires_in"], ) # https://inspirvision.cn/spa/documentCenter/ocr-finance/mark?id=3 深圳快瞳获取访问凭证接口,其中 access_token 为访问令牌,expires_in 为有效剩余时间(单位为秒) - def _refresh_hollycrm_certification(self) -> Tuple[str, float]: + def _get_hollycrm_certification(self) -> Tuple[str, float]: """ - 刷新合力亿捷访问凭证 + 获取合力亿捷访问凭证 :return: 合力亿捷访问令牌和失效时间戳 """ # 企业访问标识 @@ -142,15 +144,15 @@ class Authenticator: ) # 若非响应成功则抛出异常 if not response["success"]: - raise RuntimeError("刷新合力亿捷访问凭证发生异常") + raise RuntimeError("获取合力亿捷访问凭证发生异常") return ( response["data"], time.time() + 1 * 3600, # 访问令牌有效期为1小时 ) - def _refresh_feishu_certification(self) -> Tuple[str, float]: + def _get_feishu_certification(self) -> Tuple[str, float]: """ - 刷新飞书访问凭证 + 获取飞书访问凭证 :return: 飞书访问令牌和失效时间戳 """ response = self.request.post( @@ -162,8 +164,30 @@ class Authenticator: ) # 若非响应成功则抛出异常 if not response["code"] == 0: - raise RuntimeError("刷新飞书访问凭证发生异常") + raise RuntimeError("获取飞书访问凭证发生异常") return ( response["tenant_access_token"], time.time() + response["expire"], ) # https://open.feishu.cn/document/server-docs/api-call-guide/calling-process/get-access-token 飞书获取访问凭证接口 + + def _get_cloudreve_certification(self) -> Tuple[str, float]: + """ + 获取 Cloudreve 访问凭证 + :return: Cloudreve 访问令牌和失效时间戳 + """ + response = self.request.post( + url="https://cloudreve.liubiren.cloud/api/v4/session/token", + json={ + "email": "marslbr@qq.com", + "password": "Cl198752", + }, + ) + # 若非响应成功则抛出异常 + if not response["code"] == 0: + raise RuntimeError("获取 Cloudreve 访问凭证发生异常") + return ( + response["data"]["token"]["access_token"], + datetime.fromisoformat( + response["data"]["token"]["access_expires"] + ).timestamp(), + ) # https://docs.cloudreve.org/zh/api/auth 通过登录获取 AccessToken,时效时重新登录重新获取 diff --git a/utils/certifications.json b/utils/certifications.json index c775cdf..1bdc339 100644 --- a/utils/certifications.json +++ b/utils/certifications.json @@ -1 +1 @@ -{"feishu": ["t-g1043jkq4UBLA2TLB6HFDIUPFXIQBTCHUMZ5XPYX", 1773930401.8437002]} \ No newline at end of file +{"feishu": ["t-g1043mboYRA4SDDKUY5RTRLCHPF3YGJQIYCLGDVP", 1774157093.7511718]} \ No newline at end of file diff --git a/utils/cloudreve.py b/utils/cloudreve.py new file mode 100644 index 0000000..74db1f0 --- /dev/null +++ b/utils/cloudreve.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +封装 Cloudreve 客户端 +""" + +from email.parser import BytesParser +from email.policy import default +from email.utils import parsedate_to_datetime +from imaplib import IMAP4_SSL +import re +from time import time +from typing import Any, Dict, Optional, List + +from pydantic.config import JsonEncoder + +from authenticator import Authenticator +from request import Request +from base64 import b64encode +from urllib.parse import quote + + +class Cloudreve: + """Cloudreve 客户端""" + + def __init__(self): + # 实例化认证器 + self.authenticator = Authenticator() + # 实例化请求客户端 + self.http_client = Request() + + def _create_upload_session(self, uri: str, size: int) -> str: + """ + 创建上传会话 + :param uri: 统一资源标识符 + :param size: 文件大小 + :return: 上传会话标识 + """ + response = self.http_client.put( + url=f"https://cloudreve.liubiren.cloud/api/v4/file/upload", + headers={ + "Authorization": f"Bearer {self.authenticator.get_token(servicer="cloudreve")}", + "Content-Type": "application/json; charset=utf-8", + }, + json={ + "uri": quote(string=uri, safe=":/?&="), # 编码统一资源标识 + "size": size, + }, + ) + # 若非响应成功则抛出异常 + if not response["code"] == 0: + raise RuntimeError("创建上传会话发生异常") + return response["data"]["session_id"] + + def _upload_file_chunk(self, session_id: str, index: int, file_chunk: bytes) -> str: + """ + 上传文件块 + :param session_id: 上传会话标识 + :param index: 文件块索引 + :return: 上传文件标识 + """ + response = self.http_client.post( + url=f"https://cloudreve.liubiren.cloud/api/v4/file/upload/{session_id}/{index}", + headers={ + "Authorization": f"Bearer {self.authenticator.get_token(servicer="cloudreve")}", + "Content-Type": "application/octet-stream", + "Content-Length": len(file_chunk), + }, + json=open(file_path, "rb").read(), + ) diff --git a/utils/feishu.py b/utils/feishu.py index 6461fa6..8923823 100644 --- a/utils/feishu.py +++ b/utils/feishu.py @@ -38,7 +38,7 @@ class Feishu: """ 使用示例: feishu = Feishu() - print(feishu.get_mail_verification_code(folder="邮箱验证码", regular_expression=r"【普康健康】您的验证码是:(\\d+)")) + feishu.get_mail_verification_code(folder="邮箱验证码", regular_expression=r"【普康健康】您的验证码是:(\\d+)") 输出:123456 """ if not folder: @@ -209,15 +209,20 @@ class Feishu: return records - def download_material(self, file_token: str, stream_enabled: bool = False) -> str: + def _convert_to_cloudreve_direct_link(self, material_token: str) -> str: """ - 下载素材 - :param file_token: 素材标识 - :param stream_enabled: 使用流式传输,默认 False - :return: 素材 base64 编码的字符串 + 转为Cloudreve直链 + :param material_token: 素材标识 + :return: 素材直链地址 """ + # 获取 Cloudreve 上传 session_id + session_id = self.http_client. + + + + # 构建下载素材的请求地址 - url = f"https://open.feishu.cn/open-apis/drive/v1/medias/{file_token}/download" # https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download + url = f"https://open.feishu.cn/open-apis/drive/v1/medias/{material_token}/download" # https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download headers = self._get_headers() # 添加 Content-Type 请求头 @@ -230,9 +235,9 @@ class Feishu: response = self.http_client.download( url=url, headers=headers, - stream_enabled=stream_enabled, - ) - print(response) + stream_enabled=True, + ) # 默认使用流式传输 + a = Feishu() diff --git a/utils/request.py b/utils/request.py index 32e2215..a0d0fb2 100644 --- a/utils/request.py +++ b/utils/request.py @@ -171,8 +171,7 @@ class Caches(SQLite): class Request: """ 请求客户端,支持: - get:GET请求 - post:POST请求 + GET、PUT和POST等请求 download:下载 """ @@ -251,6 +250,14 @@ class Request: """ return self._request(method="GET", parameters=Parameters(**kwargs)) + def put(self, **kwargs) -> Any: + """ + GET请求 + :param kwargs: 请求参数 + :return: 响应内容 + """ + return self._request(method="PUT", parameters=Parameters(**kwargs)) + def post(self, **kwargs) -> Any: """ POST请求 @@ -264,12 +271,12 @@ class Request: return self._request(method="POST", parameters=Parameters(**kwargs)) def download( - self, stream_enabled: bool = False, chunk_size: int = 1024, **kwargs + self, stream_enabled: bool = False, chunk_size: int = 1024 * 1024, **kwargs ) -> Any: """ 下载 - :param stream_enabled: 使用流式传输 - :param chunk_size: 流式传输的分块大小 + :param stream_enabled: 使用流式传输,默认为关闭流式传输 + :param chunk_size: 分块大小,若开启流式传输则分块大小默认为 1MB :param kwargs: 请求参数 :return: 响应内容 """ @@ -277,15 +284,18 @@ class Request: method="GET", parameters=Parameters(**{"stream_enabled": stream_enabled, **kwargs}), ) - # 若使用流式传输则处理流式传输响应 + # 若使用流式传输则返回响应内容迭代器 if stream_enabled: return self._process_stream_response( response=response, chunk_size=chunk_size ) + return response @restrict(max_tokens=5, refill_rate=5.0) - def _request(self, method: Literal["GET", "POST"], parameters: Parameters) -> Any: + def _request( + self, method: Literal["GET", "PUT", "POST"], parameters: Parameters + ) -> Any: """ 请求 :param method: 请求方法 @@ -393,11 +403,9 @@ class Request: # 处理流式传输响应 @staticmethod - def _process_stream_response( - response: Response, chunk_size: int - ) -> Generator[bytes, None, None]: + def _process_stream_response(response: Response, chunk_size: int) -> Generator: """ - 处理流式响应 + 处理流式传输响应 :param response: 响应对象 :param chunk_size: 分块大小 :return: 响应内容迭代器