async def _checkConnectClient( runId: int, tgSession: TgSession, phoneNumber: str ) -> typing.Tuple[typing.Union[None, dict], typing.Union[None, TelegramClient]]: client = TelegramClient(tgSession.getSessionPath(phoneNumber, noExt=True), novice.py_env['apiId'], novice.py_env['apiHash']) try: novice.logNeedle.push('(runId: {}) client.connect'.format(runId)) await client.connect() except telethon.errors.PhoneNumberBannedError as err: errMsg = 'The phone {} is Banned.'.format(phoneNumber) novice.logNeedle.push('(runId: {}) PhoneNumberBannedError: {}'.format( runId, errMsg)) _mvSessionPath(sessionPath, phoneNumber, 'telethon-banned-' + novice.py_env['apiId']) return ({ 'code': -3, 'message': errMsg, 'phoneNumber': phoneNumber }, None) except Exception as err: errMsg = novice.sysTracebackException() novice.logNeedle.push('(runId: {}) {}'.format(runId, errMsg)) _mvSessionPath(sessionPath, phoneNumber, 'telethon-rm-' + novice.py_env['apiId']) return ({ 'code': -3, 'message': errMsg, 'phoneNumber': phoneNumber }, None) return (None, client)
async def _paperSlipAction_send( pageId: str, code: int, message: str, ynError = False) -> None: payload = { 'type': 'adTool.paperSlipAction', 'code': code, 'message': message, } if ynError: errInfo = novice.sysExceptionInfo() errMsg = novice.sysTracebackException() payload['message'] += '\n{}'.format(errMsg) payload['error'] = { 'name': errInfo['name'], 'message': errInfo['message'], 'stackList': errInfo['stackList'], } await serverMix.wsHouse.send( pageId, json.dumps([payload]) )
async def asyncRun(args: list, _dirpy: str, _dirname: str): forwardPeersTxt = args[1] url = args[2] msg = args[3] mainGroup = novice.py_env['peers']['adChannle'] forwardPeers = forwardPeersTxt.split(',') # 用於打印日誌 runId = random.randrange(1000000, 9999999) usedClientCount = 3 latestStatus = '' try: latestStatus = '炸群進度: 初始化...' novice.logNeedle.push('(runId: {}) {}'.format(runId, latestStatus)) tgTool = TgDefaultInit(TgBaseTool, clientCount=3, papaPhone=novice.py_env['papaPhoneNumber']) await tgTool.init() except Exception as err: latestStatus += ' (失敗)' novice.logNeedle.push('(runId: {}) {}'.format(runId, latestStatus)) raise err try: latestStatus = '炸群進度: 上傳圖片...' novice.logNeedle.push('(runId: {}) {}'.format(runId, latestStatus)) messageId = await _sendFile(tgTool, mainGroup, url, msg) if messageId == -1: raise Exception( 'Use Papa send file fail. (url: {}, msg: {})'.format(url, msg)) except Exception as err: latestStatus += ' (失敗)' novice.logNeedle.push('(runId: {}) {}'.format(runId, latestStatus)) raise err try: finalPeers = _filterGuy(tgTool, forwardPeers) finalPeersLength = len(finalPeers) bandNiUserList = [] idx = 0 async for clientInfo in tgTool.iterPickClient(-1, 1, whichNiUsers=True): readableIdx = idx + 1 myId = clientInfo['id'] client = clientInfo['client'] if novice.indexOf(bandNiUserList, myId) != -1: if len(bandNiUserList) == usedClientCount: break continue if finalPeersLength <= idx: break latestStatus = '炸群進度: {}/{}'.format(readableIdx, finalPeersLength) novice.logNeedle.push('(runId: {}) ok: {}/{}'.format( runId, readableIdx, finalPeersLength)) try: forwardPeer = finalPeers[idx] await tgTool.joinGroup(client, forwardPeer) await client( telethon.functions.messages.ForwardMessagesRequest( from_peer=mainGroup, id=[messageId], to_peer=forwardPeer, random_id=[tgTool.getRandId()])) idx += 1 except telethon.errors.ChannelsTooMuchError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) # 已加入了太多的渠道/超級群組。 novice.logNeedle.push( '(runId: {}) {} get ChannelsTooMuchError: wait 30 day.'. format(runId, myId)) maturityDate = novice.dateNowAfter(days=30) tgTool.chanDataNiUsers.pushBandData(myId, maturityDate) bandNiUserList.append(myId) except telethon.errors.FloodWaitError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) waitTimeSec = err.seconds novice.logNeedle.push( '(runId: {}) {} get FloodWaitError: wait {} seconds.'. format(runId, myId, waitTimeSec)) # TODO 秒數待驗證 if waitTimeSec < 180: await asyncio.sleep(waitTimeSec) else: maturityDate = novice.dateNowAfter(seconds=waitTimeSec) tgTool.chanDataNiUsers.pushBandData(myId, maturityDate) bandNiUserList.append(myId) except telethon.errors.PeerFloodError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) # 限制發送請求 Too many requests novice.logNeedle.push( '(runId: {}) {} get PeerFloodError: wait 1 hour.'.format( runId, myId)) # TODO 12 小時只是估計值 maturityDate = novice.dateNowAfter(hours=12) tgTool.chanDataNiUsers.pushBandData(myId, maturityDate) bandNiUserList.append(myId) except Exception as err: print(novice.sysTracebackException(ysHasTimestamp=True)) errType = type(err) novice.logNeedle.push( '(runId: {}) {} get {} Error: {} (target group: {})'. format(runId, myId, errType, err, forwardPeer)) if novice.indexOf(_invalidMessageErrorTypeList, errType) != -1: novice.logNeedle.push( 'Invalid Message Error({}): {}'.format(errType, err)) break elif novice.indexOf(_invalidPeerErrorTypeList, errType) != -1: novice.logNeedle.push('Invalid Peer Error({}): {}'.format( errType, err)) idx += 1 elif novice.indexOf(_knownErrorTypeList, errType) != -1: novice.logNeedle.push('Known Error({}): {}'.format( errType, err)) idx += 1 bandNiUserList.append(myId) else: novice.logNeedle.push('Unknown Error({}): {}'.format( type(err), err)) idx += 1 bandNiUserList.append(myId) latestStatus += ' ({})'.format('仿用戶用盡' if len(bandNiUserList) == usedClientCount else '結束') novice.logNeedle.push('(runId: {}) {}'.format(runId, latestStatus)) except Exception as err: latestStatus += ' (失敗)' novice.logNeedle.push('(runId: {}) {}'.format(runId, latestStatus)) raise err
async def asyncRun(args: list, _dirpy: str, _dirname: str): if len(args) < 5: raise ValueError( 'Usage: <phoneNumber> <loopTimes> <toGroupPeer> <forwardLink>') phoneNumber = args[1] loopTimes = int(args[2]) toGroupPeer = args[3] forwardLink = args[4] regexForwardLink = r'^https:\/\/t\.me\/([^\/]+)\/(\d+)$' matchForwardLink = re.search(regexForwardLink, forwardLink) if not matchForwardLink: raise ValueError('轉傳來源鏈結 "{}" 不如預期'.format(forwardLink)) forwardGroup = matchForwardLink.group(1) forwardMessageId = int(matchForwardLink.group(2)) tgTool = TgDefaultInit(TgSimple) print('-> 登入用戶') client = await tgTool.login(phoneNumber) myInfo = await client.get_me() print('--> I\m {} {} ({}) and my phone is +{}.'.format( str(myInfo.first_name), str(myInfo.last_name), str(myInfo.username), myInfo.phone, )) print('-> 加入聊天室') inputPeer = await client.get_entity(toGroupPeer) if type(inputPeer) != telethon.types.User: print('--> telethon.functions.channels.JoinChannelRequest') await client( telethon.functions.channels.JoinChannelRequest(channel=toGroupPeer) ) print('-> 紫爆轉傳 Hi') async for idx in tgTool.iterLoopInterval(loopTimes, 1): try: readableIdx = idx + 1 print('--> {}/{}: {} -> {}'.format(readableIdx, loopTimes, phoneNumber, toGroupPeer)) print('---> telethon.functions.messages.SendMessageRequest') await client( telethon.functions.messages.ForwardMessagesRequest( from_peer=forwardGroup, id=[forwardMessageId], to_peer=toGroupPeer, random_id=[tgTool.getRandId()])) except telethon.errors.FloodWaitError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) waitTimeSec = err.seconds print('FloodWaitError: wait {} seconds. {}'.format( waitTimeSec, err)) break except telethon.errors.PeerFloodError as err: # 限制發送請求 Too many requests print(novice.sysTracebackException(ysHasTimestamp=True)) print('PeerFloodError: {}'.format(err)) break except telethon.errors.UserIsBlockedError as err: # User is blocked print(novice.sysTracebackException(ysHasTimestamp=True)) print('UserIsBlockedError: {}'.format(err)) break except Exception as err: print(novice.sysTracebackException(ysHasTimestamp=True)) errType = type(err) if novice.indexOf(_invalidMessageErrorTypeList, errType) == -1: print('Invalid Error({}): {}'.format(errType, err)) elif novice.indexOf(_knownErrorTypeList, errType) == -1: print('Known Error({}): {}'.format(errType, err)) break else: print('Unknown Error({}): {}'.format(type(err), err)) break
async def asyncRun(args: list, _dirpy: str, _dirname: str): if len(args) < 4: raise ValueError('Usage: <phoneNumber> <loopTimes> <toGroupPeer>') phoneNumber = args[1] loopTimes = int(args[2]) toGroupPeer = args[3] tgTool = TgDefaultInit(TgSimple) print('-> 登入用戶') client = await tgTool.login(phoneNumber) myInfo = await client.get_me() print('--> I\m {} {} ({}) and my phone is +{}.'.format( str(myInfo.first_name), str(myInfo.last_name), str(myInfo.username), myInfo.phone, )) print('-> 加入聊天室') # TODO # `client.get_entity` 是一項昂貴的操作 # https://docs.telethon.dev/en/latest/modules/client.html#telethon.client.users.UserMethods.get_entity # 使用 `client.get_entity` 來解析用戶名是一項昂貴的操作, # 若在短時間內請求 50 個用戶名則會引發 "FloodWaitError" 的錯誤。 # 在實際測試中,在短時間內請求相同的群組名也僅有 200 次的許可。 (2020.02.19 紀錄) inputPeer = await client.get_entity(toGroupPeer) if type(inputPeer) != telethon.types.User: print('--> telethon.functions.channels.JoinChannelRequest') await client( telethon.functions.channels.JoinChannelRequest(channel=toGroupPeer) ) print('-> 紫爆 Hi') pushErrMsg = '' async for idx in tgTool.iterLoopInterval(loopTimes, 1): try: readableIdx = idx + 1 print('--> {}/{}: {} -> {}'.format(readableIdx, loopTimes, phoneNumber, toGroupPeer)) print('---> telethon.functions.messages.SendMessageRequest') # 在 1970 次請求後需等待 1324 秒 (2020.02.23 紀錄) await client( telethon.functions.messages.SendMessageRequest( peer=toGroupPeer, message='PurplePink {}{}'.format( readableIdx, '\n' + pushErrMsg if pushErrMsg != '' else ''), random_id=tgTool.getRandId())) pushErrMsg = '' except ValueError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) print('ValueError(沒有此用戶或群組名稱): {}'.format(err)) break except telethon.errors.MessageIdInvalidError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) print('MessageIdInvalidError: {}'.format(err)) raise err except telethon.errors.FloodWaitError as err: print(novice.sysTracebackException(ysHasTimestamp=True)) waitTimeSec = err.seconds # TODO 印象中有短秒數的 FloodWaitError 錯誤 print('FloodWaitError: {}'.format(err)) if waitTimeSec < 720: print('---> FloodWaitError: wait {} seconds.'.format( waitTimeSec)) await asyncio.sleep(waitTimeSec) pushErrMsg = 'FloodWaitError: wait {} seconds.' else: break except telethon.errors.PeerFloodError as err: # 限制發送請求 Too many requests print(novice.sysTracebackException(ysHasTimestamp=True)) print('PeerFloodError: {}'.format(err)) break except telethon.errors.ChatWriteForbiddenError as err: # You can't write in this chat print(novice.sysTracebackException(ysHasTimestamp=True)) print('ChatWriteForbiddenError: {}'.format(err)) break except Exception as err: print(novice.sysTracebackException(ysHasTimestamp=True)) print('{} Error: {} '.format(type(err), err)) break
async def _niGraph(pageId: str, receiveDatasTxt: str) -> None: try: receiveDatas = json.loads(receiveDatasTxt) # 相當於請求錯誤 if type(receiveDatas) != list: return resultDatas = [] for item in receiveDatas: resultData = {'type': ''} try: if not 'type' in item: raise KeyError('wschan: 項目缺少必要的 "type" 成員') requestMethod = item['type'] resultData['type'] = requestMethod if type(requestMethod) != str: raise TypeError('wschan: 項目 "type" 成員的類型應為字串') matchWsMethod = re.search(_regexWsMethod, requestMethod) if matchWsMethod == None: raise ValueError('wschan: 項目 "type" 成員表示式格式錯誤') fileName = matchWsMethod.group(1) methodName = matchWsMethod.group(2) if novice.indexOf(_wsChannelList, fileName) == -1: raise KeyError('wschan: 要求的方法文件不存在') pyImportPath = _wsChannelDirPyImportPath + '.' + fileName # TODO 不知為 `importlib.import_module()` 何會拋出以下訊息 # Executing <Task pending name='Task-21' coro=<ASGIWebsocketConnection.handle_websocket() running at /home/bwaycer/ys/gitman/crepo/tgNiren.py/.venv/lib/python3.8/site-packages/quart/asgi.py:147> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f72d22eb250>()] created at /usr/lib/python3.8/asyncio/base_events.py:422> cb=[_wait.<locals>._on_completion() at /usr/lib/python3.8/asyncio/tasks.py:507] created at /home/bwaycer/ys/gitman/crepo/tgNiren.py/.venv/lib/python3.8/site-packages/quart/asgi.py:110> took 0.189 seconds module = importlib.import_module(pyImportPath) if not hasattr(module, methodName): raise KeyError('wschan: 要求的方法不存在') action = getattr(importlib.import_module(pyImportPath), methodName) if 'prop' in item: result = action(pageId, item['prop']) else: result = action(pageId) if asyncio.iscoroutine(result): result = await result # 測試是否可以編譯為 JSON (若其中包含 Python 的類型則會失敗) json.dumps(result) for key in result: resultData[key] = result[key] except Exception: errInfo = novice.sysExceptionInfo() resultData['error'] = { 'name': errInfo['name'], 'message': errInfo['message'], } novice.logNeedle.push('Catch error in ws.py\n' ' resultData:\n{}\n' ' errMsg:\n{}'.format( resultData, novice.sysTracebackException())) if resultData['type'] != '': resultDatas.append(resultData) await quart.websocket.send(json.dumps(resultDatas)) except Exception as err: raise err
async def sendCode(pageId: str, prop: typing.Any = None) -> dict: innerSession = serverMix.innerSession.get(pageId) if 'interactiveLogin' in innerSession: info = innerSession['interactiveLogin'] else: info = innerSession['interactiveLogin'] = { 'runId': random.randrange(1000000, 9999999), 'phoneNumber': '', 'sessionPath': '', 'sentCode': None, 'client': None, } if type(prop) != dict: return {'code': -1, 'message': '"prop" 參數必須是 `Object` 類型。'} if not ('phoneNumber' in prop and type(prop['phoneNumber']) == str): return {'code': -1, 'message': '"prop.phoneNumber" 參數不符合預期'} runId = info['runId'] phoneNumber = prop['phoneNumber'] novice.logNeedle.push('(runId: {}) login sendCode +{}'.format( runId, phoneNumber)) # 檢查是否該仿用戶已登入 novice.logNeedle.push( '(runId: {}) test with common session path'.format(runId)) tgSession = TgSession('telethon-' + novice.py_env['apiId']) sessionPath = tgSession.getSessionPath(phoneNumber) if os.path.exists(sessionPath): errInfo, client = await _checkConnectClient(runId, tgSession, phoneNumber) if client != None and await client.is_user_authorized(): await _disconnectClient(info, client) message = '+{} 仿用戶已登入'.format(phoneNumber) novice.logNeedle.push('(runId: {}) {}'.format(message)) return {'code': 1, 'message': message, 'phoneNumber': phoneNumber} # 使用暫時的 session 路徑進行登入 novice.logNeedle.push( '(runId: {}) login with temporary session path'.format(runId)) tgSession = TgSession('telethon-tmpLogin-' + novice.py_env['apiId']) errInfo, client = await _checkConnectClient(runId, tgSession, phoneNumber) if errInfo != None: return errInfo info['phoneNumber'] = phoneNumber info['sessionPath'] = tgSession.getSessionPath(phoneNumber) info['client'] = client # 如果已登入的狀態不該存在 "tmpLogin" 前綴的路徑下 # 所以無需測試 `client.is_user_authorized()` 方法 try: # https://docs.telethon.dev/en/latest/modules/client.html#telethon.client.auth.AuthMethods.send_code_request novice.logNeedle.push( '(runId: {}) client.send_code_request'.format(runId)) sentCode = await client.send_code_request(phoneNumber) phoneCodeHash = sentCode.phone_code_hash novice.logNeedle.push('(runId: {}) sentCode:\n' ' type: {}\n' ' phone_code_hash: {}\n' ' next_type: {}\n' ' timeout: {}'.format(runId, sentCode.type, phoneCodeHash, sentCode.next_type, sentCode.timeout)) info['sentCode'] = sentCode except Exception as err: typeName = err.__class__.__name__ errMsg = _sendCodeKnownErrorTypeInfo[typeName] \ if typeName in _sendCodeKnownErrorTypeInfo \ else novice.sysTracebackException() novice.logNeedle.push('(runId: {}) sentCode +{} Failed {}'.format( runId, phoneNumber, errMsg)) return {'code': -3, 'message': errMsg, 'phoneNumber': phoneNumber} novice.logNeedle.push('(runId: {}) 請查收驗證碼'.format(runId)) return { 'code': 2, 'message': '請查收驗證碼。', 'phoneNumber': phoneNumber, 'phoneCodeHash': phoneCodeHash, }
async def verifiedCode(pageId: str, prop: typing.Any = None) -> dict: innerSession = serverMix.innerSession.get(pageId) if not 'interactiveLogin' in innerSession: return {'code': -1, 'message': '沒有待登入的用戶。'} if type(prop) != dict: return {'code': -1, 'message': '"prop" 參數必須是 `Object` 類型。'} if not ('phoneNumber' in prop and type(prop['phoneNumber']) == str): return {'code': -1, 'message': '"prop.phoneNumber" 參數不符合預期'} if not ('phoneCodeHash' in prop and type(prop['phoneCodeHash']) == str): return {'code': -1, 'message': '"prop.phoneCodeHash" 參數不符合預期'} if not ('verifiedCode' in prop and type(prop['verifiedCode']) == str): return {'code': -1, 'message': '"prop.verifiedCode" 參數不符合預期'} info = innerSession['interactiveLogin'] runId = info['runId'] sessionPath = info['sessionPath'] sentCode = info['sentCode'] client = info['client'] phoneNumber = prop['phoneNumber'] phoneCodeHash = prop['phoneCodeHash'] verifiedCode = prop['verifiedCode'] if phoneNumber != info['phoneNumber'] \ and phoneCodeHash != info['sentCode'].phone_code_hash: errMsg = '登入的仿用戶 +{} 與主機留存仿用戶 +{} 不相同'.format(phoneNumber, info['phoneNumber']) novice.logNeedle.push('(runId: {}) {}'.format(runId, errMsg)) return { 'code': -2, 'message': errMsg, 'phoneNumber': info['phoneNumber'], 'anotherPhoneNumber': phoneNumber, } novice.logNeedle.push( '(runId: {}) login +{} with {} phoneCodeHash and {} verifiedCode'. format(runId, phoneNumber, phoneCodeHash, phoneNumber)) try: await client.sign_in(phoneNumber, verifiedCode, phone_code_hash=phoneCodeHash) message = '+{} 仿用戶登入成功'.format(phoneNumber) novice.logNeedle.push('(runId: {}) {}'.format(runId, message)) await _disconnectClient(info, client) _mvSessionPath(sessionPath, phoneNumber, 'telethon-' + novice.py_env['apiId']) novice.logNeedle.push( '(runId: {}) disconnect & mv finish'.format(runId)) return {'code': 3, 'message': message, 'phoneNumber': phoneNumber} except Exception as err: typeName = err.__class__.__name__ errMsg = _signInKnownErrorTypeInfo[typeName] \ if typeName in _signInKnownErrorTypeInfo \ else novice.sysTracebackException() novice.logNeedle.push('(runId: {}) login +{} Failed {}'.format( runId, phoneNumber, errMsg)) return {'code': -3, 'message': errMsg, 'phoneNumber': phoneNumber}