├── .gitignore ├── LICENSE ├── chrome-modheader.crx ├── config_example.py ├── helper.py ├── module ├── RSShub_twitter.py ├── __init__.py ├── machine_translation.py ├── permissiongroup.py ├── pollingTwitterApi.py ├── tweettrans.py ├── twitter.py └── twitterApi.py ├── plugins ├── feedback.py ├── machine_translation.py ├── nonebotrequest.py ├── permissiongroup.py ├── tweettrans.py ├── twitter.py ├── twitterListener │ ├── RSShub.py │ └── twitterApi.py └── zhuaba.py ├── readme.md ├── readme ├── image-20200428113806819.png ├── image-20200428113938381.png └── image-20200428114040218.png ├── requirement.txt └── start.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | cache/ 3 | config.py 4 | dbtest.py 5 | chromedriver.exe 6 | *.DS_Store 7 | img/ 8 | 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | .deploy_git/ 127 | db.json 128 | node_modules/ 129 | public/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 晨轩 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chrome-modheader.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxuan353/tweetToBot/1b930c4eb287bd2e8d9ebc5bbe0ef45fcd221a32/chrome-modheader.crx -------------------------------------------------------------------------------- /config_example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from nonebot.default_config import * 3 | #也可以在此处对nonebot进行设置 4 | #不需要修改的设置 5 | SESSION_RUNNING_EXPRESSION = '' 6 | 7 | #超级管理员,拥有最高权限 8 | SUPERUSERS.add(123456) #多个就复制多行 9 | 10 | #命令起始标识 11 | COMMAND_START = {'!','!'} 12 | 13 | #维护者姓名 14 | mastername = "" #例如:master(123456) 15 | 16 | #nonebot的debug开关 17 | DEBUG = True 18 | #nonebot的监听地址与启动端口 19 | NONEBOT_HOST = '0.0.0.0' 20 | NONEBOT_PORT = 8190 #须和CQP中地址一致 21 | API_ROOT = '0.0.0.0:8890'#使用HTTP协议时POST发送地址 22 | #默认botQQ 默认推送用的bot,错误信息会使用此bot推送。 23 | default_bot_QQ = '' 24 | bot_waring_printID = '' #bot警告信息推送到的Q号,为None时不进行推送 25 | 26 | #自动消息推送开关 27 | feedback_push_switch : bool = True #推送反馈信息 28 | error_push_switch : bool = True #推送错误信息 29 | 30 | #推特更新检测方法(TweetApi,RSShub,PollingTweetApi,Twint)-暂不支持Twint 31 | UPDATA_METHOD = "PollingTweetApi"#使用何种方法进行监听 32 | 33 | #烤推图片路径(用于支持酷Q远程连接) 路径:{trans_img_path}/transtweet/transimg/file 34 | trans_img_path = 'pycache_test' #可以是本地路径 也可以是远程路径 本地路径时无法远程连接bot(需要软链接) 35 | #如使用本地路径 可以填写形如file:///<地址>(即默认的当前文件夹位置/cache) 如file:///D:\tweetToQQbot\cache 36 | 37 | #图片发送超时时间 38 | img_time_out : str= '15' #图片下载超时时间(秒) 39 | 40 | #RSShub推送配置 41 | #基础地址 https://rsshub.app http://192.168.71.150:1300 42 | RSShub_base = 'https://rsshub.app' #默认是公用的https://rsshub.app 43 | RSShub_proxy = '' #代理地址(IP:PORT) 44 | RSShub_updata_interval = 300 #更新间隔-秒(每个监测对象会多消耗1秒以上的时间) 45 | RSShub_silent_start = False #静默启动,启动时不检测更新 46 | 47 | #twitter_api需填写 48 | #推特API代理 49 | api_proxy = "" #127.0.0.1:10809 50 | #填写twitter提供的开发Key和secret 51 | consumer_key = '7yZj***************d' 52 | consumer_secret = 'fIgX******************************SJ' 53 | access_token = '848*************************************A' 54 | access_token_secret = 'ShW****************************************oy' 55 | 56 | #pollingTwitterApi可填写的多个应用密钥对 -> ['key','secret'] 57 | #推特API应用轮询系统,可增加请求量 58 | polling_silent_start = False #静默启动,启动时不检测更新 59 | polling_interval = 60 #轮询监测间隔 单位秒,每对API速率限制约为1.5次每秒(建议60秒及以上) 60 | polling_consumers = [ 61 | #示例 ['7*********************d','fIgX*****************************SJ'], 62 | [consumer_key,consumer_secret],#基础密钥对,删除影响运行 63 | #['******************','********************************************'], 64 | ] 65 | 66 | 67 | #机翻引擎配置(腾讯(tencent)->需要API 需要安装模块,谷歌(google)) 68 | MachineTrans_default = 'google' #默认翻译引擎 69 | MachineTransApi = { 70 | 'tencent':{ 71 | "switch":True,#开关,配置完成后请设置为True,关闭为False 72 | #地区 ap-guangzhou->广州 ap-hongkong->香港 na-ashburn->美国(靠近纽约) 73 | #更多详见 https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 74 | "Region":"ap-guangzhou", 75 | "key":"AK****************************8KI", 76 | "secret":"sW************************w8" 77 | }, 78 | 'google':{ 79 | "switch":True,#开关 80 | } 81 | } 82 | 83 | 84 | #推送开关默认设置(每个选项默认值不能缺失,否则将影响运行) 85 | pushunit_default_config = { 86 | 'upimg':0,#是否连带图片显示(默认不带)-发推有效,转推及评论等事件则无效 87 | 88 | #推特推送模版(为空使用默认模版) 89 | 'retweet_template':'', 90 | 'quoted_template':'', 91 | 'reply_to_status_template':'', 92 | 'reply_to_user_template':'', 93 | 'none_template':'', 94 | 95 | #推特推送开关 96 | 'retweet':0,#转推(默认不开启) 97 | 'quoted':1,#带评论转推(默认开启) 98 | 'reply_to_status':1,#回复(默认开启) 99 | #'reply_to_status_limit':0,#智能回复推送(默认关闭)-开启后仅推送监测人与有足够被关注数的人的回复 100 | 'reply_to_user':1,#提及某人-多数时候是被提及但是被提及不会接收(默认开启) 101 | 'none':1,#发推(默认开启) 102 | 103 | #智能推送(仅限推送单元设置,无法全局设置) 104 | 'ai_retweet':0,#智能推送本人转推(默认不开启)-转发值得关注的人的推特时推送 105 | 'ai_reply_to_status':0,#智能推送本人回复(默认不开启)-回复值得关注的人时推送 106 | 'ai_passive_reply_to_status':0,#智能推送 被 回复(默认不开启)-被值得关注的人回复时推送 107 | 'ai_passive_quoted':0,#智能推送 被 带评论转推(默认不开启)-被值得关注的人带评论转推时推送 108 | 'ai_passive_reply_to_user':0,#智能推送 被 提及(默认不开启)-被值得关注的人提及时推送 109 | 110 | #个人信息变化推送(非实时) 111 | 'change_ID':0, #ID修改(默认关闭) 112 | 'change_name':1, #昵称修改(默认开启) 113 | 'change_description':0, #描述修改(默认关闭) 114 | 'change_headimgchange':1, #头像更改(默认开启) 115 | } 116 | 117 | -------------------------------------------------------------------------------- /helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import config 3 | import time 4 | import requests 5 | import traceback 6 | import nonebot 7 | import json 8 | import logging 9 | import sys 10 | import os 11 | import re 12 | from nonebot import CommandSession 13 | """ 14 | 帮助函数 15 | """ 16 | file_base_path = "cache" 17 | config_file_base_path = 'config' 18 | log_file_base_path = 'log' 19 | #每天一个日志文件 20 | bindCQID = config.default_bot_QQ 21 | bot_error_printID = config.bot_waring_printID 22 | if bindCQID == '': 23 | bindCQID = None 24 | else: 25 | bindCQID = int(bindCQID) 26 | if bot_error_printID == '': 27 | bot_error_printID = None 28 | else: 29 | bot_error_printID = int(bot_error_printID) 30 | 31 | #判断目录存在性,不存在则生成 32 | def check_path(filepath:str): 33 | if not os.path.exists(os.path.join(file_base_path,filepath)): 34 | os.makedirs(os.path.join(file_base_path,filepath)) 35 | #设置nonebot的日志对象 36 | def initNonebotLogger(): 37 | logformat = logging.Formatter("[%(asctime)s %(name)s]%(levelname)s: %(message)s") 38 | trf = logging.handlers.TimedRotatingFileHandler( 39 | filename=os.path.join(file_base_path,log_file_base_path,"nonebot.log"), 40 | encoding="utf-8", 41 | when="H", 42 | interval=24, 43 | backupCount=10, 44 | ) 45 | trf.setFormatter(logformat) 46 | nonebot.logger.addHandler(trf) 47 | 48 | check_path("") 49 | check_path(os.path.join('config')) 50 | check_path(os.path.join('log')) 51 | initNonebotLogger() 52 | 53 | 54 | #获取日志对象(记录名,是否输出到控制台) 55 | def getlogger(name,printCMD:bool = True) -> logging.Logger: 56 | reslogger = logging.getLogger(name) 57 | reslogger.setLevel(logging.INFO) 58 | logformat = logging.Formatter("[%(asctime)s %(name)s]%(levelname)s: %(message)s") 59 | if printCMD == True: 60 | sh = logging.StreamHandler() 61 | sh.setFormatter(logformat) 62 | reslogger.addHandler(sh) 63 | trf = logging.handlers.TimedRotatingFileHandler( 64 | filename=os.path.join(file_base_path,log_file_base_path,name+".log"), 65 | encoding="utf-8", 66 | when="H", 67 | interval=24, 68 | backupCount=10, 69 | ) 70 | trf.setFormatter(logformat) 71 | reslogger.addHandler(trf) 72 | return reslogger 73 | logger = getlogger(__name__) 74 | 75 | #读写文件日志 76 | fileop_logger = getlogger('filesystem',False) 77 | 78 | #正则表达式处理字符串 79 | def reDealStr(pat:str,msg:str): 80 | res = [] 81 | resm = re.match(pat,msg, re.M | re.S) 82 | if resm == None: 83 | return None 84 | for reg in resm.regs: 85 | res.append(msg[reg[0]:reg[1]]) 86 | if len(res) == 1: 87 | return res[0] 88 | return res 89 | 90 | #生成多对一字典 91 | def arglimitdeal(ls:dict): 92 | res = {} 93 | for k in ls.keys(): 94 | res[k]=k 95 | if type(ls[k]) is list: 96 | for v in ls[k]: 97 | res[v]=k 98 | else: 99 | res[res[k]]=k 100 | return res 101 | 102 | #参数处理 103 | def argDeal(msg:str,arglimit:list): 104 | #使用空白字符分隔参数(全角空格、半角空格、换行符) 105 | """ 106 | arglimit = [ 107 | { 108 | 'name':'参数名', #参数名 109 | 'des':'XX参数', #参数描述 110 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 111 | 'strip':True, #是否strip 112 | 'lower':False, #是否转换为小写 113 | 'default':None, #默认值 114 | 'func':None, #函数,当存在时使用函数进行二次处理 115 | 're':None, #正则表达式匹配(match函数) 116 | 'vlimit':{ 117 | #参数限制表(限制参数内容,空表则不限制),'*':''表示允许任意字符串,值不为空时任意字符串将被转变为这个值 118 | #处理限制前不进行类型转换,但会进行int float str的类型检查 119 | 'a':'b', 120 | 'a1':'b' 121 | } 122 | },{ 123 | #第二个参数 124 | #... 125 | } 126 | ] 127 | """ 128 | typefun = { 129 | 'int':int, 130 | 'float':float, 131 | 'str':str, 132 | 'list':list, 133 | 'dict':dict 134 | } 135 | arglist = {} 136 | pat = re.compile('[  \n]{1}') 137 | lmsg = msg 138 | arglimitL = len(arglimit) 139 | try: 140 | for i in range(0,arglimitL): 141 | ad = arglimit[i] 142 | if i != arglimitL - 1 and lmsg != None: 143 | res = pat.split(lmsg,maxsplit=1) 144 | hmsg = res[0] 145 | if len(res) == 2: 146 | lmsg = res[1] 147 | else: 148 | lmsg = None 149 | elif lmsg == None: 150 | hmsg = None 151 | else: 152 | hmsg = lmsg 153 | if hmsg != None and hmsg != '': 154 | if ad['strip']: 155 | hmsg = hmsg.strip() 156 | if ad['lower']: 157 | hmsg = hmsg.lower() 158 | if ad['vlimit'] != {}: 159 | if hmsg in ad['vlimit']: 160 | hmsg = ad['vlimit'][hmsg] 161 | elif '*' in ad['vlimit']: 162 | if ad['vlimit']['*'] != '': 163 | hmsg = ad['vlimit']['*'] 164 | else: 165 | return (False,ad['des'],'非法参数(不被允许的值)') 166 | if ad['re'] != None: 167 | hmsg = reDealStr(ad['re'],hmsg) 168 | if hmsg == None: 169 | if 're_error' in ad: 170 | return (False,ad['des'],ad['re_error']) 171 | else: 172 | return (False,ad['des'],'参数不符合规则(re)') 173 | if ad['func'] != None: 174 | hmsg = ad['func'](hmsg,ad) 175 | #处理函数返回格式 176 | #其一 None/合法值 177 | #其二 tuple对象 -> (参数是否合法,处理后的参数/错误文本) 178 | if type(hmsg) is tuple: 179 | if hmsg[0]: 180 | hmsg = hmsg[1] 181 | else: 182 | return (False,ad['des'],hmsg[1]) 183 | elif hmsg == None: 184 | return (False,ad['des'],'参数不符合规则(fun)') 185 | if ad['type'] == 'int' and not (type(hmsg) is int): 186 | if not hmsg.isdecimal(): 187 | return (False,ad['des'],'数值无效') 188 | elif ad['type'] == 'float' and not (type(hmsg) is float): 189 | try: 190 | float(hmsg) 191 | except: 192 | return (False,ad['des'],'数值无效') 193 | arglist[ad['name']] = typefun[ad['type']](hmsg) 194 | else: 195 | if 'funcdealnull' in ad and ad['func'] != None and ad['funcdealnull']: 196 | hmsg = ad['func'](hmsg,ad) 197 | #处理函数返回格式 198 | #其一 None/合法值 199 | #其二 tuple对象 -> (参数是否合法,处理后的参数/错误文本) 200 | if type(hmsg) is tuple: 201 | if hmsg[0]: 202 | arglist[ad['name']] = hmsg[1] 203 | else: 204 | return (False,ad['des'],hmsg[1]) 205 | elif hmsg == None: 206 | return (False,ad['des'],'参数不符合规则(fun)') 207 | else: 208 | arglist[ad['name']] = hmsg 209 | elif ad['default'] != None: 210 | arglist[ad['name']] = ad['default'] 211 | else: 212 | return (False,ad['des'],'缺少参数') 213 | except: 214 | s = traceback.format_exc(limit=10) 215 | logger.error(s) 216 | return (False,'异常','参数提取时异常!') 217 | return (True,arglist) 218 | 219 | #酷Q插件日志预处理 220 | def CQsessionToStr(session:CommandSession): 221 | msg = 'cmd:'+session.event['raw_message']+ \ 222 | ' ;self_id:'+str(session.event['self_id']) + \ 223 | ' ;message_type:'+session.event['message_type']+ \ 224 | ' ;send_id:'+str(session.event['user_id']) if session.event['message_type']=='private' else str(session.event['group_id'])+ \ 225 | ' ;text:'+session.current_arg_text 226 | return msg 227 | #处理日志输出 228 | def msgSendToBot(reclogger:logging.Logger,message:str,*arg): 229 | for value in arg: 230 | message = message + str(value) 231 | reclogger.info(message) 232 | if bot_error_printID != None and config.error_push_switch: 233 | try: 234 | bot = nonebot.get_bot() 235 | bot.sync.send_msg( 236 | self_id=bindCQID, 237 | user_id=bot_error_printID, 238 | message=message) 239 | logger.info('向'+str(bot_error_printID)+'发送了:'+message) 240 | except ValueError: 241 | logger.warning('BOT未初始化,错误消息未发送') 242 | except: 243 | logger.error('BOT状态异常') 244 | s = traceback.format_exc(limit=10) 245 | logger.error(s) 246 | 247 | async def async_msgSendToBot(reclogger:logging.Logger,message:str,*arg): 248 | for value in arg: 249 | message = message + str(value) 250 | reclogger.info(message) 251 | if bot_error_printID != None: 252 | try: 253 | bot = nonebot.get_bot() 254 | await bot.send_msg( 255 | self_id=bindCQID, 256 | user_id=bot_error_printID, 257 | message=message) 258 | logger.info('向'+str(bot_error_printID)+'发送了:'+message) 259 | except ValueError: 260 | logger.warning('BOT未初始化,错误消息未发送') 261 | except: 262 | logger.error('BOT状态异常') 263 | s = traceback.format_exc(limit=10) 264 | logger.error(s) 265 | 266 | #数据文件操作,返回(逻辑值T/F,dict数据/错误信息) 267 | def data_read(filename:str,path:str = config_file_base_path) -> tuple: 268 | try: 269 | f = open(os.path.join(file_base_path,path,filename),mode = 'r',encoding='utf-8') 270 | data = json.load(f) 271 | fileop_logger.info('读取配置文件:'+json.dumps(data)) 272 | except IOError: 273 | logger.warning('load IOError: 未找到文件或文件不存在-'+os.path.join(file_base_path,path,filename)) 274 | return (False,'配置文件读取失败') 275 | except: 276 | logger.critical('数据文件读取解析异常') 277 | s = traceback.format_exc(limit=10) 278 | logger.critical(s) 279 | return (False,'配置文件解析异常') 280 | else: 281 | f.close() 282 | return (True,'读取成功',data) 283 | def data_save(filename:str,data,path:str = config_file_base_path) -> tuple: 284 | try: 285 | fw = open(os.path.join(file_base_path,path,filename),mode = 'w',encoding='utf-8') 286 | json.dump(data,fw,ensure_ascii=False,indent=4) 287 | except IOError: 288 | logger.error('save IOError: 未找到文件或文件不存在-'+os.path.join(file_base_path,path,filename)) 289 | pass 290 | return (False,'配置文件写入失败') 291 | except: 292 | logger.critical('数据文件写入异常') 293 | s = traceback.format_exc(limit=10) 294 | logger.error(s) 295 | return (False,'配置文件写入异常') 296 | else: 297 | fw.close() 298 | return (True,'保存成功') 299 | 300 | 301 | #临时列表 302 | class TempMemory: 303 | tm : list= None 304 | autosave : bool = None 305 | name : str = "" 306 | limit : int = 0 307 | #记录名称、记录长度(默认记录30条),默认数据,是否自动保存(默认否),是否自动读取(默认否) 308 | def __init__(self,name:str,limit:int = 30,defdata:list = [],autosave:bool = False,autoload:bool = False): 309 | check_path('templist') 310 | self.name = name 311 | self.limit = limit 312 | self.autosave = autosave 313 | if autoload: 314 | res = data_read(self.name,"templist") 315 | if res[0] == True: 316 | self.tm = res[2] 317 | if self.tm == None: 318 | self.tm = defdata.copy() 319 | def save(self): 320 | return data_save(self.name,self.tm,"templist") 321 | def join(self,data): 322 | res = None 323 | self.tm.append(data) 324 | if len(self.tm) > self.limit: 325 | res = self.tm.pop(0) 326 | if self.autosave: 327 | self.save() 328 | return res 329 | def find(self,func,val): 330 | ttm = self.tm.copy() 331 | for item in ttm: 332 | if func(item,val): 333 | return item 334 | return None 335 | #速率限制 336 | class TokenBucket(object): 337 | #作者:simpleapples 338 | #链接:https://juejin.im/post/5ab10045518825557005db65 339 | #来源:掘金 340 | #著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 341 | def __init__(self, rate, capacity, initval:int = 1): 342 | #rate是令牌发放速度(每秒发放数量),capacity是桶的大小,initval是初始大小(桶的百分比) 343 | self._rate = rate 344 | self._capacity = capacity 345 | self._current_amount = 0 346 | self._last_consume_time = 0 347 | if initval > 1 or initval < 0: 348 | raise Exception("无效的参数!") 349 | self.consume(capacity*(1-initval)) 350 | # token_amount是发送数据需要的令牌数 351 | def consume(self, token_amount): 352 | increment = (int(time.time()) - self._last_consume_time) * self._rate # 计算从上次发送到这次发送,新发放的令牌数量 353 | self._current_amount = min( 354 | increment + self._current_amount, self._capacity) # 令牌数量不能超过桶的容量 355 | if token_amount > self._current_amount: # 如果没有足够的令牌,则不能发送数据 356 | return False 357 | self._last_consume_time = int(time.time()) 358 | self._current_amount -= token_amount 359 | return True 360 | def canConsume(self, token_amount): 361 | increment = (int(time.time()) - self._last_consume_time) * self._rate # 计算从上次发送到这次发送,新发放的令牌数量 362 | self._current_amount = min( 363 | increment + self._current_amount, self._capacity) # 令牌数量不能超过桶的容量 364 | if token_amount > self._current_amount: # 如果没有足够的令牌,则不能发送数据 365 | return False 366 | return True -------------------------------------------------------------------------------- /module/RSShub_twitter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from helper import check_path 3 | from module.twitter import push_list,tweetEventDeal,tweetToStrTemplate,encode_b64,mintweetID 4 | from html.parser import HTMLParser 5 | from os import path 6 | import os 7 | import requests 8 | import xmltodict 9 | import threading 10 | import traceback 11 | import time 12 | import queue 13 | #引入配置 14 | import config 15 | #日志输出 16 | from helper import data_read,data_save,getlogger,TempMemory 17 | logger = getlogger(__name__) 18 | 19 | check_path(os.path.join('templist','RSShub','twitter')) 20 | 21 | silent_start = config.RSShub_silent_start #静默启动(启动时不推送更新) 22 | base_url = config.RSShub_base 23 | proxy = config.RSShub_proxy 24 | headers = { 25 | 'User-Agent': 'CQpy' 26 | } 27 | proxies = { 28 | "http": proxy, 29 | "https": proxy 30 | } 31 | tmemorys = {} 32 | 33 | tweetuserlist_filename = 'RSShub_tweetuserlist.json' 34 | tweetuserlist = {} 35 | res = data_read(tweetuserlist_filename) 36 | if res[0]: 37 | tweetuserlist = res[2] 38 | 39 | 40 | class MyHTMLParser(HTMLParser): 41 | def __init__(self): 42 | HTMLParser.__init__(self) 43 | self.text = "" 44 | self.media = [].copy() 45 | self.links = [].copy() 46 | def handle_starttag(self, tag, attrs): 47 | if tag == 'img': 48 | self.media.append(dict(attrs)['src']) 49 | elif tag == 'a': 50 | self.links.append(dict(attrs)['href']) 51 | 52 | def handle_endtag(self, tag): 53 | #logger.info("Encountered an end tag :" + tag) 54 | pass 55 | 56 | def handle_data(self, data): 57 | #logger.info("Encountered some data :") 58 | #logger.info(data) 59 | self.text = self.text + data 60 | 61 | 62 | dealTweetsQueue = queue.Queue(64) 63 | class twitterListener(tweetEventDeal): 64 | #事件到达 65 | def deal_event_unit(self,event,Pushunit): 66 | #事件处理单元-发送 67 | data = event['data'] 68 | #识别事件类型 69 | if event['type'] in ['retweet','quoted','reply_to_status','reply_to_user','none']: 70 | s = self.tweetToStr( 71 | data,Pushunit['nick'], 72 | push_list.getPuslunitAttr(Pushunit,'upimg')[1], 73 | push_list.getPuslunitAttr(Pushunit,event['type']+'_template')[1] 74 | ) 75 | self.send_msg(Pushunit['type'],Pushunit['pushTo'],s,Pushunit['bindCQID']) 76 | elif event['type'] in ['change_ID','change_name','change_description','change_headimgchange']: 77 | self.send_msg(Pushunit['type'],Pushunit['pushTo'],data['str'],Pushunit['bindCQID']) 78 | 79 | #将推特数据应用到模版 80 | def tweetToStr(self, tweetinfo, nick, upimg=config.pushunit_default_config['upimg'], template_text=''): 81 | if nick == '': 82 | if tweetinfo['user']['name']: 83 | nick = tweetinfo['user']['name'] 84 | else: 85 | nick = tweetinfo['user']['screen_name'] 86 | temptweetID = mintweetID.find(lambda item,val:item[0] == val,tweetinfo['id']) 87 | #模版变量初始化 88 | template_value = { 89 | 'tweet_id':tweetinfo['id_str'], #推特ID 90 | 'tweet_id_min':encode_b64(tweetinfo['id']),#压缩推特id 91 | 'tweet_id_temp':('未生成' if temptweetID == None else ('#' + str(temptweetID[1]))),#临时推特id 92 | 'tweet_nick':nick, #操作人昵称 93 | 'tweet_user_id':tweetinfo['user']['screen_name'], #操作人ID 94 | 'tweet_text':tweetinfo['text'], #发送推特的完整内容 95 | 'related_user_id':'当前模式不支持', #关联用户ID 96 | 'related_user_name':'当前模式不支持', #关联用户昵称-昵称-昵称查询不到时为ID(被评论/被转发/被提及) 97 | 'related_tweet_id':'当前模式不支持', #关联推特ID(被评论/被转发) 98 | 'related_tweet_id_min':'当前模式不支持', #关联推特ID的压缩(被评论/被转发) 99 | 'related_tweet_text':'当前模式不支持', #关联推特内容(被转发或被转发并评论时存在) 100 | 'media_img':'', #媒体 101 | } 102 | 103 | #组装图片 104 | if upimg == 1: 105 | if 'extended_entities' in tweetinfo: 106 | mis = '' 107 | for media_unit in tweetinfo['extended_entities']: 108 | #组装CQ码 109 | #file_suffix = os.path.splitext(media_unit['media_url'])[1] 110 | #s = s + '[CQ:image,timeout='+config.img_time_out+',file='+config.img_path+'tweet/' + media_unit['id_str'] + file_suffix + ']' 111 | mis = mis + '[CQ:image,timeout='+str(config.img_time_out)+',file='+ media_unit + ']' 112 | if mis != '': 113 | mis = "\n媒体:" + str(len(tweetinfo['extended_entities']))+ "个\n" + mis 114 | template_value['media_img'] = mis 115 | #生成模版类 116 | s = "" 117 | t = None 118 | if template_text == '': 119 | #默认模版 120 | if tweetinfo['type'] == 'none': 121 | deftemplate_none = "推特ID:$tweet_id_min,【$tweet_nick】发布了:\n$tweet_text\n$media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id" 122 | deftemplate_none = deftemplate_none + "\n临时推文ID:$tweet_id_temp" 123 | t = tweetToStrTemplate(deftemplate_none) 124 | elif tweetinfo['type'] == 'retweet': 125 | deftemplate_another = "推特ID:$tweet_id_min,【$tweet_nick】转了推文:\n$tweet_text\n$media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id" 126 | deftemplate_another = deftemplate_another + "\n临时推文ID:$tweet_id_temp" 127 | t = tweetToStrTemplate(deftemplate_another) 128 | elif tweetinfo['type'] == 'quoted': 129 | deftemplate_another = "推特ID:$tweet_id_min,【$tweet_nick】转发并评论了推文:\n$tweet_text\n====================\n$related_tweet_text\n$media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id" 130 | deftemplate_another = deftemplate_another + "\n临时推文ID:$tweet_id_temp" 131 | t = tweetToStrTemplate(deftemplate_another) 132 | else: 133 | deftemplate_another = "推特ID:$tweet_id_min,【$tweet_nick】回复了推文:\n$tweet_text\n$media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id" 134 | deftemplate_another = deftemplate_another + "\n临时推文ID:$tweet_id_temp" 135 | t = tweetToStrTemplate(deftemplate_another) 136 | else: 137 | #自定义模版 138 | template_text = template_text.replace("\\n","\n") 139 | t = tweetToStrTemplate(template_text) 140 | 141 | #转换为字符串 142 | s = t.safe_substitute(template_value) 143 | return s 144 | 145 | def getData(self,path:str): 146 | try: 147 | url = base_url + path 148 | r = requests.get(url,headers=headers,proxies=proxies) 149 | data = xmltodict.parse(r.text) 150 | except: 151 | s = traceback.format_exc(limit=10) 152 | logger.warning(s) 153 | return (False,'读取页面时出错') 154 | return (True,data) 155 | 156 | def updateArrives(self,data): 157 | global dealTweetsQueue 158 | s = "标识:" + data['type'] + "\n" + \ 159 | "推文ID:" + data['id_str'] + "\n" + \ 160 | "推文内容:" + data['text'] + "\n" + \ 161 | "更新时间:" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(data['created_at'])) 162 | if data['notable']: 163 | dealTweetsQueue.put(data) 164 | logger.info(s) 165 | 166 | def dealText(self,text): 167 | text = text.replace("
","\n") 168 | parser = MyHTMLParser() 169 | parser.feed(""+text+"") 170 | resText = parser.text 171 | extended_entities = parser.media 172 | return (resText,extended_entities) 173 | 174 | def dealTweet(self,tweetitem,userinfo,tmemory): 175 | val = tweetitem 176 | tweet_id = val['link'].split("/")[-1] 177 | tweetinfo = {} 178 | tweetinfo['id'] = int(tweet_id) 179 | tweetinfo['id_str'] = str(tweet_id) 180 | tweetinfo['created_at'] = int(time.mktime(time.strptime(val['pubDate'],"%a, %d %b %Y %H:%M:%S GMT"))) 181 | 182 | #RSShub模式仅能识别纯转推与回复 183 | tweetinfo['isRetweet'] = (val['author'] != userinfo['name']) 184 | tweetinfo['notable'] = not tweetinfo['isRetweet'] 185 | tweetinfo['reply_to_status'] = (val['description'][0:2] == 'Re') 186 | 187 | if tweetinfo['isRetweet']: 188 | tweetinfo['type'] = 'retweeted' 189 | elif tweetinfo['reply_to_status']: 190 | tweetinfo['type'] = 'reply_to_status' 191 | else: 192 | tweetinfo['type'] = 'none' 193 | 194 | res = self.dealText(val['description']) 195 | tweetinfo['text'] = res[0] 196 | tweetinfo['extended_entities'] = res[1] #媒体 197 | tweetinfo['link'] = val['link'] 198 | 199 | tweetinfo['user'] = {} 200 | if not tweetinfo['isRetweet']: 201 | tweetinfo['user']['id'] = userinfo['id'] 202 | tweetinfo['user']['id'] = userinfo['id_str'] 203 | tweetinfo['user']['name'] = val['author'] 204 | tweetinfo['user']['screen_name'] = val['link'].split("/")[-3] 205 | 206 | #更新推文到缓存中 207 | tmemory.join(tweetinfo) 208 | return tweetinfo 209 | 210 | def mergeTweetUser(self,ID:str): 211 | global tweetuserlist,tweetuserlist_filename 212 | if ID in tweetuserlist: 213 | t = tweetuserlist[str(ID)] 214 | else: 215 | t = int(time.time()) 216 | tweetuserlist[str(ID)] = t 217 | data_save(tweetuserlist_filename,tweetuserlist) 218 | return t 219 | def dataGetUserInfo(self,data,sid,ID): 220 | userinfo = { 221 | 'id':sid, 222 | 'id_str':str(sid), 223 | 'screen_name':ID, 224 | 'name':data['rss']['channel']['title'][:-10], 225 | 'profile_image_url':data['rss']['channel']['image']['url'], 226 | 'profile_image_url_https':'https'+data['rss']['channel']['image']['url'][4:], 227 | 'description':data['rss']['channel']['description'] 228 | } 229 | self.check_userinfo(userinfo) 230 | return userinfo 231 | def getUserInfo(self,ID): 232 | #获取数据 233 | res=self.getData('/twitter/user/'+ID) 234 | if not res[0]: 235 | return res 236 | data = res[1] 237 | sid = self.mergeTweetUser(ID) 238 | userinfo = self.dataGetUserInfo(data,sid,ID) 239 | return (True,userinfo) 240 | def dealData(self,data,ID,trigger : bool): 241 | global tmemorys 242 | sid = self.mergeTweetUser(ID) 243 | userinfo = self.dataGetUserInfo(data,sid,ID) 244 | 245 | if ID not in tmemorys: 246 | tmemorys[ID] = TempMemory(path.join('RSShub','twitter',ID+'.json'),limit=30,autoload=True,autosave=True) 247 | if tmemorys[ID].tm == []: 248 | trigger = False 249 | tmemory = tmemorys[ID] 250 | sources = data['rss']['channel']['item'] 251 | length = len(sources) - 1 252 | if length > 30: 253 | length = 30 254 | 255 | func = lambda val,fval:val['link'] == fval 256 | for i in range(length,-1,-1): 257 | val = sources[i] 258 | res = tmemory.find(func,val['link']) 259 | if res == None: 260 | tweetinfo = self.dealTweet(val,userinfo,tmemory) 261 | if trigger: 262 | self.updateArrives(tweetinfo) 263 | 264 | def findUpdata(self,ID,trigger:bool = True): 265 | #获取数据 266 | res=self.getData('/twitter/user/'+ID) 267 | if not res[0]: 268 | return res 269 | data = res[1] 270 | try: 271 | #处理数据 272 | self.dealData(data,ID,trigger) 273 | return (True,'成功') 274 | except: 275 | s = traceback.format_exc(limit=5) 276 | logger.error(s) 277 | return (False,'异常,未知错误(10564)') 278 | 279 | tweet_event_deal = twitterListener() 280 | 281 | run_info = { 282 | 'DealDataThread':None, 283 | 'queque':dealTweetsQueue, 284 | 'Thread':None, 285 | 'keepRun':True, 286 | } 287 | def setStreamOpen(b:bool): 288 | run_info['keepRun'] = b 289 | 290 | 291 | def init(): 292 | #读取推送侦听配置 293 | res = push_list.readPushList() 294 | if res[0] == True: 295 | logger.info('侦听配置读取成功') 296 | else: 297 | logger.error('侦听配置读取失败:' + res[1]) 298 | def Run(): 299 | init() 300 | #使用RSS推送源接收更新 301 | logger.info("RSS_push") 302 | spylist = push_list.spylist 303 | IDs = [] 304 | for spy in spylist: 305 | for username in tweetuserlist: 306 | if tweetuserlist[username] == int(spy): 307 | IDs.append(username) 308 | time.sleep(2 if silent_start else 10) 309 | logger.info("RSShub 启动检测正在运行") 310 | for ID in IDs: 311 | logger.info("RSShub 检测:" + ID) 312 | res = tweet_event_deal.findUpdata(ID,trigger=not silent_start) 313 | logger.info("RSShub 启动检测结束") 314 | while True: 315 | time.sleep(config.RSShub_updata_interval) 316 | if run_info['keepRun']: 317 | logger.info("RSShub 自动检测正在运行") 318 | for ID in IDs: 319 | logger.info("RSShub 检测:" + ID) 320 | res = tweet_event_deal.findUpdata(ID,queue) 321 | time.sleep(1) 322 | logger.info("RSShub 自动检测结束") 323 | #处理推特数据(独立线程) 324 | def dealTweetData(): 325 | while True: 326 | tweetinfo = run_info['queque'].get() 327 | try: 328 | #推送事件处理,输出到酷Q 329 | eventunit = tweet_event_deal.bale_event(tweetinfo['type'],tweetinfo['user']['id'],tweetinfo) 330 | tweet_event_deal.deal_event(eventunit) 331 | #控制台输出 332 | #tweet_event_deal.statusPrintToLog(tweetinfo) 333 | except: 334 | s = traceback.format_exc(limit=5) 335 | logger.warning(s) 336 | run_info['queque'].task_done() 337 | 338 | #运行推送线程 339 | def runTwitterListenerThread(): 340 | run_info['Thread'] = threading.Thread( 341 | group=None, 342 | target=Run, 343 | name='RSShub_tweetListener_thread', 344 | daemon=True 345 | ) 346 | run_info['DealDataThread'] = threading.Thread( 347 | group=None, 348 | target=dealTweetData, 349 | name='RSShub_tweetListener_DealDataThread', 350 | daemon=True 351 | ) 352 | run_info['Thread'].start() 353 | run_info['DealDataThread'].start() 354 | return run_info -------------------------------------------------------------------------------- /module/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxuan353/tweetToBot/1b930c4eb287bd2e8d9ebc5bbe0ef45fcd221a32/module/__init__.py -------------------------------------------------------------------------------- /module/machine_translation.py: -------------------------------------------------------------------------------- 1 | 2 | from helper import getlogger,CQsessionToStr,TokenBucket,TempMemory,argDeal,arglimitdeal 3 | import config 4 | import requests 5 | import random 6 | import json 7 | import traceback 8 | logger = getlogger(__name__) 9 | """ 10 | 机翻类,多引擎 11 | """ 12 | 13 | MachineTransApi = config.MachineTransApi 14 | 15 | def randUserAgent(): 16 | UAs = [ 17 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36', 19 | 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2919.83 Safari/537.36', 20 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2866.71 Safari/537.36', 21 | 'Mozilla/5.0 (X11; Ubuntu; Linux i686 on x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2820.59 Safari/537.36', 22 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0' 23 | ] 24 | return UAs[random.randint(0,len(UAs)-1)] 25 | 26 | #通用翻译语言(参数解析表) 27 | allow_st = { 28 | 'Source':arglimitdeal({ 29 | 'auto':['自动识别','自动'], 30 | 'zh':['简体中文','中文','简中','中'], 31 | #'zh-TW':['繁体中文','繁中'], 32 | 'en':['英语','英'], 33 | 'ja':['日语','日'], 34 | 'ko':['韩语','韩'], 35 | }), 36 | 'Target':arglimitdeal({ 37 | 'zh':['简体中文','中文','简中','中'], 38 | #'zh-TW':['繁体中文','繁中'], 39 | 'en':['英语','英'], 40 | 'ja':['日语','日'], 41 | 'ko':['韩语','韩'], 42 | }) 43 | } 44 | #参数解析对照表 45 | engine_nick = { 46 | 'tencent':'tencent','腾讯':'tencent', 47 | 'google':'google','谷歌翻译':'google','谷歌':'google', 48 | } 49 | #引擎设置 50 | """ 51 | name = { 52 | 'nick':'引擎昵称',#用于展示(帮助列表) 53 | "switch":MachineTransApi['tencent']['switch'],#是否启用 54 | 'bucket':TokenBucket(5,10),#速率限制的桶(一秒获取5次机会,最高存储10次使用机会) 55 | ... 56 | } 57 | """ 58 | 59 | 60 | #使用腾讯云SDK,SDK未安装时无法使用 61 | #pip install tencentcloud-sdk-python 62 | tencent = { 63 | 'nick':"腾讯", 64 | "switch":MachineTransApi['tencent']['switch'], 65 | 'bucket':TokenBucket(5,5), 66 | #地区 ap-guangzhou->广州 ap-hongkong->香港 67 | #更多详见 https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 68 | 'Region':MachineTransApi['tencent']['Region'], 69 | 'key':MachineTransApi['tencent']['key'], 70 | 'secret':MachineTransApi['tencent']['secret'], 71 | } 72 | #源文本,源文本语言,翻译到 返回值(是否成功,结果文本/错误说明,返回的源数据) 73 | def tencent_MachineTrans(SourceText:str,Source = 'auto',Target = 'zh'): 74 | if not tencent['switch']: 75 | return (False,'错误,当前引擎未启用!') 76 | if not tencent['bucket'].consume(1): 77 | return (False,'错误,速率限制!') 78 | from tencentcloud.common import credential 79 | from tencentcloud.common.profile.client_profile import ClientProfile 80 | from tencentcloud.common.profile.http_profile import HttpProfile 81 | from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException 82 | from tencentcloud.tmt.v20180321 import tmt_client, models 83 | try: 84 | cred = credential.Credential(tencent['key'], tencent['secret']) 85 | httpProfile = HttpProfile() 86 | httpProfile.endpoint = "tmt.tencentcloudapi.com" 87 | 88 | clientProfile = ClientProfile() 89 | clientProfile.httpProfile = httpProfile 90 | 91 | client = tmt_client.TmtClient(cred, tencent['Region'], clientProfile) 92 | 93 | req = models.TextTranslateRequest() 94 | 95 | req.SourceText = SourceText.replace("\n","\\n") 96 | req.Source = Source 97 | req.Target = Target 98 | req.ProjectId = 0 99 | req.UntranslatedText = "\\n" 100 | resp = client.TextTranslate(req) 101 | return (True,resp.TargetText.replace("\\n","\n"),resp) 102 | except TencentCloudSDKException as err: 103 | logger.error(err) 104 | return (False,'获取结果时错误',err) 105 | 106 | google = { 107 | 'nick':"谷歌", 108 | "switch":MachineTransApi['google']['switch'], 109 | 'bucket':TokenBucket(5,5), 110 | 'url':"http://translate.google.cn/translate_a/single?client=at&dt=t&dj=1&ie=UTF-8&sl={Source}&tl={Target}&q={SourceText}" 111 | } 112 | def google_MachineTrans(SourceText,Source = 'auto',Target = 'zh'): 113 | if not google['switch']: 114 | return (False,'错误,当前引擎未启用!') 115 | if not google['bucket'].consume(1): 116 | return (False,'错误,速率限制!') 117 | headers = { 118 | 'User-Agent':randUserAgent() 119 | } 120 | try: 121 | requrl = google['url'].format(SourceText=SourceText,Source=Source,Target=Target) 122 | r = requests.get(requrl,headers=headers) 123 | res = json.loads(r.text) 124 | except: 125 | s = traceback.format_exc(limit=10) 126 | logger.error(s) 127 | try: 128 | logger.error(r.text) 129 | except: 130 | pass 131 | return (False,'连接服务时异常') 132 | try: 133 | msg = '' 134 | for t in res['sentences']: 135 | msg = msg + t['trans'] 136 | except: 137 | logger.error(res) 138 | return (False,'获取结果时异常') 139 | return (True,msg) 140 | 141 | 142 | 143 | engine_list = { 144 | 'tencent':{ 145 | 'func':tencent_MachineTrans, 146 | 'option':tencent 147 | }, 148 | 'google':{ 149 | 'func':google_MachineTrans, 150 | 'option':google 151 | }, 152 | } 153 | 154 | default_engine = engine_list[engine_nick[config.MachineTrans_default]]['func'] 155 | default_engine_option = engine_list[engine_nick[config.MachineTrans_default]]['option'] 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /module/permissiongroup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from helper import data_save,data_read,getlogger 3 | import os 4 | import traceback 5 | import re 6 | logger = getlogger(__name__) 7 | config_filename = 'permission.json' 8 | allow_msg_type = ('private','group') 9 | permissionList = { 10 | 'private':{}, 11 | 'group':{} 12 | } 13 | legalPermissionList = {} #权限列表 14 | res = data_read(config_filename) 15 | if res[0]: 16 | permissionList = res[2] 17 | for k1 in permissionList: 18 | for k2 in list(permissionList[k1].keys()): 19 | permissionList[k1][int(k2)] = permissionList[k1][k2] 20 | del permissionList[k1][k2] 21 | #权限名称是否合法 22 | def perm_isLegalPerm(perm_group:str,perm_unit:str = None): 23 | #权限组与权限名,以字母开头,只能包含字母、数字与下划线 24 | res = re.match(r'(([A-Za-z]{1}[A-Za-z0-9_]*)|\*)',perm_group) 25 | if res == None: 26 | #权限组验证不通过 27 | return False 28 | if perm_unit != None: 29 | #以减号开头表示权限拒绝(优先于*号,-*的权限将拒绝所有) 30 | res = re.match(r'-?(([A-Za-z]{1}[A-Za-z0-9_]*)|\*)',perm_unit) 31 | if res == None: 32 | return False 33 | return True 34 | return True 35 | #获取权限组 36 | def perm_getGroup(perm_group:str): 37 | global legalPermissionList 38 | if perm_group not in legalPermissionList: 39 | return None 40 | return legalPermissionList[perm_group] 41 | #权限/权限组是否存在 42 | def perm_hasPermUnit(perm_group:str,perm_unit:str = None): 43 | global legalPermissionList 44 | res = perm_getGroup(perm_group) 45 | if perm_unit == None: 46 | return res != None 47 | if perm_unit == '*' or perm_unit == '-*': 48 | return True 49 | return (perm_unit in legalPermissionList[perm_group]['perms']) or (('-'+perm_unit) in legalPermissionList[perm_group]['perms']) 50 | #添加权限组(权限所在模块名,权限描述,权限组名) 51 | def perm_addLegalPermGroup(name:str,des:str,perm_group:str): 52 | global legalPermissionList 53 | if perm_group in legalPermissionList: 54 | return 55 | legalPermissionList[perm_group] = { 56 | 'name':name, 57 | 'des':des, 58 | 'perms':[] 59 | } 60 | #添加权限(权限组名,权限名) 61 | def perm_addLegalPermUnit(perm_group:str,perm_unit:str): 62 | global legalPermissionList 63 | if perm_group not in legalPermissionList: 64 | return 65 | legalPermissionList[perm_group]['perms'].append(perm_unit) 66 | #权限组是否存在 67 | def hasPermGroup(msg_type:str,sid:int,perm_group:str): 68 | global permissionList 69 | if msg_type not in allow_msg_type: 70 | return False 71 | if sid not in permissionList[msg_type]: 72 | return False 73 | if perm_group not in permissionList[msg_type][sid]: 74 | return False 75 | return True 76 | #授权(授权类型,授权对象,授权操作人,权限组,权限名) 77 | def perm_add(msg_type:str,sid:int,opid:int,perm_group:str,perm_unit:str = None) -> tuple: 78 | global permissionList 79 | if not perm_hasPermUnit(perm_group,perm_unit): 80 | return (False,'权限或权限组不存在') 81 | if msg_type not in allow_msg_type: 82 | return (False,'不支持的消息类型') 83 | if sid not in permissionList[msg_type]: 84 | permissionList[msg_type][sid] = {} 85 | if perm_group not in permissionList[msg_type][sid]: 86 | permissionList[msg_type][sid][perm_group] = {} 87 | if perm_unit != None: 88 | permissionList[msg_type][sid][perm_group][perm_unit] = { 89 | 'perm_unit':perm_unit, 90 | 'msg_type':msg_type, 91 | 'sid':sid, 92 | 'opid':opid, 93 | 'perm_group':perm_group 94 | } 95 | res = data_save(config_filename,permissionList) 96 | if not res[0]: 97 | return (False,'授权保存失败,请联系管理者') 98 | return (True,'授权设置成功') 99 | #取消授权 100 | def perm_del(msg_type:str,sid:int,opid:int,perm_group:str,perm_unit:str = None) -> tuple: 101 | global permissionList 102 | if not hasPermGroup(msg_type,sid,perm_group): 103 | #权限组不存在 104 | return (False,'权限组不存在') 105 | if perm_unit != None: 106 | if perm_unit in permissionList[msg_type][sid][perm_group]: 107 | del permissionList[msg_type][sid][perm_group][perm_unit] 108 | else: 109 | del permissionList[msg_type][sid][perm_group] 110 | res = data_save(config_filename,permissionList) 111 | if not res[0]: 112 | return (False,'授权保存失败,请联系管理者') 113 | return (True,'移除授权成功') 114 | 115 | #权限检查,通过为True 116 | def perm_check(msg_type:str,sid:int,perm_group:str,perm_unit:str = None) -> bool: 117 | global permissionList 118 | if not hasPermGroup(msg_type,sid,perm_group): 119 | #权限组不存在 120 | return False 121 | if perm_unit != None: 122 | if '-' + perm_unit in permissionList[msg_type][sid][perm_group]: 123 | return False 124 | if '*' in permissionList[msg_type][sid][perm_group]: 125 | return True 126 | if perm_unit in permissionList[msg_type][sid][perm_group]: 127 | return True 128 | else: 129 | return False 130 | return True 131 | #获取权限组授权列表 132 | def perm_getPermList(msg_type:str,sid:int,perm_group:str) -> tuple: 133 | global permissionList 134 | if not hasPermGroup(msg_type,sid,perm_group): 135 | #权限组不存在 136 | return (False,'权限组不存在') 137 | permgroup = { 138 | 'permlist':permissionList[msg_type][sid][perm_group], 139 | 'info':perm_getGroup(perm_group) 140 | } 141 | return (True,'获取成功',permgroup) 142 | #获取权限组列表 143 | def perm_getPermGroupList(msg_type:str,sid:int) -> list: 144 | global permissionList 145 | res = [] 146 | if sid not in permissionList[msg_type]: 147 | return (True,"成功",[]) 148 | group = permissionList[msg_type][sid] 149 | for key,val in group.items(): 150 | permgroup = { 151 | 'groupname':key, 152 | 'permlist':val, 153 | 'info':perm_getGroup(key) 154 | } 155 | res.append(permgroup) 156 | return (True,"成功",res) 157 | 158 | -------------------------------------------------------------------------------- /module/pollingTwitterApi.py: -------------------------------------------------------------------------------- 1 | from module.twitterApi import tweet_event_deal,dealTweetsQueue,push_list 2 | from helper import getlogger,CQsessionToStr,TokenBucket,msgSendToBot,TempMemory 3 | import tweepy 4 | import config 5 | import threading 6 | import time 7 | import traceback 8 | import random 9 | logger = getlogger(__name__) 10 | """ 11 | 推特API的轮询模式 12 | 仅使用应用程序验证 13 | """ 14 | polling_silent_start = not config.polling_silent_start 15 | polling_interval = int(config.polling_interval) 16 | #引入测试方法 17 | try: 18 | #on_status 19 | import dbtest as test 20 | except: 21 | test = None 22 | 23 | #应用程序匿名访问 24 | class TwitterAppApiPackage: 25 | def __init__(self,consumer_key:str,consumer_secret:str): 26 | #应用程序限制窗口 27 | self.apibucket = { 28 | 'users_timeline':TokenBucket(1.5,1500,0.5),#用户时间线 29 | 'users_show':TokenBucket(0.9,900,0.5),#用户检索 30 | 'users_lookup':TokenBucket(0.3,300,0.5),#多用户检索 31 | #'statuses_show':TokenBucket(0.45,450,0.5),#单推文检索 32 | 'statuses_lookup':TokenBucket(0.3,300,0.5),#多推文检索 33 | } 34 | self.auth = tweepy.OAuthHandler(consumer_key, consumer_secret) 35 | self.api = tweepy.API(self.auth, proxy=config.api_proxy) 36 | def users_timeline(self,autoid = None,user_id = None,screen_name = None,since_id = None): 37 | if not self.apibucket['users_timeline'].consume(1): 38 | return (False,'速率限制!') 39 | try: 40 | if autoid: 41 | tweets = self.api.user_timeline(id = autoid,since_id = since_id,tweet_mode = 'extended') 42 | elif user_id: 43 | tweets = self.api.user_timeline(user_id = user_id,tweet_mode = 'extended') 44 | elif screen_name: 45 | tweets = self.api.user_timeline(screen_name = screen_name,tweet_mode = 'extended') 46 | else: 47 | return (False,'参数错误') 48 | except tweepy.error.TweepError as e: 49 | logger.warning(e.api_code) 50 | return (False,'tweepy错误!') 51 | except: 52 | s = traceback.format_exc(limit=10) 53 | logger.warning(s) 54 | return (False,'tweepy错误!') 55 | return (True,tweets) 56 | def users_show(self,autoid = None,user_id = None,screen_name = None,since_id = None): 57 | if not self.apibucket['users_show'].consume(1): 58 | return (False,'速率限制!') 59 | try: 60 | if autoid: 61 | user = self.api.get_user(id = autoid,since_id = since_id) 62 | elif user_id: 63 | user = self.api.get_user(user_id = user_id) 64 | elif screen_name: 65 | user = self.api.get_user(screen_name = screen_name) 66 | else: 67 | return (False,'参数错误') 68 | except tweepy.error.TweepError as e: 69 | logger.warning(e) 70 | return (False,'tweepy错误!',e.response.status_code) 71 | except: 72 | s = traceback.format_exc(limit=10) 73 | logger.warning(s) 74 | return (False,'tweepy错误!') 75 | return (True,user) 76 | def statuses_lookup(self,id = None,ids:list = None): 77 | if not self.apibucket['statuses_lookup'].consume(1): 78 | return (False,'速率限制!') 79 | try: 80 | if id: 81 | tweets = self.api.statuses_lookup([id],tweet_mode = 'extended') 82 | elif ids: 83 | tweets = self.api.statuses_lookup(ids,tweet_mode = 'extended') 84 | else: 85 | return (False,'参数错误') 86 | except tweepy.error.TweepError as e: 87 | logger.warning(e) 88 | return (False,'tweepy错误!',e.response.status_code) 89 | except: 90 | s = traceback.format_exc(limit=10) 91 | logger.warning(s) 92 | return (False,'程序异常') 93 | return (True,tweets) 94 | 95 | 96 | class PollingTwitterApps: 97 | allowFunname = { 98 | 'users_timeline':1.5,#用户时间线 99 | 'users_show':0.9,#用户检索 100 | 'users_lookup':0.3,#多用户检索 101 | #'statuses_show':0.45,#单推文检索 102 | 'statuses_lookup':0.3,#多推文检索 103 | } 104 | def __init__(self,consumers:list): 105 | self.consumers = consumers.copy() 106 | self.apps = [] #应用列表 107 | self.lasti = 0 108 | for consumer in self.consumers: 109 | self.apps.append( 110 | TwitterAppApiPackage(consumer[0],consumer[1]) 111 | ) 112 | #获取可用的应用密钥,没有可用的密钥时返回None 113 | def getAllow(self,funname:str) -> TwitterAppApiPackage: 114 | if funname not in self.allowFunname: 115 | raise Exception('不被允许的方法') 116 | appl = len(self.apps) 117 | for i in range(0,appl): 118 | app = self.apps[(self.lasti + i)%appl] 119 | if app.apibucket[funname].canConsume(1): 120 | self.lasti = (self.lasti + i)%appl + 1 121 | return app 122 | return None 123 | def hasApp(self) -> bool: 124 | return self.apps != [] 125 | 126 | ptwitterapps = PollingTwitterApps(config.polling_consumers) 127 | 128 | run_info = { 129 | 'DealDataThread':None, 130 | 'queque':dealTweetsQueue, 131 | 'Thread':None, 132 | 'isRun':True, 133 | 'keepRun':True, 134 | 'lasterror':int(time.time()), 135 | 'errorlist':TempMemory('pollingTwitterApi_apierror.json',limit=30,autoload=True,autosave=True), 136 | 'error':0, 137 | } 138 | def setStreamOpen(b:bool): 139 | run_info['keepRun'] = b 140 | 141 | 142 | def init(): 143 | if not ptwitterapps.hasApp(): 144 | raise Exception("错误,APP密钥对未配置,轮询监测启动失败") 145 | #读取推送侦听配置 146 | res = push_list.readPushList() 147 | if res[0] == True: 148 | logger.info('侦听配置读取成功') 149 | else: 150 | logger.error('侦听配置读取失败:' + res[1]) 151 | 152 | def on_status(status): 153 | try: 154 | #重新组织推特数据 155 | tweetinfo = tweet_event_deal.deal_tweet(status) 156 | #只有值得关注的推文才会推送处理,降低处理压力(能降一大截……) 157 | if tweetinfo['tweetNotable']: 158 | try: 159 | dealTweetsQueue.put(tweetinfo,timeout=15) 160 | if tweetinfo['user']['id'] != tweetinfo['Related_user']['id'] and \ 161 | tweetinfo['Related_notable'] and \ 162 | tweetinfo['Related_user']['id_str'] in push_list.spylist: 163 | #使被动推送能够不完整运行 164 | rtweetinfo = tweetinfo.copy() 165 | rtweetinfo['trigger_user'] = tweetinfo['Related_user']['id'] 166 | rtweetinfo['trigger_remote'] = True #监测重定向标识 167 | dealTweetsQueue.put(rtweetinfo,timeout=15) 168 | except: 169 | s = traceback.format_exc(limit=5) 170 | msgSendToBot(logger,'推特监听处理队列溢出,请检查队列!') 171 | logger.error(s) 172 | if test != None: 173 | test.on_status(tweetinfo,status) 174 | except: 175 | s = traceback.format_exc(limit=5) 176 | logger.error(s) 177 | 178 | def get_updata(trigger : bool = True): 179 | spylist = push_list.spylist 180 | interval = polling_interval 181 | for spy in spylist: 182 | app = ptwitterapps.getAllow('users_timeline') 183 | while app == None: 184 | logger.warning('触发轮询上限,建议增加轮询间隔') 185 | app = ptwitterapps.getAllow('users_timeline') 186 | time.sleep(1) 187 | res = app.users_timeline(user_id=int(spy)) 188 | if not res[0]: 189 | logger.error("错误,未搜索到"+str(spy)+"的时间线数据") 190 | run_info['errorlist'].join(res) 191 | if int(time.time()) - run_info['lasterror'] > 300: 192 | run_info['lasterror'] = int(time.time()) 193 | run_info['error'] = 0 194 | run_info['error'] = run_info['error'] + 1 195 | if run_info['error'] > 5: 196 | #短时间错误次数过高 197 | msgSendToBot(logger,"错误,监测服务异常,请检测后手动启动") 198 | run_info['keepRun'] = False 199 | run_info['isRun'] = False 200 | break 201 | continue 202 | statuss = res[1] 203 | if not tweet_event_deal.hasUserTSInCache(spy): 204 | #初次监测不推送 205 | logger.info("初次检测:"+spy) 206 | trigger = False 207 | for i in range(len(statuss)-1,-1,-1): 208 | res = tweet_event_deal.tryGetTweet(statuss[i].id,user_id=spy) 209 | if not res: 210 | if trigger: 211 | on_status(statuss[i]) 212 | else: 213 | #组织推特数据 214 | tweetinfo = tweet_event_deal.deal_tweet(statuss[i],trigger = trigger) 215 | #缓存处理 216 | tweet_event_deal.bale_event(tweetinfo['type'],tweetinfo['trigger_user'],tweetinfo) 217 | itv = 0.5 + round(random.uniform(0,interval),2) 218 | interval = interval - itv 219 | interval = max(0.5,interval) 220 | time.sleep(itv) 221 | time.sleep(interval) 222 | 223 | 224 | def Run(): 225 | global polling_interval 226 | init() 227 | #使用PollingTweetApi接收更新 228 | logger.info("PollingTweetApi") 229 | time.sleep(5) 230 | logger.info("PollingTweetApi 启动检测正在运行") 231 | itv = polling_interval 232 | polling_interval = 1 233 | get_updata(trigger = polling_silent_start) 234 | polling_interval = itv 235 | logger.info("PollingTweetApi 启动检测结束") 236 | time.sleep(polling_interval) 237 | while True: 238 | if run_info['keepRun']: 239 | run_info['isRun'] = True 240 | logger.info("PollingTweetApi 自动检测") 241 | get_updata() 242 | else: 243 | run_info['isRun'] = False 244 | 245 | 246 | 247 | 248 | #处理推特数据(独立线程) 249 | def dealTweetData(): 250 | while True: 251 | tweetinfo = run_info['queque'].get() 252 | try: 253 | #推送事件处理,输出到酷Q 254 | eventunit = tweet_event_deal.bale_event(tweetinfo['type'],tweetinfo['trigger_user'],tweetinfo) 255 | tweet_event_deal.deal_event(eventunit) 256 | #控制台输出 257 | tweet_event_deal.statusPrintToLog(tweetinfo) 258 | except: 259 | s = traceback.format_exc(limit=5) 260 | logger.warning(s) 261 | run_info['queque'].task_done() 262 | 263 | #运行推送线程 264 | def runPollingTwitterApiThread(): 265 | run_info['Thread'] = threading.Thread( 266 | group=None, 267 | target=Run, 268 | name='Polling_tweetListener_thread', 269 | daemon=True 270 | ) 271 | run_info['DealDataThread'] = threading.Thread( 272 | group=None, 273 | target=dealTweetData, 274 | name='Polling_tweetListener_DealDataThread', 275 | daemon=True 276 | ) 277 | run_info['Thread'].start() 278 | run_info['DealDataThread'].start() 279 | return run_info -------------------------------------------------------------------------------- /module/tweettrans.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import os 3 | import traceback 4 | import time 5 | import json 6 | import random 7 | 8 | from helper import check_path,TokenBucket 9 | from selenium import webdriver 10 | from selenium.webdriver.chrome.webdriver import Options 11 | from selenium.webdriver.support.wait import WebDriverWait 12 | from selenium.webdriver.support import expected_conditions as EC 13 | from selenium.webdriver.common.by import By 14 | from helper import getlogger,data_save,data_read 15 | logger = getlogger(__name__) 16 | check_path(os.path.join('transtweet','error')) 17 | check_path(os.path.join('transtweet','unknownimg')) 18 | check_path(os.path.join('transtweet','tweetimg')) 19 | check_path(os.path.join('transtweet','tweetsimg')) 20 | check_path(os.path.join('transtweet','transimg')) 21 | 22 | def randUserAgent(): 23 | UAs = [ 24 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', 25 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36', 26 | 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2919.83 Safari/537.36', 27 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2866.71 Safari/537.36', 28 | 'Mozilla/5.0 (X11; Ubuntu; Linux i686 on x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2820.59 Safari/537.36', 29 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0' 30 | ] 31 | return UAs[random.randint(0,len(UAs)-1)] 32 | rate_limit_bucket = TokenBucket(0.1,5) 33 | class TweetTrans: 34 | driver : webdriver.Chrome = None 35 | def __init__(self): 36 | chrome_options = Options() 37 | chrome_options.add_argument("--no-sandbox") 38 | chrome_options.add_argument("--disable-dev-shm-usage") 39 | chrome_options.add_argument('--headless') 40 | chrome_options.add_argument('--disable-gpu') 41 | chrome_options.add_argument('--lang=zh_CN') 42 | #chrome_options.add_argument('lang=zh_CN.UTF-8') 43 | #chrome_options.add_argument('accept-language=zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6') 44 | #chrome_options.add_extension('chrome-modheader.crx') 45 | #chrome_options.add_argument('--disk-cache-dir='+os.path.join('.','cache','chromecache')) 46 | chrome_options.add_argument("user-agent="+randUserAgent()) 47 | self.driver = webdriver.Chrome(options=chrome_options) 48 | def __del__(self): 49 | self.driver.close() 50 | self.driver.quit() 51 | #打开页面 52 | def get(self,url:str): 53 | driver = self.driver 54 | driver.get(url) 55 | driver.maximize_window() 56 | scroll_width = driver.execute_script('return document.body.parentNode.scrollWidth') 57 | driver.set_window_size(scroll_width, 4000) 58 | return scroll_width 59 | #等待推文加载(参数:任务标识) 60 | def waitForTweetLoad(self,tasktype:str): 61 | driver = self.driver 62 | main_type = "section[aria-labelledby].css-1dbjc4n>div>div" 63 | load_type = "div[role='progressbar']" 64 | error_save_filename = os.path.join('cache','transtweet','error','waitForTweetLoad-'+tasktype+'.png') 65 | JS_get_errormsg = """ 66 | elem = document.querySelector('[data-testid="error-detail"]') 67 | if(elem)return elem.innerText 68 | return "未查找到错误信息,可能是网络波动造成的" 69 | """ 70 | #等待主元素出现 71 | try: 72 | WebDriverWait(driver, 60, 0.5).until( 73 | EC.presence_of_element_located((By.CSS_SELECTOR, main_type)) 74 | ) 75 | except: 76 | driver.save_screenshot(error_save_filename) 77 | s = traceback.format_exc(limit=10) 78 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 79 | rmsg = driver.execute_script(JS_get_errormsg) 80 | return (False,error_save_filename,rmsg) 81 | #等待加载结束 82 | try: 83 | WebDriverWait(driver, 60, 0.5).until_not( 84 | EC.presence_of_element_located((By.CSS_SELECTOR, load_type)) 85 | ) 86 | except: 87 | driver.save_screenshot(error_save_filename) 88 | s = traceback.format_exc(limit=10) 89 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 90 | rmsg = driver.execute_script(JS_get_errormsg) 91 | return (False,error_save_filename,rmsg) 92 | return (True,'success!') 93 | #二次加载(加载更多推文) 94 | def waitForTweetLoad2(self,tasktype:str,limit_scroll_height:int = 4000): 95 | driver = self.driver 96 | load_type = "div[role='progressbar']" 97 | error_save_filename = os.path.join('cache','transtweet','error','waitForTweetLoad2-'+tasktype+'.png') 98 | JS_get_errormsg = """ 99 | elem = document.querySelector('[data-testid="error-detail"]') 100 | if(elem)return elem.innerText 101 | return "未查找到错误信息" 102 | """ 103 | scroll_height = driver.execute_script('return document.body.parentNode.scrollHeight') 104 | if scroll_height > limit_scroll_height: 105 | scroll_height = limit_scroll_height 106 | driver.execute_script("window.scrollTo(0,"+str(scroll_height)+")") 107 | try: 108 | WebDriverWait(driver, 60, 0.5).until_not( 109 | EC.presence_of_element_located((By.CSS_SELECTOR, load_type)) 110 | ) 111 | except: 112 | driver.save_screenshot(error_save_filename) 113 | s = traceback.format_exc(limit=10) 114 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 115 | rmsg = driver.execute_script(JS_get_errormsg) 116 | return (False,error_save_filename,rmsg) 117 | return (True,'success!') 118 | #三次加载(等待图片) 119 | def waitForTweetLoad3(self,tasktype:str,limit_scroll_height:int = 4000): 120 | driver = self.driver 121 | JS_imgIsLoad = r""" 122 | var mainelem = document.querySelector('section[aria-labelledby].css-1dbjc4n') 123 | try{ 124 | let elems = mainelem.querySelectorAll('img') 125 | for (var i = 0;idiv') 212 | let uid = elart.querySelector('div.r-18kxxzh>div.r-18kxxzh') 213 | //头像 214 | let headimg = uid.querySelector('img').getAttribute('src') 215 | 216 | //用户ID 217 | //let userid = uie.nextSibling.innerText 218 | let userid = uid.querySelector('a').getAttribute('href').slice(1) 219 | //昵称 220 | //uie = uie.nextSibling.querySelector('a>div>div') 221 | //let nick = uie.innerText 222 | let nick = elart.querySelector('div.r-vw2c0b').innerText 223 | 224 | //推文内容表 225 | let elemtexts = elart.querySelectorAll('div.r-bnwqim') 226 | let tweettexts = [] 227 | let tweettext = "" 228 | for(var j = 0;j limitHeight)break; 263 | } catch (e) { 264 | //记录错误 265 | tweets.push({ 266 | code:1, 267 | elem:elems[i], 268 | exp:e.message 269 | }) 270 | } 271 | } 272 | } 273 | return tweets 274 | }catch (e) { 275 | return e.message 276 | } 277 | """ 278 | try: 279 | res = driver.execute_script(JS_getTweets,limitHeight) 280 | return (True,res) 281 | except: 282 | driver.save_screenshot(error_save_filename) 283 | s = traceback.format_exc(limit=10) 284 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 285 | return (False,error_save_filename,'获取推文列表异常') 286 | #保存推文主显示元素到图片 287 | def saveMainElemToImg(self,filename:str,op:int=0): 288 | driver = self.driver 289 | filepath = os.path.join('cache','transtweet',('unknownimg','tweetimg','tweetsimg')[op],filename+'.png') 290 | elem = driver.find_element_by_css_selector('section[aria-labelledby].css-1dbjc4n') 291 | file = open(filepath,'wb') 292 | file.write(elem.screenshot_as_png) 293 | file.close() 294 | #保存图片到文件(主要用于保存单一推文) 295 | def savePngToFile(self,data,filename:str,path=os.path.join('cache','transtweet','tweetimg')): 296 | file = open(os.path.join('cache',path,filename+'.png'),'wb') 297 | file.write(data) 298 | file.close() 299 | #获取推文数据 300 | def getTweetsData(self,tasktype:str,scroll_width,op:int=0): 301 | driver = self.driver 302 | tres = self.waitForTweetLoad(tasktype) 303 | if not tres[0]: 304 | return tres 305 | tres = self.waitForTweetLoad2(tasktype) 306 | if not tres[0]: 307 | return tres 308 | tres = self.tweetEndInit(tasktype) 309 | if not tres[0]: 310 | return tres 311 | tres = self.getStartHeight(tasktype) 312 | if not tres[0]: 313 | return tres 314 | startheight = tres[1] 315 | tres = self.getTweets(tasktype) 316 | if not tres[0]: 317 | return tres 318 | tweets = tres[1] 319 | scroll_height = int(tweets[len(tweets)-1]['elemy']) + int(tweets[len(tweets)-1]['elemh']) + int(startheight) + 300 320 | driver.set_window_size(scroll_width, scroll_height) 321 | self.saveMainElemToImg(tasktype,op) 322 | #额外处理 323 | self.saveTweetsToJson(tweets,tasktype) 324 | self.saveTweetsToImg(tweets,tasktype) 325 | self.web_screenshot(tasktype) 326 | return (True,tweets) 327 | #获取时间线推文列表 328 | def getTimeLine(self,tweet_user_sname:str,tasktype:str = 'def'): 329 | scroll_width = self.get('https://twitter.com/'+tweet_user_sname+'/with_replies') 330 | return self.getTweetsData(tasktype,scroll_width,2) 331 | #获取指定推文ID的推文列表 332 | def getTweetID(self,TweetID:str,tweet_user_sname:str='s',tasktype:str = 'def'): 333 | scroll_width = self.get('https://twitter.com/'+tweet_user_sname+'/status/'+TweetID) 334 | return self.getTweetsData(tasktype,scroll_width,1) 335 | #执行推文搜索,并置入数据 336 | def getSingelTweet(self,trans:dict = {},tasktype:str = 'def'): 337 | driver = self.driver 338 | error_save_filename = os.path.join('cache','transtweet','error','getSingelTweet-'+tasktype+'.png') 339 | JS = r""" 340 | /*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ 341 | var twemoji=function(){"use strict";var twemoji={base:"https://twemoji.maxcdn.com/v/13.0.0/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt,false),this)}},parse:parse,replace:replace,test:test},escaper={"&":"&","<":"<",">":">","'":"'",'"':"""},re=/(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udeeb\udeec\udef4-\udefc\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78\udd7a-\uddb4\uddb7\uddba\uddbc-\uddcb\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7a\ude80-\ude86\ude90-\udea8\udeb0-\udeb6\udec0-\udec2\uded0-\uded6]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g,UFE0Fg=/\uFE0F/g,U200D=String.fromCharCode(8205),rescaper=/[&<>'"]/g,shouldntBeParsed=/^(?:iframe|noframes|noscript|script|select|style|textarea)$/,fromCharCode=String.fromCharCode;return twemoji;function createText(text,clean){return document.createTextNode(clean?text.replace(UFE0Fg,""):text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return"".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode)}else if(nodeType===1&&!("ownerSVGElement"in subnode)&&!shouldntBeParsed.test(subnode.nodeName.toLowerCase())){grabAllTextNodes(subnode,allText)}}return allText}function grabTheRightIcon(rawText){return toCodePoint(rawText.indexOf(U200D)<0?rawText.replace(UFE0Fg,""):rawText)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,rawText,iconId,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index),true))}rawText=match[0];iconId=grabTheRightIcon(rawText);i=index+rawText.length;src=options.callback(iconId,options);if(iconId&&src){img=new Image;img.onerror=options.onerror;img.setAttribute("draggable","false");attrib=options.attributes(rawText,iconId);for(attrname in attrib){if(attrib.hasOwnProperty(attrname)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname])}}img.className=options.className;img.alt=rawText;img.src=src;modified=true;fragment.appendChild(img)}if(!img)fragment.appendChild(createText(rawText,false));img=null}if(modified){if(i")}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how}}return(typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(i$2') 356 | text = text.replace(/((https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|])/g,'$1') 357 | return twemoji.parse(text,{ 358 | attributes:attributesCallback, 359 | base:'https://abs-0.twimg.com/emoji/v2/', 360 | folder: 'svg', 361 | ext: '.svg' 362 | }); 363 | } 364 | var shotelem = document.createElement('div') 365 | shotelem.id = 'shot_elem' 366 | //可翻译推文的列表 367 | var tweets = [] 368 | //推文主元素 369 | var mainelem = document.querySelector('section[aria-labelledby].css-1dbjc4n') 370 | if(!mainelem){ 371 | return [false,"推文不存在"] 372 | } 373 | let elems = mainelem.querySelectorAll('article') 374 | var lastelem = null 375 | if(elems.length == 0){ 376 | return [false,"未发现推文,请重试"] 377 | } 378 | //搜索可翻译元素 379 | for (var i = 0;1==1;i++) { 380 | //发现元素不存在时 381 | if(typeof(elems[i])== 'undefined'){ 382 | return [false,"未搜索到推文结束位置,请联系制作者反馈!"] 383 | } 384 | //搜索推文 385 | let elart = elems[i] 386 | if(elart){ 387 | elart = elart.cloneNode(true) 388 | shotelem.append(elart); 389 | let trans = [] 390 | let elemtexts = elart.querySelectorAll('div.r-bnwqim') 391 | for(var j = 0;jspan').className 415 | let transclass = 'tweetadd css-901oao css-16my406 r-1qd0xha r-ad9z0x r-bcqeeo r-qvutc0' 416 | let frontf = "font-family:\"Source Han Sans CN\", system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, \"Helvetica Neue\", sans-serif;" 417 | //翻译标识 418 | let node_type = document.createElement('span') 419 | node_type.className = transclass 420 | node_type.innerHTML = translist.type_html //同步设置 421 | //置入翻译(遍历推文) 422 | for (let i = 0;i < tweets.length;i++) { 423 | //此次推文对应的翻译组(有可能不存在) 424 | let tran_text = null 425 | if(typeof(translist.text[(i+1)])!= 'undefined'){ 426 | tran_text = translist.text[i+1] 427 | } 428 | //处理推文主节点,次要节点一般为转发的推文 429 | //最末的推文(需要置入翻译标识) 430 | if(i == (tweets.length-1) && tweets[i][0]){ 431 | tweets[i][0].elem.append(node_type)//添加推文标识 432 | if(!tran_text){ 433 | tran_text = translist.text['main'] //翻译节点不存在时切换翻译为主翻译 434 | } 435 | }else{ 436 | //非末推文存在翻译时清空节点 437 | if(tran_text && tweets[i][0]){ 438 | tweets[i][0].elem.innerHTML = "" //存在翻译则清空节点 439 | } 440 | } 441 | if(tran_text){ 442 | if(tran_text[0] && tweets[i][0]){ 443 | let node_trans = document.createElement('div');//翻译节点 444 | //注 入 样 式 445 | node_trans.className = transclass 446 | node_trans.setAttribute('style',frontf) 447 | //置入推文主节点的翻译 448 | node_trans.innerHTML = textparse(tran_text[0],transclass) 449 | tweets[i][0].elem.appendChild(node_trans) 450 | 451 | } 452 | //次节点的翻译与次节点同时存在时 453 | if(tran_text[1] && tweets[i][1]){ 454 | let node_trans = document.createElement('div');//翻译节点 455 | //注 入 样 式 456 | node_trans.className = transclass 457 | node_trans.setAttribute('style',frontf) 458 | //清空次节点 459 | tweets[i][1].elem.innerHTML = "" 460 | //置入推文次节点的翻译 461 | node_trans.innerHTML = textparse(tran_text[1],transclass) 462 | tweets[i][1].elem.appendChild(node_trans) 463 | } 464 | } 465 | } 466 | //锁定推文高度以便截取元素 467 | //overflow:hidden;min-height:xxpx; 468 | mainelem.innerHTML = "" 469 | mainelem.append(shotelem) 470 | } catch (e) { 471 | //返回错误 472 | console.log(tweets) 473 | console.log(e) 474 | return [false,"推文分析出现异常,请联系作者"] 475 | } 476 | return [true,tweets,lastelem] 477 | """ 478 | res = None 479 | try: 480 | res = driver.execute_script(JS,trans) 481 | except: 482 | driver.save_screenshot(error_save_filename) 483 | s = traceback.format_exc(limit=10) 484 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 485 | return (False,error_save_filename,'页面调度异常') 486 | if res == None: 487 | res = (False,"推文分析未返回数据") 488 | if res[0] == False: 489 | driver.save_screenshot(error_save_filename) 490 | logger.warning("推文分析未返回数据,filename="+error_save_filename + ';url=' + driver.current_url) 491 | return (False,error_save_filename,res[1]) 492 | self.waitForTweetLoad3(tasktype) 493 | return (True,res) 494 | #对指定推文置入翻译并获取截图 495 | def getTransFromTweetID(self,TweetID:str,trans:list,tweet_user_sname:str='s',tasktype:str = 'def'): 496 | """ 497 | trans={ 498 | 'type_html':翻译标识html, 499 | 'text':{ 500 | '1':["测试翻译","次节点翻译"],//推文只处理前两个值(主节点,次节点-转发的推文) 501 | '2':["二层翻译","二层次节点"], 502 | 'main':["主翻译","主翻译节点"],//末端的主推文,置入参数前处理(先搜索对应下标再搜索main-main用于无下标置入) 503 | } 504 | } 505 | """ 506 | scroll_width = self.get('https://twitter.com/'+tweet_user_sname+'/status/'+TweetID) 507 | driver = self.driver 508 | error_save_filename = os.path.join('cache','transtweet','error','getTransFromTweetID-'+tasktype+'.png') 509 | tres = self.waitForTweetLoad(tasktype) 510 | if not tres[0]: 511 | return tres 512 | tres = self.getSingelTweet(trans,tasktype) 513 | if not tres[0]: 514 | return tres 515 | self.waitForTweetLoad3(tasktype) 516 | try: 517 | #scroll_width = driver.execute_script('return document.body.parentNode.scrollWidth') 518 | elem = driver.find_element_by_css_selector('section[aria-labelledby].css-1dbjc4n') 519 | driver.set_window_size(scroll_width, elem.size['height']+300) 520 | except: 521 | driver.save_screenshot(error_save_filename) 522 | s = traceback.format_exc(limit=10) 523 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 524 | return (False,error_save_filename,'更改渲染宽高时异常') 525 | 526 | filepath = os.path.join('cache','transtweet','transimg',tasktype+'.png') 527 | try: 528 | file = open(filepath,'wb') 529 | file.write(elem.screenshot_as_png) 530 | file.close() 531 | except: 532 | if file: 533 | file.close() 534 | driver.save_screenshot(error_save_filename) 535 | s = traceback.format_exc(limit=10) 536 | logger.warning(s+"\n filename="+error_save_filename + ';url=' + driver.current_url) 537 | return (False,error_save_filename,'保存推文翻译图片时异常') 538 | return (True,filepath,'保存成功') 539 | #处理tweets,使tweets可以json化 540 | def dealTweets(self,tweets): 541 | ttweets = [] 542 | cout_id = 0 543 | for tweet in tweets: 544 | if 'elem' in tweet: 545 | ttweet = tweet.copy() 546 | cout_id = cout_id + 1 547 | ttweet['cout_id'] = cout_id 548 | del ttweet['relem'] 549 | del ttweet['elem'] 550 | ttweet['tweettexts'] = ttweet['tweettexts'].copy() 551 | tts = ttweet['tweettexts'] 552 | for tt in tts: 553 | del tt['elem'] 554 | ttweets.append(ttweet) 555 | return ttweets 556 | #保存推文列表到json 557 | def saveTweetsToJson(self,tweets,tasktype:str): 558 | path = os.path.join('transtweet','res_tweets_json') 559 | check_path(path) 560 | filename = tasktype+'.json' 561 | ttweets = self.dealTweets(tweets) 562 | data_save(filename,ttweets,path) 563 | #保存推文列表的每个推文元素到图片 564 | def saveTweetsToImg(self,tweets,tasktype:str): 565 | driver = self.driver 566 | path = os.path.join('transtweet','res_tweets',tasktype) 567 | check_path(path) 568 | cout_id = 0 569 | for tweet in tweets: 570 | if 'elem' in tweet: 571 | cout_id = cout_id + 1 572 | #tweet['base64'] = tweet['elem'].screenshot_as_base64 573 | self.savePngToFile(tweet['relem'].screenshot_as_png,str(cout_id),path=path) 574 | driver.execute_script('window.scrollTo(0,0)') 575 | return 'success!' 576 | #保存浏览器全页面到图片 577 | def web_screenshot(self,tasktype:str): 578 | driver = self.driver 579 | path = os.path.join('transtweet','web_screenshot') 580 | check_path(path) 581 | save_filename = os.path.join('cache',path,tasktype+'.png') 582 | driver.get_screenshot_as_file(save_filename) 583 | #缩放 584 | def doczoom(self,zoom): 585 | driver = self.driver 586 | #document.body.style.zoom= ' 587 | #document.body.style.transform= ' 588 | #var page = this;page.zoomFactor = ' 589 | driver.execute_script("document.body.style.transform= '"+str(zoom)+"'") -------------------------------------------------------------------------------- /module/twitter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import string 3 | import nonebot 4 | import urllib 5 | import traceback 6 | import json 7 | import os 8 | import time 9 | #引入配置 10 | import config 11 | #日志输出 12 | from helper import data_read,data_save,getlogger,msgSendToBot,TempMemory,check_path 13 | logger = getlogger(__name__) 14 | #引入测试方法 15 | try: 16 | #event_push 17 | import dbtest as test 18 | except: 19 | test = None 20 | ''' 21 | 推送列表维护,及推送处理模版类定义 22 | 23 | 推送列表维护三个映射数组 24 | 推送关联表(加速推送转发) __push_list 25 | 对象关联表(加速对象搜索) __spy_relate 26 | 监测流列表(更新监听列表-包含所有需要监听的对象) spylist 27 | ''' 28 | 29 | PushList_config_name = 'PushListData.json' 30 | def_puth_method : str = config.UPDATA_METHOD 31 | base_tweet_id : str = "1109751762733301760" #TweetApi需要至少一个监测对象(默认 nekomataokayu) 32 | #10进制转64进制 33 | def encode_b64(n:int,offset:int = 1253881609540800000) -> str: 34 | table = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_' 35 | result = [] 36 | temp = n - offset 37 | if 0 == temp: 38 | result.append('0') 39 | else: 40 | while 0 < temp: 41 | result.append(table[int(temp) % 64]) 42 | temp = int(temp)//64 43 | return ''.join([x for x in reversed(result)]) 44 | def decode_b64(str,offset:int = 1253881609540800000) -> int: 45 | table = {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, 46 | "6": 6, "7": 7, "8": 8, "9": 9, 47 | "a": 10, "b": 11, "c": 12, "d": 13, "e": 14, "f": 15, "g": 16, 48 | "h": 17, "i": 18, "j": 19, "k": 20, "l": 21, "m": 22, "n": 23, 49 | "o": 24, "p": 25, "q": 26, "r": 27, "s": 28, "t": 29, "u": 30, 50 | "v": 31, "w": 32, "x": 33, "y": 34, "z": 35, 51 | "A": 36, "B": 37, "C": 38, "D": 39, "E": 40, "F": 41, "G": 42, 52 | "H": 43, "I": 44, "J": 45, "K": 46, "L": 47, "M": 48, "N": 49, 53 | "O": 50, "P": 51, "Q": 52, "R": 53, "S": 54, "T": 55, "U": 56, 54 | "V": 57, "W": 58, "X": 59, "Y": 60, "Z": 61, 55 | "-": 62, "_": 63} 56 | result : int = 0 57 | for i in range(len(str)): 58 | result *= 64 59 | if str[i] not in table: 60 | return -1 61 | result += table[str[i]] 62 | return result + offset 63 | #推送列表 64 | class PushList: 65 | puth_method = '' #推送方法 TweetApi、RSShub、Twint 66 | __push_list = {'private':{},'group':{}} 67 | __spy_relate = {} 68 | spylist = [] 69 | message_type_list = ('private','group') #支持的消息类型 70 | #推送单元允许编辑的配置_布尔类型 71 | Pushunit_allowEdit = ( 72 | #携带图片发送 73 | 'upimg', 74 | #消息模版 75 | 'retweet_template','quoted_template','reply_to_status_template', 76 | 'reply_to_user_template','none_template', 77 | #推特转发各类型开关 78 | 'retweet','quoted','reply_to_status', 79 | 'reply_to_user','none', 80 | 81 | #智能推送(仅限推送单元设置,无法全局设置) 82 | 'ai_retweet', 83 | 'ai_reply_to_status', 84 | 'ai_passive_reply_to_status', 85 | 'ai_passive_quoted', 86 | 'ai_passive_reply_to_user', 87 | 88 | #推特个人信息变动推送开关 89 | 'change_ID','change_name','change_description', 90 | 'change_headimgchange' 91 | ) 92 | def __init__(self,puth_method:str = def_puth_method): 93 | global base_tweet_id 94 | self.puth_method = puth_method 95 | if self.puth_method == 'TweetApi': 96 | self.spylist.append(base_tweet_id) 97 | else: 98 | base_tweet_id = "" 99 | 100 | #重置推送 101 | def clear(self): 102 | self.__push_list = {'private':{},'group':{}} 103 | self.__spy_relate = {} 104 | self.spylist = [] 105 | if self.puth_method == 'TweetApi': 106 | self.spylist.append(base_tweet_id) 107 | 108 | #获取所有推送单元 109 | def getAllPushUnit(self) -> list: 110 | sourcedata = self.__spy_relate.copy() 111 | PushUnits = [] 112 | for PushUnitList in sourcedata.values(): 113 | for PushUnit in PushUnitList: 114 | PushUnits.append(PushUnit) 115 | return PushUnits 116 | #获取所有推送对象的全局设置信息 117 | def getAllPushTo(self) -> list: 118 | PushToAttrList = [] 119 | sourcedata = self.__push_list.copy() 120 | for pushTo,frameunit in sourcedata['private'].items(): 121 | PushToAttrList.append({ 122 | 'message_type':'private', 123 | 'pushTo':pushTo, 124 | 'Pushunitattr':frameunit['Pushunitattr'].copy() 125 | }) 126 | for pushTo,frameunit in sourcedata['group'].items(): 127 | PushToAttrList.append({ 128 | 'message_type':'group', 129 | 'pushTo':pushTo, 130 | 'Pushunitattr':frameunit['Pushunitattr'].copy() 131 | }) 132 | return PushToAttrList 133 | def savePushList(self) -> tuple: 134 | PushUnits = self.getAllPushUnit() 135 | PushToAttrList = self.getAllPushTo() 136 | return data_save( 137 | self.puth_method + '_' + PushList_config_name, 138 | { 139 | 'PushUnits':PushUnits, 140 | 'PushToAttrList':PushToAttrList 141 | }, 142 | 'config' 143 | ) 144 | def readPushList(self) -> tuple: 145 | data = data_read(self.puth_method + '_' + PushList_config_name,'config') 146 | if data[0] != False: 147 | self.clear() 148 | for PushUnit in data[2]['PushUnits']: 149 | self.addPushunit(PushUnit) 150 | for PushToAttrs in data[2]['PushToAttrList']: 151 | if PushToAttrs['pushTo'] not in self.__push_list[PushToAttrs['message_type']]: 152 | continue 153 | self.__push_list[PushToAttrs['message_type']][PushToAttrs['pushTo']]['Pushunitattr'] = PushToAttrs['Pushunitattr'].copy() 154 | return (True,data[1]) 155 | return data 156 | #打包成推送单元中(推送类型-群/私聊,推送对象-群号/Q号,绑定的推特用户ID,单元描述,绑定的酷Q账号,推送模版,各推送开关) 157 | def baleToPushUnit(self,bindCQID:int, 158 | pushtype:str, 159 | pushID:int, 160 | tweet_user_id:int, 161 | create_opid:int, 162 | lastedit_opid:int, 163 | des:str, 164 | nick:str='', 165 | **config): 166 | Pushunit = {} 167 | if not config: 168 | config = {} 169 | #固有属性 170 | Pushunit['bindCQID'] = bindCQID #绑定的酷Q帐号(正式上线时将使用此帐户进行发送,用于适配多酷Q账号) 171 | Pushunit['type'] = pushtype #group/private 172 | Pushunit['pushTo'] = pushID #QQ号或者群号 173 | Pushunit['tweet_user_id'] = tweet_user_id #监测ID 174 | Pushunit['create_opid'] = create_opid #创建人Q号 175 | Pushunit['lastedit_opid'] = lastedit_opid #最后一次操作人Q号 176 | Pushunit['nick'] = nick #推送昵称(默认推送昵称为推特screen_name) 177 | Pushunit['des'] = des #单元描述 178 | Pushunit['config'] = config 179 | return Pushunit 180 | #增加一个推送单元,返回状态元组(布尔型-是否成功,字符串-消息) 181 | def addPushunit(self,Pushunit) -> tuple: 182 | if Pushunit['pushTo'] in self.__push_list[Pushunit['type']]: 183 | if Pushunit['tweet_user_id'] in self.__push_list[Pushunit['type']][Pushunit['pushTo']]['pushunits']: 184 | return ( False, '推送单元已存在' ) 185 | else: 186 | #初始化推送对象(推送全局属性) 187 | self.__push_list[Pushunit['type']][Pushunit['pushTo']] = { 188 | #为推送对象设置全局默认属性 189 | 'Pushunitattr':config.pushunit_default_config.copy(), 190 | #推送单元组 191 | 'pushunits':{} 192 | } 193 | #添加单元至推送列表 194 | self.__push_list[Pushunit['type']][Pushunit['pushTo']]['pushunits'][Pushunit['tweet_user_id']] = Pushunit 195 | #同步监测关联(内部同步了监测列表) 196 | if Pushunit['tweet_user_id'] not in self.__spy_relate: 197 | self.__spy_relate[Pushunit['tweet_user_id']] = [] 198 | if str(Pushunit['tweet_user_id']) != base_tweet_id: 199 | self.spylist.append(str(Pushunit['tweet_user_id'])) 200 | self.__spy_relate[Pushunit['tweet_user_id']].append(Pushunit) 201 | return ( True, '添加成功!' ) 202 | #删除一个推送单元,没有返回值 203 | def delPushunit(self,Pushunit): 204 | #从推送列表中移除推送单元 205 | del self.__push_list[Pushunit['type']][Pushunit['pushTo']]['pushunits'][Pushunit['tweet_user_id']] 206 | #从监测列表中移除推送单元 207 | self.__spy_relate[Pushunit['tweet_user_id']].remove(Pushunit) 208 | #检查监测对象的推送单元是否为空,为空则移出监测列表 209 | if self.__spy_relate[Pushunit['tweet_user_id']] == []: 210 | del self.__spy_relate[Pushunit['tweet_user_id']] 211 | if str(Pushunit['tweet_user_id']) != base_tweet_id: 212 | self.spylist.remove(str(Pushunit['tweet_user_id'])) 213 | #鲨掉自己 214 | del Pushunit 215 | #获取一个推送单元,返回状态列表(布尔型-是否成功,字符串-消息/Pushunit) 216 | def getPushunit(self,message_type:str,pushTo:int,tweet_user_id:int) -> list: 217 | if message_type not in self.message_type_list: 218 | raise Exception("无效的消息类型!",message_type) 219 | if pushTo not in self.__push_list[message_type]: 220 | return (False,'推送对象不存在!') 221 | if tweet_user_id not in self.__push_list[message_type][pushTo]['pushunits']: 222 | return (False,'推送单元不存在!') 223 | return (True,self.__push_list[message_type][pushTo]['pushunits'][tweet_user_id]) 224 | 225 | #获取推送单元某个属性的值 返回值-(布尔型-是否成功,字符串-消息/属性内容) 226 | def getPuslunitAttr(self,Pushunit,key) -> tuple: 227 | if key in Pushunit['config']: 228 | return (True,Pushunit['config'][key]) 229 | if key not in self.__push_list[Pushunit['type']][Pushunit['pushTo']]['Pushunitattr']: 230 | return (False,'不存在此属性') 231 | res = self.__push_list[Pushunit['type']][Pushunit['pushTo']]['Pushunitattr'][key] 232 | return (True,res) 233 | 234 | #返回监测对象关联的推送单元组,监测对象不存在时返回空列表[] 235 | def getLitsFromTweeUserID(self,tweet_user_id:int) -> list: 236 | if tweet_user_id not in self.__spy_relate: 237 | return [] 238 | return self.__spy_relate[tweet_user_id].copy() 239 | #返回推送对象关联的推送单元组,推送对象不存在时返回空列表[] 240 | def getLitsFromPushTo(self,message_type:str,pushTo:int) -> list: 241 | if message_type not in self.message_type_list: 242 | raise Exception("无效的消息类型!",message_type) 243 | if pushTo not in self.__push_list[message_type]: 244 | return [] 245 | dict_ = self.__push_list[message_type][pushTo]['pushunits'] 246 | res = [] 247 | for v in dict_.values(): 248 | res.append(v) 249 | return res 250 | #返回推送对象关联的推送单元组-带ID,推送对象不存在时返回空列表{} 251 | def getLitsFromPushToAndID(self,message_type:str,pushTo:int) -> dict: 252 | if message_type not in self.message_type_list: 253 | raise Exception("无效的消息类型!",message_type) 254 | if pushTo not in self.__push_list[message_type]: 255 | return {} 256 | return self.__push_list[message_type][pushTo]['pushunits'] 257 | #返回推送对象关联的推送属性组,推送对象不存在时返回空字典{} 258 | def getAttrLitsFromPushTo(self,message_type:str,pushTo:int) -> dict: 259 | if message_type not in self.message_type_list: 260 | raise Exception("无效的消息类型!",message_type) 261 | if pushTo not in self.__push_list[message_type]: 262 | return {} 263 | return self.__push_list[message_type][pushTo]['Pushunitattr'] 264 | 265 | #移除某个监测对象关联的所有推送单元,参数-推特用户ID 返回值-(布尔型-是否成功,字符串-消息) 266 | def delPushunitFromTweeUserID(self,tweet_user_id:int) -> tuple: 267 | if tweet_user_id not in self.__spy_relate: 268 | return (False,'此用户不在监测列表中!') 269 | table = self.getLitsFromTweeUserID(tweet_user_id) 270 | if table == []: 271 | return (True,'移除成功!') 272 | for Pushunit in table: 273 | self.delPushunit(Pushunit) 274 | return (True,'移除成功!') 275 | #移除某个推送对象关联的所有推送单元,参数-消息类型,对象ID,CQID 返回值-(布尔型-是否成功,字符串-消息) 276 | def delPushunitFromPushTo(self,message_type:str,pushTo:int,self_id:int = 0) -> tuple: 277 | if message_type not in self.message_type_list: 278 | raise Exception("无效的消息类型!",message_type) 279 | table = self.getLitsFromPushTo(message_type,pushTo) 280 | if table == []: 281 | return (True,'移除成功!') 282 | for Pushunit in table: 283 | if self_id == 0 or self_id == Pushunit['bindCQID']: 284 | self.delPushunit(Pushunit) 285 | return (True,'移除成功!') 286 | #移除某个推送单元,参数-消息类型,对象ID 返回值-(布尔型-是否成功,字符串-消息) 287 | def delPushunitFromPushToAndTweetUserID(self,message_type:str,pushTo:int,tweet_user_id:int) -> tuple: 288 | if message_type not in self.message_type_list: 289 | raise Exception("无效的消息类型!",message_type) 290 | if pushTo not in self.__push_list[message_type]: 291 | return (False,'推送对象不存在!') 292 | if tweet_user_id not in self.__push_list[message_type][pushTo]['pushunits']: 293 | return (False,'推送单元不存在!') 294 | self.delPushunit(self.__push_list[message_type][pushTo]['pushunits'][tweet_user_id]) 295 | return (True,'移除成功!') 296 | 297 | #设置指定推送对象的全局属性 298 | def PushTo_setAttr(self,message_type:str,pushTo:int,key:str,value) -> tuple: 299 | if message_type not in self.message_type_list: 300 | raise Exception("无效的消息类型!",message_type) 301 | if key not in self.Pushunit_allowEdit: 302 | return (False,'指定属性不存在!') 303 | if pushTo not in self.__push_list[message_type]: 304 | return (False,'推送对象不存在!') 305 | self.__push_list[message_type][pushTo]['Pushunitattr'][key] = value 306 | return (True,'属性已更新') 307 | #设置指定推送单元的特定属性 308 | def setPushunitAttr(self,message_type:str,pushTo:int,tweet_user_id:int,key:str,value): 309 | if message_type not in self.message_type_list: 310 | raise Exception("无效的消息类型!",message_type) 311 | if key not in self.Pushunit_allowEdit and key != 'nick' and key != 'des': 312 | return (False,'指定属性不存在!') 313 | if pushTo not in self.__push_list[message_type]: 314 | return (False,'推送对象不存在!') 315 | if tweet_user_id not in self.__push_list[message_type][pushTo]['pushunits']: 316 | return (False,'推送单元不存在!') 317 | if key == 'nick' or key == 'des': 318 | self.__push_list[message_type][pushTo]['pushunits'][tweet_user_id][key] = value 319 | else: 320 | self.__push_list[message_type][pushTo]['pushunits'][tweet_user_id]['config'][key] = value 321 | return (True,'属性已更新') 322 | #字符串模版 323 | class tweetToStrTemplate(string.Template): 324 | delimiter = '$' 325 | idpattern = '[a-z]+_[a-z_]+' 326 | #缩写推特ID(缓存1000条) 327 | mintweetID = TempMemory(def_puth_method + '_' + 'mintweetID.json',limit = 1000,autosave = True,autoload = True) 328 | #推文缓存(150条/监测对象) 329 | check_path(os.path.join('templist','twitterApi')) 330 | tweetsmemory = {} 331 | #推特用户缓存(1000条) 332 | userinfolist = TempMemory(def_puth_method + '_' + 'userinfolist.json',limit = 1000,autosave = False,autoload = False) 333 | class tweetEventDeal: 334 | #检测个人信息更新 335 | def check_userinfo(self, userinfo, isnotable = False, trigger = True): 336 | global userinfolist 337 | """ 338 | 运行数据比较 339 | 用于监测用户的信息修改 340 | 用户ID screen_name 341 | 用户昵称 name 342 | 描述 description 343 | 头像 profile_image_url 344 | """ 345 | """ 346 | tweetinfo['user']['id'] = tweet.user.id 347 | tweetinfo['user']['id_str'] = tweet.user.id_str 348 | tweetinfo['user']['name'] = tweet.user.name 349 | tweetinfo['user']['description'] = tweet.user.description 350 | tweetinfo['user']['screen_name'] = tweet.user.screen_name 351 | tweetinfo['user']['profile_image_url'] = tweet.user.profile_image_url 352 | tweetinfo['user']['profile_image_url_https'] = tweet.user.profile_image_url_https 353 | """ 354 | old_userinfo = userinfolist.find((lambda item,val:item['id'] == val),userinfo['id']) 355 | if old_userinfo != None: 356 | data = {} 357 | s = '' 358 | if old_userinfo['name'] != userinfo['name']: 359 | data['type'] = 'change_name' 360 | s = old_userinfo['name'] + "(" + old_userinfo['screen_name'] + ")" + \ 361 | ' 的昵称已更新为 ' + userinfo['name'] + "(" + userinfo['screen_name'] + ")" 362 | old_userinfo['name'] = userinfo['name'] 363 | if old_userinfo['description'] != userinfo['description']: 364 | data['type'] = 'change_description' 365 | s = old_userinfo['name'] + "(" + old_userinfo['screen_name'] + ")" + ' 的描述已更新为 ' + userinfo['description'] 366 | old_userinfo['description'] = userinfo['description'] 367 | if old_userinfo['screen_name'] != userinfo['screen_name']: 368 | data['type'] = 'change_ID' 369 | s = old_userinfo['name'] + "(" + old_userinfo['screen_name'] + ")" + \ 370 | ' 的ID已更新为 ' + userinfo['name'] + "(" + userinfo['screen_name'] + ")" 371 | old_userinfo['screen_name'] = userinfo['screen_name'] 372 | if old_userinfo['profile_image_url_https'] != userinfo['profile_image_url_https']: 373 | data['type'] = 'change_headimgchange' 374 | s = old_userinfo['name'] + "(" + old_userinfo['screen_name'] + ")" + '的头像已更新' 375 | old_userinfo['profile_image_url'] = userinfo['profile_image_url'] 376 | old_userinfo['profile_image_url_https'] = userinfo['profile_image_url_https'] 377 | s = s + "\n" + '[CQ:image,timeout='+config.img_time_out+',file=' + userinfo['profile_image_url_https'] + ']' 378 | 379 | if s != '': 380 | data['user_id'] = userinfo['id'] 381 | data['user_id_str'] = userinfo['id_str'] 382 | data['str'] = s 383 | eventunit = self.bale_event(data['type'],data['user_id'],data) 384 | if trigger: 385 | self.deal_event(eventunit) 386 | else: 387 | if isnotable or userinfo['id_str'] in push_list.spylist: 388 | userinfolist.join(userinfo) 389 | #打包事件(事件类型,引起变化的用户ID,事件数据) 390 | def bale_event(self, event_type,user_id:int,event_data): 391 | if event_type in ('retweet','quoted','reply_to_status','reply_to_user','none'): 392 | global tweetsmemory,mintweetID 393 | if mintweetID.find((lambda item,val:item[0]==val),event_data['id']) == None: 394 | count = 0 395 | if mintweetID.tm != []: 396 | count = mintweetID.tm[-1][1]+1 397 | if count > 5000: 398 | count = 0 399 | mintweetID.join([event_data['id'],count]) 400 | sdata = event_data.copy() 401 | if 'status' in sdata: 402 | del sdata['status'] 403 | ID = event_data['user']['id_str'] 404 | if ID not in tweetsmemory: 405 | tweetsmemory[ID] = TempMemory(os.path.join('twitterApi',ID+'.json'),limit=150,autoload=True,autosave=True) 406 | tmemory = tweetsmemory[ID] 407 | tmemory.join(sdata) 408 | eventunit = { 409 | 'type':event_type, 410 | 'user_id':int(user_id), 411 | 'data':event_data 412 | } 413 | return eventunit 414 | #事件预处理-发送事件 415 | def deal_event(self, event): 416 | table = push_list.getLitsFromTweeUserID(event['user_id']) 417 | if test != None: 418 | test.event_push(event) 419 | for Pushunit in table: 420 | time.sleep(0.3) #手动增加发送时间,防止发送速率过快 421 | if (event['type'] in ('retweet','quoted','reply_to_status','reply_to_user') and not event['data']['trigger_remote']) or (event['type'] not in ('retweet','quoted','reply_to_status','reply_to_user')): 422 | #获取属性判断是否可以触发事件 423 | res = push_list.getPuslunitAttr(Pushunit,event['type']) 424 | if res[0] == False: 425 | raise Exception("获取Pushunit属性值失败",Pushunit) 426 | if res[1] == 1: 427 | self.deal_event_unit(event,Pushunit) 428 | elif event['type'] in ('retweet','quoted','reply_to_status','reply_to_user'): 429 | tweetinfo = event['data'] 430 | if event['type'] in ('quoted','reply_to_status','reply_to_user') and tweetinfo['trigger_remote']: 431 | if tweetinfo['notable']: 432 | res = push_list.getPuslunitAttr(Pushunit,'ai_passive_' + event['type']) 433 | if res[0] == False: 434 | raise Exception("获取Pushunit属性值失败",Pushunit) 435 | if res[1] == 1: 436 | self.deal_event_unit(event,Pushunit) 437 | elif event['type'] in ('retweet','reply_to_status') and not tweetinfo['trigger_remote']: 438 | if tweetinfo['Related_notable']: 439 | res = push_list.getPuslunitAttr(Pushunit,'ai_' + event['type']) 440 | if res[0] == False: 441 | raise Exception("获取Pushunit属性值失败",Pushunit) 442 | if res[1] == 1: 443 | self.deal_event_unit(event,Pushunit) 444 | 445 | def deal_event_unit(self,event,Pushunit): 446 | raise Exception('未定义事件处理单元') 447 | #推特标识到中文 448 | def type_to_str(self, tweettype): 449 | if tweettype == 'retweet': 450 | return '转推' #纯转推 451 | elif tweettype == 'quoted': 452 | return '转推并评论' #推特内含引用推文(带评论转推) 453 | elif tweettype == 'reply_to_status': 454 | return '回复' #回复(推特下评论) 455 | elif tweettype == 'reply_to_user': 456 | return '提及' #提及(猜测就是艾特) 457 | else: 458 | return '发推' #未分类(估计是主动发推) 459 | #将推特数据应用到模版 460 | def tweetToStr(self, tweetinfo, nick, upimg=config.pushunit_default_config['upimg'], template_text=''): 461 | global mintweetID 462 | global userinfolist 463 | if nick == '' or ('trigger_remote' in tweetinfo and tweetinfo['trigger_remote']): 464 | if tweetinfo['user']['name']: 465 | nick = tweetinfo['user']['name'] 466 | else: 467 | nick = tweetinfo['user']['screen_name'] 468 | temptweetID = mintweetID.find(lambda item,val:item[0] == val,tweetinfo['id']) 469 | 470 | #模版变量初始化 471 | template_value = { 472 | 'tweet_id':tweetinfo['id_str'], #推特ID 473 | 'tweet_id_min':encode_b64(tweetinfo['id']),#压缩推特id 474 | 'tweet_id_temp':('未生成' if temptweetID == None else ('#' + str(temptweetID[1]))),#临时推特id 475 | 'tweet_nick':nick, #操作人昵称 476 | 'tweet_user_id':tweetinfo['user']['screen_name'], #操作人ID 477 | 'tweet_text':tweetinfo['text'], #发送推特的完整内容 478 | 'related_user_id':'', #关联用户ID 479 | 'related_user_name':'', #关联用户昵称-昵称-昵称查询不到时为ID(被评论/被转发/被提及) 480 | 'related_tweet_id':'', #关联推特ID(被评论/被转发) 481 | 'related_tweet_id_min':'', #关联推特ID的压缩(被评论/被转发) 482 | 'related_tweet_text':'', #关联推特内容(被转发或被转发并评论时存在) 483 | 'media_img':'', #媒体 484 | 'related_media_img':'' #依赖推文的媒体 485 | } 486 | 487 | if tweetinfo['type'] != 'none': 488 | template_value['related_tweet_id'] = tweetinfo['Related_tweet']['id_str'] 489 | template_value['related_tweet_id_min'] = encode_b64(tweetinfo['Related_tweet']['id']) 490 | template_value['related_tweet_text'] = tweetinfo['Related_tweet']['text'] 491 | 492 | if tweetinfo['type'] != 'none': 493 | template_value['related_user_id'] = tweetinfo['Related_user']['screen_name'] 494 | tu = self.tryGetNick(tweetinfo['Related_user']['id'],'') 495 | if tu != '': 496 | template_value['related_user_name'] = tu 497 | else: 498 | if hasattr(tweetinfo['Related_user'],'name'): 499 | template_value['related_user_name'] = tweetinfo['Related_user']['name'] 500 | else: 501 | template_value['related_user_name'] = tweetinfo['Related_user']['screen_name'] 502 | #组装图片 503 | if upimg == 1: 504 | if 'extended_entities' in tweetinfo: 505 | mis = '' 506 | for media_unit in tweetinfo['extended_entities']: 507 | #组装CQ码 508 | #file_suffix = os.path.splitext(media_unit['media_url'])[1] 509 | #s = s + '[CQ:image,timeout='+config.img_time_out+',file='+config.img_path+'tweet/' + media_unit['id_str'] + file_suffix + ']' 510 | mis = mis + '[CQ:image,timeout='+config.img_time_out+',file='+ media_unit['media_url'] + ']' 511 | if mis != '': 512 | mis = "\n媒体:" + str(len(tweetinfo['extended_entities'])) + "个\n" + mis 513 | template_value['media_img'] = mis 514 | 515 | if 'Related_extended_entities' in tweetinfo: 516 | mis = '' 517 | for media_unit in tweetinfo['Related_extended_entities']: 518 | #组装CQ码 519 | #file_suffix = os.path.splitext(media_unit['media_url'])[1] 520 | #s = s + '[CQ:image,timeout='+config.img_time_out+',file='+config.img_path+'tweet/' + media_unit['id_str'] + file_suffix + ']' 521 | mis = mis + '[CQ:image,timeout='+config.img_time_out+',file='+ media_unit['media_url'] + ']' 522 | if mis != '': 523 | mis = "\n依赖媒体:" + str(len(tweetinfo['Related_extended_entities'])) + "个\n" + mis 524 | template_value['related_media_img'] = mis 525 | 526 | #生成模版类 527 | s = "" 528 | t = None 529 | if template_text == '': 530 | #默认模版 531 | if tweetinfo['type'] == 'none': 532 | deftemplate_none = "推特ID:$tweet_id_min,【$tweet_nick】发布了:\n$tweet_text\n$media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id\n临时推文ID:$tweet_id_temp" 533 | t = tweetToStrTemplate(deftemplate_none) 534 | elif tweetinfo['type'] == 'retweet': 535 | deftemplate_another = "推特ID:$tweet_id_min,【$tweet_nick】转了【$related_user_name】的推特:\n$related_tweet_text\n$related_media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id" 536 | t = tweetToStrTemplate(deftemplate_another) 537 | elif tweetinfo['type'] == 'quoted': 538 | deftemplate_another = "推特ID:$tweet_id_min,【$tweet_nick】转发并评论了【$related_user_name】的推特:\n$tweet_text\n$media_img\n====================\n$related_tweet_text\n$related_media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id\n临时推文ID:$tweet_id_temp" 539 | t = tweetToStrTemplate(deftemplate_another) 540 | else: 541 | deftemplate_another = "推特ID:$tweet_id_min,【$tweet_nick】回复了【$related_user_name】:\n$tweet_text\n$media_img\nhttps://twitter.com/$tweet_user_id/status/$tweet_id\n临时推文ID:$tweet_id_temp" 542 | t = tweetToStrTemplate(deftemplate_another) 543 | else: 544 | #自定义模版 545 | template_text = template_text.replace("\\n","\n") 546 | t = tweetToStrTemplate(template_text) 547 | 548 | #转换为字符串 549 | s = t.safe_substitute(template_value) 550 | return s 551 | #是否存在指定ID的推文缓存 552 | def hasUserTSInCache(self,user_id:str,loadtest:bool = True): 553 | global tweetsmemory,userinfolist 554 | user_id = str(user_id) 555 | if loadtest: 556 | if user_id not in tweetsmemory: 557 | tweetsmemory[user_id] = TempMemory(os.path.join('twitterApi',user_id+'.json'),limit=150,autoload=True,autosave=True) 558 | if tweetsmemory[user_id].tm != []: 559 | tu = userinfolist.find((lambda item,val:item['id'] == val),int(user_id)) 560 | if tu == None: 561 | userinfolist.join(tweetsmemory[user_id].tm[0]['user']) 562 | return True 563 | else: 564 | return False 565 | else: 566 | return user_id in tweetsmemory 567 | #从缓存中获取推文列表 568 | def getUserTSInCache(self,user_id:str,loadtest:bool = True) -> TempMemory: 569 | global tweetsmemory 570 | user_id = str(user_id) 571 | if user_id not in tweetsmemory: 572 | if loadtest: 573 | tweetsmemory[user_id] = TempMemory(os.path.join('twitterApi',user_id+'.json'),limit=150,autoload=True,autosave=True) 574 | if tweetsmemory[user_id].tm != []: 575 | return tweetsmemory[user_id] 576 | del tweetsmemory[user_id] 577 | return None 578 | return tweetsmemory[user_id] 579 | #尝试从缓存中获取推文 580 | def tryGetTweet(self,tweet_id,user_id:str = None): 581 | global tweetsmemory 582 | if user_id: 583 | user_id = str(user_id) 584 | if user_id not in tweetsmemory: 585 | return None 586 | tweet = tweetsmemory[user_id].find((lambda item,val:item['id'] == val),tweet_id) 587 | if tweet != None: 588 | return tweet 589 | else: 590 | for unit in tweetsmemory: 591 | tweet = tweetsmemory[unit].find((lambda item,val:item['id'] == val),tweet_id) 592 | if tweet != None: 593 | return tweet 594 | return None 595 | 596 | #尝试从缓存中获取昵称 597 | def tryGetNick(self, tweet_user_id,nick): 598 | global userinfolist 599 | tu = userinfolist.find((lambda item,val:item['id'] == val),tweet_user_id) 600 | if tu != None: 601 | return tu['name'] 602 | return nick 603 | #尝试从缓存中获取用户信息,返回用户信息表 604 | def tryGetUserInfo(self, user_id:int = None,screen_name:str = None) -> list: 605 | global userinfolist 606 | if user_id: 607 | tu = userinfolist.find((lambda item,val:item['id'] == val),int(user_id)) 608 | elif screen_name: 609 | tu = userinfolist.find((lambda item,val:item['screen_name'] == val),screen_name) 610 | else: 611 | raise Exception("无效的参数") 612 | if tu != None: 613 | return tu 614 | return {} 615 | #消息发送(消息类型,消息发送到,消息内容) 616 | def send_msg(self, message_type:str, send_id:int, message:str,bindCQID:int = config.default_bot_QQ): 617 | bot = nonebot.get_bot() 618 | try: 619 | if message_type == 'private': 620 | bot.sync.send_msg(self_id=bindCQID,user_id=send_id,message=message) 621 | elif message_type == 'group': 622 | bot.sync.send_msg(self_id=bindCQID,group_id=send_id,message=message) 623 | except: 624 | s = traceback.format_exc(limit=5) 625 | logger.error(s) 626 | pass 627 | #图片保存 628 | def seve_image(self, name, url, file_path='img',canCover=False): 629 | #保存图片到磁盘文件夹 cache/file_path中,默认为当前脚本运行目录下的 cache/img 文件夹 630 | base_path = 'cache' #基准路径 631 | try: 632 | if not os.path.exists(os.path.join(base_path,file_path)): 633 | logger.info('文件夹' + os.path.join(base_path,file_path) + '不存在,重新建立') 634 | #os.mkdir(file_path) 635 | os.makedirs(os.path.join(base_path,file_path)) 636 | #获得图片后缀 637 | file_suffix = os.path.splitext(url)[1] 638 | #拼接图片名(包含路径) 639 | filename = os.path.join(base_path,file_path,name+file_suffix) 640 | #下载图片,并保存到文件夹中 641 | if os.path.isfile(filename): 642 | if canCover: 643 | os.remove(filename) 644 | else: 645 | return 646 | urllib.request.urlretrieve(url,filename=filename) 647 | except IOError: 648 | s = traceback.format_exc(limit=5) 649 | logger.error('文件操作失败'+s+"\n文件路径:"+filename) 650 | except Exception: 651 | s = traceback.format_exc(limit=5) 652 | logger.error(s) 653 | #建立列表 654 | push_list = PushList() 655 | 656 | 657 | -------------------------------------------------------------------------------- /module/twitterApi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import nonebot 3 | import tweepy 4 | import traceback 5 | import asyncio 6 | import threading 7 | import time 8 | import queue 9 | #引入配置 10 | import config 11 | #日志输出 12 | from helper import getlogger,msgSendToBot 13 | logger = getlogger(__name__) 14 | #引入推送列表、推送处理模版 15 | from module.twitter import push_list,tweetEventDeal 16 | 17 | ''' 18 | 推特API监听更新的实现类 19 | ''' 20 | 21 | #引入测试方法 22 | try: 23 | #on_status 24 | import dbtest as test 25 | except: 26 | test = None 27 | 28 | #事件处理类 29 | #推特API的事件处理类 30 | class tweetApiEventDeal(tweetEventDeal): 31 | #事件到达 32 | def deal_event_unit(self,event,Pushunit): 33 | #事件处理单元-发送 34 | data = event['data'] 35 | #额外处理 36 | if event['type'] == 'none' and push_list.getPuslunitAttr(Pushunit,'upimg')[1] == 1: 37 | self.save_media(data) 38 | #识别事件类型 39 | if event['type'] in ['retweet','quoted','reply_to_status','reply_to_user','none']: 40 | str = self.tweetToStr( 41 | data,Pushunit['nick'], 42 | push_list.getPuslunitAttr(Pushunit,'upimg')[1], 43 | push_list.getPuslunitAttr(Pushunit,event['type']+'_template')[1] 44 | ) 45 | self.send_msg(Pushunit['type'],Pushunit['pushTo'],str,Pushunit['bindCQID']) 46 | elif event['type'] in ['change_ID','change_name','change_description','change_headimgchange']: 47 | self.send_msg(Pushunit['type'],Pushunit['pushTo'],data['str'],Pushunit['bindCQID']) 48 | 49 | #媒体保存-保存推特中携带的媒体(目前仅支持图片-考虑带宽,后期不会增加对视频以及gif支持) 50 | def save_media(self, tweetinfo): 51 | if 'extended_entities' not in tweetinfo: 52 | return 53 | """ 54 | 媒体信息 55 | media_obj = {} 56 | media_obj['id'] = media_unit.id 57 | media_obj['id_str'] = media_unit.id_str 58 | media_obj['type'] = media_unit.type 59 | media_obj['media_url'] = media_unit.media_url 60 | media_obj['media_url_https'] = media_unit.media_url_https 61 | tweetinfo['extended_entities'].append(media_obj) 62 | """ 63 | for media_unit in tweetinfo['extended_entities']: 64 | self.seve_image(media_unit['id_str'],media_unit['media_url'],'tweet') 65 | 66 | #控制台输出侦听到的消息 67 | def statusPrintToLog(self, tweetinfo): 68 | if tweetinfo['type'] == 'none': 69 | str = '推特ID:' +tweetinfo['id_str']+','+ \ 70 | tweetinfo['user']['screen_name']+"发送了推文:\n"+ \ 71 | tweetinfo['text'] 72 | logger.info('(!)'+ str) 73 | elif tweetinfo['type'] == 'retweet': 74 | pass 75 | else: 76 | str = \ 77 | '标识 ' + self.type_to_str(tweetinfo['type']) + \ 78 | ',推特ID:' +tweetinfo['id_str']+','+ \ 79 | tweetinfo['user']['screen_name']+'与'+ \ 80 | tweetinfo['Related_user']['screen_name']+"的互动:\n"+ \ 81 | tweetinfo['text'] 82 | if tweetinfo['user']['id_str'] in push_list.spylist: 83 | logger.info('(!)'+ str) 84 | else: 85 | logger.info(str) 86 | #用户是否是值得关注的(粉丝/关注 大于 5k 且修改了默认图,或处于观测列表中) 87 | def isNotableUser(self,user,checkspy): 88 | if checkspy and user['id_str'] in push_list.spylist: 89 | return True 90 | if not user['default_profile_image'] and \ 91 | not user['default_profile'] and \ 92 | not user['protected'] and \ 93 | (int(user['followers_count'] / (user['friends_count']+1)) > 500 or user['followers_count'] > 20000): 94 | return True 95 | return False 96 | #重新包装推特用户信息(用户数据,是否检查更新) 97 | def get_userinfo(self,user,ck = False): 98 | userinfo = {} 99 | userinfo['id'] = user.id 100 | userinfo['id_str'] = user.id_str 101 | userinfo['name'] = user.name 102 | userinfo['description'] = user.description 103 | userinfo['screen_name'] = user.screen_name 104 | userinfo['profile_image_url'] = user.profile_image_url 105 | userinfo['profile_image_url_https'] = user.profile_image_url_https 106 | 107 | userinfo['default_profile_image'] = user.default_profile_image 108 | userinfo['default_profile'] = user.default_profile 109 | userinfo['protected'] = user.protected 110 | userinfo['followers_count'] = user.followers_count 111 | userinfo['friends_count'] = user.friends_count 112 | userinfo['verified'] = user.verified 113 | if ck and userinfo['id_str'] in push_list.spylist: 114 | self.check_userinfo(userinfo,True) 115 | return userinfo 116 | #重新包装推特信息 117 | def get_tweet_info(self, tweet,checkspy = False,trigger = True): 118 | tweetinfo = {} 119 | tweetinfo['created_at'] = int(tweet.created_at.timestamp()) 120 | tweetinfo['id'] = tweet.id 121 | tweetinfo['id_str'] = tweet.id_str 122 | #尝试获取全文 123 | if hasattr(tweet,'extended_tweet') or hasattr(tweet,'full_text'): 124 | if hasattr(tweet,'full_text'): 125 | tweetinfo['text'] = tweet.full_text.replace('<','<').replace('>','>') 126 | else: 127 | tweetinfo['text'] = tweet.extended_tweet['full_text'].replace('<','<').replace('>','>') 128 | else: 129 | tweetinfo['text'] = tweet.text.replace('<','<').replace('>','>') 130 | 131 | #处理媒体信息 132 | tweetinfo['extended_entities'] = [] 133 | if hasattr(tweet,'extended_entities'): 134 | #图片来自本地媒体时将处于这个位置 135 | if 'media' in tweet.extended_entities: 136 | for media_unit in tweet.extended_entities['media']: 137 | media_obj = {} 138 | media_obj['id'] = media_unit['id'] 139 | media_obj['id_str'] = media_unit['id_str'] 140 | media_obj['type'] = media_unit['type'] 141 | media_obj['media_url'] = media_unit['media_url'] 142 | media_obj['media_url_https'] = media_unit['media_url_https'] 143 | tweetinfo['extended_entities'].append(media_obj) 144 | elif hasattr(tweet,'entities'): 145 | #图片来自推特时将处于这个位置 146 | if 'media' in tweet.entities: 147 | for media_unit in tweet.entities['media']: 148 | media_obj = {} 149 | media_obj['id'] = media_unit['id'] 150 | media_obj['id_str'] = media_unit['id_str'] 151 | media_obj['type'] = media_unit['type'] 152 | media_obj['media_url'] = media_unit['media_url'] 153 | media_obj['media_url_https'] = media_unit['media_url_https'] 154 | tweetinfo['extended_entities'].append(media_obj) 155 | 156 | tweetinfo['user'] = self.get_userinfo(tweet.user) 157 | 158 | tweetinfo['notable'] = self.isNotableUser(tweetinfo['user'],checkspy) #值得注意的用户(用户的影响力比较高) 159 | self.check_userinfo(tweetinfo['user'],tweetinfo['notable'],trigger = trigger) #检查用户信息 160 | return tweetinfo 161 | 162 | def deal_tweet_type(self, status): 163 | if hasattr(status, 'retweeted_status'): 164 | return 'retweet' #纯转推 165 | elif hasattr(status, 'quoted_status'): 166 | return 'quoted' #推特内含引用推文(带评论转推) 167 | elif status.in_reply_to_status_id != None: 168 | return 'reply_to_status' #回复(推特下评论) 169 | elif status.in_reply_to_screen_name != None: 170 | return 'reply_to_user' #提及(猜测就是艾特) 171 | else: 172 | return 'none' #未分类(主动发推) 173 | def deal_tweet(self, status,trigger = True): 174 | #监听流:本人转推、本人发推、本人转推并评论、本人回复、被转推、被回复、被提及 175 | tweetinfo = self.get_tweet_info(status,True,trigger = trigger) 176 | tweetinfo['type'] = self.deal_tweet_type(status) 177 | tweetinfo['status'] = status #原始数据 178 | #tweetinfo['tweetNotable'] = tweetinfo['notable'] #推文发布用户是否值得关注 179 | if tweetinfo['type'] == 'retweet':#大多数情况是被转推 180 | #转推时被转推对象与转推对象同时值得关注时视为值得关注 181 | tweetinfo['retweeted'] = self.get_tweet_info(status.retweeted_status,True) 182 | tweetinfo['Related_user'] = tweetinfo['retweeted']['user'] 183 | tweetinfo['Related_tweet'] = tweetinfo['retweeted'] 184 | tweetinfo['Related_notable'] = (tweetinfo['notable'] and tweetinfo['retweeted']['notable']) 185 | tweetinfo['Related_extended_entities'] = tweetinfo['retweeted']['extended_entities'] 186 | elif tweetinfo['type'] == 'quoted': 187 | tweetinfo['quoted'] = self.get_tweet_info(status.quoted_status,True) 188 | tweetinfo['Related_user'] = tweetinfo['quoted']['user'] 189 | tweetinfo['Related_tweet'] = tweetinfo['quoted'] 190 | tweetinfo['Related_notable'] = tweetinfo['quoted']['notable'] 191 | tweetinfo['Related_extended_entities'] = tweetinfo['quoted']['extended_entities'] 192 | elif tweetinfo['type'] != 'none': 193 | tweetinfo['Related_user'] = {} 194 | tweetinfo['Related_user']['id'] = status.in_reply_to_user_id 195 | tweetinfo['Related_user']['id_str'] = status.in_reply_to_user_id_str 196 | tweetinfo['Related_user']['screen_name'] = status.in_reply_to_screen_name 197 | tweetinfo['Related_extended_entities'] = [] 198 | tweetinfo['Related_tweet'] = {} 199 | tweetinfo['Related_tweet']['id'] = status.in_reply_to_status_id 200 | tweetinfo['Related_tweet']['id_str'] = status.in_reply_to_status_id_str 201 | tweetinfo['Related_tweet']['text'] = '' 202 | if tweetinfo['Related_user']['id_str'] in push_list.spylist: 203 | tweetinfo['Related_notable'] = True 204 | else: 205 | userinfo = self.tryGetUserInfo(tweetinfo['Related_user']['id']) 206 | if userinfo != {}: 207 | tweetinfo['Related_notable'] = self.isNotableUser(userinfo,False) 208 | else: 209 | tweetinfo['Related_notable'] = False 210 | else: 211 | tweetinfo['Related_notable'] = True 212 | tweetinfo['Related_user'] = tweetinfo['user'] 213 | 214 | #推文是否值得关注 215 | if tweetinfo['user']['id_str'] in push_list.spylist: 216 | tweetinfo['tweetNotable'] = True 217 | else: 218 | tweetinfo['tweetNotable'] = tweetinfo['Related_notable'] and tweetinfo['notable'] 219 | 220 | #补正监测对象,用于智能推送 221 | if tweetinfo['user']['id_str'] in push_list.spylist: 222 | tweetinfo['trigger_user'] = tweetinfo['user']['id'] 223 | tweetinfo['trigger_remote'] = False #监测重定向标识 224 | else: 225 | tweetinfo['trigger_user'] = tweetinfo['Related_user']['id'] 226 | tweetinfo['trigger_remote'] = True #监测重定向标识 227 | return tweetinfo 228 | #推特事件处理对象 229 | tweet_event_deal = tweetApiEventDeal() 230 | dealTweetsQueue = queue.Queue(64) 231 | #推特监听者 232 | class MyStreamListener(tweepy.StreamListener): 233 | #错误处理 234 | def on_error(self, status_code): 235 | #推特错误代码https://developer.twitter.com/en/docs/basics/response-codes 236 | logger.critical(status_code) 237 | msgSendToBot(logger,"推特流错误:"+str(status_code)) 238 | #返回False结束流 239 | return False 240 | #开始链接监听 241 | def on_connect(self): 242 | msgSendToBot(logger,"推特流链接已就绪") 243 | #断开链接监听 244 | def on_disconnect(self, notice): 245 | msgSendToBot(logger,"推特流已断开链接") 246 | return False 247 | #推特事件监听 248 | def on_status(self, status): 249 | try: 250 | #重新组织推特数据 251 | tweetinfo = tweet_event_deal.deal_tweet(status) 252 | #只有值得关注的推文才会推送处理,降低处理压力(能降一大截……) 253 | if tweetinfo['tweetNotable']: 254 | try: 255 | dealTweetsQueue.put(tweetinfo,timeout=15) 256 | except: 257 | s = traceback.format_exc(limit=5) 258 | msgSendToBot(logger,'推特监听处理队列溢出,请检查队列!') 259 | logger.error(s) 260 | if test != None: 261 | test.on_status(tweetinfo,status) 262 | except: 263 | s = traceback.format_exc(limit=5) 264 | logger.error(s) 265 | 266 | #推特认证 267 | auth = tweepy.OAuthHandler(config.consumer_key, config.consumer_secret) 268 | auth.set_access_token(config.access_token, config.access_token_secret) 269 | #获取API授权 270 | api = tweepy.API(auth, proxy=config.api_proxy) 271 | 272 | #安装测试列表 273 | #test_install_push_list() 274 | #创建监听对象 275 | myStreamListener = MyStreamListener() 276 | def init(): 277 | #读取推送侦听配置 278 | res = push_list.readPushList() 279 | if res[0] == True: 280 | logger.info('侦听配置读取成功') 281 | else: 282 | logger.error('侦听配置读取失败:' + res[1]) 283 | 284 | run_info = { 285 | 'DealDataThread':None, 286 | 'queque':dealTweetsQueue, 287 | 'Thread':None, 288 | 'keepRun':True, 289 | 'isRun':False, 290 | 'error_cout':0, 291 | 'last_reboot':0, 292 | 'apiStream':None 293 | } 294 | def setStreamOpen(b:bool): 295 | run_info['error_cout'] = 0 296 | run_info['keepRun'] = b 297 | run_info['apiStream'].running = b 298 | def reSetError(): 299 | run_info['error_cout'] = 0 300 | def Run(): 301 | #五分钟内至多重启五次 302 | run_info['isRun'] = True 303 | run_info['last_reboot'] = int(time.time()) 304 | run_info['error_cout'] = 0 305 | while True: 306 | while not run_info['keepRun']: 307 | time.sleep(1) #保持运行标记为否时堵塞执行 308 | if run_info['error_cout'] > 5: 309 | msgSendToBot(logger,'重试次数过多,停止重试...') 310 | while run_info['error_cout'] > 5: 311 | time.sleep(1) #错误次数达到上限时暂停运行 312 | if run_info['error_cout'] > 0: 313 | msgSendToBot(logger,'尝试重启推特流,'+'进行第 ' + str(run_info['error_cout']) + ' 次尝试...') 314 | try: 315 | #创建监听流 316 | run_info['apiStream'] = tweepy.Stream(auth = api.auth, listener=myStreamListener) 317 | run_info['apiStream'].filter(follow=push_list.spylist,is_async=False) 318 | except: 319 | s = traceback.format_exc(limit=10) 320 | logger.error(s) 321 | msgSendToBot(logger,'推特监听异常,将在10秒后尝试重启...') 322 | time.sleep(10) 323 | else: 324 | run_info['isRun'] = False 325 | msgSendToBot(logger,'推特流已停止...') 326 | logger.info('推特流正常停止') 327 | if run_info['error_cout'] > 0 and int(time.time()) - run_info['last_reboot'] > 300: 328 | run_info['last_reboot'] = int(time.time()) #两次重启间隔五分钟以上视为重启成功 329 | run_info['error_cout'] = 0 #重置计数 330 | run_info['error_cout'] = run_info['error_cout'] + 1 331 | #处理推特数据(独立线程) 332 | def dealTweetData(): 333 | while True: 334 | tweetinfo = run_info['queque'].get() 335 | try: 336 | #推送事件处理,输出到酷Q 337 | eventunit = tweet_event_deal.bale_event(tweetinfo['type'],tweetinfo['trigger_user'],tweetinfo) 338 | tweet_event_deal.deal_event(eventunit) 339 | #控制台输出 340 | tweet_event_deal.statusPrintToLog(tweetinfo) 341 | except: 342 | s = traceback.format_exc(limit=5) 343 | logger.warning(s) 344 | run_info['queque'].task_done() 345 | 346 | #运行推送线程 347 | def runTwitterApiThread(): 348 | init() 349 | run_info['Thread'] = threading.Thread( 350 | group=None, 351 | target=Run, 352 | name='tweetListener_thread', 353 | daemon=True 354 | ) 355 | run_info['DealDataThread'] = threading.Thread( 356 | group=None, 357 | target=dealTweetData, 358 | name='tweetListener_DealDataThread', 359 | daemon=True 360 | ) 361 | run_info['Thread'].start() 362 | run_info['DealDataThread'].start() 363 | return run_info -------------------------------------------------------------------------------- /plugins/feedback.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession,permission as perm 2 | import asyncio 3 | import config 4 | from helper import getlogger,CQsessionToStr,data_read,data_save,async_msgSendToBot,TokenBucket,TempMemory,argDeal 5 | import helper 6 | import module.permissiongroup as permissiongroup 7 | logger = getlogger(__name__) 8 | __plugin_name__ = '反馈与记录' 9 | __plugin_usage__ = r""" 10 | 记录与处理反馈 11 | """ 12 | #反馈权限 13 | permgroupname = 'feedback' 14 | permissiongroup.perm_addLegalPermGroup(__name__,'反馈模块',permgroupname) 15 | permissiongroup.perm_addLegalPermUnit(permgroupname,'feedback') #反馈权限 16 | 17 | feedback_push_switch = config.feedback_push_switch 18 | #反馈存储 19 | feedbacktmemory = TempMemory('feedback.json',100,autoload=True,autosave=True) 20 | rate_limit_bucket = TokenBucket(0.1,5) 21 | 22 | def perm_check(session: CommandSession,permunit:str,Remotely:dict = None,user:bool = False): 23 | if Remotely != None: 24 | return permissiongroup.perm_check( 25 | Remotely['message_type'], 26 | Remotely['sent_id'], 27 | permgroupname, 28 | permunit 29 | ) 30 | elif user: 31 | return permissiongroup.perm_check( 32 | 'private', 33 | session.event['user_id'], 34 | permgroupname, 35 | permunit 36 | ) 37 | return permissiongroup.perm_check( 38 | session.event['message_type'], 39 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 40 | permgroupname, 41 | permunit 42 | ) 43 | 44 | #预处理 45 | def headdeal(session: CommandSession): 46 | if session.event['message_type'] == "group" and session.event.sub_type != 'normal': 47 | return False 48 | return True 49 | 50 | 51 | # on_command 装饰器将函数声明为一个命令处理器 52 | @on_command('feedback',aliases=['反馈','Feedback'],only_to_me = True) 53 | async def feedback(session: CommandSession): 54 | if not headdeal(session): 55 | return 56 | if perm_check(session,'-feedback',user = True): 57 | await session.send('操作被拒绝,权限不足(p)') 58 | return 59 | if perm_check(session,'-feedback'): 60 | await session.send('操作被拒绝,权限不足(g)') 61 | return 62 | stripped_arg = session.current_arg.strip() 63 | if stripped_arg == '': 64 | await session.send('参数为空!') 65 | return 66 | if not rate_limit_bucket.consume(1): 67 | await session.send("反馈频率过高!") 68 | return 69 | logger.info(CQsessionToStr(session)) 70 | message_type = session.event['message_type'] 71 | group_id = str((session.event['group_id'] if message_type == 'group' else 'None')) 72 | user_id = str(session.event['user_id']) 73 | nick = user_id 74 | if 'nickname' in session.event.sender: 75 | nick = session.event.sender['nickname'] 76 | count = 0 77 | if feedbacktmemory.tm != []: 78 | count = feedbacktmemory.tm[-1]['id'] + 1 79 | feedbackunit = { 80 | 'id':count, 81 | 'deal':False, 82 | 'self_id':session.self_id, 83 | 'message_type':message_type, 84 | 'group_id':group_id, 85 | 'user_id':user_id, 86 | 'nick':nick, 87 | 'text':stripped_arg 88 | } 89 | feedbacktmemory.join(feedbackunit) 90 | msg = "反馈ID:" + str(feedbackunit['id']) + "\n" 91 | if feedbackunit['message_type'] == 'group': 92 | msg = msg + '来自群聊 ' + feedbackunit['group_id'] + "\n" 93 | else: 94 | msg = msg + '来自私聊' + "\n" 95 | msg = msg + feedbackunit['nick'] + "(" + feedbackunit['user_id'] + ")" + "\n" 96 | msg = msg + "反馈了:" + feedbackunit['text'] 97 | #logger.warning('反馈:'+s) 98 | if feedback_push_switch: 99 | await async_msgSendToBot(logger,msg) 100 | await session.send('已收到反馈,反馈ID:' + str(count)) 101 | 102 | @on_command('dealfeedback',aliases=['处理反馈','反馈处理','dealFeedback'],permission=perm.SUPERUSER,only_to_me = True) 103 | async def dealfeedback(session: CommandSession): 104 | if not headdeal(session): 105 | return 106 | logger.info(CQsessionToStr(session)) 107 | arglimit = [ 108 | { 109 | 'name':'feedid', #参数名 110 | 'des':'反馈ID', #参数错误描述 111 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 112 | 'strip':True, #是否strip 113 | 'lower':False, #是否转换为小写 114 | 'default':None, #默认值 115 | 'func':None, #函数,当存在时使用函数进行二次处理 116 | 're':None, #正则表达式匹配(match函数) 117 | 'vlimit':{ 118 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 119 | } 120 | }, 121 | { 122 | 'name':'text', #参数名 123 | 'des':'反馈内容', #参数错误描述 124 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 125 | 'strip':True, #是否strip 126 | 'lower':False, #是否转换为小写 127 | 'default':'', #默认值 128 | 'func':None, #函数,当存在时使用函数进行二次处理 129 | 're':None, #正则表达式匹配(match函数) 130 | 'vlimit':{ 131 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 132 | } 133 | } 134 | ] 135 | args = argDeal(session.current_arg.strip(),arglimit) 136 | if not args[0]: 137 | await session.send(args[1] + '=>' + args[2]) 138 | return 139 | args = args[1] 140 | feedbackunit = feedbacktmemory.find((lambda item,val: item['id'] == val),args['feedid']) 141 | if feedbackunit == None: 142 | await session.send("未搜索到ID:" + str(args['feedid']) + " 的反馈记录") 143 | return 144 | if args['text'] == '': 145 | msg = "---历史反馈---\n" + "已处理:" + ("是" if feedbackunit['deal'] else "否") + "\n" 146 | msg = msg + "反馈ID:" + str(feedbackunit['id']) + "\n" 147 | if feedbackunit['message_type'] == 'group': 148 | msg = msg + '来自群聊 ' + feedbackunit['group_id'] + "\n" 149 | else: 150 | msg = msg + '来自私聊' + "\n" 151 | msg = msg + feedbackunit['nick'] + "(" + feedbackunit['user_id'] + ")" + "\n" 152 | msg = msg + "反馈了:" + feedbackunit['text'] 153 | await session.send(msg) 154 | return 155 | else: 156 | msg = "ID " + str(feedbackunit['id']) + " 的反馈回复:" + "\n" 157 | msg = msg + args['text'].replace('[','[').replace(']',']') 158 | try: 159 | if feedbackunit['message_type'] == 'group': 160 | await session.bot.send_msg_rate_limited( 161 | self_id=feedbackunit['self_id'], 162 | message_type=feedbackunit['message_type'], 163 | group_id=int(feedbackunit['group_id']), 164 | message=msg 165 | ) 166 | else: 167 | await session.bot.send_msg_rate_limited( 168 | self_id=feedbackunit['self_id'], 169 | message_type=feedbackunit['message_type'], 170 | user_id=int(feedbackunit['user_id']), 171 | message=msg 172 | ) 173 | except: 174 | await session.send("回复发送失败") 175 | return 176 | if not feedbackunit['deal']: 177 | feedbackunit['deal'] = True 178 | feedbacktmemory.save() 179 | await session.send("回复已发送") 180 | 181 | def getlist(page:int=1): 182 | ttm = feedbacktmemory.tm.copy() 183 | length = len(ttm) 184 | cout = 0 185 | s = "反馈ID,反馈处理,反馈内容简写\n" 186 | for i in range(length - 1,-1,-1): 187 | if cout >= (page-1)*5 and cout < (page)*5: 188 | feedbackunit = ttm[i] 189 | s = s + str(feedbackunit['id']) + ',' + ("是" if feedbackunit['deal'] else "否") + ',' + feedbackunit['text'][:25] + "\n" 190 | cout = cout + 1 191 | totalpage = (cout)//5 + (0 if cout%5 == 0 else 1) 192 | s = s + '页数:'+str(page)+'/'+str(totalpage)+'总记录数:'+str(cout) + '\n' 193 | s = s + '使用!处理反馈 反馈ID 获取指定反馈内容' + "\n" 194 | s = s + '使用!处理反馈 反馈ID 处理结果 对指定反馈进行处理' + "\n" 195 | return s 196 | @on_command('feedbacklist',aliases=['反馈列表'],permission=perm.SUPERUSER,only_to_me = True) 197 | async def feedbacklist(session: CommandSession): 198 | if not headdeal(session): 199 | return 200 | logger.info(CQsessionToStr(session)) 201 | arglimit = [ 202 | { 203 | 'name':'page', #参数名 204 | 'des':'页码', #参数错误描述 205 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 206 | 'strip':True, #是否strip 207 | 'lower':False, #是否转换为小写 208 | 'default':1, #默认值 209 | 'func':None, #函数,当存在时使用函数进行二次处理 210 | 're':None, #正则表达式匹配(match函数) 211 | 'vlimit':{ 212 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 213 | } 214 | } 215 | ] 216 | args = argDeal(session.current_arg_text.strip(),arglimit) 217 | if not args[0]: 218 | await session.send(args[1] + '=>' + args[2]) 219 | return 220 | args = args[1] 221 | page = args['page'] 222 | if page < 1: 223 | await session.send("页码不能为负") 224 | return 225 | s = getlist(page) 226 | await session.send(s) 227 | 228 | 229 | @on_command('about',aliases=['帮助','help','关于'],only_to_me = False) 230 | async def about(session: CommandSession): 231 | if not headdeal(session): 232 | return 233 | logger.info(CQsessionToStr(session)) 234 | msg = '维护:'+config.mastername+' 相关协作者见开源地址' + "\n" 235 | msg = msg + '*由于bot平台的影响,未来BOT推送可能有不稳定情况。' + "\n" 236 | msg = msg + '*本项目的重构及更新将被延迟' + "\n" 237 | msg = msg + '!转推帮助 -查看转推帮助' + "\n" 238 | msg = msg + '!烤推帮助 -查看烤推帮助(不支持私聊)' + "\n" 239 | msg = msg + '!机翻帮助 -查看机翻帮助' + "\n" 240 | msg = msg + '如有疑问或bug报告可以!反馈 反馈内容 进行反馈' + "\n" 241 | msg = msg + '项目开源地址:http://suo.im/5ZUGYC' 242 | await session.send(msg) 243 | -------------------------------------------------------------------------------- /plugins/machine_translation.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession,permission as perm 2 | from helper import getlogger,CQsessionToStr,TokenBucket,TempMemory,argDeal,data_read,data_save 3 | import module.permissiongroup as permissiongroup 4 | from module.machine_translation import allow_st,engine_nick,engine_list,default_engine 5 | import asyncio 6 | import config 7 | logger = getlogger(__name__) 8 | __plugin_name__ = '机器翻译' 9 | __plugin_usage__ = r""" 10 | 实现机翻以及其它特殊功能(可能使用到自然语言处理器) 11 | """ 12 | config_filename = 'mtransopt_list.json' 13 | mtransopt_list = {} 14 | res = data_read(config_filename) 15 | if res[0]: 16 | logger.info("配置文件读取成功") 17 | mtransopt_list = res[2] 18 | #预处理 19 | def headdeal(session: CommandSession): 20 | if session.event['message_type'] == "group" and session.event.sub_type != 'normal': 21 | return False 22 | return True 23 | 24 | @on_command('mtransopt',aliases=['翻译设置','设置翻译'],only_to_me = False) 25 | async def mtransopt(session: CommandSession): 26 | if not headdeal(session): 27 | return 28 | global mtransopt_list 29 | #message_type = session.event['message_type'] 30 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 31 | user_id = session.event['user_id'] 32 | 33 | #if perm_check(session,'-listener',user = True): 34 | # await session.send('操作被拒绝,权限不足(p)') 35 | # return 36 | logger.info(CQsessionToStr(session)) 37 | 38 | arglimit = [ 39 | { 40 | 'name':'engine', #参数名 41 | 'des':'翻译引擎', #参数错误描述 42 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 43 | 'strip':True, #是否strip 44 | 'lower':False, #是否转换为小写 45 | 'default':None, #默认值 46 | 'func':None, #函数,当存在时使用函数进行二次处理 47 | 're':None, #正则表达式匹配(match函数) 48 | 'vlimit':engine_nick 49 | }, 50 | { 51 | 'name':'Source', #参数名 52 | 'des':'源语言', #参数错误描述 53 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 54 | 'strip':True, #是否strip 55 | 'lower':True, #是否转换为小写 56 | 'default':'auto', #默认值 57 | 'func':None, #函数,当存在时使用函数进行二次处理 58 | 're':None, #正则表达式匹配(match函数) 59 | 'vlimit':allow_st['Source'] 60 | }, 61 | { 62 | 'name':'Target', #参数名 63 | 'des':'目标语言', #参数错误描述 64 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 65 | 'strip':True, #是否strip 66 | 'lower':True, #是否转换为小写 67 | 'default':'zh', #默认值 68 | 'func':None, #函数,当存在时使用函数进行二次处理 69 | 're':None, #正则表达式匹配(match函数) 70 | 'vlimit':allow_st['Target'] 71 | }, 72 | ] 73 | args = argDeal(session.current_arg_text.strip(),arglimit) 74 | if not args[0]: 75 | await session.send(args[1] + '=>' + args[2]) 76 | return 77 | args = args[1] 78 | user_id = str(user_id) 79 | mtransopt_list[user_id] = args 80 | data_save(config_filename,mtransopt_list) 81 | await session.send("设置已保存") 82 | 83 | @on_command('mtrans',aliases=['mt','翻译','机翻'],only_to_me = False) 84 | async def mtrans(session: CommandSession): 85 | if not headdeal(session): 86 | return 87 | #message_type = session.event['message_type'] 88 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 89 | user_id = session.event['user_id'] 90 | logger.info(CQsessionToStr(session)) 91 | user_id = str(user_id) 92 | if user_id in mtransopt_list: 93 | engine_func = engine_list[engine_nick[mtransopt_list[user_id]['engine']]]['func'] 94 | res = engine_func(session.current_arg_text.strip(),mtransopt_list[user_id]['Source'],mtransopt_list[user_id]['Target']) 95 | else: 96 | res = default_engine(session.current_arg_text.strip()) 97 | await session.send("---翻译结果---\n" + res[1]) 98 | def engineListToStr(): 99 | global engine_list 100 | #("开" if MachineTransApi['google']['switch'] else "关") 101 | """ 102 | name = { 103 | 'nick':'引擎昵称',#用于展示(帮助列表) 104 | "switch":MachineTransApi['tencent']['switch'],#是否启用 105 | 'bucket':TokenBucket(5,10),#速率限制的桶(一秒获取5次机会,最高存储10次使用机会) 106 | ... 107 | } 108 | """ 109 | msg = '' 110 | for eg in engine_list.values(): 111 | msg = msg + "{nick}({switch}) ".format( 112 | nick = eg['option']['nick'], 113 | switch = ("开" if eg['option']['switch'] else "关") 114 | ) 115 | return msg 116 | @on_command('mtranshelp',aliases=['翻译帮助','机翻帮助'],only_to_me = False) 117 | async def mtranshelp(session: CommandSession): 118 | if not headdeal(session): 119 | return 120 | logger.info(CQsessionToStr(session)) 121 | msg = '--机器翻译帮助--' + "\n" 122 | msg = msg + '!翻译设置 引擎 源语言 目标语言 -设置翻译 可以在私聊设置' + "\n" 123 | msg = msg + '!翻译 翻译内容 - 翻译指定内容' + "\n" 124 | msg = msg + '引擎支持:' + engineListToStr() + "\n" 125 | msg = msg + '语言支持:中日英韩,源语言额外支持自动' + "\n" 126 | msg = msg + '翻译设置每人独立,源和目标语言设置时可忽略,默认为自动翻译到中文' + "\n" 127 | msg = msg + '如有疑问或bug报告可以!反馈 反馈内容 进行反馈' 128 | await session.send(msg) -------------------------------------------------------------------------------- /plugins/nonebotrequest.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession,RequestSession,on_request,permission as perm,message_preprocessor 2 | import aiocqhttp 3 | from nonebot import NoneBot 4 | 5 | import asyncio 6 | from helper import getlogger,msgSendToBot,CQsessionToStr,data_read,data_save,argDeal 7 | logger = getlogger(__name__) 8 | __plugin_name__ = '通知处理与功能附加' 9 | __plugin_usage__ = r""" 10 | 处理加群验证好友验证 退群信息等 11 | 同时添加部分bot操作功能 12 | """ 13 | 14 | # 将函数注册为群请求处理器 15 | @on_request('group') 16 | async def _(session: RequestSession): 17 | # 判断验证信息是否符合要求 18 | #if session.event.comment == '暗号': 19 | # # 验证信息正确,同意入群 20 | # await session.approve() 21 | # return 22 | ## 验证信息错误,拒绝入群 23 | #await session.reject('请说暗号') 24 | self_id = session.self_id 25 | user_id = session.event.user_id 26 | nick = str(user_id) 27 | if "nickname" in session.event.sender: 28 | nick = session.event.sender['nickname'] 29 | comment = session.event.comment 30 | msg = '来自 ' + str(self_id) + ' 的入群请求:' + "\n" 31 | msg = msg + "用户:" + nick + '(' + str(user_id) + ')' + "\n" 32 | msg = msg + "验证信息:" + comment 33 | msgSendToBot(logger,msg) 34 | -------------------------------------------------------------------------------- /plugins/permissiongroup.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession,permission as perm 2 | import asyncio 3 | from helper import getlogger,msgSendToBot,CQsessionToStr,data_read,data_save,argDeal 4 | from module.permissiongroup import perm_getPermList,perm_getPermGroupList,hasPermGroup,perm_getGroup,perm_check,perm_add,perm_del,legalPermissionList 5 | logger = getlogger(__name__) 6 | __plugin_name__ = '权限组' 7 | __plugin_usage__ = r""" 8 | 权限管理插件 9 | """ 10 | @on_command('legalGroupList',aliases=['合法权限组列表'],permission=perm.SUPERUSER,only_to_me = True) 11 | async def legalGroupList(session: CommandSession): 12 | message_type = session.event['message_type'] 13 | if message_type != 'private': 14 | return 15 | global legalPermissionList 16 | if legalPermissionList == []: 17 | await session.send("权限组列表为空!") 18 | return 19 | s = "权限组列表:" 20 | for key,permlist in legalPermissionList.items(): 21 | s = s + "\n" + key + "(" + permlist['name'] + ")" 22 | await session.send(s) 23 | @on_command('legalPermList',aliases=['合法权限列表'],permission=perm.SUPERUSER,only_to_me = True) 24 | async def legalPermList(session: CommandSession): 25 | message_type = session.event['message_type'] 26 | if message_type != 'private': 27 | return 28 | logger.info(CQsessionToStr(session)) 29 | arglimit = [ 30 | { 31 | 'name':'groupname', #参数名 32 | 'des':'权限组名', #参数错误描述 33 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 34 | 'strip':True, #是否strip 35 | 'lower':False, #是否转换为小写 36 | 'default':None, #默认值 37 | 'func':None, #函数,当存在时使用函数进行二次处理 38 | 're':None, #正则表达式匹配(match函数) 39 | 'vlimit':{ 40 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 41 | } 42 | } 43 | ] 44 | res = argDeal(session.current_arg_text.strip(),arglimit) 45 | if not res[0]: 46 | await session.send(res[1]+'=>'+res[2]) 47 | return 48 | args = res[1] 49 | permlist = perm_getGroup(args['groupname']) 50 | if permlist == None: 51 | await session.send("权限组不存在!") 52 | return 53 | s = "权限ID:" + permlist['name'] + "\n" + \ 54 | "描述:" + permlist['des'] + "\n" + \ 55 | "---权限列表---" 56 | for g in permlist['perms']: 57 | s = s + "\n" + g 58 | await session.send(s) 59 | 60 | 61 | def perm_GroupListToStr(grouplist): 62 | msg = "" 63 | for group in grouplist: 64 | msg = msg + group['groupname'] + ',' 65 | if group['info'] != None: 66 | msg = msg + group['info']['des'] 67 | else: 68 | msg = msg + "无描述" 69 | msg = msg + '\n' 70 | msg = msg + '总计:' + str(len(grouplist)) 71 | return msg 72 | @on_command('permGroupList',aliases=['权限组列表'],permission=perm.SUPERUSER,only_to_me = True) 73 | async def permgroupList(session: CommandSession): 74 | message_type = session.event['message_type'] 75 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 76 | user_id = session.event['user_id'] 77 | if message_type != 'private': 78 | return 79 | logger.info(CQsessionToStr(session)) 80 | arglimit = [ 81 | { 82 | 'name':'msgtype', #参数名 83 | 'des':'消息类型', #参数错误描述 84 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 85 | 'strip':True, #是否strip 86 | 'lower':True, #是否转换为小写 87 | 'default':message_type, #默认值 88 | 'func':None, #函数,当存在时使用函数进行二次处理 89 | 're':None, #正则表达式匹配(match函数) 90 | 'vlimit':{ 91 | #参数限制表(限制参数内容,空表则不限制),'*':''表示允许任意字符串,值不为空时任意字符串将被转变为这个值 92 | '私聊':'private', 93 | 'private':'private', 94 | '群聊':'group', 95 | 'group':'group', 96 | '好友':'private', 97 | '群':'group', 98 | } 99 | }, 100 | { 101 | 'name':'send_id', #参数名 102 | 'des':'对象ID', #参数错误描述 103 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 104 | 'strip':True, #是否strip 105 | 'lower':False, #是否转换为小写 106 | 'default':user_id, #默认值 107 | 'func':None, #函数,当存在时使用函数进行二次处理 108 | 're':None, #正则表达式匹配(match函数) 109 | 'vlimit':{ 110 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 111 | } 112 | } 113 | ] 114 | res = argDeal(session.current_arg_text.strip(),arglimit) 115 | if not res[0]: 116 | await session.send(res[1]+'=>'+res[2]) 117 | return 118 | args = res[1] 119 | res = perm_getPermGroupList(args['msgtype'],args['send_id']) 120 | if not res[0]: 121 | await session.send(res[1]) 122 | return 123 | s = perm_GroupListToStr(res[2]) 124 | await session.send(s) 125 | 126 | def perm_GroupToStr(groupname,PermList,info = None): 127 | msg = groupname 128 | if info == None: 129 | info = perm_getGroup(groupname) 130 | if info != None: 131 | msg = msg + '(' + info['des'] + ')' 132 | else: 133 | msg = msg + '(无描述)' 134 | msg = msg + "\n" 135 | for unit in PermList: 136 | msg = msg + ('+' if unit[:1] != '-' else '') + unit + "\n" 137 | msg = msg + '总计:' + str(len(PermList)) 138 | return msg 139 | @on_command('permList',aliases=['权限列表'],permission=perm.SUPERUSER,only_to_me = True) 140 | async def permList(session: CommandSession): 141 | message_type = session.event['message_type'] 142 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 143 | user_id = session.event['user_id'] 144 | if message_type != 'private': 145 | return 146 | #if perm_check(session,'-listener',user = True): 147 | # await session.send('操作被拒绝,权限不足(p)') 148 | # return 149 | logger.info(CQsessionToStr(session)) 150 | arglimit = [ 151 | { 152 | 'name':'groupname', #参数名 153 | 'des':'权限组名', #参数错误描述 154 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 155 | 'strip':True, #是否strip 156 | 'lower':False, #是否转换为小写 157 | 'default':None, #默认值 158 | 'func':None, #函数,当存在时使用函数进行二次处理 159 | 're':None, #正则表达式匹配(match函数) 160 | 'vlimit':{ 161 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 162 | } 163 | }, 164 | { 165 | 'name':'msgtype', #参数名 166 | 'des':'消息类型', #参数错误描述 167 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 168 | 'strip':True, #是否strip 169 | 'lower':True, #是否转换为小写 170 | 'default':message_type, #默认值 171 | 'func':None, #函数,当存在时使用函数进行二次处理 172 | 're':None, #正则表达式匹配(match函数) 173 | 'vlimit':{ 174 | #参数限制表(限制参数内容,空表则不限制),'*':''表示允许任意字符串,值不为空时任意字符串将被转变为这个值 175 | '私聊':'private', 176 | 'private':'private', 177 | '群聊':'group', 178 | 'group':'group', 179 | '好友':'private', 180 | '群':'group', 181 | } 182 | }, 183 | { 184 | 'name':'send_id', #参数名 185 | 'des':'对象ID', #参数错误描述 186 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 187 | 'strip':True, #是否strip 188 | 'lower':False, #是否转换为小写 189 | 'default':user_id, #默认值 190 | 'func':None, #函数,当存在时使用函数进行二次处理 191 | 're':None, #正则表达式匹配(match函数) 192 | 'vlimit':{ 193 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 194 | } 195 | } 196 | ] 197 | res = argDeal(session.current_arg_text.strip(),arglimit) 198 | if not res[0]: 199 | await session.send(res[1]+'=>'+res[2]) 200 | return 201 | args = res[1] 202 | res = perm_getPermList(args['msgtype'],args['send_id'],args['groupname']) 203 | if not res[0]: 204 | await session.send(res[1]) 205 | return 206 | s = perm_GroupToStr(args['groupname'],res[2]['permlist'],res[2]['info']) 207 | await session.send(s) 208 | 209 | @on_command('permAdd',aliases=['添加权限'],permission=perm.SUPERUSER,only_to_me = True) 210 | async def permAdd(session: CommandSession): 211 | message_type = session.event['message_type'] 212 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 213 | user_id = session.event['user_id'] 214 | if message_type != 'private': 215 | return 216 | #if perm_check(session,'-listener',user = True): 217 | # await session.send('操作被拒绝,权限不足(p)') 218 | # return 219 | logger.info(CQsessionToStr(session)) 220 | arglimit = [ 221 | { 222 | 'name':'groupname', #参数名 223 | 'des':'权限组名', #参数错误描述 224 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 225 | 'strip':True, #是否strip 226 | 'lower':False, #是否转换为小写 227 | 'default':None, #默认值 228 | 'func':None, #函数,当存在时使用函数进行二次处理 229 | 're':None, #正则表达式匹配(match函数) 230 | 'vlimit':{ 231 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 232 | } 233 | }, 234 | { 235 | 'name':'perm_unit', #参数名 236 | 'des':'权限组名', #参数错误描述 237 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 238 | 'strip':True, #是否strip 239 | 'lower':False, #是否转换为小写 240 | 'default':'', #默认值 241 | 'func':None, #函数,当存在时使用函数进行二次处理 242 | 're':None, #正则表达式匹配(match函数) 243 | 'vlimit':{ 244 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 245 | } 246 | }, 247 | { 248 | 'name':'msgtype', #参数名 249 | 'des':'消息类型', #参数错误描述 250 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 251 | 'strip':True, #是否strip 252 | 'lower':True, #是否转换为小写 253 | 'default':message_type, #默认值 254 | 'func':None, #函数,当存在时使用函数进行二次处理 255 | 're':None, #正则表达式匹配(match函数) 256 | 'vlimit':{ 257 | #参数限制表(限制参数内容,空表则不限制),'*':''表示允许任意字符串,值不为空时任意字符串将被转变为这个值 258 | '私聊':'private', 259 | 'private':'private', 260 | '群聊':'group', 261 | 'group':'group', 262 | '好友':'private', 263 | '群':'group', 264 | } 265 | }, 266 | { 267 | 'name':'send_id', #参数名 268 | 'des':'对象ID', #参数错误描述 269 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 270 | 'strip':True, #是否strip 271 | 'lower':False, #是否转换为小写 272 | 'default':user_id, #默认值 273 | 'func':None, #函数,当存在时使用函数进行二次处理 274 | 're':None, #正则表达式匹配(match函数) 275 | 'vlimit':{ 276 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 277 | } 278 | } 279 | ] 280 | res = argDeal(session.current_arg_text.strip(),arglimit) 281 | if not res[0]: 282 | await session.send(res[1]+'=>'+res[2]) 283 | return 284 | args = res[1] 285 | if args['perm_unit'] == '': 286 | args['perm_unit'] = None 287 | 288 | if perm_check(args['msgtype'],args['send_id'],args['groupname'],args['perm_unit']): 289 | await session.send("该权限已存在!") 290 | return 291 | res = perm_add(args['msgtype'],args['send_id'],user_id,args['groupname'],args['perm_unit']) 292 | await session.send(res[1]) 293 | 294 | @on_command('permDel',aliases=['移除权限'],permission=perm.SUPERUSER,only_to_me = True) 295 | async def permDel(session: CommandSession): 296 | message_type = session.event['message_type'] 297 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 298 | user_id = session.event['user_id'] 299 | if message_type != 'private': 300 | return 301 | #if perm_check(session,'-listener',user = True): 302 | # await session.send('操作被拒绝,权限不足(p)') 303 | # return 304 | logger.info(CQsessionToStr(session)) 305 | arglimit = [ 306 | { 307 | 'name':'groupname', #参数名 308 | 'des':'权限组名', #参数错误描述 309 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 310 | 'strip':True, #是否strip 311 | 'lower':False, #是否转换为小写 312 | 'default':None, #默认值 313 | 'func':None, #函数,当存在时使用函数进行二次处理 314 | 're':None, #正则表达式匹配(match函数) 315 | 'vlimit':{ 316 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 317 | } 318 | }, 319 | { 320 | 'name':'perm_unit', #参数名 321 | 'des':'权限组名', #参数错误描述 322 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 323 | 'strip':True, #是否strip 324 | 'lower':False, #是否转换为小写 325 | 'default':'', #默认值 326 | 'func':None, #函数,当存在时使用函数进行二次处理 327 | 're':None, #正则表达式匹配(match函数) 328 | 'vlimit':{ 329 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 330 | } 331 | }, 332 | { 333 | 'name':'msgtype', #参数名 334 | 'des':'消息类型', #参数错误描述 335 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 336 | 'strip':True, #是否strip 337 | 'lower':True, #是否转换为小写 338 | 'default':message_type, #默认值 339 | 'func':None, #函数,当存在时使用函数进行二次处理 340 | 're':None, #正则表达式匹配(match函数) 341 | 'vlimit':{ 342 | #参数限制表(限制参数内容,空表则不限制),'*':''表示允许任意字符串,值不为空时任意字符串将被转变为这个值 343 | '私聊':'private', 344 | 'private':'private', 345 | '群聊':'group', 346 | 'group':'group', 347 | '好友':'private', 348 | '群':'group', 349 | } 350 | }, 351 | { 352 | 'name':'send_id', #参数名 353 | 'des':'对象ID', #参数错误描述 354 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 355 | 'strip':True, #是否strip 356 | 'lower':False, #是否转换为小写 357 | 'default':user_id, #默认值 358 | 'func':None, #函数,当存在时使用函数进行二次处理 359 | 're':None, #正则表达式匹配(match函数) 360 | 'vlimit':{ 361 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 362 | } 363 | } 364 | ] 365 | res = argDeal(session.current_arg_text.strip(),arglimit) 366 | if not res[0]: 367 | await session.send(res[1]+'=>'+res[2]) 368 | return 369 | args = res[1] 370 | if args['perm_unit'] == '': 371 | args['perm_unit'] = None 372 | 373 | if perm_check(args['msgtype'],args['send_id'],args['groupname'],args['perm_unit']): 374 | await session.send("该权限已存在!") 375 | return 376 | res = perm_del(args['msgtype'],args['send_id'],user_id,args['groupname'],args['perm_unit']) 377 | await session.send(res[1]) 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | -------------------------------------------------------------------------------- /plugins/tweettrans.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from nonebot import on_command, CommandSession,NoticeSession,on_notice,permission as perm 3 | from helper import getlogger,msgSendToBot,CQsessionToStr,TempMemory,argDeal,data_read,data_save 4 | from module.twitter import decode_b64,encode_b64,mintweetID 5 | from plugins.twitter import tweet_event_deal 6 | from module.tweettrans import TweetTrans,rate_limit_bucket 7 | import nonebot 8 | import time 9 | import asyncio 10 | import os 11 | import traceback 12 | import re 13 | import module.permissiongroup as permissiongroup 14 | import config 15 | logger = getlogger(__name__) 16 | __plugin_name__ = '烤推' 17 | __plugin_usage__ = r""" 18 | 烤推指令前端 19 | """ 20 | #线程池 21 | from concurrent.futures import ThreadPoolExecutor 22 | pool = ThreadPoolExecutor(max_workers=64,thread_name_prefix="trans_Threads") 23 | #烤推列表缓存 24 | trans_tmemory = TempMemory('trans_tmemory.json',limit=300,autoload=True,autosave=True) 25 | #烤推权限 26 | permgroupname = 'transtweet' 27 | permissiongroup.perm_addLegalPermGroup(__name__,'烤推模块',permgroupname) 28 | permissiongroup.perm_addLegalPermUnit(permgroupname,'switch') #烤推切换权限 29 | permissiongroup.perm_addLegalPermUnit(permgroupname,'trans') #烤推权限 30 | 31 | trans_img_path = os.path.join(config.trans_img_path,'transtweet','transimg','') 32 | 33 | transtemplate_filename = 'transtemplate.json' 34 | transtemplate = { 35 | #默认模版 36 | '0':'

翻译自日文

' 37 | } 38 | def loadTranstemplate(): 39 | global transtemplate 40 | res = data_read(transtemplate_filename) 41 | if res[0]: 42 | transtemplate = res[2] 43 | return res 44 | def transtemplateInit(): 45 | global transtemplate 46 | res = loadTranstemplate() 47 | if not res[0]: 48 | data_save(transtemplate_filename,transtemplate) 49 | transtemplateInit() 50 | def setTranstemplate(key,value): 51 | transtemplate[key] = value 52 | data_save(transtemplate_filename,transtemplate) 53 | 54 | def perm_check(session: CommandSession,permunit:str,Remotely:dict = None,user:bool = False): 55 | if Remotely != None: 56 | return permissiongroup.perm_check( 57 | Remotely['message_type'], 58 | Remotely['sent_id'], 59 | permgroupname, 60 | permunit 61 | ) 62 | elif user: 63 | return permissiongroup.perm_check( 64 | 'private', 65 | session.event['user_id'], 66 | permgroupname, 67 | permunit 68 | ) 69 | return permissiongroup.perm_check( 70 | session.event['message_type'], 71 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 72 | permgroupname, 73 | permunit 74 | ) 75 | def perm_del(session: CommandSession,permunit:str,Remotely:dict = None): 76 | if Remotely != None: 77 | return permissiongroup.perm_del( 78 | Remotely['message_type'], 79 | Remotely['sent_id'], 80 | Remotely['op_id'], 81 | permgroupname, 82 | permunit 83 | ) 84 | return permissiongroup.perm_del( 85 | session.event['message_type'], 86 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 87 | session.event['user_id'], 88 | permgroupname, 89 | permunit 90 | ) 91 | def perm_add(session: CommandSession,permunit:str,Remotely:dict = None): 92 | if Remotely != None: 93 | return permissiongroup.perm_add( 94 | Remotely['message_type'], 95 | Remotely['sent_id'], 96 | Remotely['op_id'], 97 | permgroupname, 98 | permunit 99 | ) 100 | return permissiongroup.perm_add( 101 | session.event['message_type'], 102 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 103 | session.event['user_id'], 104 | permgroupname, 105 | permunit 106 | ) 107 | 108 | #预处理 109 | def headdeal(session: CommandSession): 110 | if session.event['message_type'] == "group" and session.event.sub_type != 'normal': 111 | return False 112 | return True 113 | 114 | 115 | @on_command('transReloadTemplate',aliases=['重载烤推模版'], permission=perm.SUPERUSER,only_to_me = True) 116 | async def transReloadTemplate(session: CommandSession): 117 | if not headdeal(session): 118 | return 119 | res = loadTranstemplate() 120 | if res[0]: 121 | await session.send('重载成功') 122 | else: 123 | await session.send(res[1]) 124 | 125 | async def transswitch_group(session: CommandSession): 126 | if perm_check(session,'-switch',user = True): 127 | await session.send('操作被拒绝,权限不足(p)') 128 | return 129 | if perm_check(session,'-switch'): 130 | await session.send('操作被拒绝,权限不足(g)') 131 | return 132 | if perm_check(session,'*'): 133 | await session.send('操作无效,存在“*”权限') 134 | return 135 | if perm_check(session,'trans'): 136 | perm_del(session,'trans') 137 | await session.send('烤推授权关闭') 138 | else: 139 | perm_add(session,'trans') 140 | await session.send('烤推授权开启') 141 | async def transswitch_private(session: CommandSession): 142 | user_id = session.event['user_id'] 143 | arglimit = [ 144 | { 145 | 'name':'msgtype', #参数名 146 | 'des':'消息类型', #参数错误描述 147 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 148 | 'strip':True, #是否strip 149 | 'lower':True, #是否转换为小写 150 | 'default':None, #默认值 151 | 'func':None, #函数,当存在时使用函数进行二次处理 152 | 're':None, #正则表达式匹配(match函数) 153 | 'vlimit':{ 154 | #参数限制表(限制参数内容,空表则不限制),'*':''表示允许任意字符串,值不为空时任意字符串将被转变为这个值 155 | #'私聊':'private', 156 | #'private':'private', 157 | '群聊':'group', 158 | 'group':'group', 159 | #'好友':'private', 160 | '群':'group', 161 | } 162 | }, 163 | { 164 | 'name':'send_id', #参数名 165 | 'des':'对象ID', #参数错误描述 166 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 167 | 'strip':True, #是否strip 168 | 'lower':False, #是否转换为小写 169 | 'default':None, #默认值 170 | 'func':None, #函数,当存在时使用函数进行二次处理 171 | 're':None, #正则表达式匹配(match函数) 172 | 'vlimit':{ 173 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 174 | } 175 | } 176 | ] 177 | res = argDeal(session.current_arg_text.strip(),arglimit) 178 | if not res[0]: 179 | await session.send(res[1]+'=>'+res[2]) 180 | return 181 | args = res[1] 182 | remote = { 183 | 'message_type':'group', 184 | 'sent_id':args['sendid'], 185 | 'op_id':user_id 186 | } 187 | if perm_check(session,'-switch'): 188 | await session.send('操作被拒绝,权限不足(p)') 189 | return 190 | if perm_check(session,'-switch',remote): 191 | await session.send('操作被拒绝,权限不足(g)') 192 | return 193 | if perm_check(session,'*',remote): 194 | await session.send('操作无效,存在“*”权限(g)') 195 | return 196 | if perm_check(session,'trans',remote): 197 | perm_del(session,'trans',remote) 198 | await session.send('烤推授权关闭') 199 | else: 200 | perm_add(session,'trans',) 201 | await session.send('烤推授权开启') 202 | @on_command('transswitch',aliases=['ts','烤推授权'], permission=perm.SUPERUSER,only_to_me = True) 203 | async def transswitch(session: CommandSession): 204 | if not headdeal(session): 205 | return 206 | message_type = session.event['message_type'] 207 | if message_type == 'group': 208 | await transswitch_group(session) 209 | else: 210 | await transswitch_private(session) 211 | 212 | 213 | def deal_trans(arg,ad) -> dict: 214 | trans = { 215 | 'type_html':'', 216 | 'source':arg, 217 | 'text':{} 218 | } 219 | tests = arg.split('##') 220 | if len(tests) == 1: 221 | kvc = tests[0].partition("#!") 222 | trans['text']['main'] = [] 223 | trans['text']['main'].append(kvc[0].strip()) 224 | if kvc[2] != '': 225 | trans['text']['main'].append(kvc[2].strip()) 226 | else: 227 | for test in tests: 228 | test = test.strip() 229 | if test == '': 230 | continue 231 | kv = re.findall(r'^([0-9]{1,2}|main|m)\s{1}(.+)',test,re.S) 232 | if kv == []: 233 | return None #格式不正确 234 | kv = list(kv[0]) 235 | if kv[0].isnumeric(): 236 | kv[0] = str(int(kv[0])) 237 | elif kv[0] == 'm': 238 | kv[0] = 'main' 239 | kvc = kv[1].partition("#!") 240 | trans['text'][kv[0]] = [] 241 | trans['text'][kv[0]].append(kvc[0].strip()) 242 | if kvc[2] != '': 243 | trans['text'][kv[0]].append(kvc[2].strip()) 244 | return trans 245 | def send_msg(session: CommandSession,msg): 246 | session.bot.sync.send_msg(self_id=session.self_id,group_id=session.event['group_id'],message=msg) 247 | def send_res(session: CommandSession,args): 248 | global transtemplate 249 | group_id =session.event['group_id'] 250 | user_id = session.event['user_id'] 251 | tweet_id = args['tweet_id'] 252 | trans = args['trans'] 253 | try: 254 | #使用64进制减少长度 255 | tasktype = encode_b64(int(time.time()),offset = 0) 256 | type_html = transtemplate['0'] 257 | if str(group_id) in transtemplate: 258 | type_html = transtemplate[str(group_id)] 259 | trans['type_html'] = type_html 260 | 261 | #检查推文缓存 262 | tweet_sname = 's' 263 | tweet = tweet_event_deal.tryGetTweet(tweet_id) 264 | if tweet != None: 265 | logger.info('检测到缓存:' + tweet['id_str'] + '(' + tweet['user']['screen_name'] + ')') 266 | #logger.info(tweet) 267 | tweet_cache = tweet 268 | tweet_sname = tweet_cache['user']['screen_name'] 269 | 270 | tt = TweetTrans() 271 | res = tt.getTransFromTweetID( 272 | str(tweet_id), 273 | args['trans'], 274 | tweet_sname, 275 | encode_b64(group_id,offset=0)+'-'+str(tasktype) 276 | ) 277 | if res[0]: 278 | time.sleep(1) 279 | if 'nickname' in session.event.sender: 280 | nick = session.event.sender['nickname'] 281 | else: 282 | nick = str(user_id) 283 | 284 | trans_tmemory.join({ 285 | 'id':tweet_id, 286 | 'group':group_id, 287 | 'mintrans':trans['source'][0:15].replace("\n"," "), 288 | 'tweetid':encode_b64(tweet_id), 289 | 'tasktype':tasktype, 290 | 'trans':trans, 291 | 'op':user_id, 292 | 'opnick':nick 293 | }) 294 | send_msg(session, 295 | trans_img_path + encode_b64(group_id,offset=0)+'-'+str(tasktype) + '.png' +"\n" + \ 296 | str('[CQ:image,timeout=' + config.img_time_out + \ 297 | ',file='+trans_img_path + encode_b64(group_id,offset=0)+'-'+str(tasktype) + '.png' + ']') + "\n"\ 298 | "使用 !tl 查看烤推历史" 299 | ) 300 | else: 301 | send_msg(session,"错误,"+res[2]) 302 | del tt 303 | except: 304 | s = traceback.format_exc(limit=10) 305 | logger.error(s) 306 | send_msg(session,"错误,烤推服务异常!") 307 | @on_command('trans',aliases=['t','烤推'], permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_OWNER | perm.GROUP,only_to_me = False) 308 | async def trans(session: CommandSession): 309 | if not headdeal(session): 310 | return 311 | message_type = session.event['message_type'] 312 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 313 | #user_id = session.event['user_id'] 314 | if message_type != 'group': 315 | return 316 | if perm_check(session,'-switch',user = True): 317 | await session.send('操作被拒绝,权限不足(p)') 318 | return 319 | if not perm_check(session,'trans'): 320 | await session.send('操作未授权') 321 | return 322 | logger.info(CQsessionToStr(session)) 323 | if not rate_limit_bucket.consume(1): 324 | await session.send("烤推繁忙,请稍后再试") 325 | return 326 | def checkTweetId(a,ad): 327 | if a[:1] == '#': 328 | ta = a[1:] 329 | if not ta.isdecimal(): 330 | return None 331 | res = mintweetID.find(lambda item,val:item[1]==val,int(ta)) 332 | if res == None: 333 | return None 334 | return res[0] 335 | elif a.isdecimal() and int(a) > 1253881609540800000: 336 | return a 337 | else: 338 | res = decode_b64(a) 339 | if res == -1: 340 | return None 341 | return res 342 | arglimit = [ 343 | { 344 | 'name':'tweet_id', #参数名 345 | 'des':'推特ID', #参数错误描述 346 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 347 | 'strip':True, #是否strip 348 | 'lower':False, #是否转换为小写 349 | 'default':None, #默认值 350 | 'func':checkTweetId, #函数,当存在时使用函数进行二次处理 351 | 're':None, #正则表达式匹配(match函数) 352 | 'vlimit':{ 353 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 354 | } 355 | },{ 356 | 'name':'trans', #参数名 357 | 'des':'翻译内容', #参数错误描述 358 | 'type':'dict', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 359 | 'strip':True, #是否strip 360 | 'lower':False, #是否转换为小写 361 | 'default':{ 362 | 'type_html':'', 363 | 'source':'', 364 | 'text':{} 365 | }, #默认值 366 | 'func':deal_trans, #函数,当存在时使用函数进行二次处理 367 | 're':None, #正则表达式匹配(match函数) 368 | 'vlimit':{ 369 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 370 | } 371 | } 372 | ] 373 | args = argDeal(session.current_arg_text.strip(),arglimit) 374 | if not args[0]: 375 | await session.send(args[1] + '=>' + args[2]) 376 | return 377 | pool.submit(send_res,session,args[1]) 378 | await session.send("图片合成中...") 379 | 380 | def getlist(groupid:int,page:int=1): 381 | ttm = trans_tmemory.tm.copy() 382 | length = len(ttm) 383 | cout = 0 384 | s = "昵称,任务标识,推文标识,翻译简写\n" 385 | for i in range(length - 1,-1,-1): 386 | if ttm[i]['group'] == groupid: 387 | if cout >= (page-1)*5 and cout < (page)*5: 388 | s = s + str(ttm[i]['opnick'] if ttm[i]['opnick'] else ttm[i]['op']) + ',' + ttm[i]['tasktype'] + ',' + ttm[i]['tweetid'] + ',' + ttm[i]['mintrans'] + "\n" 389 | cout = cout + 1 390 | totalpage = (cout)//5 + (0 if cout%5 == 0 else 1) 391 | s = s + '页数:'+str(page)+'/'+str(totalpage)+'总记录数:'+str(cout) + '\n' 392 | s = s + '使用!tgt 任务标识 获取指定任务图片' + "\n" 393 | s = s + '使用!gt 推文标识 获取指定推文最后的译文图片' 394 | return s 395 | @on_command('translist',aliases=['tl','烤推列表'], permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_OWNER | perm.GROUP,only_to_me = False) 396 | async def translist(session: CommandSession): 397 | if not headdeal(session): 398 | return 399 | message_type = session.event['message_type'] 400 | group_id = (session.event['group_id'] if message_type == 'group' else None) 401 | #user_id = session.event['user_id'] 402 | if message_type != 'group': 403 | return 404 | if perm_check(session,'-switch',user = True): 405 | await session.send('操作被拒绝,权限不足(p)') 406 | return 407 | if not perm_check(session,'trans'): 408 | await session.send('操作未授权') 409 | return 410 | logger.info(CQsessionToStr(session)) 411 | arglimit = [ 412 | { 413 | 'name':'page', #参数名 414 | 'des':'页码', #参数错误描述 415 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 416 | 'strip':True, #是否strip 417 | 'lower':False, #是否转换为小写 418 | 'default':1, #默认值 419 | 'func':None, #函数,当存在时使用函数进行二次处理 420 | 're':None, #正则表达式匹配(match函数) 421 | 'vlimit':{ 422 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 423 | } 424 | } 425 | ] 426 | args = argDeal(session.current_arg_text.strip(),arglimit) 427 | if not args[0]: 428 | await session.send(args[1] + '=>' + args[2]) 429 | return 430 | args = args[1] 431 | page = args['page'] 432 | if page < 1: 433 | await session.send("页码不能为负") 434 | return 435 | s = getlist(group_id,page) 436 | await session.send(s) 437 | 438 | @on_command('gettrans',aliases=['gt','获取翻译'], permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_OWNER | perm.GROUP,only_to_me = False) 439 | async def gettrans(session: CommandSession): 440 | if not headdeal(session): 441 | return 442 | message_type = session.event['message_type'] 443 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 444 | #user_id = session.event['user_id'] 445 | if message_type != 'group': 446 | return 447 | if perm_check(session,'-switch',user = True): 448 | await session.send('操作被拒绝,权限不足(p)') 449 | return 450 | if not perm_check(session,'trans'): 451 | await session.send('操作未授权') 452 | return 453 | logger.info(CQsessionToStr(session)) 454 | def checkTweetId(a,ad): 455 | if a[:1] == '#': 456 | ta = a[1:] 457 | if not ta.isdecimal(): 458 | return None 459 | res = mintweetID.find(lambda item,val:item[1]==val,int(ta)) 460 | if res == None: 461 | return None 462 | return res[0] 463 | elif a.isdecimal() and int(a) > 1253881609540800000: 464 | return a 465 | else: 466 | res = decode_b64(a) 467 | if res == -1: 468 | return None 469 | return res 470 | arglimit = [ 471 | { 472 | 'name':'tweet_id', #参数名 473 | 'des':'推特ID', #参数错误描述 474 | 'type':'int', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 475 | 'strip':True, #是否strip 476 | 'lower':False, #是否转换为小写 477 | 'default':None, #默认值 478 | 'func':checkTweetId, #函数,当存在时使用函数进行二次处理 479 | 're':None, #正则表达式匹配(match函数) 480 | 'vlimit':{ 481 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 482 | } 483 | } 484 | ] 485 | args = argDeal(session.current_arg_text.strip(),arglimit) 486 | if not args[0]: 487 | await session.send(args[1] + '=>' + args[2]) 488 | return 489 | args = args[1] 490 | tweet_id = args['tweet_id'] 491 | ttm = trans_tmemory.tm.copy() 492 | length = len(ttm) 493 | for i in range(length - 1,-1,-1): 494 | if ttm[i]['id'] == tweet_id: 495 | await session.send(trans_img_path + encode_b64(ttm[i]['group'],offset=0)+'-'+str(ttm[i]['tasktype']) + '.png' +"\n" + \ 496 | str('[CQ:image,timeout=' + config.img_time_out + \ 497 | ',file='+trans_img_path + encode_b64(ttm[i]['group'],offset=0)+'-'+str(ttm[i]['tasktype']) + '.png' + ']')) 498 | return 499 | await session.send("未查找到推文翻译") 500 | 501 | @on_command('typeGettrans',aliases=['tgt'], permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_OWNER | perm.GROUP,only_to_me = False) 502 | async def typeGettrans(session: CommandSession): 503 | if not headdeal(session): 504 | return 505 | message_type = session.event['message_type'] 506 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 507 | #user_id = session.event['user_id'] 508 | if message_type != 'group': 509 | return 510 | if perm_check(session,'-switch',user = True): 511 | await session.send('操作被拒绝,权限不足(p)') 512 | return 513 | if not perm_check(session,'trans'): 514 | await session.send('操作未授权') 515 | return 516 | logger.info(CQsessionToStr(session)) 517 | arg = session.current_arg_text.strip() 518 | if arg == '': 519 | await session.send('缺少参数') 520 | return 521 | ttm = trans_tmemory.tm.copy() 522 | length = len(ttm) 523 | for i in range(length - 1,-1,-1): 524 | if ttm[i]['tasktype'] == arg: 525 | await session.send(trans_img_path + encode_b64(ttm[i]['group'],offset=0)+'-'+str(ttm[i]['tasktype']) + '.png' +"\n" + \ 526 | str('[CQ:image,timeout=' + config.img_time_out + \ 527 | ',file='+trans_img_path + encode_b64(ttm[i]['group'],offset=0)+'-'+str(ttm[i]['tasktype']) + '.png' + ']')) 528 | return 529 | await session.send("未查找到推文翻译") 530 | 531 | @on_command('transabout',aliases=['ta','烤推帮助'],only_to_me = False) 532 | async def transabout(session: CommandSession): 533 | if not headdeal(session): 534 | return 535 | message_type = session.event['message_type'] 536 | if message_type != 'group': 537 | return 538 | res = perm_check(session,'trans') 539 | logger.info(CQsessionToStr(session)) 540 | msg = '当前版本为烤推机测试版V2.33' + "\n" + \ 541 | '授权状态:' + ("已授权" if res else "未授权") + "\n" + \ 542 | '!ts -切换烤推授权' + "\n" + \ 543 | '!t 推文ID 翻译 -合成翻译' + "\n" + \ 544 | '!tl -已翻译推文列表' + "\n" + \ 545 | '!gt 推文ID/推文标识 -获取最后翻译' + "\n" + \ 546 | '!tgt 任务标识 -获取指定翻译' + "\n" + \ 547 | '!gtt 推文ID/推文标识 -获取指定推文内容' + "\n" + \ 548 | '多层回复翻译:' + "\n" + \ 549 | '##1 第一层翻译' + "\n" + \ 550 | '#! 第一层层内推文(转推并评论类型里的内嵌推文)' + "\n" + \ 551 | '##2 第二层翻译' + "\n" + \ 552 | '##main 主翻译' + "\n" + \ 553 | '烤推支持换行参数,如有需要可以更换翻译自日文到任意图片或文字' + "\n" + \ 554 | '如果出现问题可以 !反馈 反馈内容 反馈信息' 555 | await session.send(msg) -------------------------------------------------------------------------------- /plugins/twitterListener/RSShub.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # -*- coding: UTF-8 -*- 3 | from nonebot import on_command, CommandSession, permission as perm 4 | from helper import getlogger,msgSendToBot,CQsessionToStr,argDeal 5 | from tweepy import TweepError 6 | import module.permissiongroup as permissiongroup 7 | from module.twitter import push_list 8 | import module.RSShub_twitter as tweetListener 9 | import traceback 10 | import re 11 | import asyncio 12 | import os 13 | import config 14 | logger = getlogger(__name__) 15 | """ 16 | 包含了推特API特有命令 17 | """ 18 | __plugin_name__ = 'RSShub监测特有命令' 19 | __plugin_usage__ = r""" 20 | 用于配置推特监听及调用RSShub监测 21 | 详见: 22 | https://github.com/chenxuan353/tweetToQQbot 23 | """ 24 | permgroupname = 'tweetListener' 25 | def perm_check(session: CommandSession,permunit:str,Remotely:dict = None,user:bool = False): 26 | if Remotely != None: 27 | return permissiongroup.perm_check( 28 | Remotely['message_type'], 29 | Remotely['sent_id'], 30 | permgroupname, 31 | permunit 32 | ) 33 | elif user: 34 | return permissiongroup.perm_check( 35 | 'private', 36 | session.event['user_id'], 37 | permgroupname, 38 | permunit 39 | ) 40 | return permissiongroup.perm_check( 41 | session.event['message_type'], 42 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 43 | permgroupname, 44 | permunit 45 | ) 46 | def perm_del(session: CommandSession,permunit:str,Remotely:dict = None): 47 | if Remotely != None: 48 | return permissiongroup.perm_del( 49 | Remotely['message_type'], 50 | Remotely['sent_id'], 51 | Remotely['op_id'], 52 | permgroupname, 53 | permunit 54 | ) 55 | return permissiongroup.perm_del( 56 | session.event['message_type'], 57 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 58 | session.event['user_id'], 59 | permgroupname, 60 | permunit 61 | ) 62 | def perm_add(session: CommandSession,permunit:str,Remotely:dict = None): 63 | if Remotely != None: 64 | return permissiongroup.perm_add( 65 | Remotely['message_type'], 66 | Remotely['sent_id'], 67 | Remotely['op_id'], 68 | permgroupname, 69 | permunit 70 | ) 71 | return permissiongroup.perm_add( 72 | session.event['message_type'], 73 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 74 | session.event['user_id'], 75 | permgroupname, 76 | permunit 77 | ) 78 | 79 | #预处理 80 | def headdeal(session: CommandSession): 81 | if session.event['message_type'] == "group" and session.event.sub_type != 'normal': 82 | return False 83 | return True 84 | 85 | @on_command('switchTweetListener',aliases=['切换监听'], permission=perm.SUPERUSER,only_to_me = False) 86 | async def switchTweetListener(session: CommandSession): 87 | if not headdeal(session): 88 | return 89 | await asyncio.sleep(0.2) 90 | if tweetListener.run_info['keepRun']: 91 | tweetListener.run_info['keepRun'] = False 92 | await session.send('监听已停止...') 93 | 94 | else: 95 | tweetListener.run_info['keepRun'] = True 96 | await session.send('监听已启动...') 97 | logger.info(CQsessionToStr(session)) 98 | 99 | @on_command('delone',aliases=['我不想D了','俺不想D了'],permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_ADMIN | perm.GROUP_OWNER,only_to_me = True) 100 | async def delOne(session: CommandSession): 101 | if not headdeal(session): 102 | return 103 | message_type = session.event['message_type'] 104 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 105 | #user_id = session.event['user_id'] 106 | if perm_check(session,'-listener',user=True): 107 | await session.send('操作被拒绝,权限不足(p)') 108 | return 109 | if message_type == 'group': 110 | if not perm_check(session,'listener'): 111 | await session.send('操作被拒绝,权限不足(g)') 112 | return 113 | else: 114 | await session.send('未收录的消息类型:'+message_type) 115 | return 116 | logger.info(CQsessionToStr(session)) 117 | 118 | stripped_arg = session.current_arg_text.strip() 119 | if stripped_arg == '': 120 | await session.send("在?为什么看别的女人连单推的名字都忘了写?") 121 | return 122 | if re.match('[A-Za-z0-9_]+$', stripped_arg, flags=0) == None: 123 | await session.send("用户名/用户ID 只能包含字母、数字或下划线") 124 | return 125 | #获取数据 126 | res=tweetListener.tweet_event_deal.getUserInfo(stripped_arg) 127 | if not res[0]: 128 | await session.send("查询不到信息,不愧是你(๑´ㅂ`๑)") 129 | return 130 | userinfo = res[1] 131 | res = push_list.delPushunitFromPushToAndTweetUserID( 132 | message_type, 133 | session.event[('group_id' if session.event['message_type'] == 'group' else 'user_id')], 134 | userinfo['id'] 135 | ) 136 | s = '标识:'+ str(userinfo['id']) + "\n" + \ 137 | '用户ID:' + userinfo['screen_name'] + "\n" + \ 138 | '用户昵称:' + userinfo['name'] + "\n" + \ 139 | '头像:' + '[CQ:image,timeout='+config.img_time_out+',file=' + userinfo['profile_image_url_https'] + ']'+ "\n" + \ 140 | ('已经从监听列表中叉出去了哦' if res[0] == True else '移除失败了Σ(゚д゚lll):'+res[1]) 141 | push_list.savePushList() 142 | logger.info(CQsessionToStr(session)) 143 | await session.send(s) 144 | 145 | @on_command('addone',aliases=['给俺D一个'],permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_ADMIN | perm.GROUP_OWNER,only_to_me = True) 146 | async def addOne(session: CommandSession): 147 | if not headdeal(session): 148 | return 149 | message_type = session.event['message_type'] 150 | group_id = (session.event['group_id'] if message_type == 'group' else None) 151 | user_id = session.event['user_id'] 152 | if perm_check(session,'-listener',user=True): 153 | await session.send('操作被拒绝,权限不足(p)') 154 | return 155 | sent_id = 0 156 | if message_type == 'private': 157 | sent_id = user_id 158 | elif message_type == 'group': 159 | if not perm_check(session,'listener'): 160 | await session.send('操作被拒绝,权限不足(g)') 161 | return 162 | sent_id = group_id 163 | else: 164 | await session.send('未收录的消息类型:'+message_type) 165 | return 166 | logger.info(CQsessionToStr(session)) 167 | 168 | arglimit = [ 169 | { 170 | 'name':'tweet_user_id', #参数名 171 | 'des':'推特用户ID', #参数错误描述 172 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 173 | 'strip':True, #是否strip 174 | 'lower':False, #是否转换为小写 175 | 'default':None, #默认值 176 | 'func':None, #函数,当存在时使用函数进行二次处理 177 | 're':'[A-Za-z0-9_]+$', #正则表达式匹配(match函数) 178 | 'vlimit':{ 179 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 180 | } 181 | }, 182 | { 183 | 'name':'nick', #参数名 184 | 'des':'昵称', #参数错误描述 185 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 186 | 'strip':True, #是否strip 187 | 'lower':False, #是否转换为小写 188 | 'default':'', #默认值 189 | 'func':None, #函数,当存在时使用函数进行二次处理 190 | 're':r'[\s\S]{0,50}', #正则表达式匹配(match函数) 191 | 'vlimit':{ 192 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 193 | } 194 | }, 195 | { 196 | 'name':'des', #参数名 197 | 'des':'描述', #参数错误描述 198 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 199 | 'strip':True, #是否strip 200 | 'lower':False, #是否转换为小写 201 | 'default':'', #默认值 202 | 'func':None, #函数,当存在时使用函数进行二次处理 203 | 're':r'[\s\S]{0,100}', #正则表达式匹配(match函数) 204 | 'vlimit':{ 205 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 206 | } 207 | } 208 | ] 209 | args = argDeal(session.current_arg_text.strip(),arglimit) 210 | if not args[0]: 211 | await session.send(args[1] + '=>' + args[2]) 212 | return 213 | args = args[1] 214 | tweet_user_id = args['tweet_user_id'] 215 | #获取数据 216 | res=tweetListener.tweet_event_deal.getUserInfo(tweet_user_id) 217 | if not res[0]: 218 | await session.send("查询不到信息,你D都能D歪来!?(・_・;?") 219 | return 220 | userinfo = res[1] 221 | 222 | nick = args['nick'] 223 | des = args['des'] 224 | if des == '': 225 | des = userinfo['name']+'('+userinfo['screen_name']+')' 226 | 227 | PushUnit = push_list.baleToPushUnit( 228 | session.event['self_id'], 229 | message_type,sent_id, 230 | userinfo['id'], 231 | user_id,user_id, 232 | des, 233 | nick = nick 234 | ) 235 | res = push_list.addPushunit(PushUnit) 236 | s = '标识:'+ str(userinfo['id']) + "\n" + \ 237 | '用户ID:' + userinfo['screen_name'] + "\n" + \ 238 | '用户昵称:' + userinfo['name'] + "\n" + \ 239 | '头像:' + '[CQ:image,timeout='+config.img_time_out+',file=' + userinfo['profile_image_url_https'] + ']'+ "\n" + \ 240 | ('已经加入了DD名单了哦' if res[0] == True else '添加失败:'+res[1]) 241 | push_list.savePushList() 242 | await session.send(s) 243 | -------------------------------------------------------------------------------- /plugins/twitterListener/twitterApi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from nonebot import on_command, CommandSession, permission as perm 3 | from helper import getlogger,msgSendToBot,CQsessionToStr,argDeal 4 | from tweepy import TweepError 5 | import module.permissiongroup as permissiongroup 6 | from module.pollingTwitterApi import ptwitterapps 7 | from plugins.twitter import tweetListener,push_list 8 | import traceback 9 | import re 10 | import asyncio 11 | import os 12 | import config 13 | logger = getlogger(__name__) 14 | """ 15 | 包含了推特API特有命令 16 | """ 17 | __plugin_name__ = '推特API特有命令' 18 | __plugin_usage__ = r""" 19 | 用于配置推特监听及调用推特API 20 | 详见: 21 | https://github.com/chenxuan353/tweetToQQbot 22 | """ 23 | 24 | permgroupname = 'tweetListener' 25 | def perm_check(session: CommandSession,permunit:str,Remotely:dict = None,user:bool = False): 26 | if Remotely != None: 27 | return permissiongroup.perm_check( 28 | Remotely['message_type'], 29 | Remotely['sent_id'], 30 | permgroupname, 31 | permunit 32 | ) 33 | elif user: 34 | return permissiongroup.perm_check( 35 | 'private', 36 | session.event['user_id'], 37 | permgroupname, 38 | permunit 39 | ) 40 | return permissiongroup.perm_check( 41 | session.event['message_type'], 42 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 43 | permgroupname, 44 | permunit 45 | ) 46 | def perm_del(session: CommandSession,permunit:str,Remotely:dict = None): 47 | if Remotely != None: 48 | return permissiongroup.perm_del( 49 | Remotely['message_type'], 50 | Remotely['sent_id'], 51 | Remotely['op_id'], 52 | permgroupname, 53 | permunit 54 | ) 55 | return permissiongroup.perm_del( 56 | session.event['message_type'], 57 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 58 | session.event['user_id'], 59 | permgroupname, 60 | permunit 61 | ) 62 | def perm_add(session: CommandSession,permunit:str,Remotely:dict = None): 63 | if Remotely != None: 64 | return permissiongroup.perm_add( 65 | Remotely['message_type'], 66 | Remotely['sent_id'], 67 | Remotely['op_id'], 68 | permgroupname, 69 | permunit 70 | ) 71 | return permissiongroup.perm_add( 72 | session.event['message_type'], 73 | (session.event['group_id'] if session.event['message_type'] == 'group' else session.event['user_id']), 74 | session.event['user_id'], 75 | permgroupname, 76 | permunit 77 | ) 78 | 79 | 80 | #预处理 81 | def headdeal(session: CommandSession): 82 | if session.event['message_type'] == "group" and session.event.sub_type != 'normal': 83 | return False 84 | return True 85 | 86 | @on_command('runTweetListener',aliases=['启动监听'], permission=perm.SUPERUSER,only_to_me = False) 87 | async def runTweetListener(session: CommandSession): 88 | if not headdeal(session): 89 | return 90 | await asyncio.sleep(0.2) 91 | if tweetListener.run_info['isRun']: 92 | await session.send('推特监听仍在运行中,不可以二次启动的哦ヽ(`Д´)ノ') 93 | return 94 | tweetListener.setStreamOpen(True) 95 | await session.send('监听启动中...') 96 | logger.info(CQsessionToStr(session)) 97 | 98 | @on_command('stopTweetListener',aliases=['停止监听'], permission=perm.SUPERUSER,only_to_me = False) 99 | async def stopTweetListener(session: CommandSession): 100 | if not headdeal(session): 101 | return 102 | await asyncio.sleep(0.2) 103 | if not tweetListener.run_info['isRun']: 104 | await session.send('推特监听处于停止状态!') 105 | return 106 | tweetListener.setStreamOpen(False) 107 | await session.send('监听已关闭') 108 | logger.info(CQsessionToStr(session)) 109 | 110 | 111 | #获取监听错误列表 112 | def get_tweeterrorlist(page:int): 113 | table = tweetListener.run_info['errorlist'].tm 114 | msg = "错误原因,错误代码(-1表示非API错误)" + "\n" 115 | unit_cout = 0 116 | for i in range(len(table)-1,-1,-1): 117 | if unit_cout >= (page-1)*5 and unit_cout < (page)*5: 118 | msg = msg + table[i][1] + ',' + (str(table[i][2]) if (len(table[i])>2) else '-1') + '\n' 119 | unit_cout = unit_cout + 1 120 | totalpage = unit_cout//5 + (0 if (unit_cout%5 == 0) else 1) 121 | if unit_cout > 5 or page != 1: 122 | msg = msg + '页数:' + str(page) + '/' + str(totalpage) + ' ' 123 | msg = msg + '错误记录数:' + str(unit_cout) 124 | return msg 125 | @on_command('gettweeterrorlist',aliases=['监听错误列表'],permission=perm.SUPERUSER,only_to_me = False) 126 | async def tweeallpushlist(session: CommandSession): 127 | if not headdeal(session): 128 | return 129 | if 'errorlist' not in tweetListener.run_info: 130 | await session.send("错误列表不存在") 131 | return 132 | await asyncio.sleep(0.1) 133 | page = 1 134 | stripped_arg = session.current_arg_text.strip().lower() 135 | if stripped_arg != '': 136 | if not stripped_arg.isdecimal(): 137 | await session.send("参数似乎有点不对劲?请再次检查o( ̄▽ ̄)o") 138 | return 139 | page = int(stripped_arg) 140 | if page < 1: 141 | await session.send("参数似乎有点不对劲?请再次检查o( ̄▽ ̄)o") 142 | return 143 | s = get_tweeterrorlist(page) 144 | await session.send(s) 145 | logger.info(CQsessionToStr(session)) 146 | 147 | @on_command('getuserinfo',aliases=['查询推特用户'],permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_ADMIN | perm.GROUP_OWNER,only_to_me = True) 148 | async def getuserinfo(session: CommandSession): 149 | if not headdeal(session): 150 | return 151 | message_type = session.event['message_type'] 152 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 153 | #user_id = session.event['user_id'] 154 | if perm_check(session,'-listener',user=True): 155 | await session.send('操作被拒绝,权限不足(p)') 156 | return 157 | if message_type == 'group': 158 | if not perm_check(session,'listener'): 159 | await session.send('操作被拒绝,权限不足(g)') 160 | return 161 | elif message_type != 'private': 162 | await session.send('未收录的消息类型:'+message_type) 163 | return 164 | logger.info(CQsessionToStr(session)) 165 | 166 | arglimit = [ 167 | { 168 | 'name':'tweet_user_id', #参数名 169 | 'des':'推特用户ID', #参数错误描述 170 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 171 | 'strip':True, #是否strip 172 | 'lower':False, #是否转换为小写 173 | 'default':None, #默认值 174 | 'func':None, #函数,当存在时使用函数进行二次处理 175 | 're':'[A-Za-z0-9_]+$', #正则表达式匹配(match函数) 176 | 're_error':'用户名/用户ID 只能包含字母、数字或下划线', 177 | 'vlimit':{ 178 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 179 | } 180 | } 181 | ] 182 | args = argDeal(session.current_arg_text.strip(),arglimit) 183 | if not args[0]: 184 | await session.send(args[1] + '=>' + args[2]) 185 | return 186 | args = args[1] 187 | tweet_user_id = args['tweet_user_id'] 188 | 189 | app = ptwitterapps.getAllow('users_show') 190 | if app == None: 191 | await session.send("速率限制,请稍后再试") 192 | return 193 | if tweet_user_id.isdecimal(): 194 | res = app.users_show(user_id = int(tweet_user_id)) 195 | else: 196 | res = app.users_show(screen_name = tweet_user_id) 197 | if not res[0]: 198 | await session.send("查询不到这位V哦~复制都能弄歪来┐(゚~゚)┌") 199 | userinfo = res[1] 200 | #检测信息更新 201 | tweetListener.tweet_event_deal.get_userinfo(userinfo,True) 202 | #tweetListener.tweet_event_deal.seve_image(userinfo.screen_name,userinfo.profile_image_url_https,'userinfo',canCover=True) 203 | #file_suffix = os.path.splitext(userinfo.profile_image_url_https)[1] 204 | #'头像:' + '[CQ:image,timeout='+config.img_time_out+',file='+config.img_path+'userinfo/' + userinfo.screen_name + file_suffix + ']'+ "\n" + \ 205 | s = '用户UID:'+ str(userinfo.id) + "\n" + \ 206 | '用户ID:' + userinfo.screen_name + "\n" + \ 207 | '用户昵称:' + userinfo.name + "\n" + \ 208 | '头像:' + '[CQ:image,timeout='+config.img_time_out+',file=' + userinfo.profile_image_url_https + ']'+ "\n" + \ 209 | '描述:' + userinfo.description + "\n" + \ 210 | '推文受保护:' + str(userinfo.protected) + "\n" + \ 211 | '被关注数:' + str(userinfo.followers_count) + "\n" + \ 212 | '关注数:' + str(userinfo.friends_count) + "\n" + \ 213 | '发推数(包括转发):' + str(userinfo.statuses_count) + "\n" + \ 214 | '账户创建时间:' + str(userinfo.created_at) 215 | logger.info(CQsessionToStr(session)) 216 | await session.send(s) 217 | 218 | @on_command('delone',aliases=['我不想D了','俺不想D了'],permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_ADMIN | perm.GROUP_OWNER,only_to_me = True) 219 | async def delOne(session: CommandSession): 220 | if not headdeal(session): 221 | return 222 | message_type = session.event['message_type'] 223 | #group_id = (session.event['group_id'] if message_type == 'group' else None) 224 | #user_id = session.event['user_id'] 225 | if perm_check(session,'-listener',user=True): 226 | await session.send('操作被拒绝,权限不足(p)') 227 | return 228 | if message_type == 'group': 229 | if not perm_check(session,'listener'): 230 | await session.send('操作被拒绝,权限不足(g)') 231 | return 232 | elif message_type != 'private': 233 | await session.send('未收录的消息类型:'+message_type) 234 | return 235 | logger.info(CQsessionToStr(session)) 236 | arglimit = [ 237 | { 238 | 'name':'tweet_user_id', #参数名 239 | 'des':'推特用户ID', #参数错误描述 240 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 241 | 'strip':True, #是否strip 242 | 'lower':False, #是否转换为小写 243 | 'default':None, #默认值 244 | 'func':None, #函数,当存在时使用函数进行二次处理 245 | 're':'[A-Za-z0-9_]+$', #正则表达式匹配(match函数) 246 | 're_error':'用户名/用户ID 只能包含字母、数字或下划线', 247 | 'vlimit':{ 248 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 249 | } 250 | } 251 | ] 252 | args = argDeal(session.current_arg_text.strip(),arglimit) 253 | if not args[0]: 254 | await session.send(args[1] + '=>' + args[2]) 255 | return 256 | args = args[1] 257 | tweet_user_id = args['tweet_user_id'] 258 | 259 | app = ptwitterapps.getAllow('users_show') 260 | if app == None: 261 | await session.send("速率限制,请稍后再试") 262 | return 263 | if tweet_user_id.isdecimal(): 264 | res = tweetListener.tweet_event_deal.tryGetUserInfo(user_id=int(tweet_user_id)) 265 | if res == {}: 266 | res = app.users_show(user_id = int(tweet_user_id)) 267 | if res[0]: 268 | res = list(res) 269 | res[1] = tweetListener.tweet_event_deal.get_userinfo(res[1]) 270 | else: 271 | res = (True,res) 272 | else: 273 | res = tweetListener.tweet_event_deal.tryGetUserInfo(screen_name = tweet_user_id) 274 | if res == {}: 275 | res = app.users_show(screen_name = tweet_user_id) 276 | if res[0]: 277 | res = list(res) 278 | res[1] = tweetListener.tweet_event_deal.get_userinfo(res[1]) 279 | else: 280 | res = (True,res) 281 | if not res[0]: 282 | await session.send("查询不到这位V哦~复制都能弄歪来┐(゚~゚)┌") 283 | userinfo = res[1] 284 | 285 | #tweetListener.tweet_event_deal.seve_image(userinfo.screen_name,userinfo.profile_image_url_https,'userinfo') 286 | #file_suffix = os.path.splitext(userinfo.profile_image_url_https)[1] 287 | #'头像:' + '[CQ:image,timeout='+config.img_time_out+',file='+config.img_path+'userinfo/' + userinfo.screen_name + file_suffix + ']'+ "\n" + \ 288 | res = push_list.delPushunitFromPushToAndTweetUserID( 289 | session.event['message_type'], 290 | session.event[('group_id' if session.event['message_type'] == 'group' else 'user_id')], 291 | userinfo['id'] 292 | ) 293 | s = '用户UID:'+ str(userinfo['id']) + "\n" + \ 294 | '用户ID:' + userinfo['screen_name'] + "\n" + \ 295 | '用户昵称:' + userinfo['name'] + "\n" + \ 296 | '头像:' + '[CQ:image,timeout='+config.img_time_out+',file=' + userinfo['profile_image_url_https'] + ']'+ "\n" + \ 297 | ('已经从监听列表中叉出去了哦' if res[0] == True else '移除失败了Σ(゚д゚lll):'+res[1]) 298 | push_list.savePushList() 299 | await session.send(s) 300 | 301 | @on_command('addone',aliases=['给俺D一个'],permission=perm.SUPERUSER | perm.PRIVATE_FRIEND | perm.GROUP_ADMIN | perm.GROUP_OWNER,only_to_me = True) 302 | async def addOne(session: CommandSession): 303 | if not headdeal(session): 304 | return 305 | message_type = session.event['message_type'] 306 | group_id = (session.event['group_id'] if message_type == 'group' else None) 307 | user_id = session.event['user_id'] 308 | if perm_check(session,'-listener',user=True): 309 | await session.send('操作被拒绝,权限不足(p)') 310 | return 311 | sent_id = 0 312 | if message_type == 'private': 313 | sent_id = user_id 314 | elif message_type == 'group': 315 | if not perm_check(session,'listener'): 316 | await session.send('操作被拒绝,权限不足(g)') 317 | return 318 | sent_id = group_id 319 | elif message_type != 'private': 320 | await session.send('未收录的消息类型:'+message_type) 321 | return 322 | arglimit = [ 323 | { 324 | 'name':'tweet_user_id', #参数名 325 | 'des':'推特用户ID', #参数错误描述 326 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 327 | 'strip':True, #是否strip 328 | 'lower':False, #是否转换为小写 329 | 'default':None, #默认值 330 | 'func':None, #函数,当存在时使用函数进行二次处理 331 | 're':'[A-Za-z0-9_]+$', #正则表达式匹配(match函数) 332 | 're_error':'用户名/用户ID 只能包含字母、数字或下划线', 333 | 'vlimit':{ 334 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 335 | } 336 | }, 337 | { 338 | 'name':'nick', #参数名 339 | 'des':'昵称', #参数错误描述 340 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 341 | 'strip':True, #是否strip 342 | 'lower':False, #是否转换为小写 343 | 'default':'', #默认值 344 | 'func':None, #函数,当存在时使用函数进行二次处理 345 | 're':r'[\s\S]{0,50}', #正则表达式匹配(match函数) 346 | 'vlimit':{ 347 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 348 | } 349 | }, 350 | { 351 | 'name':'des', #参数名 352 | 'des':'描述', #参数错误描述 353 | 'type':'str', #参数类型int float str list dict (list与dict需要使用函数或正则表达式进行二次处理) 354 | 'strip':True, #是否strip 355 | 'lower':False, #是否转换为小写 356 | 'default':'', #默认值 357 | 'func':None, #函数,当存在时使用函数进行二次处理 358 | 're':r'[\s\S]{0,100}', #正则表达式匹配(match函数) 359 | 'vlimit':{ 360 | #参数限制表(限制参数内容,空表则不限制),'*':''表示匹配任意字符串,值不为空时任意字符串将被转变为这个值 361 | } 362 | } 363 | ] 364 | args = argDeal(session.current_arg_text.strip(),arglimit) 365 | if not args[0]: 366 | await session.send(args[1] + '=>' + args[2]) 367 | return 368 | args = args[1] 369 | tweet_user_id = args['tweet_user_id'] 370 | app = ptwitterapps.getAllow('users_show') 371 | if app == None: 372 | await session.send("速率限制,请稍后再试") 373 | return 374 | if tweet_user_id.isdecimal(): 375 | res = tweetListener.tweet_event_deal.tryGetUserInfo(user_id=int(tweet_user_id)) 376 | if res == {}: 377 | res = app.users_show(user_id = int(tweet_user_id)) 378 | if res[0]: 379 | res = list(res) 380 | res[1] = tweetListener.tweet_event_deal.get_userinfo(res[1]) 381 | else: 382 | res = (True,res) 383 | else: 384 | res = tweetListener.tweet_event_deal.tryGetUserInfo(screen_name = tweet_user_id) 385 | if res == {}: 386 | res = app.users_show(screen_name = tweet_user_id) 387 | if res[0]: 388 | res = list(res) 389 | res[1] = tweetListener.tweet_event_deal.get_userinfo(res[1]) 390 | else: 391 | res = (True,res) 392 | if not res[0]: 393 | await session.send("查询不到这位V哦~复制都能弄歪来┐(゚~゚)┌") 394 | userinfo = res[1] 395 | 396 | nick = args['nick'] 397 | des = args['des'] 398 | if des == '': 399 | des = userinfo['name']+'('+userinfo['screen_name']+')' 400 | PushUnit = push_list.baleToPushUnit( 401 | session.event['self_id'], 402 | message_type,sent_id, 403 | userinfo['id'], 404 | user_id,user_id, 405 | des, 406 | nick = nick 407 | ) 408 | res = push_list.addPushunit(PushUnit) 409 | s = '用户UID:'+ str(userinfo['id']) + "\n" + \ 410 | '用户ID:' + userinfo['screen_name'] + "\n" + \ 411 | '用户昵称:' + userinfo['name'] + "\n" + \ 412 | '头像:' + '[CQ:image,timeout='+config.img_time_out+',file=' + userinfo['profile_image_url_https'] + ']'+ "\n" + \ 413 | ('已经加入了DD名单了哦' if res[0] == True else '添加失败:'+res[1]) 414 | push_list.savePushList() 415 | await session.send(s) 416 | 417 | -------------------------------------------------------------------------------- /plugins/zhuaba.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession 2 | import asyncio 3 | import json 4 | import random 5 | # on_command 装饰器将函数声明为一个命令处理器 6 | @on_command('爪巴',only_to_me = False) 7 | async def pa(session: CommandSession): 8 | # stripped_arg = session.current_arg_text.strip() 9 | await asyncio.sleep(0.2) 10 | str = ['我爬 我现在就爬Orz','我爪巴','你给爷爬OuO','呜呜呜别骂了 再骂BOT就傻了TAT','就不爬>_<','欺负可爱BOT 建议超级加倍TuT'] 11 | index = random.randint(0,len(str)-1) 12 | await session.send(str[index]) 13 | 14 | # @on_command('图来',only_to_me = False) 15 | # async def _ (session:CommandSession): 16 | # await asyncio.sleep(0.2) 17 | # #await session.send('[CQ:]') 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 可提供一对多转推服务的bot后端 2 | [![License](https://img.shields.io/github/license/richardchien/nonebot.svg)](LICENSE)![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)![CQHTTP Version](https://img.shields.io/badge/cqhttp-4.8+-black.svg) 3 | 4 | ## 简介 5 | 6 | 本仓库主要依赖于模块[nonebot](https://github.com/nonebot/nonebot)和[tweepy](https://github.com/tweepy/tweepy),与CoolQ的通信依赖于CQHTTP; 7 | 8 | 特别鸣谢[richardchien](https://github.com/richardchien)对上述封装项目的贡献。 9 | 10 | **本项目目前仅支持 Python 3.7 及 CQHTTP 插件 v4.8+** 11 | 12 | 13 | 14 | 15 | 16 | --- 17 | 18 | *由于平台影响,本项目可能转移分离。或停止更新* 19 | 20 | --- 21 | 22 | 23 | 24 | 25 | 26 | ## 文档 27 | 28 | 原项目文档已停止维护,新文档(v3)参见[文档](https://bothbot-documentation.readthedocs.io/zh_CN/latest/) -------------------------------------------------------------------------------- /readme/image-20200428113806819.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxuan353/tweetToBot/1b930c4eb287bd2e8d9ebc5bbe0ef45fcd221a32/readme/image-20200428113806819.png -------------------------------------------------------------------------------- /readme/image-20200428113938381.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxuan353/tweetToBot/1b930c4eb287bd2e8d9ebc5bbe0ef45fcd221a32/readme/image-20200428113938381.png -------------------------------------------------------------------------------- /readme/image-20200428114040218.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenxuan353/tweetToBot/1b930c4eb287bd2e8d9ebc5bbe0ef45fcd221a32/readme/image-20200428114040218.png -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | requests==2.23.0 2 | aiocqhttp==1.2.5 3 | nonebot==1.6.0 4 | APScheduler==3.6.3 5 | selenium==3.141.0 6 | xmltodict==2020.5.8 7 | tweepy==3.8.0 8 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import os 3 | from os import path 4 | from helper import check_path,initNonebotLogger 5 | import sys 6 | import nonebot 7 | import module.twitterApi as tweetListener 8 | import module.RSShub_twitter as RSShub_twitter 9 | import module.pollingTwitterApi as pollingTwitterApi 10 | import traceback 11 | import time 12 | import asyncio 13 | import threading 14 | 15 | #配置 16 | import config 17 | #日志输出 18 | from helper import getlogger,msgSendToBot 19 | logger = getlogger('START') 20 | ''' 21 | nonebot封装的CQHTTP插件 22 | ''' 23 | #初始化文件夹 24 | base_path = 'cache' 25 | #推特监听对象 26 | runTweetListener = None 27 | runTweetPlugin = '' 28 | 29 | def init(): 30 | global runTweetListener,runTweetPlugin 31 | allow_start_method = { 32 | 'TweetApi':{ 33 | 'Listener':tweetListener.runTwitterApiThread, 34 | 'plugin':'plugins.twitterListener.twitterApi', 35 | }, 36 | 'PollingTweetApi':{ 37 | 'Listener':pollingTwitterApi.runPollingTwitterApiThread, 38 | 'plugin':'plugins.twitterListener.twitterApi', 39 | }, 40 | 'RSShub':{ 41 | 'Listener':RSShub_twitter.runTwitterListenerThread, 42 | 'plugin':'plugins.twitterListener.RSShub', 43 | }, 44 | 'Twint':{ 45 | 'Listener':tweetListener.runTwitterApiThread, 46 | 'plugin':'plugins.twitterListener.twitterApi', 47 | } 48 | } 49 | if config.UPDATA_METHOD not in allow_start_method: 50 | msg = '配置的更新检测(UPDATA_METHOD)方法不合法:'+str(config.UPDATA_METHOD) 51 | logger.critical(msg) 52 | raise Exception(msg) 53 | runTweetListener = allow_start_method[config.UPDATA_METHOD]['Listener'] 54 | runTweetPlugin = allow_start_method[config.UPDATA_METHOD]['plugin'] 55 | if runTweetListener == None: 56 | msg = '配置的更新检测(UPDATA_METHOD)方法未实现:'+str(config.UPDATA_METHOD) 57 | logger.critical(msg) 58 | raise Exception(msg) 59 | 60 | 61 | if __name__ == "__main__": 62 | #初始化 63 | init() 64 | #启动线程 65 | time.sleep(2) 66 | logger.info('启动推特流...') 67 | runTweetListener() 68 | logger.info('启动nonebot...') 69 | nonebot.init(config) 70 | nonebot.load_plugins( 71 | path.join(path.dirname(__file__), 'plugins'), 72 | 'plugins' 73 | ) 74 | nonebot.load_plugin(runTweetPlugin) #加载侦听对应的插件 75 | nonebot.run(host=config.NONEBOT_HOST, port = config.NONEBOT_PORT) #阻塞 76 | sys.exit() #退出程序(报错后可能存在非守护线程不退出的情况,所以主动退出) 77 | 78 | 79 | 80 | --------------------------------------------------------------------------------