├── ScfOperate.py ├── index.py └── readme.md /ScfOperate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import time 4 | import json 5 | from tencentcloud.common import credential 6 | from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException 7 | from tencentcloud.scf.v20180416 import scf_client, models 8 | 9 | # 定义关于腾讯云的变量 10 | # 规范:全部单词首字母大写,中间无连接符号 11 | global random_time_lowerlimit, random_time_toplimit, SecretId, SecretKey, Region 12 | # 下次触发前的随机时间的范围: 13 | random_time_lowerlimit = 150 14 | random_time_toplimit = 300 15 | 16 | # 读取环境变量中的腾讯云api 17 | SecretId = "" 18 | SecretKey = "" 19 | 20 | # 读取环境变量中的腾讯云地域 21 | Region = "" 22 | 23 | 24 | # 写入环境变量(主要是为了更新refresh_token供下次使用) 25 | 26 | 27 | def EnvWrite(FunctionName, Namespace, ms_id, ms_secret, refresh_token, Another=None): 28 | global SecretId, SecretKey, Region 29 | try: 30 | cred = credential.Credential(SecretId, SecretKey) 31 | client = scf_client.ScfClient(cred, Region) 32 | except TencentCloudSDKException as err: 33 | print(err) 34 | req = models.UpdateFunctionConfigurationRequest() 35 | params = '{"FunctionName":"' + FunctionName + '",' 36 | params += '"Namespace":"' + Namespace + '",' 37 | params += '"Environment":{"Variables":[' 38 | params += '{"Key":"Region","Value":"' + Region + '"},' 39 | params += '{"Key":"refresh_token","Value":"' + refresh_token + '"},' 40 | params += '{"Key":"ms_id","Value":"' + ms_id + '"},' 41 | params += '{"Key":"ms_secret","Value":"' + ms_secret + '"},' 42 | params += '{"Key":"SecretId","Value":"' + SecretId + '"},' 43 | params += '{"Key":"SecretKey","Value":"' + SecretKey + '"},' 44 | if Another: 45 | for key in Another: 46 | if Another[key]: 47 | params += '{"Key":"'+ key + '","Value":"' + Another[key] + '"},' 48 | params = params.rstrip(",") 49 | params += ']}}' 50 | #print(params) 51 | req.from_json_string(params) 52 | try: 53 | # 以下照抄腾讯云sdk例程 54 | resp = client.UpdateFunctionConfiguration(req) 55 | print(resp.to_json_string()) 56 | except TencentCloudSDKException as err: 57 | print(err) 58 | return 59 | 60 | 61 | # 用于AB结构下的触发器更新 62 | def ABTrigUpdate(AnotherFunctionName, AnotherRegion, AnotherNamespace, TriggerName): 63 | global SecretId, SecretKey, Region 64 | try: 65 | cred = credential.Credential(SecretId, SecretKey) 66 | client = scf_client.ScfClient(cred, AnotherRegion) 67 | except TencentCloudSDKException as err: 68 | print(err) 69 | # 删除旧触发器 70 | try: 71 | req = models.DeleteTriggerRequest() 72 | params = '{"FunctionName":"' + AnotherFunctionName + '",' 73 | params += '"TriggerName":"' + TriggerName + '",' 74 | params += '"Namespace":"' + AnotherNamespace + '",' 75 | params += '"Type":"timer"}' 76 | req.from_json_string(params) 77 | resp = client.DeleteTrigger(req) 78 | print(resp.to_json_string()) 79 | except TencentCloudSDKException as err: 80 | print(err) 81 | 82 | # 添加新触发器 83 | try: 84 | next_time = time.time() 85 | next_time += random.randint(random_time_lowerlimit, 86 | random_time_toplimit) 87 | 88 | req = models.CreateTriggerRequest() 89 | params = '{"FunctionName":"' + AnotherFunctionName + '",' 90 | params += '"TriggerName":"' + TriggerName + '",' 91 | params += '"Namespace":"' + AnotherNamespace + '",' 92 | params += '"Type":"timer",' 93 | params += '"Enable":"OPEN",' 94 | params += '"TriggerDesc":"' + \ 95 | to_china_timezone_cron(next_time) + '"}' 96 | req.from_json_string(params) 97 | resp = client.CreateTrigger(req) 98 | print(resp.to_json_string()) 99 | except TencentCloudSDKException as err: 100 | print(err) 101 | return 102 | 103 | 104 | # 用于主从结构下的触发器更新 105 | def MSTrigUpdate_Master(AnotherFunctionName, AnotherRegion, AnotherNamespace, TriggerName, FunctionName, Namespace): 106 | global SecretId, SecretKey, Region 107 | try: 108 | cred = credential.Credential(SecretId, SecretKey) 109 | client = scf_client.ScfClient(cred, AnotherRegion) 110 | except TencentCloudSDKException as err: 111 | print(err) 112 | # 启动从函数 113 | pass_params = { 114 | "FunctionName": FunctionName, 115 | "Region": Region, 116 | "Namespace": Namespace, 117 | "TriggerName": TriggerName, 118 | "SecretId": SecretId, 119 | "SecretKey": SecretKey 120 | } 121 | pass_params_str = json.dumps(pass_params) 122 | req = models.InvokeRequest() 123 | params = '{"FunctionName":"' + AnotherFunctionName + '",' 124 | params += '"InvocationType":"Event",' 125 | params += '"ClientContext":"' + pass_params_str + '",' 126 | params += '"Namespace":"' + AnotherNamespace + '"}' 127 | req.from_json_string(params) 128 | return 129 | 130 | 131 | def MSTrigUpdate_Slave(event, content): 132 | FunctionName = content["FunctionName"] 133 | Region = content["Region"] 134 | Namespace = content["Namespace"] 135 | TriggerName = content["TriggerName"] 136 | SecretId = content["SecretId"] 137 | SecretKey = content["SecretKey"] 138 | ABTrigUpdate(FunctionName, Region, 139 | Namespace, TriggerName) 140 | return 141 | 142 | def to_china_timezone_cron(timestamp): return time.strftime( 143 | "%S %M %H %d %m * %Y", time.gmtime(timestamp+8*60*60)) 144 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import requests as req 2 | import json 3 | import os 4 | import ScfOperate 5 | 6 | 7 | # 获取环境变量用的字符串是一大坑点! 8 | # 为什么Python没有nameof方法?? 9 | # (然而如果一开始就规范好变量命名规则就没那么多事了 10 | 11 | # 定义关于Azure AD的变量 12 | # 规范:使用全小写,中间用下划线连接 13 | 14 | # 读取环境变量中的微软应用api 15 | ms_id = "" 16 | ms_secret = "" 17 | 18 | # 读取环境变量中的refresh_token 19 | refresh_token = "" 20 | 21 | # 定义关于腾讯云的变量 22 | # 规范:全部单词首字母大写,中间无连接符号 23 | FunctionName = "" 24 | Namespace = "" 25 | # 下面是递归触发相关 26 | TrigUpdateType = "" 27 | AnotherFunctionName = "" 28 | AnotherRegion = "" 29 | AnotherNamespace = "" 30 | TriggerName = "" 31 | TriggerName_Default = "RandomTimer" 32 | 33 | # 微软api列表,直接复制粘贴自原脚本 34 | api_list = [ 35 | r"https://graph.microsoft.com/v1.0/me/drive/root", 36 | r"https://graph.microsoft.com/v1.0/me/drive", 37 | r"https://graph.microsoft.com/v1.0/users", 38 | r"https://graph.microsoft.com/v1.0/me/messages", 39 | r"https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules", 40 | r"https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules", 41 | r"https://graph.microsoft.com/v1.0/me/drive/root/children", 42 | r"https://api.powerbi.com/v1.0/myorg/apps", 43 | r"https://graph.microsoft.com/v1.0/me/mailFolders", 44 | r"https://graph.microsoft.com/v1.0/me/outlook/masterCategories" 45 | ] 46 | 47 | 48 | def get_api(url, headers): 49 | return req.get(url, headers=headers).status_code == 200 50 | 51 | 52 | # 拿到最新的access_token 53 | def get_token(): 54 | global refresh_token, ms_id, ms_secret 55 | headers = {"Content-Type": "application/x-www-form-urlencoded" 56 | } 57 | data = {"grant_type": "refresh_token", 58 | "refresh_token": refresh_token, 59 | "client_id": ms_id, 60 | "client_secret": ms_secret, 61 | "redirect_uri": "http://localhost:53682/" 62 | } 63 | html = req.post( 64 | "https://login.microsoftonline.com/common/oauth2/v2.0/token", data=data, headers=headers) 65 | jsontxt = json.loads(html.text) 66 | refresh_token = jsontxt["refresh_token"] 67 | access_token = jsontxt["access_token"] 68 | return access_token 69 | 70 | 71 | def exe(event, content): 72 | # 全局变量要手动指定才能修改…… 73 | global FunctionName, Namespace 74 | 75 | global ms_id, ms_secret, refresh_token 76 | 77 | # 读取环境变量中的微软应用api 78 | ms_id = os.environ.get("ms_id") 79 | ms_secret = os.environ.get("ms_secret") 80 | 81 | # 读取环境变量中的refresh_token 82 | refresh_token = os.environ.get("refresh_token") 83 | 84 | ScfOperate.SecretId = os.environ.get("SecretId") 85 | ScfOperate.SecretKey = os.environ.get("SecretKey") 86 | ScfOperate.Region = os.environ.get("Region") 87 | 88 | # 从api入参处读取 89 | FunctionName = content["function_name"] 90 | Namespace = content["namespace"] 91 | 92 | access_token = get_token() 93 | headers = { 94 | "Authorization": access_token, 95 | "Content-Type": "application/json" 96 | } 97 | try: 98 | for api in api_list: 99 | get_api(api, headers) 100 | except: 101 | print("出现错误,跳过剩余") 102 | print("API触发结束") 103 | # 设定下一个触发时间 104 | # ScfOperate.TrigUpdate(FunctionName, Namespace, TriggerName) 105 | 106 | # 经过确认,运行状态下的云函数的触发器不能被修改 107 | # 因此在云函数系统更新前,只能使用双函数递归调用实现随机时间 108 | # 有AB、主从两种方案,干脆都写了 109 | global TrigUpdateType 110 | TrigUpdateType = os.environ.get("TrigUpdateType") 111 | if not TrigUpdateType: 112 | print("TrigUpdateType为空,将不会尝试设置随机时间") 113 | ScfOperate.EnvWrite(FunctionName, Namespace, ms_id, 114 | ms_secret, refresh_token) 115 | return 116 | global AnotherFunctionName, AnotherRegion, AnotherNamespace, TriggerName 117 | 118 | AnotherFunctionName = os.environ.get("AnotherFunctionName") 119 | if not AnotherFunctionName: 120 | print("AnotherFunctionName为空,将不会尝试设置随机时间") 121 | return 122 | 123 | AnotherRegion = os.environ.get("AnotherRegion") 124 | if not AnotherRegion: 125 | print("AnotherRegion为空,将会默认为相同") 126 | AnotherRegion = os.environ.get("Region") 127 | 128 | AnotherNamespace = os.environ.get("AnotherNamespace") 129 | if not AnotherNamespace: 130 | print("AnotherNamespace为空,将会默认为相同") 131 | AnotherNamespace = Namespace 132 | 133 | TriggerName = os.environ.get("AnotherTriggerName") 134 | if not TriggerName: 135 | print("TriggerName为空,将会使用默认值" + TriggerName_Default) 136 | TriggerName = TriggerName_Default 137 | 138 | # 类型判断 139 | if TrigUpdateType == "AB": 140 | print("TrigUpdateType为A+B型") 141 | ScfOperate.ABTrigUpdate(AnotherFunctionName, 142 | AnotherRegion, AnotherNamespace, TriggerName) 143 | elif TrigUpdateType == "MS": 144 | print("TrigUpdateType为M+S型") 145 | ScfOperate.MSTrigUpdate_Master(AnotherFunctionName, AnotherRegion, 146 | AnotherNamespace, TriggerName, FunctionName, Namespace) 147 | Another = { 148 | "TrigUpdateType": TrigUpdateType, 149 | "AnotherFunctionName": AnotherFunctionName, 150 | "AnotherRegion": os.environ.get("AnotherRegion"), 151 | "AnotherNamespace": os.environ.get("AnotherNamespace"), 152 | "TriggerName": os.environ.get("TriggerName"), 153 | } 154 | #print(Another) 155 | ScfOperate.EnvWrite(FunctionName, Namespace, ms_id, 156 | ms_secret, refresh_token, Another) 157 | return 158 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # E5 Renew SCF 2 | 3 | 如题,[原版刷API脚本](http://file.heimu.ltd/1.py)的修改版,单次执行,专门用于腾讯云的无服务器云函数。 4 | 5 | ### 配置 6 | 7 | 参数获取请参见[office e5刷API脚本分享以及教程](https://blog.432100.xyz/index.php/archives/50/)、[E5刷API脚本分享以及教程](https://huengyamm.xyz/2020/03/15/E5-API/)(后面这个有图)。 8 | 9 | 入口函数为`index.exe`。 10 | 11 | 需要预先配置以下环境变量: 12 | 13 | `Region`:该云函数所在服务器地域,地域代码参见[地域列表](https://cloud.tencent.com/document/product/583/17238#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8) 14 | 15 | `refresh_token`:`rclone`所获取的同名参数,[点击这里下载`rclone`](http://file.heimu.ltd/rclone.exe) 16 | 17 | `ms_id`:Azure AD上的**应用程序(客户端) ID** 18 | 19 | `ms_secret`:Azure AD上的**客户端密码**(不是用户密码) 20 | 21 | `SecretId`、`SecretKey`:先到腾讯云[API密钥管理](https://console.cloud.tencent.com/cam/capi)创建,用于刷新环境变量中的`refresh_token` 22 | 23 | 如果你有新的API要加入,只要请求的header格式一致,可以直接加入`api_list`列表(那两个`messageRules`是原本就有两条的,我暂且蒙在鼓里)。 24 | 25 | 另外,云函数不会自己运行,请自行设置触发器。 26 | 27 | **如果你想使用子账号,请注意给予的权限中要包括以下Action:** 28 | 29 | | Action名称 | 描述 | 30 | | -------------------------------------- | -------------- | 31 | | GetFunction | 获取函数详情 | 32 | | UpdateFunctionConfiguration | 更新函数配置 | 33 | | SetTrigger(仅设置随机时间需要,下同) | 设置函数触发器 | 34 | | UpdateTrigger | 更新触发器配置 | 35 | | DeleteTrigger | 删除函数触发器 | 36 | 37 | ### 随机时间 38 | 39 | 原本的设想是直接删除旧的触发器,然后添加一个随机时间的触发器,没想到居然不行。 40 | 41 | > [TencentCloudSDKException] code:FailedOperation.CreateTrigger message:创建触发器失败 (当前函数状态无法进行此操作) 42 | 43 | 明明连修改环境变量都可以……没办法,不能在一个函数内实现随机时间了。(除非你想靠`sleep(random())`卡时间,但这样会浪费不少计费时间) 44 | 45 | 如果你不嫌麻烦的话,确实可以通过两个函数互相调用来实现随机时间。 46 | 47 | #### A+B类型 48 | 49 | 通过用两个具有完整功能的函数互相设置定时触发器来实现。 50 | 51 | 环境变量: 52 | 53 | `TrigUpdateType`:*AB* 54 | 55 | **注意:**云函数提供了包括环境变量在内的**复制**功能。使用复制功能前不要在环境变量中填写`refresh_token`。 56 | 57 | `AnotherFunctionName`:另一个函数的`FunctionName` 58 | 59 | `AnotherRegion`:(可选)另一个函数的`Region`,默认相同 60 | 61 | `AnotherNamespace`:(可选)另一个函数的`Namespace`,默认相同 62 | 63 | `random_time_lowerlimit`、`random_time_toplimit`:(可选)随机时间的下限、上限,默认150、300(秒) 64 | 65 | `TriggerName`:(可选)触发器的名称,每次更新都会先删除这个名字的触发器再添加一个同名的,默认值为*RandomTimer*。 66 | 67 | 无论使用哪种类型,建议只设置一个api触发器,因为程序在初次运行后,就能自动递归触发下一次。 68 | 69 | ### 响应 70 | 71 | 目前还想不到能响应什么有用的东西,毕竟大部分时间都是无值守运行。 72 | 73 | 所有`print()`的输出请到云函数的运行日志查看。 74 | 75 | ### 消耗 76 | 77 | 测试挂了一天,设置的触发器每5分钟触发一次,共使用资源约120GBs,出流量0.01GB,完全不会超出免费额度;另外,每次触发平均耗时4s,内存消耗基本都在20MB左右。 78 | 79 | ### TODO 80 | 81 | - [x] ~~自行递归添加下一个触发器(以实现真正随机时间而不是靠`sleep(random())`卡时间实现)~~ 82 | 83 | - [x] 搞懂为什么不能在定义全局变量的时候用环境变量赋值 84 | 85 | > 目前自行定义的环境变量和一些会有变动的环境只能在入口函数中获取,是因为是在调用入口函数的时候注入的。——腾讯云工作人员回复(为什么不写在文档里?) 86 | 87 | - [ ] 把那些手打的`params`用字典转json代替(对于那种连多打一个逗号都要报错的解析器表示强烈cnm 88 | 89 | - [ ] 欢迎issue。 --------------------------------------------------------------------------------