├── .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)
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 |
--------------------------------------------------------------------------------