From cde69d4d8ab653c29afee1db0b89c3224585ce52 Mon Sep 17 00:00:00 2001 From: flaskfarm Date: Fri, 7 Oct 2022 01:48:42 +0900 Subject: [PATCH] update --- files/notify.yaml.template | 23 + files/requirements.txt | 3 +- lib/framework/__init__.py | 4 +- lib/framework/init_declare.py | 4 +- lib/framework/init_main.py | 6 +- lib/framework/init_menu.py | 2 +- lib/framework/init_plugin.py | 101 +- lib/framework/log_viewer.py | 2 +- lib/framework/scheduler.py | 2 - lib/framework/static/js/ff_global1.js | 2 +- lib/plugin/create_plugin.py | 11 +- lib/plugin/route.py | 32 +- lib/support/__init__.py | 40 +- lib/support/base/__init__.py | 11 +- lib/support/base/file.py | 99 +- lib/support/base/string.py | 4 +- lib/{tool_base => support/base}/subprocess.py | 82 +- lib/support/base/telegram.py | 17 +- lib/support/telepot2/__init__.py | 1405 ----------------- lib/support/telepot2/aio/__init__.py | 926 ----------- lib/support/telepot2/aio/api.py | 168 -- lib/support/telepot2/aio/delegate.py | 106 -- lib/support/telepot2/aio/hack.py | 36 - lib/support/telepot2/aio/helper.py | 372 ----- lib/support/telepot2/aio/loop.py | 205 --- lib/support/telepot2/aio/routing.py | 46 - lib/support/telepot2/api.py | 164 -- lib/support/telepot2/delegate.py | 420 ----- lib/support/telepot2/exception.py | 111 -- lib/support/telepot2/filtering.py | 34 - lib/support/telepot2/hack.py | 16 - lib/support/telepot2/helper.py | 1170 -------------- lib/support/telepot2/loop.py | 313 ---- lib/support/telepot2/namedtuple.py | 865 ---------- lib/support/telepot2/routing.py | 223 --- lib/support/telepot2/text.py | 88 -- lib/system/logic_auth.py | 26 +- lib/system/logic_env.py | 23 +- lib/system/logic_plugin.py | 2 +- lib/system/mod_home.py | 2 +- lib/system/mod_plugin.py | 42 + lib/system/mod_setting.py | 59 +- lib/system/plugin.py | 34 +- lib/system/setup.py | 14 +- ...em_plugin.html => system_plugin_list.html} | 0 .../templates/system_plugin_setting.html | 30 + .../templates/system_setting_basic copy.html | 420 ----- .../templates/system_setting_basic.html | 110 +- .../templates/system_setting_notify.html | 143 +- lib/tool/__init__.py | 3 + lib/tool/notify.py | 119 +- lib/tool_base/__init__.py | 19 - lib/tool_base/ffmpeg.py | 2 +- lib/tool_base/file.py | 51 +- lib/tool_base/rclone.py | 14 +- 55 files changed, 523 insertions(+), 7703 deletions(-) create mode 100644 files/notify.yaml.template rename lib/{tool_base => support/base}/subprocess.py (50%) delete mode 100644 lib/support/telepot2/__init__.py delete mode 100644 lib/support/telepot2/aio/__init__.py delete mode 100644 lib/support/telepot2/aio/api.py delete mode 100644 lib/support/telepot2/aio/delegate.py delete mode 100644 lib/support/telepot2/aio/hack.py delete mode 100644 lib/support/telepot2/aio/helper.py delete mode 100644 lib/support/telepot2/aio/loop.py delete mode 100644 lib/support/telepot2/aio/routing.py delete mode 100644 lib/support/telepot2/api.py delete mode 100644 lib/support/telepot2/delegate.py delete mode 100644 lib/support/telepot2/exception.py delete mode 100644 lib/support/telepot2/filtering.py delete mode 100644 lib/support/telepot2/hack.py delete mode 100644 lib/support/telepot2/helper.py delete mode 100644 lib/support/telepot2/loop.py delete mode 100644 lib/support/telepot2/namedtuple.py delete mode 100644 lib/support/telepot2/routing.py delete mode 100644 lib/support/telepot2/text.py create mode 100644 lib/system/mod_plugin.py rename lib/system/templates/{system_plugin.html => system_plugin_list.html} (100%) create mode 100644 lib/system/templates/system_plugin_setting.html delete mode 100644 lib/system/templates/system_setting_basic copy.html create mode 100644 lib/tool/__init__.py diff --git a/files/notify.yaml.template b/files/notify.yaml.template new file mode 100644 index 0000000..73cba9e --- /dev/null +++ b/files/notify.yaml.template @@ -0,0 +1,23 @@ +# DEFAULT 필수 +# message id 별로 각각 알림 설정 추가 +# message id 가 없을 경우 DEFAULT 값 사용 +# 공통: type, enable_time (시작시간-종료시간. 항상 받을 경우 생략) + +- DEFAULT: + - type: 'telegram' + token: '' + chat_id: '' + disable_notification: false + enable_time: '09-23' + - type: 'discord' + webhook: '' + enable_time: '09-23' + + +- system_start: + - type: 'telegram' + token: '' + chat_id: '' + disable_notification: false + - type: 'discord' + webhook: '' diff --git a/files/requirements.txt b/files/requirements.txt index de7e22a..4ec8b89 100644 --- a/files/requirements.txt +++ b/files/requirements.txt @@ -19,4 +19,5 @@ pytz requests==2.26.0 discord-webhook pyyaml -pycryptodome \ No newline at end of file +pycryptodome +telepot-mod \ No newline at end of file diff --git a/lib/framework/__init__.py b/lib/framework/__init__.py index 131fa34..889962e 100644 --- a/lib/framework/__init__.py +++ b/lib/framework/__init__.py @@ -1,6 +1,4 @@ VERSION="4.0.0" -from support import d - from .init_main import Framework frame = Framework.get_instance() @@ -14,8 +12,8 @@ socketio = frame.socketio path_app_root = frame.path_app_root path_data = frame.path_data get_logger = frame.get_logger - from flask_login import login_required +from support import d from .init_declare import User, check_api from .scheduler import Job diff --git a/lib/framework/init_declare.py b/lib/framework/init_declare.py index 04dc3d2..c466309 100644 --- a/lib/framework/init_declare.py +++ b/lib/framework/init_declare.py @@ -45,7 +45,7 @@ class CustomFormatter(logging.Formatter): green = "\x1B[32m" # pathname filename #format = "[%(asctime)s|%(name)s|%(levelname)s - %(message)s (%(filename)s:%(lineno)d)" - format = '[{yellow}%(asctime)s{reset}|{color}%(levelname)s{reset}|{green}%(name)s{reset}|%(pathname)s:%(lineno)s] {color}%(message)s{reset}' + format = '[{yellow}%(asctime)s{reset}|{color}%(levelname)s{reset}|{green}%(name)s{reset} %(pathname)s:%(lineno)s] {color}%(message)s{reset}' FORMATS = { logging.DEBUG: format.format(color=grey, reset=reset, yellow=yellow, green=green), @@ -86,7 +86,7 @@ class User: return str(r) def can_login(self, passwd_hash): - from support.base.aes import SupportAES + from support import SupportAES tmp = SupportAES.decrypt(self.passwd_hash) return passwd_hash == tmp diff --git a/lib/framework/init_main.py b/lib/framework/init_main.py index 0bacd2d..175238b 100644 --- a/lib/framework/init_main.py +++ b/lib/framework/init_main.py @@ -56,6 +56,7 @@ class Framework: def __initialize(self): + os.environ['FF'] = "true" self.__config_initialize("first") self.__make_default_dir() @@ -155,13 +156,13 @@ class Framework: self.logger.error('CRITICAL db.create_all()!!!') self.logger.error(f'Exception:{str(e)}') self.logger.error(traceback.format_exc()) + self.SystemModelSetting = SystemInstance.ModelSetting SystemInstance.plugin_load() self.app.register_blueprint(SystemInstance.blueprint) self.config['flag_system_loading'] = True self.__config_initialize('member') self.__config_initialize('system_loading_after') - self.SystemModelSetting = SystemInstance.ModelSetting - + def initialize_plugin(self): from system.setup import P as SP @@ -222,6 +223,7 @@ class Framework: self.__load_config() self.__init_define() self.config['menu_yaml_filepath'] = os.path.join(self.config['path_data'], 'db', 'menu.yaml') + self.config['notify_yaml_filepath'] = os.path.join(self.config['path_data'], 'db', 'notify.yaml') elif mode == "flask": self.app.secret_key = os.urandom(24) #self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data/db/system.db?check_same_thread=False' diff --git a/lib/framework/init_menu.py b/lib/framework/init_menu.py index 3557133..0b2d769 100644 --- a/lib/framework/init_menu.py +++ b/lib/framework/init_menu.py @@ -1,7 +1,7 @@ import os import shutil -from support.base.yaml import SupportYaml +from support import SupportYaml from framework import F diff --git a/lib/framework/init_plugin.py b/lib/framework/init_plugin.py index 581eccd..8c0b596 100644 --- a/lib/framework/init_plugin.py +++ b/lib/framework/init_plugin.py @@ -1,7 +1,13 @@ import os +import platform +import shutil import sys import threading import traceback +import zipfile + +import requests +from support import SupportFile, SupportSubprocess, SupportYaml from framework import F @@ -139,7 +145,7 @@ class PluginManager: #logger.error(traceback.format_exc()) #mod_plugin_info = getattr(mod, 'setup') - F.logger.warning(f'[!] PLUGIN_INFO not exist : [{plugin_name}]') + F.logger.info(f'[!] PLUGIN_INFO not exist : [{plugin_name}] - is FF') if mod_plugin_info == None: try: mod = __import__(f'{plugin_name}.setup', fromlist=['setup']) @@ -296,3 +302,96 @@ class PluginManager: except Exception as e: F.logger.error(f'Exception:{str(e)}') F.logger.error(traceback.format_exc()) + + @classmethod + def plugin_install(cls, plugin_git, zip_url=None, zip_filename=None): + is_git = True if plugin_git != None and plugin_git != '' else False + ret = {} + try: + if is_git: + name = plugin_git.split('/')[-1] + else: + name = zip_filename.split('.')[0] + + plugin_all_path = os.path.join(F.config['path_data'], 'plugins') + plugin_path = os.path.join(plugin_all_path, name) + plugin_info = None + if os.path.exists(plugin_path): + ret['ret'] = 'danger' + ret['msg'] = '이미 설치되어 있습니다.' + ret['status'] = 'already_exist' + return ret + + if plugin_git and plugin_git.startswith('http'): + for tag in ['main', 'master']: + try: + info_url = plugin_git.replace('github.com', 'raw.githubusercontent.com') + f'/{tag}/info.yaml' + plugin_info = requests.get(info_url).json() + if plugin_info is not None: + break + except: + pass + + if zip_filename and zip_filename != '': + zip_filepath = os.path.join(F.config['path_data'], 'tmp', zip_filename) + extract_filepath = os.path.join(F.config['path_data'], 'tmp', name) + if SupportFile.download(zip_url, zip_filepath): + with zipfile.ZipFile(zip_filepath, 'r') as zip_ref: + zip_ref.extractall(extract_filepath) + plugin_info_filepath = os.path.join(extract_filepath, 'info.yaml') + if os.path.exists(plugin_info_filepath): + plugin_info = SupportYaml.read_yaml(plugin_info_filepath) + + if plugin_info == None: + plugin_info = {} + + flag = True + tmp = plugin_info.get('require_os', '') + if tmp != '' and type(tmp) == type([]) and platform.system() not in tmp: + ret['ret'] = 'danger' + ret['msg'] = '설치 가능한 OS가 아닙니다.' + ret['status'] = 'not_support_os' + flag = False + + tmp = plugin_info.get('require_running_type', '') + if tmp != '' and type(tmp) == type([]) and F.config['running_type'] not in tmp: + ret['ret'] = 'danger' + ret['msg'] = '설치 가능한 실행타입이 아닙니다.' + ret['status'] = 'not_support_running_type' + flag = False + + + if flag: + if plugin_git and plugin_git.startswith('http'): + command = ['git', '-C', plugin_all_path, 'clone', plugin_git + '.git', '--depth', '1'] + log = SupportSubprocess.execute_command_return(command) + if zip_filename and zip_filename != '': + + if os.path.exists(plugin_path) == False: + shutil.move(extract_filepath, plugin_path) + else: + for tmp in os.listdir(extract_filepath): + shutil.move(os.path.join(extract_filepath, tmp), plugin_path) + log = '' + + # 2021-12-31 + tmp = plugin_info.get('require_plugin', '') + if tmp != '' and type(tmp) == type([]) and len(tmp) > 0: + for need_plugin in plugin_info['require_plugin']: + if need_plugin['package_name'] in cls.plugin_init: + F.logger.debug(f"Dependency 설치 - 이미 설치됨 : {need_plugin['package_name']}") + continue + else: + F.logger.debug(f"Dependency 설치 : {need_plugin['package_name']}") + cls.plugin_install(need_plugin['home'], None, None) + + ret['ret'] = 'success' + ret['msg'] = ['정상적으로 설치하였습니다. 재시작시 적용됩니다.', log] + ret['msg'] = '
'.join(log) + + except Exception as e: + F.logger.error(f'Exception:{str(e)}') + F.logger.error(traceback.format_exc()) + ret['ret'] = 'danger' + ret['msg'] = str(e) + return ret diff --git a/lib/framework/log_viewer.py b/lib/framework/log_viewer.py index af1eb37..d8b63ee 100644 --- a/lib/framework/log_viewer.py +++ b/lib/framework/log_viewer.py @@ -4,7 +4,7 @@ import time import traceback from flask import request -from support.base.util import SingletonClass +from support import SingletonClass from framework import F diff --git a/lib/framework/scheduler.py b/lib/framework/scheduler.py index 280d462..faeb9ca 100644 --- a/lib/framework/scheduler.py +++ b/lib/framework/scheduler.py @@ -6,7 +6,6 @@ from random import randint from apscheduler.jobstores.base import JobLookupError from apscheduler.triggers.cron import CronTrigger from pytz import timezone -from support.base.util import pt class Scheduler(object): @@ -27,7 +26,6 @@ class Scheduler(object): self.sched.start() self.logger.info('SCHEDULER start..') - @pt def first_run_check_thread_function(self): try: #time.sleep(60) diff --git a/lib/framework/static/js/ff_global1.js b/lib/framework/static/js/ff_global1.js index 20ef948..50a77c1 100644 --- a/lib/framework/static/js/ff_global1.js +++ b/lib/framework/static/js/ff_global1.js @@ -128,7 +128,7 @@ $("body").on('click', '#globalEditBtn', function(e) { /////////////////////////////////////// function globalSendCommand(command, arg1, arg2, arg3, modal_title, callback) { - console.log("globalSendCommand [" + command + '] [' + arg1 + '] [' + arg2 + '] [' + arg3 + '] [' + modal_title + '] [' + callback); + console.log("globalSendCommand [" + command + '] [' + arg1 + '] [' + arg2 + '] [' + arg3 + '] [' + modal_title + '] [' + callback + ']'); console.log('/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command'); $.ajax({ diff --git a/lib/plugin/create_plugin.py b/lib/plugin/create_plugin.py index 1211524..a98b4e7 100644 --- a/lib/plugin/create_plugin.py +++ b/lib/plugin/create_plugin.py @@ -1,8 +1,13 @@ -import os, traceback +import os +import traceback + from flask import Blueprint from framework import F -from support.base.yaml import SupportYaml -from . import get_model_setting, Logic, default_route, default_route_single_module +from support import SupportYaml + +from . import (Logic, default_route, default_route_single_module, + get_model_setting) + class PluginBase(object): package_name = None diff --git a/lib/plugin/route.py b/lib/plugin/route.py index 74d05c5..97b5696 100644 --- a/lib/plugin/route.py +++ b/lib/plugin/route.py @@ -1,19 +1,11 @@ -# -*- coding: utf-8 -*- -# python -import traceback, os import json +import os +import traceback -# third-party -from flask import Blueprint, request, render_template, redirect, jsonify +from flask import jsonify, redirect, render_template, request from flask_login import login_required -from flask_socketio import SocketIO, emit, send - -# sjva 공용 -from framework import socketio, check_api -from support.base.util import AlchemyEncoder -# 패키지 - -######################################################### +from framework import F +from support import AlchemyEncoder def default_route(P): @@ -178,7 +170,7 @@ def default_route(P): ######################################################### # 단일 모듈인 경우 모듈이름을 붙이기 불편하여 추가. @P.blueprint.route('/api/', methods=['GET', 'POST']) - @check_api + @F.check_api def api_first(sub2): try: for module in P.module_list: @@ -188,7 +180,7 @@ def default_route(P): P.logger.error(traceback.format_exc()) @P.blueprint.route('/api//', methods=['GET', 'POST']) - @check_api + @F.check_api def api(sub, sub2): try: for module in P.module_list: @@ -284,7 +276,7 @@ def default_route_single_module(P): P.logger.error(traceback.format_exc()) @P.blueprint.route('/api/', methods=['GET', 'POST']) - @check_api + @F.check_api def api(sub): try: return P.module_list[0].process_api(sub, request) @@ -330,7 +322,7 @@ def default_route_socketio_module(module): if module.socketio_list is None: module.socketio_list = [] - @socketio.on('connect', namespace=f'/{P.package_name}/{module.name}') + @F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}') def connect(): try: P.logger.debug(f'socket_connect : {P.package_name} - {module.name}') @@ -342,7 +334,7 @@ def default_route_socketio_module(module): P.logger.error(traceback.format_exc()) - @socketio.on('disconnect', namespace='/{package_name}/{sub}'.format(package_name=P.package_name, sub=module.name)) + @F.socketio.on('disconnect', namespace='/{package_name}/{sub}'.format(package_name=P.package_name, sub=module.name)) def disconnect(): try: P.logger.debug('socket_disconnect : %s - %s', P.package_name, module.name) @@ -358,7 +350,7 @@ def default_route_socketio_module(module): if encoding: data = json.dumps(data, cls=AlchemyEncoder) data = json.loads(data) - socketio.emit(cmd, data, namespace='/{package_name}/{sub}'.format(package_name=P.package_name, sub=module.name), broadcast=True) + F.socketio.emit(cmd, data, namespace='/{package_name}/{sub}'.format(package_name=P.package_name, sub=module.name), broadcast=True) module.socketio_callback = socketio_callback @@ -420,4 +412,4 @@ def default_route_socketio_page(page): data = json.loads(data) socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}/{page.name}', broadcast=True) - page.socketio_callback = socketio_callback \ No newline at end of file + page.socketio_callback = socketio_callback diff --git a/lib/support/__init__.py b/lib/support/__init__.py index 21d15f8..611efa9 100644 --- a/lib/support/__init__.py +++ b/lib/support/__init__.py @@ -5,12 +5,42 @@ def d(data): else: return str(data) -from .logger import get_logger -logger = get_logger() +def load(): + from .base.aes import SupportAES + from .base.discord import SupportDiscord + from .base.file import SupportFile + from .base.process import SupportProcess + from .base.string import SupportString + from .base.subprocess import SupportSubprocess + from .base.telegram import SupportTelegram + from .base.util import (AlchemyEncoder, SingletonClass, SupportUtil, + default_headers, pt) + from .base.yaml import SupportYaml -def set_logger(l): - global logger - logger = l +import os + +logger = None + +if os.environ.get('FF') == 'true': + def set_logger(l): + global logger + logger = l + +else: + from .logger import get_logger + logger = get_logger() + + +from .base.aes import SupportAES +from .base.discord import SupportDiscord +from .base.file import SupportFile +from .base.process import SupportProcess +from .base.string import SupportString +from .base.subprocess import SupportSubprocess +from .base.telegram import SupportTelegram +from .base.util import (AlchemyEncoder, SingletonClass, SupportUtil, + default_headers, pt) +from .base.yaml import SupportYaml # 일반 cli 사용 겸용이다. # set_logger 로 인한 진입이 아니고 import가 되면 기본 경로로 로그파일을 diff --git a/lib/support/base/__init__.py b/lib/support/base/__init__.py index fd073f7..6974f4f 100644 --- a/lib/support/base/__init__.py +++ b/lib/support/base/__init__.py @@ -1,13 +1,12 @@ -from support import logger -""" -from support import d, get_logger, logger +from support import d, logger + +from .aes import SupportAES from .discord import SupportDiscord from .ffmpeg import SupportFfmpeg from .file import SupportFile from .image import SupportImage from .process import SupportProcess from .string import SupportString -from .util import SupportUtil, pt, default_headers, SingletonClass, AlchemyEncoder -from .aes import SupportAES +from .util import (AlchemyEncoder, SingletonClass, SupportUtil, + default_headers, pt) from .yaml import SupportYaml -""" diff --git a/lib/support/base/file.py b/lib/support/base/file.py index 940d610..326392a 100644 --- a/lib/support/base/file.py +++ b/lib/support/base/file.py @@ -1,12 +1,18 @@ -import os, traceback, re, json, codecs +import codecs +import json +import os +import re +import traceback + from . import logger + class SupportFile(object): @classmethod - def read_file(cls, filename): + def read_file(cls, filename, mode='r'): try: - ifp = codecs.open(filename, 'r', encoding='utf8') + ifp = codecs.open(filename, mode, encoding='utf8') data = ifp.read() ifp.close() return data @@ -15,10 +21,10 @@ class SupportFile(object): logger.error(traceback.format_exc()) @classmethod - def write_file(cls, filename, data): + def write_file(cls, filename, data, mode='w'): try: import codecs - ofp = codecs.open(filename, 'w', encoding='utf8') + ofp = codecs.open(filename, mode, encoding='utf8') ofp.write(data) ofp.close() except Exception as exception: @@ -26,25 +32,8 @@ class SupportFile(object): logger.error(traceback.format_exc()) - - - - - - - - - - - - - - - - - @classmethod - def download(cls, url, filepath): + def download_file(cls, url, filepath): try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36', @@ -54,11 +43,8 @@ class SupportFile(object): } import requests - response = requests.get(url, headers=headers) # get request - if len(response.content) == 0: - return False - with open(filepath, "wb") as file_is: # open in binary mode + response = requests.get(url, headers=headers) # get request file_is.write(response.content) # write to file return True except Exception as exception: @@ -67,6 +53,47 @@ class SupportFile(object): return False + + + + + + + + + + + + + + @classmethod + def text_for_filename(cls, text): + #text = text.replace('/', '') + # 2021-07-31 X:X + #text = text.replace(':', ' ') + text = re.sub('[\\/:*?\"<>|]', ' ', text).strip() + text = re.sub("\s{2,}", ' ', text) + return text + + + + + + + + + + + + + + + + + + + + @classmethod def write(cls, data, filepath, mode='w'): try: @@ -83,14 +110,7 @@ class SupportFile(object): return False - @classmethod - def text_for_filename(cls, text): - #text = text.replace('/', '') - # 2021-07-31 X:X - #text = text.replace(':', ' ') - text = re.sub('[\\/:*?\"<>|]', ' ', text).strip() - text = re.sub("\s{2,}", ' ', text) - return text + @classmethod @@ -106,7 +126,8 @@ class SupportFile(object): @classmethod def file_move(cls, source_path, target_dir, target_filename): try: - import time, shutil + import shutil + import time if os.path.exists(target_dir) == False: os.makedirs(target_dir) target_path = os.path.join(target_dir, target_filename) @@ -217,7 +238,8 @@ class SupportFile(object): @classmethod def makezip(cls, zip_path, zip_extension='zip', remove_zip_path=True): - import zipfile, shutil + import shutil + import zipfile try: if os.path.exists(zip_path) == False: return False @@ -248,7 +270,8 @@ class SupportFile(object): @classmethod def makezip_all(cls, zip_path, zip_filepath=None, zip_extension='zip', remove_zip_path=True): - import zipfile, shutil + import shutil + import zipfile from pathlib import Path try: if os.path.exists(zip_path) == False: diff --git a/lib/support/base/string.py b/lib/support/base/string.py index 3882fdc..14f07d0 100644 --- a/lib/support/base/string.py +++ b/lib/support/base/string.py @@ -1,9 +1,7 @@ -import os, traceback, io, re, json, codecs from . import logger + class SupportString(object): - - @classmethod def get_cate_char_by_first(cls, title): # get_first value = ord(title[0].upper()) diff --git a/lib/tool_base/subprocess.py b/lib/support/base/subprocess.py similarity index 50% rename from lib/tool_base/subprocess.py rename to lib/support/base/subprocess.py index 6694991..5cacf40 100644 --- a/lib/tool_base/subprocess.py +++ b/lib/support/base/subprocess.py @@ -1,16 +1,25 @@ -# -*- coding: utf-8 -*- -######################################################### -import os, traceback, subprocess, json -from framework import frame +import json +import os +import platform +import subprocess +import traceback + +from . import logger -class ToolSubprocess(object): +class SupportSubprocess(object): + # 2021-10-25 + # timeout 적용 @classmethod - def execute_command_return(cls, command, format=None, force_log=False, shell=False, env=None): + def execute_command_return(cls, command, format=None, force_log=False, shell=False, env=None, timeout=None, uid=0, gid=0): + def demote(user_uid, user_gid): + def result(): + os.setgid(user_gid) + os.setuid(user_uid) + return result try: - #logger.debug('execute_command_return : %s', ' '.join(command)) - if frame.config['running_type'] == 'windows': + if platform.system() == 'Windows': tmp = [] if type(command) == type([]): for x in command: @@ -21,60 +30,7 @@ class ToolSubprocess(object): command = ' '.join(tmp) iter_arg = '' - process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8') - - #process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8') - ret = [] - with process.stdout: - for line in iter(process.stdout.readline, iter_arg): - ret.append(line.strip()) - if force_log: - logger.debug(ret[-1]) - process.wait() # wait for the subprocess to exit - - if format is None: - ret2 = '\n'.join(ret) - elif format == 'json': - try: - index = 0 - for idx, tmp in enumerate(ret): - #logger.debug(tmp) - if tmp.startswith('{') or tmp.startswith('['): - index = idx - break - ret2 = json.loads(''.join(ret[index:])) - except: - ret2 = None - - return ret2 - except Exception as exception: - logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) - logger.error('command : %s', command) - - - # 2021-10-25 - # timeout 적용 - @classmethod - def execute_command_return2(cls, command, format=None, force_log=False, shell=False, env=None, timeout=None, uid=0, gid=0, pid_dict=None): - def demote(user_uid, user_gid): - def result(): - os.setgid(user_gid) - os.setuid(user_uid) - return result - try: - if app.config['config']['running_type'] == 'windows': - tmp = [] - if type(command) == type([]): - for x in command: - if x.find(' ') == -1: - tmp.append(x) - else: - tmp.append(f'"{x}"') - command = ' '.join(tmp) - - iter_arg = b'' if app.config['config']['is_py2'] else '' - if app.config['config']['running_type'] == 'windows': + if platform.system() == 'Windows': process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8') else: process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, preexec_fn=demote(uid, gid), encoding='utf8') @@ -117,4 +73,4 @@ class ToolSubprocess(object): except Exception as exception: logger.error('Exception:%s', exception) logger.error(traceback.format_exc()) - logger.error('command : %s', command) \ No newline at end of file + logger.error('command : %s', command) diff --git a/lib/support/base/telegram.py b/lib/support/base/telegram.py index 70873c2..59c3682 100644 --- a/lib/support/base/telegram.py +++ b/lib/support/base/telegram.py @@ -1,3 +1,7 @@ +import traceback + +from telepot_mod import Bot + from . import logger @@ -5,24 +9,11 @@ class SupportTelegram: @classmethod def send_telegram_message(cls, text, bot_token=None, chat_id=None, image_url=None, disable_notification=None): - from system.model import ModelSetting as SystemModelSetting try: - if bot_token is None: - bot_token = SystemModelSetting.get('notify_telegram_token') - - if chat_id is None: - chat_id = SystemModelSetting.get('notify_telegram_chat_id') - - if disable_notification is None: - disable_notification = SystemModelSetting.get_bool('notify_telegram_disable_notification') - bot = Bot(bot_token) if image_url is not None: - #bot.sendPhoto(chat_id, text, caption=caption, disable_notification=disable_notification) bot.sendPhoto(chat_id, image_url, disable_notification=disable_notification) bot.sendMessage(chat_id, text, disable_web_page_preview=True, disable_notification=disable_notification) - #elif mime == 'video': - # bot.sendVideo(chat_id, text, disable_notification=disable_notification) return True except Exception as exception: logger.error('Exception:%s', exception) diff --git a/lib/support/telepot2/__init__.py b/lib/support/telepot2/__init__.py deleted file mode 100644 index 3fed308..0000000 --- a/lib/support/telepot2/__init__.py +++ /dev/null @@ -1,1405 +0,0 @@ -import sys -import io -import time -import json -import threading -import traceback -import collections -import bisect - -try: - import Queue as queue -except ImportError: - import queue - -# Patch urllib3 for sending unicode filename -from . import hack - -from . import exception - - -__version_info__ = (12, 7) -__version__ = '.'.join(map(str, __version_info__)) - - -def flavor(msg): - """ - Return flavor of message or event. - - A message's flavor may be one of these: - - - ``chat`` - - ``callback_query`` - - ``inline_query`` - - ``chosen_inline_result`` - - ``shipping_query`` - - ``pre_checkout_query`` - - An event's flavor is determined by the single top-level key. - """ - if 'message_id' in msg: - return 'chat' - elif 'id' in msg and 'chat_instance' in msg: - return 'callback_query' - elif 'id' in msg and 'query' in msg: - return 'inline_query' - elif 'result_id' in msg: - return 'chosen_inline_result' - elif 'id' in msg and 'shipping_address' in msg: - return 'shipping_query' - elif 'id' in msg and 'total_amount' in msg: - return 'pre_checkout_query' - else: - top_keys = list(msg.keys()) - if len(top_keys) == 1: - return top_keys[0] - - raise exception.BadFlavor(msg) - - -chat_flavors = ['chat'] -inline_flavors = ['inline_query', 'chosen_inline_result'] - - -def _find_first_key(d, keys): - for k in keys: - if k in d: - return k - raise KeyError('No suggested keys %s in %s' % (str(keys), str(d))) - - -all_content_types = [ - 'text', 'audio', 'document', 'game', 'photo', 'sticker', 'video', 'voice', 'video_note', - 'contact', 'location', 'venue', 'new_chat_member', 'left_chat_member', 'new_chat_title', - 'new_chat_photo', 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created', - 'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', - 'new_chat_members', 'invoice', 'successful_payment', 'my_chat_member' -] - -def glance(msg, flavor='chat', long=False): - """ - Extract "headline" info about a message. - Use parameter ``long`` to control whether a short or long tuple is returned. - - When ``flavor`` is ``chat`` - (``msg`` being a `Message `_ object): - - - short: (content_type, ``msg['chat']['type']``, ``msg['chat']['id']``) - - long: (content_type, ``msg['chat']['type']``, ``msg['chat']['id']``, ``msg['date']``, ``msg['message_id']``) - - *content_type* can be: ``text``, ``audio``, ``document``, ``game``, ``photo``, ``sticker``, ``video``, ``voice``, - ``video_note``, ``contact``, ``location``, ``venue``, ``new_chat_member``, ``left_chat_member``, ``new_chat_title``, - ``new_chat_photo``, ``delete_chat_photo``, ``group_chat_created``, ``supergroup_chat_created``, - ``channel_chat_created``, ``migrate_to_chat_id``, ``migrate_from_chat_id``, ``pinned_message``, - ``new_chat_members``, ``invoice``, ``successful_payment``. - - When ``flavor`` is ``callback_query`` - (``msg`` being a `CallbackQuery `_ object): - - - regardless: (``msg['id']``, ``msg['from']['id']``, ``msg['data']``) - - When ``flavor`` is ``inline_query`` - (``msg`` being a `InlineQuery `_ object): - - - short: (``msg['id']``, ``msg['from']['id']``, ``msg['query']``) - - long: (``msg['id']``, ``msg['from']['id']``, ``msg['query']``, ``msg['offset']``) - - When ``flavor`` is ``chosen_inline_result`` - (``msg`` being a `ChosenInlineResult `_ object): - - - regardless: (``msg['result_id']``, ``msg['from']['id']``, ``msg['query']``) - - When ``flavor`` is ``shipping_query`` - (``msg`` being a `ShippingQuery `_ object): - - - regardless: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``) - - When ``flavor`` is ``pre_checkout_query`` - (``msg`` being a `PreCheckoutQuery `_ object): - - - short: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``) - - long: (``msg['id']``, ``msg['from']['id']``, ``msg['invoice_payload']``, ``msg['currency']``, ``msg['total_amount']``) - """ - def gl_chat(): - content_type = _find_first_key(msg, all_content_types) - - if long: - return content_type, msg['chat']['type'], msg['chat']['id'], msg['date'], msg['message_id'] - else: - return content_type, msg['chat']['type'], msg['chat']['id'] - - def gl_callback_query(): - return msg['id'], msg['from']['id'], msg['data'] - - def gl_inline_query(): - if long: - return msg['id'], msg['from']['id'], msg['query'], msg['offset'] - else: - return msg['id'], msg['from']['id'], msg['query'] - - def gl_chosen_inline_result(): - return msg['result_id'], msg['from']['id'], msg['query'] - - def gl_shipping_query(): - return msg['id'], msg['from']['id'], msg['invoice_payload'] - - def gl_pre_checkout_query(): - if long: - return msg['id'], msg['from']['id'], msg['invoice_payload'], msg['currency'], msg['total_amount'] - else: - return msg['id'], msg['from']['id'], msg['invoice_payload'] - - try: - fn = {'chat': gl_chat, - 'callback_query': gl_callback_query, - 'inline_query': gl_inline_query, - 'chosen_inline_result': gl_chosen_inline_result, - 'shipping_query': gl_shipping_query, - 'pre_checkout_query': gl_pre_checkout_query}[flavor] - except KeyError: - raise exception.BadFlavor(flavor) - - return fn() - - -def flance(msg, long=False): - """ - A combination of :meth:`telepot.flavor` and :meth:`telepot.glance`, - return a 2-tuple (flavor, headline_info), where *headline_info* is whatever extracted by - :meth:`telepot.glance` depending on the message flavor and the ``long`` parameter. - """ - f = flavor(msg) - g = glance(msg, flavor=f, long=long) - return f,g - - -def peel(event): - """ - Remove an event's top-level skin (where its flavor is determined), and return - the core content. - """ - return list(event.values())[0] - - -def fleece(event): - """ - A combination of :meth:`telepot.flavor` and :meth:`telepot.peel`, - return a 2-tuple (flavor, content) of an event. - """ - return flavor(event), peel(event) - - -def is_event(msg): - """ - Return whether the message looks like an event. That is, whether it has a flavor - that starts with an underscore. - """ - return flavor(msg).startswith('_') - - -def origin_identifier(msg): - """ - Extract the message identifier of a callback query's origin. Returned value - is guaranteed to be a tuple. - - ``msg`` is expected to be ``callback_query``. - """ - if 'message' in msg: - return msg['message']['chat']['id'], msg['message']['message_id'] - elif 'inline_message_id' in msg: - return msg['inline_message_id'], - else: - raise ValueError() - -def message_identifier(msg): - """ - Extract an identifier for message editing. Useful with :meth:`telepot.Bot.editMessageText` - and similar methods. Returned value is guaranteed to be a tuple. - - ``msg`` is expected to be ``chat`` or ``choson_inline_result``. - """ - if 'chat' in msg and 'message_id' in msg: - return msg['chat']['id'], msg['message_id'] - elif 'inline_message_id' in msg: - return msg['inline_message_id'], - else: - raise ValueError() - -def _dismantle_message_identifier(f): - if isinstance(f, tuple): - if len(f) == 2: - return {'chat_id': f[0], 'message_id': f[1]} - elif len(f) == 1: - return {'inline_message_id': f[0]} - else: - raise ValueError() - else: - return {'inline_message_id': f} - -def _split_input_media_array(media_array): - def ensure_dict(input_media): - if isinstance(input_media, tuple) and hasattr(input_media, '_asdict'): - return input_media._asdict() - elif isinstance(input_media, dict): - return input_media - else: - raise ValueError() - - def given_attach_name(input_media): - if isinstance(input_media['media'], tuple): - return input_media['media'][0] - else: - return None - - def attach_name_generator(used_names): - x = 0 - while 1: - x += 1 - name = 'media' + str(x) - if name in used_names: - continue; - yield name - - def split_media(input_media, name_generator): - file_spec = input_media['media'] - - # file_id, URL - if _isstring(file_spec): - return (input_media, None) - - # file-object - # (attach-name, file-object) - # (attach-name, (filename, file-object)) - if isinstance(file_spec, tuple): - name, f = file_spec - else: - name, f = next(name_generator), file_spec - - m = input_media.copy() - m['media'] = 'attach://' + name - - return (m, (name, f)) - - ms = [ensure_dict(m) for m in media_array] - - used_names = [given_attach_name(m) for m in ms if given_attach_name(m) is not None] - name_generator = attach_name_generator(used_names) - - splitted = [split_media(m, name_generator) for m in ms] - - legal_media, attachments = map(list, zip(*splitted)) - files_to_attach = dict([a for a in attachments if a is not None]) - - return (legal_media, files_to_attach) - - -PY_3 = sys.version_info.major >= 3 -_string_type = str if PY_3 else basestring -_file_type = io.IOBase if PY_3 else file - -def _isstring(s): - return isinstance(s, _string_type) - -def _isfile(f): - return isinstance(f, _file_type) - - -from . import helper - -def flavor_router(routing_table): - router = helper.Router(flavor, routing_table) - return router.route - - -class _BotBase(object): - def __init__(self, token): - self._token = token - self._file_chunk_size = 65536 - - -def _strip(params, more=[]): - return {key: value for key,value in params.items() if key not in ['self']+more} - -def _rectify(params): - def make_jsonable(value): - if isinstance(value, list): - return [make_jsonable(v) for v in value] - elif isinstance(value, dict): - return {k:make_jsonable(v) for k,v in value.items() if v is not None} - elif isinstance(value, tuple) and hasattr(value, '_asdict'): - return {k:make_jsonable(v) for k,v in value._asdict().items() if v is not None} - else: - return value - - def flatten(value): - v = make_jsonable(value) - - if isinstance(v, (dict, list)): - return json.dumps(v, separators=(',',':')) - else: - return v - - # remove None, then json-serialize if needed - return {k: flatten(v) for k,v in params.items() if v is not None} - - -from . import api - -class Bot(_BotBase): - class Scheduler(threading.Thread): - # A class that is sorted by timestamp. Use `bisect` module to ensure order in event queue. - Event = collections.namedtuple('Event', ['timestamp', 'data']) - Event.__eq__ = lambda self, other: self.timestamp == other.timestamp - Event.__ne__ = lambda self, other: self.timestamp != other.timestamp - Event.__gt__ = lambda self, other: self.timestamp > other.timestamp - Event.__ge__ = lambda self, other: self.timestamp >= other.timestamp - Event.__lt__ = lambda self, other: self.timestamp < other.timestamp - Event.__le__ = lambda self, other: self.timestamp <= other.timestamp - - def __init__(self): - super(Bot.Scheduler, self).__init__() - self._eventq = [] - self._lock = threading.RLock() # reentrant lock to allow locked method calling locked method - self._event_handler = None - - def _locked(fn): - def k(self, *args, **kwargs): - with self._lock: - return fn(self, *args, **kwargs) - return k - - @_locked - def _insert_event(self, data, when): - ev = self.Event(when, data) - bisect.insort(self._eventq, ev) - return ev - - @_locked - def _remove_event(self, event): - # Find event according to its timestamp. - # Index returned should be one behind. - i = bisect.bisect(self._eventq, event) - - # Having two events with identical timestamp is unlikely but possible. - # I am going to move forward and compare timestamp AND object address - # to make sure the correct object is found. - - while i > 0: - i -= 1 - e = self._eventq[i] - - if e.timestamp != event.timestamp: - raise exception.EventNotFound(event) - elif id(e) == id(event): - self._eventq.pop(i) - return - - raise exception.EventNotFound(event) - - @_locked - def _pop_expired_event(self): - if not self._eventq: - return None - - if self._eventq[0].timestamp <= time.time(): - return self._eventq.pop(0) - else: - return None - - def event_at(self, when, data): - """ - Schedule some data to emit at an absolute timestamp. - - :type when: int or float - :type data: dictionary - :return: an internal Event object - """ - return self._insert_event(data, when) - - def event_later(self, delay, data): - """ - Schedule some data to emit after a number of seconds. - - :type delay: int or float - :type data: dictionary - :return: an internal Event object - """ - return self._insert_event(data, time.time()+delay) - - def event_now(self, data): - """ - Emit some data as soon as possible. - - :type data: dictionary - :return: an internal Event object - """ - return self._insert_event(data, time.time()) - - def cancel(self, event): - """ - Cancel an event. - - :type event: an internal Event object - """ - self._remove_event(event) - - def run(self): - while 1: - e = self._pop_expired_event() - while e: - if callable(e.data): - d = e.data() # call the data-producing function - if d is not None: - self._event_handler(d) - else: - self._event_handler(e.data) - - e = self._pop_expired_event() - time.sleep(0.1) - - def run_as_thread(self): - self.daemon = True - self.start() - - def on_event(self, fn): - self._event_handler = fn - - def __init__(self, token): - super(Bot, self).__init__(token) - - self._scheduler = self.Scheduler() - - self._router = helper.Router(flavor, {'chat': lambda msg: self.on_chat_message(msg), - 'callback_query': lambda msg: self.on_callback_query(msg), - 'inline_query': lambda msg: self.on_inline_query(msg), - 'chosen_inline_result': lambda msg: self.on_chosen_inline_result(msg)}) - # use lambda to delay evaluation of self.on_ZZZ to runtime because - # I don't want to require defining all methods right here. - - @property - def scheduler(self): - return self._scheduler - - @property - def router(self): - return self._router - - def handle(self, msg): - self._router.route(msg) - - def _api_request(self, method, params=None, files=None, **kwargs): - return api.request((self._token, method, params, files), **kwargs) - - def _api_request_with_file(self, method, params, file_key, file_value, **kwargs): - if _isstring(file_value): - params[file_key] = file_value - return self._api_request(method, _rectify(params), **kwargs) - else: - files = {file_key: file_value} - return self._api_request(method, _rectify(params), files, **kwargs) - - def getMe(self): - """ See: https://core.telegram.org/bots/api#getme """ - return self._api_request('getMe') - - def sendMessage(self, chat_id, text, - parse_mode=None, - disable_web_page_preview=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendmessage """ - p = _strip(locals()) - return self._api_request('sendMessage', _rectify(p)) - - def forwardMessage(self, chat_id, from_chat_id, message_id, - disable_notification=None): - """ See: https://core.telegram.org/bots/api#forwardmessage """ - p = _strip(locals()) - return self._api_request('forwardMessage', _rectify(p)) - - def sendPhoto(self, chat_id, photo, - caption=None, - parse_mode=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendphoto - - :param photo: - - string: ``file_id`` for a photo existing on Telegram servers - - string: HTTP URL of a photo from the Internet - - file-like object: obtained by ``open(path, 'rb')`` - - tuple: (filename, file-like object). If the filename contains - non-ASCII characters and you are using Python 2.7, make sure the - filename is a unicode string. - """ - p = _strip(locals(), more=['photo']) - return self._api_request_with_file('sendPhoto', _rectify(p), 'photo', photo) - - def sendAudio(self, chat_id, audio, - caption=None, - parse_mode=None, - duration=None, - performer=None, - title=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendaudio - - :param audio: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` - """ - p = _strip(locals(), more=['audio']) - return self._api_request_with_file('sendAudio', _rectify(p), 'audio', audio) - - def sendDocument(self, chat_id, document, - caption=None, - parse_mode=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#senddocument - - :param document: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` - """ - p = _strip(locals(), more=['document']) - return self._api_request_with_file('sendDocument', _rectify(p), 'document', document) - - def sendVideo(self, chat_id, video, - duration=None, - width=None, - height=None, - caption=None, - parse_mode=None, - supports_streaming=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendvideo - - :param video: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` - """ - p = _strip(locals(), more=['video']) - return self._api_request_with_file('sendVideo', _rectify(p), 'video', video) - - def sendVoice(self, chat_id, voice, - caption=None, - parse_mode=None, - duration=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendvoice - - :param voice: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` - """ - p = _strip(locals(), more=['voice']) - return self._api_request_with_file('sendVoice', _rectify(p), 'voice', voice) - - def sendVideoNote(self, chat_id, video_note, - duration=None, - length=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendvideonote - - :param video_note: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` - - :param length: - Although marked as optional, this method does not seem to work without - it being specified. Supply any integer you want. It seems to have no effect - on the video note's display size. - """ - p = _strip(locals(), more=['video_note']) - return self._api_request_with_file('sendVideoNote', _rectify(p), 'video_note', video_note) - - def sendMediaGroup(self, chat_id, media, - disable_notification=None, - reply_to_message_id=None): - """ - See: https://core.telegram.org/bots/api#sendmediagroup - - :type media: array of `InputMedia `_ objects - :param media: - To indicate media locations, each InputMedia object's ``media`` field - should be one of these: - - - string: ``file_id`` for a file existing on Telegram servers - - string: HTTP URL of a file from the Internet - - file-like object: obtained by ``open(path, 'rb')`` - - tuple: (form-data name, file-like object) - - tuple: (form-data name, (filename, file-like object)) - - In case of uploading, you may supply customized multipart/form-data - names for each uploaded file (as in last 2 options above). Otherwise, - telepot assigns unique names to each uploaded file. Names assigned by - telepot will not collide with user-supplied names, if any. - """ - p = _strip(locals(), more=['media']) - legal_media, files_to_attach = _split_input_media_array(media) - - p['media'] = legal_media - return self._api_request('sendMediaGroup', _rectify(p), files_to_attach) - - def sendLocation(self, chat_id, latitude, longitude, - live_period=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendlocation """ - p = _strip(locals()) - return self._api_request('sendLocation', _rectify(p)) - - def editMessageLiveLocation(self, msg_identifier, latitude, longitude, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagelivelocation - - :param msg_identifier: Same as in :meth:`.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return self._api_request('editMessageLiveLocation', _rectify(p)) - - def stopMessageLiveLocation(self, msg_identifier, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#stopmessagelivelocation - - :param msg_identifier: Same as in :meth:`.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return self._api_request('stopMessageLiveLocation', _rectify(p)) - - def sendVenue(self, chat_id, latitude, longitude, title, address, - foursquare_id=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendvenue """ - p = _strip(locals()) - return self._api_request('sendVenue', _rectify(p)) - - def sendContact(self, chat_id, phone_number, first_name, - last_name=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendcontact """ - p = _strip(locals()) - return self._api_request('sendContact', _rectify(p)) - - def sendGame(self, chat_id, game_short_name, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendgame """ - p = _strip(locals()) - return self._api_request('sendGame', _rectify(p)) - - def sendInvoice(self, chat_id, title, description, payload, - provider_token, start_parameter, currency, prices, - provider_data=None, - photo_url=None, - photo_size=None, - photo_width=None, - photo_height=None, - need_name=None, - need_phone_number=None, - need_email=None, - need_shipping_address=None, - is_flexible=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendinvoice """ - p = _strip(locals()) - return self._api_request('sendInvoice', _rectify(p)) - - def sendChatAction(self, chat_id, action): - """ See: https://core.telegram.org/bots/api#sendchataction """ - p = _strip(locals()) - return self._api_request('sendChatAction', _rectify(p)) - - def getUserProfilePhotos(self, user_id, - offset=None, - limit=None): - """ See: https://core.telegram.org/bots/api#getuserprofilephotos """ - p = _strip(locals()) - return self._api_request('getUserProfilePhotos', _rectify(p)) - - def getFile(self, file_id): - """ See: https://core.telegram.org/bots/api#getfile """ - p = _strip(locals()) - return self._api_request('getFile', _rectify(p)) - - def kickChatMember(self, chat_id, user_id, - until_date=None): - """ See: https://core.telegram.org/bots/api#kickchatmember """ - p = _strip(locals()) - return self._api_request('kickChatMember', _rectify(p)) - - def unbanChatMember(self, chat_id, user_id): - """ See: https://core.telegram.org/bots/api#unbanchatmember """ - p = _strip(locals()) - return self._api_request('unbanChatMember', _rectify(p)) - - def restrictChatMember(self, chat_id, user_id, - until_date=None, - can_send_messages=None, - can_send_media_messages=None, - can_send_other_messages=None, - can_add_web_page_previews=None): - """ See: https://core.telegram.org/bots/api#restrictchatmember """ - p = _strip(locals()) - return self._api_request('restrictChatMember', _rectify(p)) - - def promoteChatMember(self, chat_id, user_id, - can_change_info=None, - can_post_messages=None, - can_edit_messages=None, - can_delete_messages=None, - can_invite_users=None, - can_restrict_members=None, - can_pin_messages=None, - can_promote_members=None): - """ See: https://core.telegram.org/bots/api#promotechatmember """ - p = _strip(locals()) - return self._api_request('promoteChatMember', _rectify(p)) - - def exportChatInviteLink(self, chat_id): - """ See: https://core.telegram.org/bots/api#exportchatinvitelink """ - p = _strip(locals()) - return self._api_request('exportChatInviteLink', _rectify(p)) - - def setChatPhoto(self, chat_id, photo): - """ See: https://core.telegram.org/bots/api#setchatphoto """ - p = _strip(locals(), more=['photo']) - return self._api_request_with_file('setChatPhoto', _rectify(p), 'photo', photo) - - def deleteChatPhoto(self, chat_id): - """ See: https://core.telegram.org/bots/api#deletechatphoto """ - p = _strip(locals()) - return self._api_request('deleteChatPhoto', _rectify(p)) - - def setChatTitle(self, chat_id, title): - """ See: https://core.telegram.org/bots/api#setchattitle """ - p = _strip(locals()) - return self._api_request('setChatTitle', _rectify(p)) - - def setChatDescription(self, chat_id, - description=None): - """ See: https://core.telegram.org/bots/api#setchatdescription """ - p = _strip(locals()) - return self._api_request('setChatDescription', _rectify(p)) - - def pinChatMessage(self, chat_id, message_id, - disable_notification=None): - """ See: https://core.telegram.org/bots/api#pinchatmessage """ - p = _strip(locals()) - return self._api_request('pinChatMessage', _rectify(p)) - - def unpinChatMessage(self, chat_id): - """ See: https://core.telegram.org/bots/api#unpinchatmessage """ - p = _strip(locals()) - return self._api_request('unpinChatMessage', _rectify(p)) - - def leaveChat(self, chat_id): - """ See: https://core.telegram.org/bots/api#leavechat """ - p = _strip(locals()) - return self._api_request('leaveChat', _rectify(p)) - - def getChat(self, chat_id): - """ See: https://core.telegram.org/bots/api#getchat """ - p = _strip(locals()) - return self._api_request('getChat', _rectify(p)) - - def getChatAdministrators(self, chat_id): - """ See: https://core.telegram.org/bots/api#getchatadministrators """ - p = _strip(locals()) - return self._api_request('getChatAdministrators', _rectify(p)) - - def getChatMembersCount(self, chat_id): - """ See: https://core.telegram.org/bots/api#getchatmemberscount """ - p = _strip(locals()) - return self._api_request('getChatMembersCount', _rectify(p)) - - def getChatMember(self, chat_id, user_id): - """ See: https://core.telegram.org/bots/api#getchatmember """ - p = _strip(locals()) - return self._api_request('getChatMember', _rectify(p)) - - def setChatStickerSet(self, chat_id, sticker_set_name): - """ See: https://core.telegram.org/bots/api#setchatstickerset """ - p = _strip(locals()) - return self._api_request('setChatStickerSet', _rectify(p)) - - def deleteChatStickerSet(self, chat_id): - """ See: https://core.telegram.org/bots/api#deletechatstickerset """ - p = _strip(locals()) - return self._api_request('deleteChatStickerSet', _rectify(p)) - - def answerCallbackQuery(self, callback_query_id, - text=None, - show_alert=None, - url=None, - cache_time=None): - """ See: https://core.telegram.org/bots/api#answercallbackquery """ - p = _strip(locals()) - return self._api_request('answerCallbackQuery', _rectify(p)) - - def answerShippingQuery(self, shipping_query_id, ok, - shipping_options=None, - error_message=None): - """ See: https://core.telegram.org/bots/api#answershippingquery """ - p = _strip(locals()) - return self._api_request('answerShippingQuery', _rectify(p)) - - def answerPreCheckoutQuery(self, pre_checkout_query_id, ok, - error_message=None): - """ See: https://core.telegram.org/bots/api#answerprecheckoutquery """ - p = _strip(locals()) - return self._api_request('answerPreCheckoutQuery', _rectify(p)) - - def editMessageText(self, msg_identifier, text, - parse_mode=None, - disable_web_page_preview=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagetext - - :param msg_identifier: - a 2-tuple (``chat_id``, ``message_id``), - a 1-tuple (``inline_message_id``), - or simply ``inline_message_id``. - You may extract this value easily with :meth:`telepot.message_identifier` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return self._api_request('editMessageText', _rectify(p)) - - def editMessageCaption(self, msg_identifier, - caption=None, - parse_mode=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagecaption - - :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return self._api_request('editMessageCaption', _rectify(p)) - - def editMessageReplyMarkup(self, msg_identifier, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagereplymarkup - - :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return self._api_request('editMessageReplyMarkup', _rectify(p)) - - def deleteMessage(self, msg_identifier): - """ - See: https://core.telegram.org/bots/api#deletemessage - - :param msg_identifier: - Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText`, - except this method does not work on inline messages. - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return self._api_request('deleteMessage', _rectify(p)) - - def sendSticker(self, chat_id, sticker, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendsticker - - :param sticker: Same as ``photo`` in :meth:`telepot.Bot.sendPhoto` - """ - p = _strip(locals(), more=['sticker']) - return self._api_request_with_file('sendSticker', _rectify(p), 'sticker', sticker) - - def getStickerSet(self, name): - """ - See: https://core.telegram.org/bots/api#getstickerset - """ - p = _strip(locals()) - return self._api_request('getStickerSet', _rectify(p)) - - def uploadStickerFile(self, user_id, png_sticker): - """ - See: https://core.telegram.org/bots/api#uploadstickerfile - """ - p = _strip(locals(), more=['png_sticker']) - return self._api_request_with_file('uploadStickerFile', _rectify(p), 'png_sticker', png_sticker) - - def createNewStickerSet(self, user_id, name, title, png_sticker, emojis, - contains_masks=None, - mask_position=None): - """ - See: https://core.telegram.org/bots/api#createnewstickerset - """ - p = _strip(locals(), more=['png_sticker']) - return self._api_request_with_file('createNewStickerSet', _rectify(p), 'png_sticker', png_sticker) - - def addStickerToSet(self, user_id, name, png_sticker, emojis, - mask_position=None): - """ - See: https://core.telegram.org/bots/api#addstickertoset - """ - p = _strip(locals(), more=['png_sticker']) - return self._api_request_with_file('addStickerToSet', _rectify(p), 'png_sticker', png_sticker) - - def setStickerPositionInSet(self, sticker, position): - """ - See: https://core.telegram.org/bots/api#setstickerpositioninset - """ - p = _strip(locals()) - return self._api_request('setStickerPositionInSet', _rectify(p)) - - def deleteStickerFromSet(self, sticker): - """ - See: https://core.telegram.org/bots/api#deletestickerfromset - """ - p = _strip(locals()) - return self._api_request('deleteStickerFromSet', _rectify(p)) - - def answerInlineQuery(self, inline_query_id, results, - cache_time=None, - is_personal=None, - next_offset=None, - switch_pm_text=None, - switch_pm_parameter=None): - """ See: https://core.telegram.org/bots/api#answerinlinequery """ - p = _strip(locals()) - return self._api_request('answerInlineQuery', _rectify(p)) - - def getUpdates(self, - offset=None, - limit=None, - timeout=None, - allowed_updates=None): - """ See: https://core.telegram.org/bots/api#getupdates """ - p = _strip(locals()) - return self._api_request('getUpdates', _rectify(p)) - - def setWebhook(self, - url=None, - certificate=None, - max_connections=None, - allowed_updates=None): - """ See: https://core.telegram.org/bots/api#setwebhook """ - p = _strip(locals(), more=['certificate']) - - if certificate: - files = {'certificate': certificate} - return self._api_request('setWebhook', _rectify(p), files) - else: - return self._api_request('setWebhook', _rectify(p)) - - def deleteWebhook(self): - """ See: https://core.telegram.org/bots/api#deletewebhook """ - return self._api_request('deleteWebhook') - - def getWebhookInfo(self): - """ See: https://core.telegram.org/bots/api#getwebhookinfo """ - return self._api_request('getWebhookInfo') - - def setGameScore(self, user_id, score, game_message_identifier, - force=None, - disable_edit_message=None): - """ - See: https://core.telegram.org/bots/api#setgamescore - - :param game_message_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` - """ - p = _strip(locals(), more=['game_message_identifier']) - p.update(_dismantle_message_identifier(game_message_identifier)) - return self._api_request('setGameScore', _rectify(p)) - - def getGameHighScores(self, user_id, game_message_identifier): - """ - See: https://core.telegram.org/bots/api#getgamehighscores - - :param game_message_identifier: Same as ``msg_identifier`` in :meth:`telepot.Bot.editMessageText` - """ - p = _strip(locals(), more=['game_message_identifier']) - p.update(_dismantle_message_identifier(game_message_identifier)) - return self._api_request('getGameHighScores', _rectify(p)) - - def download_file(self, file_id, dest): - """ - Download a file to local disk. - - :param dest: a path or a ``file`` object - """ - f = self.getFile(file_id) - try: - d = dest if _isfile(dest) else open(dest, 'wb') - - r = api.download((self._token, f['file_path']), preload_content=False) - - while 1: - data = r.read(self._file_chunk_size) - if not data: - break - d.write(data) - finally: - if not _isfile(dest) and 'd' in locals(): - d.close() - - if 'r' in locals(): - r.release_conn() - - def message_loop(self, callback=None, relax=0.1, - timeout=20, allowed_updates=None, - source=None, ordered=True, maxhold=3, - run_forever=False): - """ - :deprecated: will be removed in future. Use :class:`.MessageLoop` instead. - - Spawn a thread to constantly ``getUpdates`` or pull updates from a queue. - Apply ``callback`` to every message received. Also starts the scheduler thread - for internal events. - - :param callback: - a function that takes one argument (the message), or a routing table. - If ``None``, the bot's ``handle`` method is used. - - A *routing table* is a dictionary of ``{flavor: function}``, mapping messages to appropriate - handler functions according to their flavors. It allows you to define functions specifically - to handle one flavor of messages. It usually looks like this: ``{'chat': fn1, - 'callback_query': fn2, 'inline_query': fn3, ...}``. Each handler function should take - one argument (the message). - - :param source: - Source of updates. - If ``None``, ``getUpdates`` is used to obtain new messages from Telegram servers. - If it is a synchronized queue (``Queue.Queue`` in Python 2.7 or - ``queue.Queue`` in Python 3), new messages are pulled from the queue. - A web application implementing a webhook can dump updates into the queue, - while the bot pulls from it. This is how telepot can be integrated with webhooks. - - Acceptable contents in queue: - - - ``str``, ``unicode`` (Python 2.7), or ``bytes`` (Python 3, decoded using UTF-8) - representing a JSON-serialized `Update `_ object. - - a ``dict`` representing an Update object. - - When ``source`` is ``None``, these parameters are meaningful: - - :type relax: float - :param relax: seconds between each ``getUpdates`` - - :type timeout: int - :param timeout: - ``timeout`` parameter supplied to :meth:`telepot.Bot.getUpdates`, - controlling how long to poll. - - :type allowed_updates: array of string - :param allowed_updates: - ``allowed_updates`` parameter supplied to :meth:`telepot.Bot.getUpdates`, - controlling which types of updates to receive. - - When ``source`` is a queue, these parameters are meaningful: - - :type ordered: bool - :param ordered: - If ``True``, ensure in-order delivery of messages to ``callback`` - (i.e. updates with a smaller ``update_id`` always come before those with - a larger ``update_id``). - If ``False``, no re-ordering is done. ``callback`` is applied to messages - as soon as they are pulled from queue. - - :type maxhold: float - :param maxhold: - Applied only when ``ordered`` is ``True``. The maximum number of seconds - an update is held waiting for a not-yet-arrived smaller ``update_id``. - When this number of seconds is up, the update is delivered to ``callback`` - even if some smaller ``update_id``\s have not yet arrived. If those smaller - ``update_id``\s arrive at some later time, they are discarded. - - Finally, there is this parameter, meaningful always: - - :type run_forever: bool or str - :param run_forever: - If ``True`` or any non-empty string, append an infinite loop at the end of - this method, so it never returns. Useful as the very last line in a program. - A non-empty string will also be printed, useful as an indication that the - program is listening. - """ - if callback is None: - callback = self.handle - elif isinstance(callback, dict): - callback = flavor_router(callback) - - collect_queue = queue.Queue() - - def collector(): - while 1: - try: - item = collect_queue.get(block=True) - callback(item) - except: - # Localize error so thread can keep going. - traceback.print_exc() - - def relay_to_collector(update): - key = _find_first_key(update, ['message', - 'edited_message', - 'channel_post', - 'edited_channel_post', - 'callback_query', - 'inline_query', - 'chosen_inline_result', - 'shipping_query', - 'pre_checkout_query']) - collect_queue.put(update[key]) - return update['update_id'] - - def get_from_telegram_server(): - offset = None # running offset - allowed_upd = allowed_updates - while 1: - try: - result = self.getUpdates(offset=offset, - timeout=timeout, - allowed_updates=allowed_upd) - - # Once passed, this parameter is no longer needed. - allowed_upd = None - - if len(result) > 0: - # No sort. Trust server to give messages in correct order. - # Update offset to max(update_id) + 1 - offset = max([relay_to_collector(update) for update in result]) + 1 - - except exception.BadHTTPResponse as e: - traceback.print_exc() - - # Servers probably down. Wait longer. - if e.status == 502: - time.sleep(30) - except: - traceback.print_exc() - finally: - time.sleep(relax) - - def dictify3(data): - if type(data) is bytes: - return json.loads(data.decode('utf-8')) - elif type(data) is str: - return json.loads(data) - elif type(data) is dict: - return data - else: - raise ValueError() - - def dictify27(data): - if type(data) in [str, unicode]: - return json.loads(data) - elif type(data) is dict: - return data - else: - raise ValueError() - - def get_from_queue_unordered(qu): - dictify = dictify3 if sys.version_info >= (3,) else dictify27 - while 1: - try: - data = qu.get(block=True) - update = dictify(data) - relay_to_collector(update) - except: - traceback.print_exc() - - def get_from_queue(qu): - dictify = dictify3 if sys.version_info >= (3,) else dictify27 - - # Here is the re-ordering mechanism, ensuring in-order delivery of updates. - max_id = None # max update_id passed to callback - buffer = collections.deque() # keep those updates which skip some update_id - qwait = None # how long to wait for updates, - # because buffer's content has to be returned in time. - - while 1: - try: - data = qu.get(block=True, timeout=qwait) - update = dictify(data) - - if max_id is None: - # First message received, handle regardless. - max_id = relay_to_collector(update) - - elif update['update_id'] == max_id + 1: - # No update_id skipped, handle naturally. - max_id = relay_to_collector(update) - - # clear contagious updates in buffer - if len(buffer) > 0: - buffer.popleft() # first element belongs to update just received, useless now. - while 1: - try: - if type(buffer[0]) is dict: - max_id = relay_to_collector(buffer.popleft()) # updates that arrived earlier, handle them. - else: - break # gap, no more contagious updates - except IndexError: - break # buffer empty - - elif update['update_id'] > max_id + 1: - # Update arrives pre-maturely, insert to buffer. - nbuf = len(buffer) - if update['update_id'] <= max_id + nbuf: - # buffer long enough, put update at position - buffer[update['update_id'] - max_id - 1] = update - else: - # buffer too short, lengthen it - expire = time.time() + maxhold - for a in range(nbuf, update['update_id']-max_id-1): - buffer.append(expire) # put expiry time in gaps - buffer.append(update) - - else: - pass # discard - - except queue.Empty: - # debug message - # print('Timeout') - - # some buffer contents have to be handled - # flush buffer until a non-expired time is encountered - while 1: - try: - if type(buffer[0]) is dict: - max_id = relay_to_collector(buffer.popleft()) - else: - expire = buffer[0] - if expire <= time.time(): - max_id += 1 - buffer.popleft() - else: - break # non-expired - except IndexError: - break # buffer empty - except: - traceback.print_exc() - finally: - try: - # don't wait longer than next expiry time - qwait = buffer[0] - time.time() - if qwait < 0: - qwait = 0 - except IndexError: - # buffer empty, can wait forever - qwait = None - - # debug message - # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id) - - collector_thread = threading.Thread(target=collector) - collector_thread.daemon = True - collector_thread.start() - - if source is None: - message_thread = threading.Thread(target=get_from_telegram_server) - elif isinstance(source, queue.Queue): - if ordered: - message_thread = threading.Thread(target=get_from_queue, args=(source,)) - else: - message_thread = threading.Thread(target=get_from_queue_unordered, args=(source,)) - else: - raise ValueError('Invalid source') - - message_thread.daemon = True # need this for main thread to be killable by Ctrl-C - message_thread.start() - - self._scheduler.on_event(collect_queue.put) - self._scheduler.run_as_thread() - - if run_forever: - if _isstring(run_forever): - print(run_forever) - while 1: - time.sleep(10) - - -import inspect - -class SpeakerBot(Bot): - def __init__(self, token): - super(SpeakerBot, self).__init__(token) - self._mic = helper.Microphone() - - @property - def mic(self): - return self._mic - - def create_listener(self): - q = queue.Queue() - self._mic.add(q) - ln = helper.Listener(self._mic, q) - return ln - - -class DelegatorBot(SpeakerBot): - def __init__(self, token, delegation_patterns): - """ - :param delegation_patterns: a list of (seeder, delegator) tuples. - """ - super(DelegatorBot, self).__init__(token) - self._delegate_records = [p+({},) for p in delegation_patterns] - - def _startable(self, delegate): - return ((hasattr(delegate, 'start') and inspect.ismethod(delegate.start)) and - (hasattr(delegate, 'is_alive') and inspect.ismethod(delegate.is_alive))) - - def _tuple_is_valid(self, t): - return len(t) == 3 and callable(t[0]) and type(t[1]) in [list, tuple] and type(t[2]) is dict - - def _ensure_startable(self, delegate): - if self._startable(delegate): - return delegate - elif callable(delegate): - return threading.Thread(target=delegate) - elif type(delegate) is tuple and self._tuple_is_valid(delegate): - func, args, kwargs = delegate - return threading.Thread(target=func, args=args, kwargs=kwargs) - else: - raise RuntimeError('Delegate does not have the required methods, is not callable, and is not a valid tuple.') - - def handle(self, msg): - self._mic.send(msg) - - for calculate_seed, make_delegate, dict in self._delegate_records: - id = calculate_seed(msg) - - if id is None: - continue - elif isinstance(id, collections.Hashable): - if id not in dict or not dict[id].is_alive(): - d = make_delegate((self, msg, id)) - d = self._ensure_startable(d) - - dict[id] = d - dict[id].start() - else: - d = make_delegate((self, msg, id)) - d = self._ensure_startable(d) - d.start() diff --git a/lib/support/telepot2/aio/__init__.py b/lib/support/telepot2/aio/__init__.py deleted file mode 100644 index 8e688fa..0000000 --- a/lib/support/telepot2/aio/__init__.py +++ /dev/null @@ -1,926 +0,0 @@ -import io -import json -import time -import asyncio -import traceback -import collections -from concurrent.futures._base import CancelledError -from . import helper, api -from .. import ( - _BotBase, flavor, _find_first_key, _isstring, _strip, _rectify, - _dismantle_message_identifier, _split_input_media_array -) - -# Patch aiohttp for sending unicode filename -from . import hack - -from .. import exception - - -def flavor_router(routing_table): - router = helper.Router(flavor, routing_table) - return router.route - - -class Bot(_BotBase): - class Scheduler(object): - def __init__(self, loop): - self._loop = loop - self._callback = None - - def on_event(self, callback): - self._callback = callback - - def event_at(self, when, data): - delay = when - time.time() - return self._loop.call_later(delay, self._callback, data) - # call_at() uses event loop time, not unix time. - # May as well use call_later here. - - def event_later(self, delay, data): - return self._loop.call_later(delay, self._callback, data) - - def event_now(self, data): - return self._loop.call_soon(self._callback, data) - - def cancel(self, event): - return event.cancel() - - def __init__(self, token, loop=None): - super(Bot, self).__init__(token) - - self._loop = loop or asyncio.get_event_loop() - api._loop = self._loop # sync loop with api module - - self._scheduler = self.Scheduler(self._loop) - - self._router = helper.Router(flavor, {'chat': helper._create_invoker(self, 'on_chat_message'), - 'callback_query': helper._create_invoker(self, 'on_callback_query'), - 'inline_query': helper._create_invoker(self, 'on_inline_query'), - 'chosen_inline_result': helper._create_invoker(self, 'on_chosen_inline_result')}) - - @property - def loop(self): - return self._loop - - @property - def scheduler(self): - return self._scheduler - - @property - def router(self): - return self._router - - async def handle(self, msg): - await self._router.route(msg) - - async def _api_request(self, method, params=None, files=None, **kwargs): - return await api.request((self._token, method, params, files), **kwargs) - - async def _api_request_with_file(self, method, params, file_key, file_value, **kwargs): - if _isstring(file_value): - params[file_key] = file_value - return await self._api_request(method, _rectify(params), **kwargs) - else: - files = {file_key: file_value} - return await self._api_request(method, _rectify(params), files, **kwargs) - - async def getMe(self): - """ See: https://core.telegram.org/bots/api#getme """ - return await self._api_request('getMe') - - async def sendMessage(self, chat_id, text, - parse_mode=None, - disable_web_page_preview=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendmessage """ - p = _strip(locals()) - return await self._api_request('sendMessage', _rectify(p)) - - async def forwardMessage(self, chat_id, from_chat_id, message_id, - disable_notification=None): - """ See: https://core.telegram.org/bots/api#forwardmessage """ - p = _strip(locals()) - return await self._api_request('forwardMessage', _rectify(p)) - - async def sendPhoto(self, chat_id, photo, - caption=None, - parse_mode=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendphoto - - :param photo: - - string: ``file_id`` for a photo existing on Telegram servers - - string: HTTP URL of a photo from the Internet - - file-like object: obtained by ``open(path, 'rb')`` - - tuple: (filename, file-like object). If the filename contains - non-ASCII characters and you are using Python 2.7, make sure the - filename is a unicode string. - """ - p = _strip(locals(), more=['photo']) - return await self._api_request_with_file('sendPhoto', _rectify(p), 'photo', photo) - - async def sendAudio(self, chat_id, audio, - caption=None, - parse_mode=None, - duration=None, - performer=None, - title=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendaudio - - :param audio: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto` - """ - p = _strip(locals(), more=['audio']) - return await self._api_request_with_file('sendAudio', _rectify(p), 'audio', audio) - - async def sendDocument(self, chat_id, document, - caption=None, - parse_mode=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#senddocument - - :param document: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto` - """ - p = _strip(locals(), more=['document']) - return await self._api_request_with_file('sendDocument', _rectify(p), 'document', document) - - async def sendVideo(self, chat_id, video, - duration=None, - width=None, - height=None, - caption=None, - parse_mode=None, - supports_streaming=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendvideo - - :param video: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto` - """ - p = _strip(locals(), more=['video']) - return await self._api_request_with_file('sendVideo', _rectify(p), 'video', video) - - async def sendVoice(self, chat_id, voice, - caption=None, - parse_mode=None, - duration=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendvoice - - :param voice: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto` - """ - p = _strip(locals(), more=['voice']) - return await self._api_request_with_file('sendVoice', _rectify(p), 'voice', voice) - - async def sendVideoNote(self, chat_id, video_note, - duration=None, - length=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendvideonote - - :param voice: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto` - - :param length: - Although marked as optional, this method does not seem to work without - it being specified. Supply any integer you want. It seems to have no effect - on the video note's display size. - """ - p = _strip(locals(), more=['video_note']) - return await self._api_request_with_file('sendVideoNote', _rectify(p), 'video_note', video_note) - - async def sendMediaGroup(self, chat_id, media, - disable_notification=None, - reply_to_message_id=None): - """ - See: https://core.telegram.org/bots/api#sendmediagroup - - :type media: array of `InputMedia `_ objects - :param media: - To indicate media locations, each InputMedia object's ``media`` field - should be one of these: - - - string: ``file_id`` for a file existing on Telegram servers - - string: HTTP URL of a file from the Internet - - file-like object: obtained by ``open(path, 'rb')`` - - tuple: (form-data name, file-like object) - - tuple: (form-data name, (filename, file-like object)) - - In case of uploading, you may supply customized multipart/form-data - names for each uploaded file (as in last 2 options above). Otherwise, - telepot assigns unique names to each uploaded file. Names assigned by - telepot will not collide with user-supplied names, if any. - """ - p = _strip(locals(), more=['media']) - legal_media, files_to_attach = _split_input_media_array(media) - - p['media'] = legal_media - return await self._api_request('sendMediaGroup', _rectify(p), files_to_attach) - - async def sendLocation(self, chat_id, latitude, longitude, - live_period=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendlocation """ - p = _strip(locals()) - return await self._api_request('sendLocation', _rectify(p)) - - async def editMessageLiveLocation(self, msg_identifier, latitude, longitude, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagelivelocation - - :param msg_identifier: Same as in :meth:`.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return await self._api_request('editMessageLiveLocation', _rectify(p)) - - async def stopMessageLiveLocation(self, msg_identifier, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#stopmessagelivelocation - - :param msg_identifier: Same as in :meth:`.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return await self._api_request('stopMessageLiveLocation', _rectify(p)) - - async def sendVenue(self, chat_id, latitude, longitude, title, address, - foursquare_id=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendvenue """ - p = _strip(locals()) - return await self._api_request('sendVenue', _rectify(p)) - - async def sendContact(self, chat_id, phone_number, first_name, - last_name=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendcontact """ - p = _strip(locals()) - return await self._api_request('sendContact', _rectify(p)) - - async def sendGame(self, chat_id, game_short_name, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendgame """ - p = _strip(locals()) - return await self._api_request('sendGame', _rectify(p)) - - async def sendInvoice(self, chat_id, title, description, payload, - provider_token, start_parameter, currency, prices, - provider_data=None, - photo_url=None, - photo_size=None, - photo_width=None, - photo_height=None, - need_name=None, - need_phone_number=None, - need_email=None, - need_shipping_address=None, - is_flexible=None, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ See: https://core.telegram.org/bots/api#sendinvoice """ - p = _strip(locals()) - return await self._api_request('sendInvoice', _rectify(p)) - - async def sendChatAction(self, chat_id, action): - """ See: https://core.telegram.org/bots/api#sendchataction """ - p = _strip(locals()) - return await self._api_request('sendChatAction', _rectify(p)) - - async def getUserProfilePhotos(self, user_id, - offset=None, - limit=None): - """ See: https://core.telegram.org/bots/api#getuserprofilephotos """ - p = _strip(locals()) - return await self._api_request('getUserProfilePhotos', _rectify(p)) - - async def getFile(self, file_id): - """ See: https://core.telegram.org/bots/api#getfile """ - p = _strip(locals()) - return await self._api_request('getFile', _rectify(p)) - - async def kickChatMember(self, chat_id, user_id, - until_date=None): - """ See: https://core.telegram.org/bots/api#kickchatmember """ - p = _strip(locals()) - return await self._api_request('kickChatMember', _rectify(p)) - - async def unbanChatMember(self, chat_id, user_id): - """ See: https://core.telegram.org/bots/api#unbanchatmember """ - p = _strip(locals()) - return await self._api_request('unbanChatMember', _rectify(p)) - - async def restrictChatMember(self, chat_id, user_id, - until_date=None, - can_send_messages=None, - can_send_media_messages=None, - can_send_other_messages=None, - can_add_web_page_previews=None): - """ See: https://core.telegram.org/bots/api#restrictchatmember """ - p = _strip(locals()) - return await self._api_request('restrictChatMember', _rectify(p)) - - async def promoteChatMember(self, chat_id, user_id, - can_change_info=None, - can_post_messages=None, - can_edit_messages=None, - can_delete_messages=None, - can_invite_users=None, - can_restrict_members=None, - can_pin_messages=None, - can_promote_members=None): - """ See: https://core.telegram.org/bots/api#promotechatmember """ - p = _strip(locals()) - return await self._api_request('promoteChatMember', _rectify(p)) - - async def exportChatInviteLink(self, chat_id): - """ See: https://core.telegram.org/bots/api#exportchatinvitelink """ - p = _strip(locals()) - return await self._api_request('exportChatInviteLink', _rectify(p)) - - async def setChatPhoto(self, chat_id, photo): - """ See: https://core.telegram.org/bots/api#setchatphoto """ - p = _strip(locals(), more=['photo']) - return await self._api_request_with_file('setChatPhoto', _rectify(p), 'photo', photo) - - async def deleteChatPhoto(self, chat_id): - """ See: https://core.telegram.org/bots/api#deletechatphoto """ - p = _strip(locals()) - return await self._api_request('deleteChatPhoto', _rectify(p)) - - async def setChatTitle(self, chat_id, title): - """ See: https://core.telegram.org/bots/api#setchattitle """ - p = _strip(locals()) - return await self._api_request('setChatTitle', _rectify(p)) - - async def setChatDescription(self, chat_id, - description=None): - """ See: https://core.telegram.org/bots/api#setchatdescription """ - p = _strip(locals()) - return await self._api_request('setChatDescription', _rectify(p)) - - async def pinChatMessage(self, chat_id, message_id, - disable_notification=None): - """ See: https://core.telegram.org/bots/api#pinchatmessage """ - p = _strip(locals()) - return await self._api_request('pinChatMessage', _rectify(p)) - - async def unpinChatMessage(self, chat_id): - """ See: https://core.telegram.org/bots/api#unpinchatmessage """ - p = _strip(locals()) - return await self._api_request('unpinChatMessage', _rectify(p)) - - async def leaveChat(self, chat_id): - """ See: https://core.telegram.org/bots/api#leavechat """ - p = _strip(locals()) - return await self._api_request('leaveChat', _rectify(p)) - - async def getChat(self, chat_id): - """ See: https://core.telegram.org/bots/api#getchat """ - p = _strip(locals()) - return await self._api_request('getChat', _rectify(p)) - - async def getChatAdministrators(self, chat_id): - """ See: https://core.telegram.org/bots/api#getchatadministrators """ - p = _strip(locals()) - return await self._api_request('getChatAdministrators', _rectify(p)) - - async def getChatMembersCount(self, chat_id): - """ See: https://core.telegram.org/bots/api#getchatmemberscount """ - p = _strip(locals()) - return await self._api_request('getChatMembersCount', _rectify(p)) - - async def getChatMember(self, chat_id, user_id): - """ See: https://core.telegram.org/bots/api#getchatmember """ - p = _strip(locals()) - return await self._api_request('getChatMember', _rectify(p)) - - async def setChatStickerSet(self, chat_id, sticker_set_name): - """ See: https://core.telegram.org/bots/api#setchatstickerset """ - p = _strip(locals()) - return await self._api_request('setChatStickerSet', _rectify(p)) - - async def deleteChatStickerSet(self, chat_id): - """ See: https://core.telegram.org/bots/api#deletechatstickerset """ - p = _strip(locals()) - return await self._api_request('deleteChatStickerSet', _rectify(p)) - - async def answerCallbackQuery(self, callback_query_id, - text=None, - show_alert=None, - url=None, - cache_time=None): - """ See: https://core.telegram.org/bots/api#answercallbackquery """ - p = _strip(locals()) - return await self._api_request('answerCallbackQuery', _rectify(p)) - - async def answerShippingQuery(self, shipping_query_id, ok, - shipping_options=None, - error_message=None): - """ See: https://core.telegram.org/bots/api#answershippingquery """ - p = _strip(locals()) - return await self._api_request('answerShippingQuery', _rectify(p)) - - async def answerPreCheckoutQuery(self, pre_checkout_query_id, ok, - error_message=None): - """ See: https://core.telegram.org/bots/api#answerprecheckoutquery """ - p = _strip(locals()) - return await self._api_request('answerPreCheckoutQuery', _rectify(p)) - - async def editMessageText(self, msg_identifier, text, - parse_mode=None, - disable_web_page_preview=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagetext - - :param msg_identifier: - a 2-tuple (``chat_id``, ``message_id``), - a 1-tuple (``inline_message_id``), - or simply ``inline_message_id``. - You may extract this value easily with :meth:`telepot.message_identifier` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return await self._api_request('editMessageText', _rectify(p)) - - async def editMessageCaption(self, msg_identifier, - caption=None, - parse_mode=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagecaption - - :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return await self._api_request('editMessageCaption', _rectify(p)) - - async def editMessageReplyMarkup(self, msg_identifier, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#editmessagereplymarkup - - :param msg_identifier: Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText` - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return await self._api_request('editMessageReplyMarkup', _rectify(p)) - - async def deleteMessage(self, msg_identifier): - """ - See: https://core.telegram.org/bots/api#deletemessage - - :param msg_identifier: - Same as ``msg_identifier`` in :meth:`telepot.aio.Bot.editMessageText`, - except this method does not work on inline messages. - """ - p = _strip(locals(), more=['msg_identifier']) - p.update(_dismantle_message_identifier(msg_identifier)) - return await self._api_request('deleteMessage', _rectify(p)) - - async def sendSticker(self, chat_id, sticker, - disable_notification=None, - reply_to_message_id=None, - reply_markup=None): - """ - See: https://core.telegram.org/bots/api#sendsticker - - :param sticker: Same as ``photo`` in :meth:`telepot.aio.Bot.sendPhoto` - """ - p = _strip(locals(), more=['sticker']) - return await self._api_request_with_file('sendSticker', _rectify(p), 'sticker', sticker) - - async def getStickerSet(self, name): - """ - See: https://core.telegram.org/bots/api#getstickerset - """ - p = _strip(locals()) - return await self._api_request('getStickerSet', _rectify(p)) - - async def uploadStickerFile(self, user_id, png_sticker): - """ - See: https://core.telegram.org/bots/api#uploadstickerfile - """ - p = _strip(locals(), more=['png_sticker']) - return await self._api_request_with_file('uploadStickerFile', _rectify(p), 'png_sticker', png_sticker) - - async def createNewStickerSet(self, user_id, name, title, png_sticker, emojis, - contains_masks=None, - mask_position=None): - """ - See: https://core.telegram.org/bots/api#createnewstickerset - """ - p = _strip(locals(), more=['png_sticker']) - return await self._api_request_with_file('createNewStickerSet', _rectify(p), 'png_sticker', png_sticker) - - async def addStickerToSet(self, user_id, name, png_sticker, emojis, - mask_position=None): - """ - See: https://core.telegram.org/bots/api#addstickertoset - """ - p = _strip(locals(), more=['png_sticker']) - return await self._api_request_with_file('addStickerToSet', _rectify(p), 'png_sticker', png_sticker) - - async def setStickerPositionInSet(self, sticker, position): - """ - See: https://core.telegram.org/bots/api#setstickerpositioninset - """ - p = _strip(locals()) - return await self._api_request('setStickerPositionInSet', _rectify(p)) - - async def deleteStickerFromSet(self, sticker): - """ - See: https://core.telegram.org/bots/api#deletestickerfromset - """ - p = _strip(locals()) - return await self._api_request('deleteStickerFromSet', _rectify(p)) - - async def answerInlineQuery(self, inline_query_id, results, - cache_time=None, - is_personal=None, - next_offset=None, - switch_pm_text=None, - switch_pm_parameter=None): - """ See: https://core.telegram.org/bots/api#answerinlinequery """ - p = _strip(locals()) - return await self._api_request('answerInlineQuery', _rectify(p)) - - async def getUpdates(self, - offset=None, - limit=None, - timeout=None, - allowed_updates=None): - """ See: https://core.telegram.org/bots/api#getupdates """ - p = _strip(locals()) - return await self._api_request('getUpdates', _rectify(p)) - - async def setWebhook(self, - url=None, - certificate=None, - max_connections=None, - allowed_updates=None): - """ See: https://core.telegram.org/bots/api#setwebhook """ - p = _strip(locals(), more=['certificate']) - - if certificate: - files = {'certificate': certificate} - return await self._api_request('setWebhook', _rectify(p), files) - else: - return await self._api_request('setWebhook', _rectify(p)) - - async def deleteWebhook(self): - """ See: https://core.telegram.org/bots/api#deletewebhook """ - return await self._api_request('deleteWebhook') - - async def getWebhookInfo(self): - """ See: https://core.telegram.org/bots/api#getwebhookinfo """ - return await self._api_request('getWebhookInfo') - - async def setGameScore(self, user_id, score, game_message_identifier, - force=None, - disable_edit_message=None): - """ See: https://core.telegram.org/bots/api#setgamescore """ - p = _strip(locals(), more=['game_message_identifier']) - p.update(_dismantle_message_identifier(game_message_identifier)) - return await self._api_request('setGameScore', _rectify(p)) - - async def getGameHighScores(self, user_id, game_message_identifier): - """ See: https://core.telegram.org/bots/api#getgamehighscores """ - p = _strip(locals(), more=['game_message_identifier']) - p.update(_dismantle_message_identifier(game_message_identifier)) - return await self._api_request('getGameHighScores', _rectify(p)) - - async def download_file(self, file_id, dest): - """ - Download a file to local disk. - - :param dest: a path or a ``file`` object - """ - f = await self.getFile(file_id) - - try: - d = dest if isinstance(dest, io.IOBase) else open(dest, 'wb') - - session, request = api.download((self._token, f['file_path'])) - - async with session: - async with request as r: - while 1: - chunk = await r.content.read(self._file_chunk_size) - if not chunk: - break - d.write(chunk) - d.flush() - finally: - if not isinstance(dest, io.IOBase) and 'd' in locals(): - d.close() - - async def message_loop(self, handler=None, relax=0.1, - timeout=20, allowed_updates=None, - source=None, ordered=True, maxhold=3): - """ - Return a task to constantly ``getUpdates`` or pull updates from a queue. - Apply ``handler`` to every message received. - - :param handler: - a function that takes one argument (the message), or a routing table. - If ``None``, the bot's ``handle`` method is used. - - A *routing table* is a dictionary of ``{flavor: function}``, mapping messages to appropriate - handler functions according to their flavors. It allows you to define functions specifically - to handle one flavor of messages. It usually looks like this: ``{'chat': fn1, - 'callback_query': fn2, 'inline_query': fn3, ...}``. Each handler function should take - one argument (the message). - - :param source: - Source of updates. - If ``None``, ``getUpdates`` is used to obtain new messages from Telegram servers. - If it is a ``asyncio.Queue``, new messages are pulled from the queue. - A web application implementing a webhook can dump updates into the queue, - while the bot pulls from it. This is how telepot can be integrated with webhooks. - - Acceptable contents in queue: - - - ``str`` or ``bytes`` (decoded using UTF-8) - representing a JSON-serialized `Update `_ object. - - a ``dict`` representing an Update object. - - When ``source`` is a queue, these parameters are meaningful: - - :type ordered: bool - :param ordered: - If ``True``, ensure in-order delivery of messages to ``handler`` - (i.e. updates with a smaller ``update_id`` always come before those with - a larger ``update_id``). - If ``False``, no re-ordering is done. ``handler`` is applied to messages - as soon as they are pulled from queue. - - :type maxhold: float - :param maxhold: - Applied only when ``ordered`` is ``True``. The maximum number of seconds - an update is held waiting for a not-yet-arrived smaller ``update_id``. - When this number of seconds is up, the update is delivered to ``handler`` - even if some smaller ``update_id``\s have not yet arrived. If those smaller - ``update_id``\s arrive at some later time, they are discarded. - - :type timeout: int - :param timeout: - ``timeout`` parameter supplied to :meth:`telepot.aio.Bot.getUpdates`, - controlling how long to poll in seconds. - - :type allowed_updates: array of string - :param allowed_updates: - ``allowed_updates`` parameter supplied to :meth:`telepot.aio.Bot.getUpdates`, - controlling which types of updates to receive. - """ - if handler is None: - handler = self.handle - elif isinstance(handler, dict): - handler = flavor_router(handler) - - def create_task_for(msg): - self.loop.create_task(handler(msg)) - - if asyncio.iscoroutinefunction(handler): - callback = create_task_for - else: - callback = handler - - def handle(update): - try: - key = _find_first_key(update, ['message', - 'edited_message', - 'channel_post', - 'edited_channel_post', - 'callback_query', - 'inline_query', - 'chosen_inline_result', - 'shipping_query', - 'pre_checkout_query']) - - callback(update[key]) - except: - # Localize the error so message thread can keep going. - traceback.print_exc() - finally: - return update['update_id'] - - async def get_from_telegram_server(): - offset = None # running offset - allowed_upd = allowed_updates - while 1: - try: - result = await self.getUpdates(offset=offset, - timeout=timeout, - allowed_updates=allowed_upd) - - # Once passed, this parameter is no longer needed. - allowed_upd = None - - if len(result) > 0: - # No sort. Trust server to give messages in correct order. - # Update offset to max(update_id) + 1 - offset = max([handle(update) for update in result]) + 1 - except CancelledError: - raise - except exception.BadHTTPResponse as e: - traceback.print_exc() - - # Servers probably down. Wait longer. - if e.status == 502: - await asyncio.sleep(30) - except: - traceback.print_exc() - await asyncio.sleep(relax) - else: - await asyncio.sleep(relax) - - def dictify(data): - if type(data) is bytes: - return json.loads(data.decode('utf-8')) - elif type(data) is str: - return json.loads(data) - elif type(data) is dict: - return data - else: - raise ValueError() - - async def get_from_queue_unordered(qu): - while 1: - try: - data = await qu.get() - update = dictify(data) - handle(update) - except: - traceback.print_exc() - - async def get_from_queue(qu): - # Here is the re-ordering mechanism, ensuring in-order delivery of updates. - max_id = None # max update_id passed to callback - buffer = collections.deque() # keep those updates which skip some update_id - qwait = None # how long to wait for updates, - # because buffer's content has to be returned in time. - - while 1: - try: - data = await asyncio.wait_for(qu.get(), qwait) - update = dictify(data) - - if max_id is None: - # First message received, handle regardless. - max_id = handle(update) - - elif update['update_id'] == max_id + 1: - # No update_id skipped, handle naturally. - max_id = handle(update) - - # clear contagious updates in buffer - if len(buffer) > 0: - buffer.popleft() # first element belongs to update just received, useless now. - while 1: - try: - if type(buffer[0]) is dict: - max_id = handle(buffer.popleft()) # updates that arrived earlier, handle them. - else: - break # gap, no more contagious updates - except IndexError: - break # buffer empty - - elif update['update_id'] > max_id + 1: - # Update arrives pre-maturely, insert to buffer. - nbuf = len(buffer) - if update['update_id'] <= max_id + nbuf: - # buffer long enough, put update at position - buffer[update['update_id'] - max_id - 1] = update - else: - # buffer too short, lengthen it - expire = time.time() + maxhold - for a in range(nbuf, update['update_id']-max_id-1): - buffer.append(expire) # put expiry time in gaps - buffer.append(update) - - else: - pass # discard - - except asyncio.TimeoutError: - # debug message - # print('Timeout') - - # some buffer contents have to be handled - # flush buffer until a non-expired time is encountered - while 1: - try: - if type(buffer[0]) is dict: - max_id = handle(buffer.popleft()) - else: - expire = buffer[0] - if expire <= time.time(): - max_id += 1 - buffer.popleft() - else: - break # non-expired - except IndexError: - break # buffer empty - except: - traceback.print_exc() - finally: - try: - # don't wait longer than next expiry time - qwait = buffer[0] - time.time() - if qwait < 0: - qwait = 0 - except IndexError: - # buffer empty, can wait forever - qwait = None - - # debug message - # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id) - - self._scheduler._callback = callback - - if source is None: - await get_from_telegram_server() - elif isinstance(source, asyncio.Queue): - if ordered: - await get_from_queue(source) - else: - await get_from_queue_unordered(source) - else: - raise ValueError('Invalid source') - - -class SpeakerBot(Bot): - def __init__(self, token, loop=None): - super(SpeakerBot, self).__init__(token, loop) - self._mic = helper.Microphone() - - @property - def mic(self): - return self._mic - - def create_listener(self): - q = asyncio.Queue() - self._mic.add(q) - ln = helper.Listener(self._mic, q) - return ln - - -class DelegatorBot(SpeakerBot): - def __init__(self, token, delegation_patterns, loop=None): - """ - :param delegation_patterns: a list of (seeder, delegator) tuples. - """ - super(DelegatorBot, self).__init__(token, loop) - self._delegate_records = [p+({},) for p in delegation_patterns] - - def handle(self, msg): - self._mic.send(msg) - - for calculate_seed, make_coroutine_obj, dict in self._delegate_records: - id = calculate_seed(msg) - - if id is None: - continue - elif isinstance(id, collections.Hashable): - if id not in dict or dict[id].done(): - c = make_coroutine_obj((self, msg, id)) - - if not asyncio.iscoroutine(c): - raise RuntimeError('You must produce a coroutine *object* as delegate.') - - dict[id] = self._loop.create_task(c) - else: - c = make_coroutine_obj((self, msg, id)) - self._loop.create_task(c) diff --git a/lib/support/telepot2/aio/api.py b/lib/support/telepot2/aio/api.py deleted file mode 100644 index 8454363..0000000 --- a/lib/support/telepot2/aio/api.py +++ /dev/null @@ -1,168 +0,0 @@ -import asyncio -import aiohttp -import async_timeout -import atexit -import re -import json -from .. import exception -from ..api import _methodurl, _which_pool, _fileurl, _guess_filename - -_loop = asyncio.get_event_loop() - -_pools = { - 'default': aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=10), - loop=_loop) -} - -_timeout = 30 -_proxy = None # (url, (username, password)) - -def set_proxy(url, basic_auth=None): - global _proxy - if not url: - _proxy = None - else: - _proxy = (url, basic_auth) if basic_auth else (url,) - -def _proxy_kwargs(): - if _proxy is None or len(_proxy) == 0: - return {} - elif len(_proxy) == 1: - return {'proxy': _proxy[0]} - elif len(_proxy) == 2: - return {'proxy': _proxy[0], 'proxy_auth': aiohttp.BasicAuth(*_proxy[1])} - else: - raise RuntimeError("_proxy has invalid length") - -async def _close_pools(): - global _pools - for s in _pools.values(): - await s.close() - -atexit.register(lambda: _loop.create_task(_close_pools())) # have to wrap async function - -def _create_onetime_pool(): - return aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=1, force_close=True), - loop=_loop) - -def _default_timeout(req, **user_kw): - return _timeout - -def _compose_timeout(req, **user_kw): - token, method, params, files = req - - if method == 'getUpdates' and params and 'timeout' in params: - # Ensure HTTP timeout is longer than getUpdates timeout - return params['timeout'] + _default_timeout(req, **user_kw) - elif files: - # Disable timeout if uploading files. For some reason, the larger the file, - # the longer it takes for the server to respond (after upload is finished). - # It is unclear how long timeout should be. - return None - else: - return _default_timeout(req, **user_kw) - -def _compose_data(req, **user_kw): - token, method, params, files = req - - data = aiohttp.FormData() - - if params: - for key,value in params.items(): - data.add_field(key, str(value)) - - if files: - for key,f in files.items(): - if isinstance(f, tuple): - if len(f) == 2: - filename, fileobj = f - else: - raise ValueError('Tuple must have exactly 2 elements: filename, fileobj') - else: - filename, fileobj = _guess_filename(f) or key, f - - data.add_field(key, fileobj, filename=filename) - - return data - -def _transform(req, **user_kw): - timeout = _compose_timeout(req, **user_kw) - - data = _compose_data(req, **user_kw) - - url = _methodurl(req, **user_kw) - - name = _which_pool(req, **user_kw) - - if name is None: - session = _create_onetime_pool() - cleanup = session.close # one-time session: remember to close - else: - session = _pools[name] - cleanup = None # reuse: do not close - - kwargs = {'data':data} - kwargs.update(user_kw) - - return session.post, (url,), kwargs, timeout, cleanup - -async def _parse(response): - try: - data = await response.json() - if data is None: - raise ValueError() - except (ValueError, json.JSONDecodeError, aiohttp.ClientResponseError): - text = await response.text() - raise exception.BadHTTPResponse(response.status, text, response) - - if data['ok']: - return data['result'] - else: - description, error_code = data['description'], data['error_code'] - - # Look for specific error ... - for e in exception.TelegramError.__subclasses__(): - n = len(e.DESCRIPTION_PATTERNS) - if any(map(re.search, e.DESCRIPTION_PATTERNS, n*[description], n*[re.IGNORECASE])): - raise e(description, error_code, data) - - # ... or raise generic error - raise exception.TelegramError(description, error_code, data) - -async def request(req, **user_kw): - fn, args, kwargs, timeout, cleanup = _transform(req, **user_kw) - - kwargs.update(_proxy_kwargs()) - try: - if timeout is None: - async with fn(*args, **kwargs) as r: - return await _parse(r) - else: - try: - with async_timeout.timeout(timeout): - async with fn(*args, **kwargs) as r: - return await _parse(r) - - except asyncio.TimeoutError: - raise exception.TelegramError('Response timeout', 504, {}) - - except aiohttp.ClientConnectionError: - raise exception.TelegramError('Connection Error', 400, {}) - - finally: - if cleanup: # e.g. closing one-time session - if asyncio.iscoroutinefunction(cleanup): - await cleanup() - else: - cleanup() - -def download(req): - session = _create_onetime_pool() - - kwargs = {} - kwargs.update(_proxy_kwargs()) - - return session, session.get(_fileurl(req), timeout=_timeout, **kwargs) - # Caller should close session after download is complete diff --git a/lib/support/telepot2/aio/delegate.py b/lib/support/telepot2/aio/delegate.py deleted file mode 100644 index d316c5c..0000000 --- a/lib/support/telepot2/aio/delegate.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Like :mod:`telepot.delegate`, this module has a bunch of seeder factories -and delegator factories. - -.. autofunction:: per_chat_id -.. autofunction:: per_chat_id_in -.. autofunction:: per_chat_id_except -.. autofunction:: per_from_id -.. autofunction:: per_from_id_in -.. autofunction:: per_from_id_except -.. autofunction:: per_inline_from_id -.. autofunction:: per_inline_from_id_in -.. autofunction:: per_inline_from_id_except -.. autofunction:: per_application -.. autofunction:: per_message -.. autofunction:: per_event_source_id -.. autofunction:: per_callback_query_chat_id -.. autofunction:: per_callback_query_origin -.. autofunction:: per_invoice_payload -.. autofunction:: until -.. autofunction:: chain -.. autofunction:: pair -.. autofunction:: pave_event_space -.. autofunction:: include_callback_query_chat_id -.. autofunction:: intercept_callback_query_origin -""" - -import asyncio -import traceback -from .. import exception -from . import helper - -# Mirror traditional version to avoid having to import one more module -from ..delegate import ( - per_chat_id, per_chat_id_in, per_chat_id_except, - per_from_id, per_from_id_in, per_from_id_except, - per_inline_from_id, per_inline_from_id_in, per_inline_from_id_except, - per_application, per_message, per_event_source_id, - per_callback_query_chat_id, per_callback_query_origin, per_invoice_payload, - until, chain, pair, pave_event_space, - include_callback_query_chat_id, intercept_callback_query_origin -) - -def _ensure_coroutine_function(fn): - return fn if asyncio.iscoroutinefunction(fn) else asyncio.coroutine(fn) - -def call(corofunc, *args, **kwargs): - """ - :return: - a delegator function that returns a coroutine object by calling - ``corofunc(seed_tuple, *args, **kwargs)``. - """ - corofunc = _ensure_coroutine_function(corofunc) - def f(seed_tuple): - return corofunc(seed_tuple, *args, **kwargs) - return f - -def create_run(cls, *args, **kwargs): - """ - :return: - a delegator function that calls the ``cls`` constructor whose arguments being - a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns - a coroutine object by calling the object's ``run`` method, which should be - a coroutine function. - """ - def f(seed_tuple): - j = cls(seed_tuple, *args, **kwargs) - return _ensure_coroutine_function(j.run)() - return f - -def create_open(cls, *args, **kwargs): - """ - :return: - a delegator function that calls the ``cls`` constructor whose arguments being - a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns - a looping coroutine object that uses the object's ``listener`` to wait for - messages and invokes instance method ``open``, ``on_message``, and ``on_close`` - accordingly. - """ - def f(seed_tuple): - j = cls(seed_tuple, *args, **kwargs) - - async def wait_loop(): - bot, msg, seed = seed_tuple - try: - handled = await helper._invoke(j.open, msg, seed) - if not handled: - await helper._invoke(j.on_message, msg) - - while 1: - msg = await j.listener.wait() - await helper._invoke(j.on_message, msg) - - # These exceptions are "normal" exits. - except (exception.IdleTerminate, exception.StopListening) as e: - await helper._invoke(j.on_close, e) - - # Any other exceptions are accidents. **Print it out.** - # This is to prevent swallowing exceptions in the case that on_close() - # gets overridden but fails to account for unexpected exceptions. - except Exception as e: - traceback.print_exc() - await helper._invoke(j.on_close, e) - - return wait_loop() - return f diff --git a/lib/support/telepot2/aio/hack.py b/lib/support/telepot2/aio/hack.py deleted file mode 100644 index 4a5deb2..0000000 --- a/lib/support/telepot2/aio/hack.py +++ /dev/null @@ -1,36 +0,0 @@ -try: - import aiohttp - from urllib.parse import quote - - def content_disposition_header(disptype, quote_fields=True, **params): - if not disptype or not (aiohttp.helpers.TOKEN > set(disptype)): - raise ValueError('bad content disposition type {!r}' - ''.format(disptype)) - - value = disptype - if params: - lparams = [] - for key, val in params.items(): - if not key or not (aiohttp.helpers.TOKEN > set(key)): - raise ValueError('bad content disposition parameter' - ' {!r}={!r}'.format(key, val)) - - ###### Do not encode filename - if key == 'filename': - qval = val - else: - qval = quote(val, '') if quote_fields else val - - lparams.append((key, '"%s"' % qval)) - - sparams = '; '.join('='.join(pair) for pair in lparams) - value = '; '.join((value, sparams)) - return value - - # Override original version - aiohttp.payload.content_disposition_header = content_disposition_header - -# In case aiohttp changes and this hack no longer works, I don't want it to -# bog down the entire library. -except (ImportError, AttributeError): - pass diff --git a/lib/support/telepot2/aio/helper.py b/lib/support/telepot2/aio/helper.py deleted file mode 100644 index 3ab598f..0000000 --- a/lib/support/telepot2/aio/helper.py +++ /dev/null @@ -1,372 +0,0 @@ -import asyncio -import traceback -from .. import filtering, helper, exception -from .. import ( - flavor, chat_flavors, inline_flavors, is_event, - message_identifier, origin_identifier) - -# Mirror traditional version -from ..helper import ( - Sender, Administrator, Editor, openable, - StandardEventScheduler, StandardEventMixin) - - -async def _invoke(fn, *args, **kwargs): - if asyncio.iscoroutinefunction(fn): - return await fn(*args, **kwargs) - else: - return fn(*args, **kwargs) - - -def _create_invoker(obj, method_name): - async def d(*a, **kw): - method = getattr(obj, method_name) - return await _invoke(method, *a, **kw) - return d - - -class Microphone(object): - def __init__(self): - self._queues = set() - - def add(self, q): - self._queues.add(q) - - def remove(self, q): - self._queues.remove(q) - - def send(self, msg): - for q in self._queues: - try: - q.put_nowait(msg) - except asyncio.QueueFull: - traceback.print_exc() - pass - - -class Listener(helper.Listener): - async def wait(self): - """ - Block until a matched message appears. - """ - if not self._patterns: - raise RuntimeError('Listener has nothing to capture') - - while 1: - msg = await self._queue.get() - - if any(map(lambda p: filtering.match_all(msg, p), self._patterns)): - return msg - - -from concurrent.futures._base import CancelledError - -class Answerer(object): - """ - When processing inline queries, ensures **at most one active task** per user id. - """ - - def __init__(self, bot, loop=None): - self._bot = bot - self._loop = loop if loop is not None else asyncio.get_event_loop() - self._working_tasks = {} - - def answer(self, inline_query, compute_fn, *compute_args, **compute_kwargs): - """ - Create a task that calls ``compute fn`` (along with additional arguments - ``*compute_args`` and ``**compute_kwargs``), then applies the returned value to - :meth:`.Bot.answerInlineQuery` to answer the inline query. - If a preceding task is already working for a user, that task is cancelled, - thus ensuring at most one active task per user id. - - :param inline_query: - The inline query to be processed. The originating user is inferred from ``msg['from']['id']``. - - :param compute_fn: - A function whose returned value is given to :meth:`.Bot.answerInlineQuery` to send. - May return: - - - a *list* of `InlineQueryResult `_ - - a *tuple* whose first element is a list of `InlineQueryResult `_, - followed by positional arguments to be supplied to :meth:`.Bot.answerInlineQuery` - - a *dictionary* representing keyword arguments to be supplied to :meth:`.Bot.answerInlineQuery` - - :param \*compute_args: positional arguments to ``compute_fn`` - :param \*\*compute_kwargs: keyword arguments to ``compute_fn`` - """ - - from_id = inline_query['from']['id'] - - async def compute_and_answer(): - try: - query_id = inline_query['id'] - - ans = await _invoke(compute_fn, *compute_args, **compute_kwargs) - - if isinstance(ans, list): - await self._bot.answerInlineQuery(query_id, ans) - elif isinstance(ans, tuple): - await self._bot.answerInlineQuery(query_id, *ans) - elif isinstance(ans, dict): - await self._bot.answerInlineQuery(query_id, **ans) - else: - raise ValueError('Invalid answer format') - except CancelledError: - # Cancelled. Record has been occupied by new task. Don't touch. - raise - except: - # Die accidentally. Remove myself from record. - del self._working_tasks[from_id] - raise - else: - # Die naturally. Remove myself from record. - del self._working_tasks[from_id] - - if from_id in self._working_tasks: - self._working_tasks[from_id].cancel() - - t = self._loop.create_task(compute_and_answer()) - self._working_tasks[from_id] = t - - -class AnswererMixin(helper.AnswererMixin): - Answerer = Answerer # use async Answerer class - - -class CallbackQueryCoordinator(helper.CallbackQueryCoordinator): - def augment_send(self, send_func): - async def augmented(*aa, **kw): - sent = await send_func(*aa, **kw) - - if self._enable_chat and self._contains_callback_data(kw): - self.capture_origin(message_identifier(sent)) - - return sent - return augmented - - def augment_edit(self, edit_func): - async def augmented(msg_identifier, *aa, **kw): - edited = await edit_func(msg_identifier, *aa, **kw) - - if (edited is True and self._enable_inline) or (isinstance(edited, dict) and self._enable_chat): - if self._contains_callback_data(kw): - self.capture_origin(msg_identifier) - else: - self.uncapture_origin(msg_identifier) - - return edited - return augmented - - def augment_delete(self, delete_func): - async def augmented(msg_identifier, *aa, **kw): - deleted = await delete_func(msg_identifier, *aa, **kw) - - if deleted is True: - self.uncapture_origin(msg_identifier) - - return deleted - return augmented - - def augment_on_message(self, handler): - async def augmented(msg): - if (self._enable_inline - and flavor(msg) == 'chosen_inline_result' - and 'inline_message_id' in msg): - inline_message_id = msg['inline_message_id'] - self.capture_origin(inline_message_id) - - return await _invoke(handler, msg) - return augmented - - -class InterceptCallbackQueryMixin(helper.InterceptCallbackQueryMixin): - CallbackQueryCoordinator = CallbackQueryCoordinator - - -class IdleEventCoordinator(helper.IdleEventCoordinator): - def augment_on_message(self, handler): - async def augmented(msg): - # Reset timer if this is an external message - is_event(msg) or self.refresh() - return await _invoke(handler, msg) - return augmented - - def augment_on_close(self, handler): - async def augmented(ex): - try: - if self._timeout_event: - self._scheduler.cancel(self._timeout_event) - self._timeout_event = None - # This closing may have been caused by my own timeout, in which case - # the timeout event can no longer be found in the scheduler. - except exception.EventNotFound: - self._timeout_event = None - return await _invoke(handler, ex) - return augmented - - -class IdleTerminateMixin(helper.IdleTerminateMixin): - IdleEventCoordinator = IdleEventCoordinator - - -class Router(helper.Router): - async def route(self, msg, *aa, **kw): - """ - Apply key function to ``msg`` to obtain a key, look up routing table - to obtain a handler function, then call the handler function with - positional and keyword arguments, if any is returned by the key function. - - ``*aa`` and ``**kw`` are dummy placeholders for easy nesting. - Regardless of any number of arguments returned by the key function, - multi-level routing may be achieved like this:: - - top_router.routing_table['key1'] = sub_router1.route - top_router.routing_table['key2'] = sub_router2.route - """ - k = self.key_function(msg) - - if isinstance(k, (tuple, list)): - key, args, kwargs = {1: tuple(k) + ((),{}), - 2: tuple(k) + ({},), - 3: tuple(k),}[len(k)] - else: - key, args, kwargs = k, (), {} - - try: - fn = self.routing_table[key] - except KeyError as e: - # Check for default handler, key=None - if None in self.routing_table: - fn = self.routing_table[None] - else: - raise RuntimeError('No handler for key: %s, and default handler not defined' % str(e.args)) - - return await _invoke(fn, msg, *args, **kwargs) - - -class DefaultRouterMixin(object): - def __init__(self, *args, **kwargs): - self._router = Router(flavor, {'chat': _create_invoker(self, 'on_chat_message'), - 'callback_query': _create_invoker(self, 'on_callback_query'), - 'inline_query': _create_invoker(self, 'on_inline_query'), - 'chosen_inline_result': _create_invoker(self, 'on_chosen_inline_result'), - 'shipping_query': _create_invoker(self, 'on_shipping_query'), - 'pre_checkout_query': _create_invoker(self, 'on_pre_checkout_query'), - '_idle': _create_invoker(self, 'on__idle')}) - - super(DefaultRouterMixin, self).__init__(*args, **kwargs) - - @property - def router(self): - """ See :class:`.helper.Router` """ - return self._router - - async def on_message(self, msg): - """ - Called when a message is received. - By default, call :meth:`Router.route` to handle the message. - """ - await self._router.route(msg) - - -@openable -class Monitor(helper.ListenerContext, DefaultRouterMixin): - def __init__(self, seed_tuple, capture, **kwargs): - """ - A delegate that never times-out, probably doing some kind of background monitoring - in the application. Most naturally paired with :func:`telepot.aio.delegate.per_application`. - - :param capture: a list of patterns for ``listener`` to capture - """ - bot, initial_msg, seed = seed_tuple - super(Monitor, self).__init__(bot, seed, **kwargs) - - for pattern in capture: - self.listener.capture(pattern) - - -@openable -class ChatHandler(helper.ChatContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, - include_callback_query=False, **kwargs): - """ - A delegate to handle a chat. - """ - bot, initial_msg, seed = seed_tuple - super(ChatHandler, self).__init__(bot, seed, **kwargs) - - self.listener.capture([{'chat': {'id': self.chat_id}}]) - - if include_callback_query: - self.listener.capture([{'message': {'chat': {'id': self.chat_id}}}]) - - -@openable -class UserHandler(helper.UserContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, - include_callback_query=False, - flavors=chat_flavors+inline_flavors, **kwargs): - """ - A delegate to handle a user's actions. - - :param flavors: - A list of flavors to capture. ``all`` covers all flavors. - """ - bot, initial_msg, seed = seed_tuple - super(UserHandler, self).__init__(bot, seed, **kwargs) - - if flavors == 'all': - self.listener.capture([{'from': {'id': self.user_id}}]) - else: - self.listener.capture([lambda msg: flavor(msg) in flavors, {'from': {'id': self.user_id}}]) - - if include_callback_query: - self.listener.capture([{'message': {'chat': {'id': self.user_id}}}]) - - -class InlineUserHandler(UserHandler): - def __init__(self, seed_tuple, **kwargs): - """ - A delegate to handle a user's inline-related actions. - """ - super(InlineUserHandler, self).__init__(seed_tuple, flavors=inline_flavors, **kwargs) - - -@openable -class CallbackQueryOriginHandler(helper.CallbackQueryOriginContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, **kwargs): - """ - A delegate to handle callback query from one origin. - """ - bot, initial_msg, seed = seed_tuple - super(CallbackQueryOriginHandler, self).__init__(bot, seed, **kwargs) - - self.listener.capture([ - lambda msg: - flavor(msg) == 'callback_query' and origin_identifier(msg) == self.origin - ]) - - -@openable -class InvoiceHandler(helper.InvoiceContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, **kwargs): - """ - A delegate to handle messages related to an invoice. - """ - bot, initial_msg, seed = seed_tuple - super(InvoiceHandler, self).__init__(bot, seed, **kwargs) - - self.listener.capture([{'invoice_payload': self.payload}]) - self.listener.capture([{'successful_payment': {'invoice_payload': self.payload}}]) diff --git a/lib/support/telepot2/aio/loop.py b/lib/support/telepot2/aio/loop.py deleted file mode 100644 index 46f4b4e..0000000 --- a/lib/support/telepot2/aio/loop.py +++ /dev/null @@ -1,205 +0,0 @@ -import asyncio -import time -import traceback -import collections -from concurrent.futures._base import CancelledError - -from . import flavor_router - -from ..loop import _extract_message, _dictify -from .. import exception - - -class GetUpdatesLoop(object): - def __init__(self, bot, on_update): - self._bot = bot - self._update_handler = on_update - - async def run_forever(self, relax=0.1, offset=None, timeout=20, allowed_updates=None): - """ - Process new updates in infinity loop - - :param relax: float - :param offset: int - :param timeout: int - :param allowed_updates: bool - """ - while 1: - try: - result = await self._bot.getUpdates(offset=offset, - timeout=timeout, - allowed_updates=allowed_updates) - - # Once passed, this parameter is no longer needed. - allowed_updates = None - - # No sort. Trust server to give messages in correct order. - for update in result: - self._update_handler(update) - offset = update['update_id'] + 1 - - except CancelledError: - break - except exception.BadHTTPResponse as e: - traceback.print_exc() - - # Servers probably down. Wait longer. - if e.status == 502: - await asyncio.sleep(30) - except: - traceback.print_exc() - await asyncio.sleep(relax) - else: - await asyncio.sleep(relax) - - -def _infer_handler_function(bot, h): - if h is None: - handler = bot.handle - elif isinstance(h, dict): - handler = flavor_router(h) - else: - handler = h - - def create_task_for(msg): - bot.loop.create_task(handler(msg)) - - if asyncio.iscoroutinefunction(handler): - return create_task_for - else: - return handler - - -class MessageLoop(object): - def __init__(self, bot, handle=None): - self._bot = bot - self._handle = _infer_handler_function(bot, handle) - self._task = None - - async def run_forever(self, *args, **kwargs): - updatesloop = GetUpdatesLoop(self._bot, - lambda update: - self._handle(_extract_message(update)[1])) - - self._task = self._bot.loop.create_task(updatesloop.run_forever(*args, **kwargs)) - - self._bot.scheduler.on_event(self._handle) - - def cancel(self): - self._task.cancel() - - -class Webhook(object): - def __init__(self, bot, handle=None): - self._bot = bot - self._handle = _infer_handler_function(bot, handle) - - async def run_forever(self): - self._bot.scheduler.on_event(self._handle) - - def feed(self, data): - update = _dictify(data) - self._handle(_extract_message(update)[1]) - - -class OrderedWebhook(object): - def __init__(self, bot, handle=None): - self._bot = bot - self._handle = _infer_handler_function(bot, handle) - self._update_queue = asyncio.Queue(loop=bot.loop) - - async def run_forever(self, maxhold=3): - self._bot.scheduler.on_event(self._handle) - - def extract_handle(update): - try: - self._handle(_extract_message(update)[1]) - except: - # Localize the error so message thread can keep going. - traceback.print_exc() - finally: - return update['update_id'] - - # Here is the re-ordering mechanism, ensuring in-order delivery of updates. - max_id = None # max update_id passed to callback - buffer = collections.deque() # keep those updates which skip some update_id - qwait = None # how long to wait for updates, - # because buffer's content has to be returned in time. - - while 1: - try: - update = await asyncio.wait_for(self._update_queue.get(), qwait) - - if max_id is None: - # First message received, handle regardless. - max_id = extract_handle(update) - - elif update['update_id'] == max_id + 1: - # No update_id skipped, handle naturally. - max_id = extract_handle(update) - - # clear contagious updates in buffer - if len(buffer) > 0: - buffer.popleft() # first element belongs to update just received, useless now. - while 1: - try: - if type(buffer[0]) is dict: - max_id = extract_handle(buffer.popleft()) # updates that arrived earlier, handle them. - else: - break # gap, no more contagious updates - except IndexError: - break # buffer empty - - elif update['update_id'] > max_id + 1: - # Update arrives pre-maturely, insert to buffer. - nbuf = len(buffer) - if update['update_id'] <= max_id + nbuf: - # buffer long enough, put update at position - buffer[update['update_id'] - max_id - 1] = update - else: - # buffer too short, lengthen it - expire = time.time() + maxhold - for a in range(nbuf, update['update_id']-max_id-1): - buffer.append(expire) # put expiry time in gaps - buffer.append(update) - - else: - pass # discard - - except asyncio.TimeoutError: - # debug message - # print('Timeout') - - # some buffer contents have to be handled - # flush buffer until a non-expired time is encountered - while 1: - try: - if type(buffer[0]) is dict: - max_id = extract_handle(buffer.popleft()) - else: - expire = buffer[0] - if expire <= time.time(): - max_id += 1 - buffer.popleft() - else: - break # non-expired - except IndexError: - break # buffer empty - except: - traceback.print_exc() - finally: - try: - # don't wait longer than next expiry time - qwait = buffer[0] - time.time() - if qwait < 0: - qwait = 0 - except IndexError: - # buffer empty, can wait forever - qwait = None - - # debug message - # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id) - - def feed(self, data): - update = _dictify(data) - self._update_queue.put_nowait(update) diff --git a/lib/support/telepot2/aio/routing.py b/lib/support/telepot2/aio/routing.py deleted file mode 100644 index a1af9ca..0000000 --- a/lib/support/telepot2/aio/routing.py +++ /dev/null @@ -1,46 +0,0 @@ -from .helper import _create_invoker -from .. import all_content_types - -# Mirror traditional version to avoid having to import one more module -from ..routing import ( - by_content_type, by_command, by_chat_command, by_text, by_data, by_regex, - process_key, lower_key, upper_key -) - -def make_routing_table(obj, keys, prefix='on_'): - """ - :return: - a dictionary roughly equivalent to ``{'key1': obj.on_key1, 'key2': obj.on_key2, ...}``, - but ``obj`` does not have to define all methods. It may define the needed ones only. - - :param obj: the object - - :param keys: a list of keys - - :param prefix: a string to be prepended to keys to make method names - """ - def maptuple(k): - if isinstance(k, tuple): - if len(k) == 2: - return k - elif len(k) == 1: - return k[0], _create_invoker(obj, prefix+k[0]) - else: - raise ValueError() - else: - return k, _create_invoker(obj, prefix+k) - - return dict([maptuple(k) for k in keys]) - -def make_content_type_routing_table(obj, prefix='on_'): - """ - :return: - a dictionary covering all available content types, roughly equivalent to - ``{'text': obj.on_text, 'photo': obj.on_photo, ...}``, - but ``obj`` does not have to define all methods. It may define the needed ones only. - - :param obj: the object - - :param prefix: a string to be prepended to content types to make method names - """ - return make_routing_table(obj, all_content_types, prefix) diff --git a/lib/support/telepot2/api.py b/lib/support/telepot2/api.py deleted file mode 100644 index 3ffa788..0000000 --- a/lib/support/telepot2/api.py +++ /dev/null @@ -1,164 +0,0 @@ -import urllib3 -import logging -import json -import re -import os - -from . import exception, _isstring - -# Suppress InsecurePlatformWarning -urllib3.disable_warnings() - - -_default_pool_params = dict(num_pools=3, maxsize=10, retries=3, timeout=30) -_onetime_pool_params = dict(num_pools=1, maxsize=1, retries=3, timeout=30) - -_pools = { - 'default': urllib3.PoolManager(**_default_pool_params), -} - -_onetime_pool_spec = (urllib3.PoolManager, _onetime_pool_params) - - -def set_proxy(url, basic_auth=None): - """ - Access Bot API through a proxy. - - :param url: proxy URL - :param basic_auth: 2-tuple ``('username', 'password')`` - """ - global _pools, _onetime_pool_spec - if not url: - _pools['default'] = urllib3.PoolManager(**_default_pool_params) - _onetime_pool_spec = (urllib3.PoolManager, _onetime_pool_params) - elif basic_auth: - h = urllib3.make_headers(proxy_basic_auth=':'.join(basic_auth)) - _pools['default'] = urllib3.ProxyManager(url, proxy_headers=h, **_default_pool_params) - _onetime_pool_spec = (urllib3.ProxyManager, dict(proxy_url=url, proxy_headers=h, **_onetime_pool_params)) - else: - _pools['default'] = urllib3.ProxyManager(url, **_default_pool_params) - _onetime_pool_spec = (urllib3.ProxyManager, dict(proxy_url=url, **_onetime_pool_params)) - -def _create_onetime_pool(): - cls, kw = _onetime_pool_spec - return cls(**kw) - -def _methodurl(req, **user_kw): - token, method, params, files = req - return 'https://api.telegram.org/bot%s/%s' % (token, method) - -def _which_pool(req, **user_kw): - token, method, params, files = req - return None if files else 'default' - -def _guess_filename(obj): - name = getattr(obj, 'name', None) - if name and _isstring(name) and name[0] != '<' and name[-1] != '>': - return os.path.basename(name) - -def _filetuple(key, f): - if not isinstance(f, tuple): - return (_guess_filename(f) or key, f.read()) - elif len(f) == 1: - return (_guess_filename(f[0]) or key, f[0].read()) - elif len(f) == 2: - return (f[0], f[1].read()) - elif len(f) == 3: - return (f[0], f[1].read(), f[2]) - else: - raise ValueError() - -import sys -PY_3 = sys.version_info.major >= 3 -def _fix_type(v): - if isinstance(v, float if PY_3 else (long, float)): - return str(v) - else: - return v - -def _compose_fields(req, **user_kw): - token, method, params, files = req - - fields = {k:_fix_type(v) for k,v in params.items()} if params is not None else {} - if files: - fields.update({k:_filetuple(k,v) for k,v in files.items()}) - - return fields - -def _default_timeout(req, **user_kw): - name = _which_pool(req, **user_kw) - if name is None: - return _onetime_pool_spec[1]['timeout'] - else: - return _pools[name].connection_pool_kw['timeout'] - -def _compose_kwargs(req, **user_kw): - token, method, params, files = req - kw = {} - - if not params and not files: - kw['encode_multipart'] = False - - if method == 'getUpdates' and params and 'timeout' in params: - # Ensure HTTP timeout is longer than getUpdates timeout - kw['timeout'] = params['timeout'] + _default_timeout(req, **user_kw) - elif files: - # Disable timeout if uploading files. For some reason, the larger the file, - # the longer it takes for the server to respond (after upload is finished). - # It is unclear how long timeout should be. - kw['timeout'] = None - - # Let user-supplied arguments override - kw.update(user_kw) - return kw - -def _transform(req, **user_kw): - kwargs = _compose_kwargs(req, **user_kw) - - fields = _compose_fields(req, **user_kw) - - url = _methodurl(req, **user_kw) - - name = _which_pool(req, **user_kw) - - if name is None: - pool = _create_onetime_pool() - else: - pool = _pools[name] - - return pool.request_encode_body, ('POST', url, fields), kwargs - -def _parse(response): - try: - text = response.data.decode('utf-8') - data = json.loads(text) - except ValueError: # No JSON object could be decoded - raise exception.BadHTTPResponse(response.status, text, response) - - if data['ok']: - return data['result'] - else: - description, error_code = data['description'], data['error_code'] - - # Look for specific error ... - for e in exception.TelegramError.__subclasses__(): - n = len(e.DESCRIPTION_PATTERNS) - if any(map(re.search, e.DESCRIPTION_PATTERNS, n*[description], n*[re.IGNORECASE])): - raise e(description, error_code, data) - - # ... or raise generic error - raise exception.TelegramError(description, error_code, data) - -def request(req, **user_kw): - fn, args, kwargs = _transform(req, **user_kw) - r = fn(*args, **kwargs) # `fn` must be thread-safe - return _parse(r) - -def _fileurl(req): - token, path = req - return 'https://api.telegram.org/file/bot%s/%s' % (token, path) - -def download(req, **user_kw): - pool = _create_onetime_pool() - r = pool.request('GET', _fileurl(req), **user_kw) - return r diff --git a/lib/support/telepot2/delegate.py b/lib/support/telepot2/delegate.py deleted file mode 100644 index 91b981e..0000000 --- a/lib/support/telepot2/delegate.py +++ /dev/null @@ -1,420 +0,0 @@ -import traceback -from functools import wraps -from . import exception -from . import flavor, peel, is_event, chat_flavors, inline_flavors - -def _wrap_none(fn): - def w(*args, **kwargs): - try: - return fn(*args, **kwargs) - except (KeyError, exception.BadFlavor): - return None - return w - -def per_chat_id(types='all'): - """ - :param types: - ``all`` or a list of chat types (``private``, ``group``, ``channel``) - - :return: - a seeder function that returns the chat id only if the chat type is in ``types``. - """ - return _wrap_none(lambda msg: - msg['chat']['id'] - if types == 'all' or msg['chat']['type'] in types - else None) - -def per_chat_id_in(s, types='all'): - """ - :param s: - a list or set of chat id - - :param types: - ``all`` or a list of chat types (``private``, ``group``, ``channel``) - - :return: - a seeder function that returns the chat id only if the chat id is in ``s`` - and chat type is in ``types``. - """ - return _wrap_none(lambda msg: - msg['chat']['id'] - if (types == 'all' or msg['chat']['type'] in types) and msg['chat']['id'] in s - else None) - -def per_chat_id_except(s, types='all'): - """ - :param s: - a list or set of chat id - - :param types: - ``all`` or a list of chat types (``private``, ``group``, ``channel``) - - :return: - a seeder function that returns the chat id only if the chat id is *not* in ``s`` - and chat type is in ``types``. - """ - return _wrap_none(lambda msg: - msg['chat']['id'] - if (types == 'all' or msg['chat']['type'] in types) and msg['chat']['id'] not in s - else None) - -def per_from_id(flavors=chat_flavors+inline_flavors): - """ - :param flavors: - ``all`` or a list of flavors - - :return: - a seeder function that returns the from id only if the message flavor is - in ``flavors``. - """ - return _wrap_none(lambda msg: - msg['from']['id'] - if flavors == 'all' or flavor(msg) in flavors - else None) - -def per_from_id_in(s, flavors=chat_flavors+inline_flavors): - """ - :param s: - a list or set of from id - - :param flavors: - ``all`` or a list of flavors - - :return: - a seeder function that returns the from id only if the from id is in ``s`` - and message flavor is in ``flavors``. - """ - return _wrap_none(lambda msg: - msg['from']['id'] - if (flavors == 'all' or flavor(msg) in flavors) and msg['from']['id'] in s - else None) - -def per_from_id_except(s, flavors=chat_flavors+inline_flavors): - """ - :param s: - a list or set of from id - - :param flavors: - ``all`` or a list of flavors - - :return: - a seeder function that returns the from id only if the from id is *not* in ``s`` - and message flavor is in ``flavors``. - """ - return _wrap_none(lambda msg: - msg['from']['id'] - if (flavors == 'all' or flavor(msg) in flavors) and msg['from']['id'] not in s - else None) - -def per_inline_from_id(): - """ - :return: - a seeder function that returns the from id only if the message flavor - is ``inline_query`` or ``chosen_inline_result`` - """ - return per_from_id(flavors=inline_flavors) - -def per_inline_from_id_in(s): - """ - :param s: a list or set of from id - :return: - a seeder function that returns the from id only if the message flavor - is ``inline_query`` or ``chosen_inline_result`` and the from id is in ``s``. - """ - return per_from_id_in(s, flavors=inline_flavors) - -def per_inline_from_id_except(s): - """ - :param s: a list or set of from id - :return: - a seeder function that returns the from id only if the message flavor - is ``inline_query`` or ``chosen_inline_result`` and the from id is *not* in ``s``. - """ - return per_from_id_except(s, flavors=inline_flavors) - -def per_application(): - """ - :return: - a seeder function that always returns 1, ensuring at most one delegate is ever spawned - for the entire application. - """ - return lambda msg: 1 - -def per_message(flavors='all'): - """ - :param flavors: ``all`` or a list of flavors - :return: - a seeder function that returns a non-hashable only if the message flavor - is in ``flavors``. - """ - return _wrap_none(lambda msg: [] if flavors == 'all' or flavor(msg) in flavors else None) - -def per_event_source_id(event_space): - """ - :return: - a seeder function that returns an event's source id only if that event's - source space equals to ``event_space``. - """ - def f(event): - if is_event(event): - v = peel(event) - if v['source']['space'] == event_space: - return v['source']['id'] - else: - return None - else: - return None - return _wrap_none(f) - -def per_callback_query_chat_id(types='all'): - """ - :param types: - ``all`` or a list of chat types (``private``, ``group``, ``channel``) - - :return: - a seeder function that returns a callback query's originating chat id - if the chat type is in ``types``. - """ - def f(msg): - if (flavor(msg) == 'callback_query' and 'message' in msg - and (types == 'all' or msg['message']['chat']['type'] in types)): - return msg['message']['chat']['id'] - else: - return None - return f - -def per_callback_query_origin(origins='all'): - """ - :param origins: - ``all`` or a list of origin types (``chat``, ``inline``) - - :return: - a seeder function that returns a callback query's origin identifier if - that origin type is in ``origins``. The origin identifier is guaranteed - to be a tuple. - """ - def f(msg): - def origin_type_ok(): - return (origins == 'all' - or ('chat' in origins and 'message' in msg) - or ('inline' in origins and 'inline_message_id' in msg)) - - if flavor(msg) == 'callback_query' and origin_type_ok(): - if 'inline_message_id' in msg: - return msg['inline_message_id'], - else: - return msg['message']['chat']['id'], msg['message']['message_id'] - else: - return None - return f - -def per_invoice_payload(): - """ - :return: - a seeder function that returns the invoice payload. - """ - def f(msg): - if 'successful_payment' in msg: - return msg['successful_payment']['invoice_payload'] - else: - return msg['invoice_payload'] - - return _wrap_none(f) - -def call(func, *args, **kwargs): - """ - :return: - a delegator function that returns a tuple (``func``, (seed tuple,)+ ``args``, ``kwargs``). - That is, seed tuple is inserted before supplied positional arguments. - By default, a thread wrapping ``func`` and all those arguments is spawned. - """ - def f(seed_tuple): - return func, (seed_tuple,)+args, kwargs - return f - -def create_run(cls, *args, **kwargs): - """ - :return: - a delegator function that calls the ``cls`` constructor whose arguments being - a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns - the object's ``run`` method. By default, a thread wrapping that ``run`` method - is spawned. - """ - def f(seed_tuple): - j = cls(seed_tuple, *args, **kwargs) - return j.run - return f - -def create_open(cls, *args, **kwargs): - """ - :return: - a delegator function that calls the ``cls`` constructor whose arguments being - a seed tuple followed by supplied ``*args`` and ``**kwargs``, then returns - a looping function that uses the object's ``listener`` to wait for messages - and invokes instance method ``open``, ``on_message``, and ``on_close`` accordingly. - By default, a thread wrapping that looping function is spawned. - """ - def f(seed_tuple): - j = cls(seed_tuple, *args, **kwargs) - - def wait_loop(): - bot, msg, seed = seed_tuple - try: - handled = j.open(msg, seed) - if not handled: - j.on_message(msg) - - while 1: - msg = j.listener.wait() - j.on_message(msg) - - # These exceptions are "normal" exits. - except (exception.IdleTerminate, exception.StopListening) as e: - j.on_close(e) - - # Any other exceptions are accidents. **Print it out.** - # This is to prevent swallowing exceptions in the case that on_close() - # gets overridden but fails to account for unexpected exceptions. - except Exception as e: - traceback.print_exc() - j.on_close(e) - - return wait_loop - return f - -def until(condition, fns): - """ - Try a list of seeder functions until a condition is met. - - :param condition: - a function that takes one argument - a seed - and returns ``True`` - or ``False`` - - :param fns: - a list of seeder functions - - :return: - a "composite" seeder function that calls each supplied function in turn, - and returns the first seed where the condition is met. If the condition - is never met, it returns ``None``. - """ - def f(msg): - for fn in fns: - seed = fn(msg) - if condition(seed): - return seed - return None - return f - -def chain(*fns): - """ - :return: - a "composite" seeder function that calls each supplied function in turn, - and returns the first seed that is not ``None``. - """ - return until(lambda seed: seed is not None, fns) - -def _ensure_seeders_list(fn): - @wraps(fn) - def e(seeders, *aa, **kw): - return fn(seeders if isinstance(seeders, list) else [seeders], *aa, **kw) - return e - -@_ensure_seeders_list -def pair(seeders, delegator_factory, *args, **kwargs): - """ - The basic pair producer. - - :return: - a (seeder, delegator_factory(\*args, \*\*kwargs)) tuple. - - :param seeders: - If it is a seeder function or a list of one seeder function, it is returned - as the final seeder. If it is a list of more than one seeder function, they - are chained together before returned as the final seeder. - """ - return (chain(*seeders) if len(seeders) > 1 else seeders[0], - delegator_factory(*args, **kwargs)) - -def _natural_numbers(): - x = 0 - while 1: - x += 1 - yield x - -_event_space = _natural_numbers() - -def pave_event_space(fn=pair): - """ - :return: - a pair producer that ensures the seeder and delegator share the same event space. - """ - global _event_space - event_space = next(_event_space) - - @_ensure_seeders_list - def p(seeders, delegator_factory, *args, **kwargs): - return fn(seeders + [per_event_source_id(event_space)], - delegator_factory, *args, event_space=event_space, **kwargs) - return p - -def include_callback_query_chat_id(fn=pair, types='all'): - """ - :return: - a pair producer that enables static callback query capturing - across seeder and delegator. - - :param types: - ``all`` or a list of chat types (``private``, ``group``, ``channel``) - """ - @_ensure_seeders_list - def p(seeders, delegator_factory, *args, **kwargs): - return fn(seeders + [per_callback_query_chat_id(types=types)], - delegator_factory, *args, include_callback_query=True, **kwargs) - return p - -from . import helper - -def intercept_callback_query_origin(fn=pair, origins='all'): - """ - :return: - a pair producer that enables dynamic callback query origin mapping - across seeder and delegator. - - :param origins: - ``all`` or a list of origin types (``chat``, ``inline``). - Origin mapping is only enabled for specified origin types. - """ - origin_map = helper.SafeDict() - - # For key functions that returns a tuple as key (e.g. per_callback_query_origin()), - # wrap the key in another tuple to prevent router from mistaking it as - # a key followed by some arguments. - def tuplize(fn): - def tp(msg): - return (fn(msg),) - return tp - - router = helper.Router(tuplize(per_callback_query_origin(origins=origins)), - origin_map) - - def modify_origin_map(origin, dest, set): - if set: - origin_map[origin] = dest - else: - try: - del origin_map[origin] - except KeyError: - pass - - if origins == 'all': - intercept = modify_origin_map - else: - intercept = (modify_origin_map if 'chat' in origins else False, - modify_origin_map if 'inline' in origins else False) - - @_ensure_seeders_list - def p(seeders, delegator_factory, *args, **kwargs): - return fn(seeders + [_wrap_none(router.map)], - delegator_factory, *args, intercept_callback_query=intercept, **kwargs) - return p diff --git a/lib/support/telepot2/exception.py b/lib/support/telepot2/exception.py deleted file mode 100644 index 820a522..0000000 --- a/lib/support/telepot2/exception.py +++ /dev/null @@ -1,111 +0,0 @@ -import sys - -class TelepotException(Exception): - """ Base class of following exceptions. """ - pass - -class BadFlavor(TelepotException): - def __init__(self, offender): - super(BadFlavor, self).__init__(offender) - - @property - def offender(self): - return self.args[0] - -PY_3 = sys.version_info.major >= 3 - -class BadHTTPResponse(TelepotException): - """ - All requests to Bot API should result in a JSON response. If non-JSON, this - exception is raised. While it is hard to pinpoint exactly when this might happen, - the following situations have been observed to give rise to it: - - - an unreasonable token, e.g. ``abc``, ``123``, anything that does not even - remotely resemble a correct token. - - a bad gateway, e.g. when Telegram servers are down. - """ - - def __init__(self, status, text, response): - super(BadHTTPResponse, self).__init__(status, text, response) - - @property - def status(self): - return self.args[0] - - @property - def text(self): - return self.args[1] - - @property - def response(self): - return self.args[2] - -class EventNotFound(TelepotException): - def __init__(self, event): - super(EventNotFound, self).__init__(event) - - @property - def event(self): - return self.args[0] - -class WaitTooLong(TelepotException): - def __init__(self, seconds): - super(WaitTooLong, self).__init__(seconds) - - @property - def seconds(self): - return self.args[0] - -class IdleTerminate(WaitTooLong): - pass - -class StopListening(TelepotException): - pass - -class TelegramError(TelepotException): - """ - To indicate erroneous situations, Telegram returns a JSON object containing - an *error code* and a *description*. This will cause a ``TelegramError`` to - be raised. Before raising a generic ``TelegramError``, telepot looks for - a more specific subclass that "matches" the error. If such a class exists, - an exception of that specific subclass is raised. This allows you to either - catch specific errors or to cast a wide net (by a catch-all ``TelegramError``). - This also allows you to incorporate custom ``TelegramError`` easily. - - Subclasses must define a class variable ``DESCRIPTION_PATTERNS`` which is a list - of regular expressions. If an error's *description* matches any of the regular expressions, - an exception of that subclass is raised. - """ - - def __init__(self, description, error_code, json): - super(TelegramError, self).__init__(description, error_code, json) - - @property - def description(self): - return self.args[0] - - @property - def error_code(self): - return self.args[1] - - @property - def json(self): - return self.args[2] - -class UnauthorizedError(TelegramError): - DESCRIPTION_PATTERNS = ['unauthorized'] - -class BotWasKickedError(TelegramError): - DESCRIPTION_PATTERNS = ['bot.*kicked'] - -class BotWasBlockedError(TelegramError): - DESCRIPTION_PATTERNS = ['bot.*blocked'] - -class TooManyRequestsError(TelegramError): - DESCRIPTION_PATTERNS = ['too *many *requests'] - -class MigratedToSupergroupChatError(TelegramError): - DESCRIPTION_PATTERNS = ['migrated.*supergroup *chat'] - -class NotEnoughRightsError(TelegramError): - DESCRIPTION_PATTERNS = ['not *enough *rights'] diff --git a/lib/support/telepot2/filtering.py b/lib/support/telepot2/filtering.py deleted file mode 100644 index 52ed749..0000000 --- a/lib/support/telepot2/filtering.py +++ /dev/null @@ -1,34 +0,0 @@ -def pick(obj, keys): - def pick1(k): - if type(obj) is dict: - return obj[k] - else: - return getattr(obj, k) - - if isinstance(keys, list): - return [pick1(k) for k in keys] - else: - return pick1(keys) - -def match(data, template): - if isinstance(template, dict) and isinstance(data, dict): - def pick_and_match(kv): - template_key, template_value = kv - if hasattr(template_key, 'search'): # regex - data_keys = list(filter(template_key.search, data.keys())) - if not data_keys: - return False - elif template_key in data: - data_keys = [template_key] - else: - return False - return any(map(lambda data_value: match(data_value, template_value), pick(data, data_keys))) - - return all(map(pick_and_match, template.items())) - elif callable(template): - return template(data) - else: - return data == template - -def match_all(msg, templates): - return all(map(lambda t: match(msg, t), templates)) diff --git a/lib/support/telepot2/hack.py b/lib/support/telepot2/hack.py deleted file mode 100644 index 029bd19..0000000 --- a/lib/support/telepot2/hack.py +++ /dev/null @@ -1,16 +0,0 @@ -try: - import urllib3.fields - - # Do not encode unicode filename, so Telegram servers understand it. - def _noencode_filename(fn): - def w(name, value): - if name == 'filename': - return '%s="%s"' % (name, value) - else: - return fn(name, value) - return w - - urllib3.fields.format_header_param = _noencode_filename(urllib3.fields.format_header_param) - -except (ImportError, AttributeError): - pass diff --git a/lib/support/telepot2/helper.py b/lib/support/telepot2/helper.py deleted file mode 100644 index 1b57b84..0000000 --- a/lib/support/telepot2/helper.py +++ /dev/null @@ -1,1170 +0,0 @@ -import time -import traceback -import threading -import logging -import collections -import re -import inspect -from functools import partial -from . import filtering, exception -from . import ( - flavor, chat_flavors, inline_flavors, is_event, - message_identifier, origin_identifier) - -try: - import Queue as queue -except ImportError: - import queue - - -class Microphone(object): - def __init__(self): - self._queues = set() - self._lock = threading.Lock() - - def _locked(func): - def k(self, *args, **kwargs): - with self._lock: - return func(self, *args, **kwargs) - return k - - @_locked - def add(self, q): - self._queues.add(q) - - @_locked - def remove(self, q): - self._queues.remove(q) - - @_locked - def send(self, msg): - for q in self._queues: - try: - q.put_nowait(msg) - except queue.Full: - traceback.print_exc() - - -class Listener(object): - def __init__(self, mic, q): - self._mic = mic - self._queue = q - self._patterns = [] - - def __del__(self): - self._mic.remove(self._queue) - - def capture(self, pattern): - """ - Add a pattern to capture. - - :param pattern: a list of templates. - - A template may be a function that: - - takes one argument - a message - - returns ``True`` to indicate a match - - A template may also be a dictionary whose: - - **keys** are used to *select* parts of message. Can be strings or - regular expressions (as obtained by ``re.compile()``) - - **values** are used to match against the selected parts. Can be - typical data or a function. - - All templates must produce a match for a message to be considered a match. - """ - self._patterns.append(pattern) - - def wait(self): - """ - Block until a matched message appears. - """ - if not self._patterns: - raise RuntimeError('Listener has nothing to capture') - - while 1: - msg = self._queue.get(block=True) - - if any(map(lambda p: filtering.match_all(msg, p), self._patterns)): - return msg - - -class Sender(object): - """ - When you are dealing with a particular chat, it is tedious to have to supply - the same ``chat_id`` every time to send a message, or to send anything. - - This object is a proxy to a bot's ``send*`` and ``forwardMessage`` methods, - automatically fills in a fixed chat id for you. Available methods have - identical signatures as those of the underlying bot, **except there is no need - to supply the aforementioned** ``chat_id``: - - - :meth:`.Bot.sendMessage` - - :meth:`.Bot.forwardMessage` - - :meth:`.Bot.sendPhoto` - - :meth:`.Bot.sendAudio` - - :meth:`.Bot.sendDocument` - - :meth:`.Bot.sendSticker` - - :meth:`.Bot.sendVideo` - - :meth:`.Bot.sendVoice` - - :meth:`.Bot.sendVideoNote` - - :meth:`.Bot.sendMediaGroup` - - :meth:`.Bot.sendLocation` - - :meth:`.Bot.sendVenue` - - :meth:`.Bot.sendContact` - - :meth:`.Bot.sendGame` - - :meth:`.Bot.sendChatAction` - """ - - def __init__(self, bot, chat_id): - for method in ['sendMessage', - 'forwardMessage', - 'sendPhoto', - 'sendAudio', - 'sendDocument', - 'sendSticker', - 'sendVideo', - 'sendVoice', - 'sendVideoNote', - 'sendMediaGroup', - 'sendLocation', - 'sendVenue', - 'sendContact', - 'sendGame', - 'sendChatAction',]: - setattr(self, method, partial(getattr(bot, method), chat_id)) - # Essentially doing: - # self.sendMessage = partial(bot.sendMessage, chat_id) - - -class Administrator(object): - """ - When you are dealing with a particular chat, it is tedious to have to supply - the same ``chat_id`` every time to get a chat's info or to perform administrative - tasks. - - This object is a proxy to a bot's chat administration methods, - automatically fills in a fixed chat id for you. Available methods have - identical signatures as those of the underlying bot, **except there is no need - to supply the aforementioned** ``chat_id``: - - - :meth:`.Bot.kickChatMember` - - :meth:`.Bot.unbanChatMember` - - :meth:`.Bot.restrictChatMember` - - :meth:`.Bot.promoteChatMember` - - :meth:`.Bot.exportChatInviteLink` - - :meth:`.Bot.setChatPhoto` - - :meth:`.Bot.deleteChatPhoto` - - :meth:`.Bot.setChatTitle` - - :meth:`.Bot.setChatDescription` - - :meth:`.Bot.pinChatMessage` - - :meth:`.Bot.unpinChatMessage` - - :meth:`.Bot.leaveChat` - - :meth:`.Bot.getChat` - - :meth:`.Bot.getChatAdministrators` - - :meth:`.Bot.getChatMembersCount` - - :meth:`.Bot.getChatMember` - - :meth:`.Bot.setChatStickerSet` - - :meth:`.Bot.deleteChatStickerSet` - """ - - def __init__(self, bot, chat_id): - for method in ['kickChatMember', - 'unbanChatMember', - 'restrictChatMember', - 'promoteChatMember', - 'exportChatInviteLink', - 'setChatPhoto', - 'deleteChatPhoto', - 'setChatTitle', - 'setChatDescription', - 'pinChatMessage', - 'unpinChatMessage', - 'leaveChat', - 'getChat', - 'getChatAdministrators', - 'getChatMembersCount', - 'getChatMember', - 'setChatStickerSet', - 'deleteChatStickerSet']: - setattr(self, method, partial(getattr(bot, method), chat_id)) - - -class Editor(object): - """ - If you want to edit a message over and over, it is tedious to have to supply - the same ``msg_identifier`` every time. - - This object is a proxy to a bot's message-editing methods, automatically fills - in a fixed message identifier for you. Available methods have - identical signatures as those of the underlying bot, **except there is no need - to supply the aforementioned** ``msg_identifier``: - - - :meth:`.Bot.editMessageText` - - :meth:`.Bot.editMessageCaption` - - :meth:`.Bot.editMessageReplyMarkup` - - :meth:`.Bot.deleteMessage` - - :meth:`.Bot.editMessageLiveLocation` - - :meth:`.Bot.stopMessageLiveLocation` - - A message's identifier can be easily extracted with :func:`telepot.message_identifier`. - """ - - def __init__(self, bot, msg_identifier): - """ - :param msg_identifier: - a message identifier as mentioned above, or a message (whose - identifier will be automatically extracted). - """ - # Accept dict as argument. Maybe expand this convenience to other cases in future. - if isinstance(msg_identifier, dict): - msg_identifier = message_identifier(msg_identifier) - - for method in ['editMessageText', - 'editMessageCaption', - 'editMessageReplyMarkup', - 'deleteMessage', - 'editMessageLiveLocation', - 'stopMessageLiveLocation']: - setattr(self, method, partial(getattr(bot, method), msg_identifier)) - - -class Answerer(object): - """ - When processing inline queries, ensure **at most one active thread** per user id. - """ - - def __init__(self, bot): - self._bot = bot - self._workers = {} # map: user id --> worker thread - self._lock = threading.Lock() # control access to `self._workers` - - def answer(outerself, inline_query, compute_fn, *compute_args, **compute_kwargs): - """ - Spawns a thread that calls ``compute fn`` (along with additional arguments - ``*compute_args`` and ``**compute_kwargs``), then applies the returned value to - :meth:`.Bot.answerInlineQuery` to answer the inline query. - If a preceding thread is already working for a user, that thread is cancelled, - thus ensuring at most one active thread per user id. - - :param inline_query: - The inline query to be processed. The originating user is inferred from ``msg['from']['id']``. - - :param compute_fn: - A **thread-safe** function whose returned value is given to :meth:`.Bot.answerInlineQuery` to send. - May return: - - - a *list* of `InlineQueryResult `_ - - a *tuple* whose first element is a list of `InlineQueryResult `_, - followed by positional arguments to be supplied to :meth:`.Bot.answerInlineQuery` - - a *dictionary* representing keyword arguments to be supplied to :meth:`.Bot.answerInlineQuery` - - :param \*compute_args: positional arguments to ``compute_fn`` - :param \*\*compute_kwargs: keyword arguments to ``compute_fn`` - """ - - from_id = inline_query['from']['id'] - - class Worker(threading.Thread): - def __init__(innerself): - super(Worker, innerself).__init__() - innerself._cancelled = False - - def cancel(innerself): - innerself._cancelled = True - - def run(innerself): - try: - query_id = inline_query['id'] - - if innerself._cancelled: - return - - # Important: compute function must be thread-safe. - ans = compute_fn(*compute_args, **compute_kwargs) - - if innerself._cancelled: - return - - if isinstance(ans, list): - outerself._bot.answerInlineQuery(query_id, ans) - elif isinstance(ans, tuple): - outerself._bot.answerInlineQuery(query_id, *ans) - elif isinstance(ans, dict): - outerself._bot.answerInlineQuery(query_id, **ans) - else: - raise ValueError('Invalid answer format') - finally: - with outerself._lock: - # Delete only if I have NOT been cancelled. - if not innerself._cancelled: - del outerself._workers[from_id] - - # If I have been cancelled, that position in `outerself._workers` - # no longer belongs to me. I should not delete that key. - - # Several threads may access `outerself._workers`. Use `outerself._lock` to protect. - with outerself._lock: - if from_id in outerself._workers: - outerself._workers[from_id].cancel() - - outerself._workers[from_id] = Worker() - outerself._workers[from_id].start() - - -class AnswererMixin(object): - """ - Install an :class:`.Answerer` to handle inline query. - """ - Answerer = Answerer # let subclass customize Answerer class - - def __init__(self, *args, **kwargs): - self._answerer = self.Answerer(self.bot) - super(AnswererMixin, self).__init__(*args, **kwargs) - - @property - def answerer(self): - return self._answerer - - -class CallbackQueryCoordinator(object): - def __init__(self, id, origin_set, enable_chat, enable_inline): - """ - :param origin_set: - Callback query whose origin belongs to this set will be captured - - :param enable_chat: - - ``False``: Do not intercept *chat-originated* callback query - - ``True``: Do intercept - - Notifier function: Do intercept and call the notifier function - on adding or removing an origin - - :param enable_inline: - Same meaning as ``enable_chat``, but apply to *inline-originated* - callback query - - Notifier functions should have the signature ``notifier(origin, id, adding)``: - - - On adding an origin, ``notifier(origin, my_id, True)`` will be called. - - On removing an origin, ``notifier(origin, my_id, False)`` will be called. - """ - self._id = id - self._origin_set = origin_set - - def dissolve(enable): - if not enable: - return False, None - elif enable is True: - return True, None - elif callable(enable): - return True, enable - else: - raise ValueError() - - self._enable_chat, self._chat_notify = dissolve(enable_chat) - self._enable_inline, self._inline_notify = dissolve(enable_inline) - - def configure(self, listener): - """ - Configure a :class:`.Listener` to capture callback query - """ - listener.capture([ - lambda msg: flavor(msg) == 'callback_query', - {'message': self._chat_origin_included} - ]) - - listener.capture([ - lambda msg: flavor(msg) == 'callback_query', - {'inline_message_id': self._inline_origin_included} - ]) - - def _chat_origin_included(self, msg): - try: - return (msg['chat']['id'], msg['message_id']) in self._origin_set - except KeyError: - return False - - def _inline_origin_included(self, inline_message_id): - return (inline_message_id,) in self._origin_set - - def _rectify(self, msg_identifier): - if isinstance(msg_identifier, tuple): - if len(msg_identifier) == 2: - return msg_identifier, self._chat_notify - elif len(msg_identifier) == 1: - return msg_identifier, self._inline_notify - else: - raise ValueError() - else: - return (msg_identifier,), self._inline_notify - - def capture_origin(self, msg_identifier, notify=True): - msg_identifier, notifier = self._rectify(msg_identifier) - self._origin_set.add(msg_identifier) - notify and notifier and notifier(msg_identifier, self._id, True) - - def uncapture_origin(self, msg_identifier, notify=True): - msg_identifier, notifier = self._rectify(msg_identifier) - self._origin_set.discard(msg_identifier) - notify and notifier and notifier(msg_identifier, self._id, False) - - def _contains_callback_data(self, message_kw): - def contains(obj, key): - if isinstance(obj, dict): - return key in obj - else: - return hasattr(obj, key) - - if contains(message_kw, 'reply_markup'): - reply_markup = filtering.pick(message_kw, 'reply_markup') - if contains(reply_markup, 'inline_keyboard'): - inline_keyboard = filtering.pick(reply_markup, 'inline_keyboard') - for array in inline_keyboard: - if any(filter(lambda button: contains(button, 'callback_data'), array)): - return True - return False - - def augment_send(self, send_func): - """ - :param send_func: - a function that sends messages, such as :meth:`.Bot.send\*` - - :return: - a function that wraps around ``send_func`` and examines whether the - sent message contains an inline keyboard with callback data. If so, - future callback query originating from the sent message will be captured. - """ - def augmented(*aa, **kw): - sent = send_func(*aa, **kw) - - if self._enable_chat and self._contains_callback_data(kw): - self.capture_origin(message_identifier(sent)) - - return sent - return augmented - - def augment_edit(self, edit_func): - """ - :param edit_func: - a function that edits messages, such as :meth:`.Bot.edit*` - - :return: - a function that wraps around ``edit_func`` and examines whether the - edited message contains an inline keyboard with callback data. If so, - future callback query originating from the edited message will be captured. - If not, such capturing will be stopped. - """ - def augmented(msg_identifier, *aa, **kw): - edited = edit_func(msg_identifier, *aa, **kw) - - if (edited is True and self._enable_inline) or (isinstance(edited, dict) and self._enable_chat): - if self._contains_callback_data(kw): - self.capture_origin(msg_identifier) - else: - self.uncapture_origin(msg_identifier) - - return edited - return augmented - - def augment_delete(self, delete_func): - """ - :param delete_func: - a function that deletes messages, such as :meth:`.Bot.deleteMessage` - - :return: - a function that wraps around ``delete_func`` and stops capturing - callback query originating from that deleted message. - """ - def augmented(msg_identifier, *aa, **kw): - deleted = delete_func(msg_identifier, *aa, **kw) - - if deleted is True: - self.uncapture_origin(msg_identifier) - - return deleted - return augmented - - def augment_on_message(self, handler): - """ - :param handler: - an ``on_message()`` handler function - - :return: - a function that wraps around ``handler`` and examines whether the - incoming message is a chosen inline result with an ``inline_message_id`` - field. If so, future callback query originating from this chosen - inline result will be captured. - """ - def augmented(msg): - if (self._enable_inline - and flavor(msg) == 'chosen_inline_result' - and 'inline_message_id' in msg): - inline_message_id = msg['inline_message_id'] - self.capture_origin(inline_message_id) - - return handler(msg) - return augmented - - def augment_bot(self, bot): - """ - :return: - a proxy to ``bot`` with these modifications: - - - all ``send*`` methods augmented by :meth:`augment_send` - - all ``edit*`` methods augmented by :meth:`augment_edit` - - ``deleteMessage()`` augmented by :meth:`augment_delete` - - all other public methods, including properties, copied unchanged - """ - # Because a plain object cannot be set attributes, we need a class. - class BotProxy(object): - pass - - proxy = BotProxy() - - send_methods = ['sendMessage', - 'forwardMessage', - 'sendPhoto', - 'sendAudio', - 'sendDocument', - 'sendSticker', - 'sendVideo', - 'sendVoice', - 'sendVideoNote', - 'sendLocation', - 'sendVenue', - 'sendContact', - 'sendGame', - 'sendInvoice', - 'sendChatAction',] - - for method in send_methods: - setattr(proxy, method, self.augment_send(getattr(bot, method))) - - edit_methods = ['editMessageText', - 'editMessageCaption', - 'editMessageReplyMarkup',] - - for method in edit_methods: - setattr(proxy, method, self.augment_edit(getattr(bot, method))) - - delete_methods = ['deleteMessage'] - - for method in delete_methods: - setattr(proxy, method, self.augment_delete(getattr(bot, method))) - - def public_untouched(nv): - name, value = nv - return (not name.startswith('_') - and name not in send_methods + edit_methods + delete_methods) - - for name, value in filter(public_untouched, inspect.getmembers(bot)): - setattr(proxy, name, value) - - return proxy - - -class SafeDict(dict): - """ - A subclass of ``dict``, thread-safety added:: - - d = SafeDict() # Thread-safe operations include: - d['a'] = 3 # key assignment - d['a'] # key retrieval - del d['a'] # key deletion - """ - - def __init__(self, *args, **kwargs): - super(SafeDict, self).__init__(*args, **kwargs) - self._lock = threading.Lock() - - def _locked(func): - def k(self, *args, **kwargs): - with self._lock: - return func(self, *args, **kwargs) - return k - - @_locked - def __getitem__(self, key): - return super(SafeDict, self).__getitem__(key) - - @_locked - def __setitem__(self, key, value): - return super(SafeDict, self).__setitem__(key, value) - - @_locked - def __delitem__(self, key): - return super(SafeDict, self).__delitem__(key) - - -_cqc_origins = SafeDict() - -class InterceptCallbackQueryMixin(object): - """ - Install a :class:`.CallbackQueryCoordinator` to capture callback query - dynamically. - - Using this mixin has one consequence. The :meth:`self.bot` property no longer - returns the original :class:`.Bot` object. Instead, it returns an augmented - version of the :class:`.Bot` (augmented by :class:`.CallbackQueryCoordinator`). - The original :class:`.Bot` can be accessed with ``self.__bot`` (double underscore). - """ - CallbackQueryCoordinator = CallbackQueryCoordinator - - def __init__(self, intercept_callback_query, *args, **kwargs): - """ - :param intercept_callback_query: - a 2-tuple (enable_chat, enable_inline) to pass to - :class:`.CallbackQueryCoordinator` - """ - global _cqc_origins - - # Restore origin set to CallbackQueryCoordinator - if self.id in _cqc_origins: - origin_set = _cqc_origins[self.id] - else: - origin_set = set() - _cqc_origins[self.id] = origin_set - - if isinstance(intercept_callback_query, tuple): - cqc_enable = intercept_callback_query - else: - cqc_enable = (intercept_callback_query,) * 2 - - self._callback_query_coordinator = self.CallbackQueryCoordinator(self.id, origin_set, *cqc_enable) - cqc = self._callback_query_coordinator - cqc.configure(self.listener) - - self.__bot = self._bot # keep original version of bot - self._bot = cqc.augment_bot(self._bot) # modify send* and edit* methods - self.on_message = cqc.augment_on_message(self.on_message) # modify on_message() - - super(InterceptCallbackQueryMixin, self).__init__(*args, **kwargs) - - def __del__(self): - global _cqc_origins - if self.id in _cqc_origins and not _cqc_origins[self.id]: - del _cqc_origins[self.id] - # Remove empty set from dictionary - - @property - def callback_query_coordinator(self): - return self._callback_query_coordinator - - -class IdleEventCoordinator(object): - def __init__(self, scheduler, timeout): - self._scheduler = scheduler - self._timeout_seconds = timeout - self._timeout_event = None - - def refresh(self): - """ Refresh timeout timer """ - try: - if self._timeout_event: - self._scheduler.cancel(self._timeout_event) - - # Timeout event has been popped from queue prematurely - except exception.EventNotFound: - pass - - # Ensure a new event is scheduled always - finally: - self._timeout_event = self._scheduler.event_later( - self._timeout_seconds, - ('_idle', {'seconds': self._timeout_seconds})) - - def augment_on_message(self, handler): - """ - :return: - a function wrapping ``handler`` to refresh timer for every - non-event message - """ - def augmented(msg): - # Reset timer if this is an external message - is_event(msg) or self.refresh() - - # Ignore timeout event that have been popped from queue prematurely - if flavor(msg) == '_idle' and msg is not self._timeout_event.data: - return - - return handler(msg) - return augmented - - def augment_on_close(self, handler): - """ - :return: - a function wrapping ``handler`` to cancel timeout event - """ - def augmented(ex): - try: - if self._timeout_event: - self._scheduler.cancel(self._timeout_event) - self._timeout_event = None - # This closing may have been caused by my own timeout, in which case - # the timeout event can no longer be found in the scheduler. - except exception.EventNotFound: - self._timeout_event = None - return handler(ex) - return augmented - - -class IdleTerminateMixin(object): - """ - Install an :class:`.IdleEventCoordinator` to manage idle timeout. Also define - instance method ``on__idle()`` to handle idle timeout events. - """ - IdleEventCoordinator = IdleEventCoordinator - - def __init__(self, timeout, *args, **kwargs): - self._idle_event_coordinator = self.IdleEventCoordinator(self.scheduler, timeout) - idlec = self._idle_event_coordinator - idlec.refresh() # start timer - self.on_message = idlec.augment_on_message(self.on_message) - self.on_close = idlec.augment_on_close(self.on_close) - super(IdleTerminateMixin, self).__init__(*args, **kwargs) - - @property - def idle_event_coordinator(self): - return self._idle_event_coordinator - - def on__idle(self, event): - """ - Raise an :class:`.IdleTerminate` to close the delegate. - """ - raise exception.IdleTerminate(event['_idle']['seconds']) - - -class StandardEventScheduler(object): - """ - A proxy to the underlying :class:`.Bot`\'s scheduler, this object implements - the *standard event format*. A standard event looks like this:: - - {'_flavor': { - 'source': { - 'space': event_space, 'id': source_id} - 'custom_key1': custom_value1, - 'custom_key2': custom_value2, - ... }} - - - There is a single top-level key indicating the flavor, starting with an _underscore. - - On the second level, there is a ``source`` key indicating the event source. - - An event source consists of an *event space* and a *source id*. - - An event space is shared by all delegates in a group. Source id simply refers - to a delegate's id. They combine to ensure a delegate is always able to capture - its own events, while its own events would not be mistakenly captured by others. - - Events scheduled through this object always have the second-level ``source`` key fixed, - while the flavor and other data may be customized. - """ - def __init__(self, scheduler, event_space, source_id): - self._base = scheduler - self._event_space = event_space - self._source_id = source_id - - @property - def event_space(self): - return self._event_space - - def configure(self, listener): - """ - Configure a :class:`.Listener` to capture events with this object's - event space and source id. - """ - listener.capture([{re.compile('^_.+'): {'source': {'space': self._event_space, 'id': self._source_id}}}]) - - def make_event_data(self, flavor, data): - """ - Marshall ``flavor`` and ``data`` into a standard event. - """ - if not flavor.startswith('_'): - raise ValueError('Event flavor must start with _underscore') - - d = {'source': {'space': self._event_space, 'id': self._source_id}} - d.update(data) - return {flavor: d} - - def event_at(self, when, data_tuple): - """ - Schedule an event to be emitted at a certain time. - - :param when: an absolute timestamp - :param data_tuple: a 2-tuple (flavor, data) - :return: an event object, useful for cancelling. - """ - return self._base.event_at(when, self.make_event_data(*data_tuple)) - - def event_later(self, delay, data_tuple): - """ - Schedule an event to be emitted after a delay. - - :param delay: number of seconds - :param data_tuple: a 2-tuple (flavor, data) - :return: an event object, useful for cancelling. - """ - return self._base.event_later(delay, self.make_event_data(*data_tuple)) - - def event_now(self, data_tuple): - """ - Schedule an event to be emitted now. - - :param data_tuple: a 2-tuple (flavor, data) - :return: an event object, useful for cancelling. - """ - return self._base.event_now(self.make_event_data(*data_tuple)) - - def cancel(self, event): - """ Cancel an event. """ - return self._base.cancel(event) - - -class StandardEventMixin(object): - """ - Install a :class:`.StandardEventScheduler`. - """ - StandardEventScheduler = StandardEventScheduler - - def __init__(self, event_space, *args, **kwargs): - self._scheduler = self.StandardEventScheduler(self.bot.scheduler, event_space, self.id) - self._scheduler.configure(self.listener) - super(StandardEventMixin, self).__init__(*args, **kwargs) - - @property - def scheduler(self): - return self._scheduler - - -class ListenerContext(object): - def __init__(self, bot, context_id, *args, **kwargs): - # Initialize members before super() so mixin could use them. - self._bot = bot - self._id = context_id - self._listener = bot.create_listener() - super(ListenerContext, self).__init__(*args, **kwargs) - - @property - def bot(self): - """ - The underlying :class:`.Bot` or an augmented version thereof - """ - return self._bot - - @property - def id(self): - return self._id - - @property - def listener(self): - """ See :class:`.Listener` """ - return self._listener - - -class ChatContext(ListenerContext): - def __init__(self, bot, context_id, *args, **kwargs): - super(ChatContext, self).__init__(bot, context_id, *args, **kwargs) - self._chat_id = context_id - self._sender = Sender(self.bot, self._chat_id) - self._administrator = Administrator(self.bot, self._chat_id) - - @property - def chat_id(self): - return self._chat_id - - @property - def sender(self): - """ A :class:`.Sender` for this chat """ - return self._sender - - @property - def administrator(self): - """ An :class:`.Administrator` for this chat """ - return self._administrator - - -class UserContext(ListenerContext): - def __init__(self, bot, context_id, *args, **kwargs): - super(UserContext, self).__init__(bot, context_id, *args, **kwargs) - self._user_id = context_id - self._sender = Sender(self.bot, self._user_id) - - @property - def user_id(self): - return self._user_id - - @property - def sender(self): - """ A :class:`.Sender` for this user """ - return self._sender - - -class CallbackQueryOriginContext(ListenerContext): - def __init__(self, bot, context_id, *args, **kwargs): - super(CallbackQueryOriginContext, self).__init__(bot, context_id, *args, **kwargs) - self._origin = context_id - self._editor = Editor(self.bot, self._origin) - - @property - def origin(self): - """ Mesasge identifier of callback query's origin """ - return self._origin - - @property - def editor(self): - """ An :class:`.Editor` to the originating message """ - return self._editor - - -class InvoiceContext(ListenerContext): - def __init__(self, bot, context_id, *args, **kwargs): - super(InvoiceContext, self).__init__(bot, context_id, *args, **kwargs) - self._payload = context_id - - @property - def payload(self): - return self._payload - - -def openable(cls): - """ - A class decorator to fill in certain methods and properties to ensure - a class can be used by :func:`.create_open`. - - These instance methods and property will be added, if not defined - by the class: - - - ``open(self, initial_msg, seed)`` - - ``on_message(self, msg)`` - - ``on_close(self, ex)`` - - ``close(self, ex=None)`` - - property ``listener`` - """ - - def open(self, initial_msg, seed): - pass - - def on_message(self, msg): - raise NotImplementedError() - - def on_close(self, ex): - logging.error('on_close() called due to %s: %s', type(ex).__name__, ex) - - def close(self, ex=None): - raise ex if ex else exception.StopListening() - - @property - def listener(self): - raise NotImplementedError() - - def ensure_method(name, fn): - if getattr(cls, name, None) is None: - setattr(cls, name, fn) - - # set attribute if no such attribute - ensure_method('open', open) - ensure_method('on_message', on_message) - ensure_method('on_close', on_close) - ensure_method('close', close) - ensure_method('listener', listener) - - return cls - - -class Router(object): - """ - Map a message to a handler function, using a **key function** and - a **routing table** (dictionary). - - A *key function* digests a message down to a value. This value is treated - as a key to the *routing table* to look up a corresponding handler function. - """ - - def __init__(self, key_function, routing_table): - """ - :param key_function: - A function that takes one argument (the message) and returns - one of the following: - - - a key to the routing table - - a 1-tuple (key,) - - a 2-tuple (key, (positional, arguments, ...)) - - a 3-tuple (key, (positional, arguments, ...), {keyword: arguments, ...}) - - Extra arguments, if returned, will be applied to the handler function - after using the key to look up the routing table. - - :param routing_table: - A dictionary of ``{key: handler}``. A ``None`` key acts as a default - catch-all. If the key being looked up does not exist in the routing - table, the ``None`` key and its corresponding handler is used. - """ - super(Router, self).__init__() - self.key_function = key_function - self.routing_table = routing_table - - def map(self, msg): - """ - Apply key function to ``msg`` to obtain a key. Return the routing table entry. - """ - k = self.key_function(msg) - key = k[0] if isinstance(k, (tuple, list)) else k - return self.routing_table[key] - - def route(self, msg, *aa, **kw): - """ - Apply key function to ``msg`` to obtain a key, look up routing table - to obtain a handler function, then call the handler function with - positional and keyword arguments, if any is returned by the key function. - - ``*aa`` and ``**kw`` are dummy placeholders for easy chaining. - Regardless of any number of arguments returned by the key function, - multi-level routing may be achieved like this:: - - top_router.routing_table['key1'] = sub_router1.route - top_router.routing_table['key2'] = sub_router2.route - """ - k = self.key_function(msg) - - if isinstance(k, (tuple, list)): - key, args, kwargs = {1: tuple(k) + ((),{}), - 2: tuple(k) + ({},), - 3: tuple(k),}[len(k)] - else: - key, args, kwargs = k, (), {} - - try: - fn = self.routing_table[key] - except KeyError as e: - # Check for default handler, key=None - if None in self.routing_table: - fn = self.routing_table[None] - else: - raise RuntimeError('No handler for key: %s, and default handler not defined' % str(e.args)) - - return fn(msg, *args, **kwargs) - - -class DefaultRouterMixin(object): - """ - Install a default :class:`.Router` and the instance method ``on_message()``. - """ - def __init__(self, *args, **kwargs): - self._router = Router(flavor, {'chat': lambda msg: self.on_chat_message(msg), - 'callback_query': lambda msg: self.on_callback_query(msg), - 'inline_query': lambda msg: self.on_inline_query(msg), - 'chosen_inline_result': lambda msg: self.on_chosen_inline_result(msg), - 'shipping_query': lambda msg: self.on_shipping_query(msg), - 'pre_checkout_query': lambda msg: self.on_pre_checkout_query(msg), - '_idle': lambda event: self.on__idle(event)}) - # use lambda to delay evaluation of self.on_ZZZ to runtime because - # I don't want to require defining all methods right here. - - super(DefaultRouterMixin, self).__init__(*args, **kwargs) - - @property - def router(self): - return self._router - - def on_message(self, msg): - """ Call :meth:`.Router.route` to handle the message. """ - self._router.route(msg) - - -@openable -class Monitor(ListenerContext, DefaultRouterMixin): - def __init__(self, seed_tuple, capture, **kwargs): - """ - A delegate that never times-out, probably doing some kind of background monitoring - in the application. Most naturally paired with :func:`.per_application`. - - :param capture: a list of patterns for :class:`.Listener` to capture - """ - bot, initial_msg, seed = seed_tuple - super(Monitor, self).__init__(bot, seed, **kwargs) - - for pattern in capture: - self.listener.capture(pattern) - - -@openable -class ChatHandler(ChatContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, - include_callback_query=False, **kwargs): - """ - A delegate to handle a chat. - """ - bot, initial_msg, seed = seed_tuple - super(ChatHandler, self).__init__(bot, seed, **kwargs) - - self.listener.capture([{'chat': {'id': self.chat_id}}]) - - if include_callback_query: - self.listener.capture([{'message': {'chat': {'id': self.chat_id}}}]) - - -@openable -class UserHandler(UserContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, - include_callback_query=False, - flavors=chat_flavors+inline_flavors, **kwargs): - """ - A delegate to handle a user's actions. - - :param flavors: - A list of flavors to capture. ``all`` covers all flavors. - """ - bot, initial_msg, seed = seed_tuple - super(UserHandler, self).__init__(bot, seed, **kwargs) - - if flavors == 'all': - self.listener.capture([{'from': {'id': self.user_id}}]) - else: - self.listener.capture([lambda msg: flavor(msg) in flavors, {'from': {'id': self.user_id}}]) - - if include_callback_query: - self.listener.capture([{'message': {'chat': {'id': self.user_id}}}]) - - -class InlineUserHandler(UserHandler): - def __init__(self, seed_tuple, **kwargs): - """ - A delegate to handle a user's inline-related actions. - """ - super(InlineUserHandler, self).__init__(seed_tuple, flavors=inline_flavors, **kwargs) - - -@openable -class CallbackQueryOriginHandler(CallbackQueryOriginContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, **kwargs): - """ - A delegate to handle callback query from one origin. - """ - bot, initial_msg, seed = seed_tuple - super(CallbackQueryOriginHandler, self).__init__(bot, seed, **kwargs) - - self.listener.capture([ - lambda msg: - flavor(msg) == 'callback_query' and origin_identifier(msg) == self.origin - ]) - - -@openable -class InvoiceHandler(InvoiceContext, - DefaultRouterMixin, - StandardEventMixin, - IdleTerminateMixin): - def __init__(self, seed_tuple, **kwargs): - """ - A delegate to handle messages related to an invoice. - """ - bot, initial_msg, seed = seed_tuple - super(InvoiceHandler, self).__init__(bot, seed, **kwargs) - - self.listener.capture([{'invoice_payload': self.payload}]) - self.listener.capture([{'successful_payment': {'invoice_payload': self.payload}}]) diff --git a/lib/support/telepot2/loop.py b/lib/support/telepot2/loop.py deleted file mode 100644 index d701374..0000000 --- a/lib/support/telepot2/loop.py +++ /dev/null @@ -1,313 +0,0 @@ -import sys -import time -import json -import threading -import traceback -import collections - -try: - import Queue as queue -except ImportError: - import queue - -from . import exception -from . import _find_first_key, flavor_router - - -class RunForeverAsThread(object): - def run_as_thread(self, *args, **kwargs): - t = threading.Thread(target=self.run_forever, args=args, kwargs=kwargs) - t.daemon = True - t.start() - - -class CollectLoop(RunForeverAsThread): - def __init__(self, handle): - self._handle = handle - self._inqueue = queue.Queue() - - @property - def input_queue(self): - return self._inqueue - - def run_forever(self): - while 1: - try: - msg = self._inqueue.get(block=True) - self._handle(msg) - except: - traceback.print_exc() - - -class GetUpdatesLoop(RunForeverAsThread): - def __init__(self, bot, on_update): - self._bot = bot - self._update_handler = on_update - - def run_forever(self, relax=0.1, offset=None, timeout=20, allowed_updates=None): - """ - Process new updates in infinity loop - - :param relax: float - :param offset: int - :param timeout: int - :param allowed_updates: bool - """ - while 1: - try: - result = self._bot.getUpdates(offset=offset, - timeout=timeout, - allowed_updates=allowed_updates) - - # Once passed, this parameter is no longer needed. - allowed_updates = None - - # No sort. Trust server to give messages in correct order. - for update in result: - self._update_handler(update) - offset = update['update_id'] + 1 - - except exception.BadHTTPResponse as e: - traceback.print_exc() - - # Servers probably down. Wait longer. - if e.status == 502: - time.sleep(30) - except: - traceback.print_exc() - finally: - time.sleep(relax) - - -def _dictify3(data): - if type(data) is bytes: - return json.loads(data.decode('utf-8')) - elif type(data) is str: - return json.loads(data) - elif type(data) is dict: - return data - else: - raise ValueError() - -def _dictify27(data): - if type(data) in [str, unicode]: - return json.loads(data) - elif type(data) is dict: - return data - else: - raise ValueError() - -_dictify = _dictify3 if sys.version_info >= (3,) else _dictify27 - -def _extract_message(update): - key = _find_first_key(update, ['message', - 'edited_message', - 'channel_post', - 'edited_channel_post', - 'callback_query', - 'inline_query', - 'chosen_inline_result', - 'shipping_query', - 'pre_checkout_query', 'my_chat_member']) - return key, update[key] - -def _infer_handler_function(bot, h): - if h is None: - return bot.handle - elif isinstance(h, dict): - return flavor_router(h) - else: - return h - - -class MessageLoop(RunForeverAsThread): - def __init__(self, bot, handle=None): - self._bot = bot - self._handle = _infer_handler_function(bot, handle) - - def run_forever(self, *args, **kwargs): - """ - :type relax: float - :param relax: seconds between each :meth:`.getUpdates` - - :type offset: int - :param offset: - initial ``offset`` parameter supplied to :meth:`.getUpdates` - - :type timeout: int - :param timeout: - ``timeout`` parameter supplied to :meth:`.getUpdates`, controlling - how long to poll. - - :type allowed_updates: array of string - :param allowed_updates: - ``allowed_updates`` parameter supplied to :meth:`.getUpdates`, - controlling which types of updates to receive. - - Calling this method will block forever. Use :meth:`.run_as_thread` to - run it non-blockingly. - """ - collectloop = CollectLoop(self._handle) - updatesloop = GetUpdatesLoop(self._bot, - lambda update: - collectloop.input_queue.put(_extract_message(update)[1])) - # feed messages to collect loop - # feed events to collect loop - self._bot.scheduler.on_event(collectloop.input_queue.put) - self._bot.scheduler.run_as_thread() - - updatesloop.run_as_thread(*args, **kwargs) - collectloop.run_forever() # blocking - - -class Webhook(RunForeverAsThread): - def __init__(self, bot, handle=None): - self._bot = bot - self._collectloop = CollectLoop(_infer_handler_function(bot, handle)) - - def run_forever(self): - # feed events to collect loop - self._bot.scheduler.on_event(self._collectloop.input_queue.put) - self._bot.scheduler.run_as_thread() - - self._collectloop.run_forever() - - def feed(self, data): - update = _dictify(data) - self._collectloop.input_queue.put(_extract_message(update)[1]) - - -class Orderer(RunForeverAsThread): - def __init__(self, on_ordered_update): - self._on_ordered_update = on_ordered_update - self._inqueue = queue.Queue() - - @property - def input_queue(self): - return self._inqueue - - def run_forever(self, maxhold=3): - def handle(update): - self._on_ordered_update(update) - return update['update_id'] - - # Here is the re-ordering mechanism, ensuring in-order delivery of updates. - max_id = None # max update_id passed to callback - buffer = collections.deque() # keep those updates which skip some update_id - qwait = None # how long to wait for updates, - # because buffer's content has to be returned in time. - - while 1: - try: - update = self._inqueue.get(block=True, timeout=qwait) - - if max_id is None: - # First message received, handle regardless. - max_id = handle(update) - - elif update['update_id'] == max_id + 1: - # No update_id skipped, handle naturally. - max_id = handle(update) - - # clear contagious updates in buffer - if len(buffer) > 0: - buffer.popleft() # first element belongs to update just received, useless now. - while 1: - try: - if type(buffer[0]) is dict: - max_id = handle(buffer.popleft()) # updates that arrived earlier, handle them. - else: - break # gap, no more contagious updates - except IndexError: - break # buffer empty - - elif update['update_id'] > max_id + 1: - # Update arrives pre-maturely, insert to buffer. - nbuf = len(buffer) - if update['update_id'] <= max_id + nbuf: - # buffer long enough, put update at position - buffer[update['update_id'] - max_id - 1] = update - else: - # buffer too short, lengthen it - expire = time.time() + maxhold - for a in range(nbuf, update['update_id']-max_id-1): - buffer.append(expire) # put expiry time in gaps - buffer.append(update) - - else: - pass # discard - - except queue.Empty: - # debug message - # print('Timeout') - - # some buffer contents have to be handled - # flush buffer until a non-expired time is encountered - while 1: - try: - if type(buffer[0]) is dict: - max_id = handle(buffer.popleft()) - else: - expire = buffer[0] - if expire <= time.time(): - max_id += 1 - buffer.popleft() - else: - break # non-expired - except IndexError: - break # buffer empty - except: - traceback.print_exc() - finally: - try: - # don't wait longer than next expiry time - qwait = buffer[0] - time.time() - if qwait < 0: - qwait = 0 - except IndexError: - # buffer empty, can wait forever - qwait = None - - # debug message - # print ('Buffer:', str(buffer), ', To Wait:', qwait, ', Max ID:', max_id) - - -class OrderedWebhook(RunForeverAsThread): - def __init__(self, bot, handle=None): - self._bot = bot - self._collectloop = CollectLoop(_infer_handler_function(bot, handle)) - self._orderer = Orderer(lambda update: - self._collectloop.input_queue.put(_extract_message(update)[1])) - # feed messages to collect loop - - def run_forever(self, *args, **kwargs): - """ - :type maxhold: float - :param maxhold: - The maximum number of seconds an update is held waiting for a - not-yet-arrived smaller ``update_id``. When this number of seconds - is up, the update is delivered to the message-handling function - even if some smaller ``update_id``\s have not yet arrived. If those - smaller ``update_id``\s arrive at some later time, they are discarded. - - Calling this method will block forever. Use :meth:`.run_as_thread` to - run it non-blockingly. - """ - # feed events to collect loop - self._bot.scheduler.on_event(self._collectloop.input_queue.put) - self._bot.scheduler.run_as_thread() - - self._orderer.run_as_thread(*args, **kwargs) - self._collectloop.run_forever() - - def feed(self, data): - """ - :param data: - One of these: - - - ``str``, ``unicode`` (Python 2.7), or ``bytes`` (Python 3, decoded using UTF-8) - representing a JSON-serialized `Update `_ object. - - a ``dict`` representing an Update object. - """ - update = _dictify(data) - self._orderer.input_queue.put(update) diff --git a/lib/support/telepot2/namedtuple.py b/lib/support/telepot2/namedtuple.py deleted file mode 100644 index a5c570b..0000000 --- a/lib/support/telepot2/namedtuple.py +++ /dev/null @@ -1,865 +0,0 @@ -import collections -import warnings -import sys - -class _Field(object): - def __init__(self, name, constructor=None, default=None): - self.name = name - self.constructor = constructor - self.default = default - -# Function to produce namedtuple classes. -def _create_class(typename, fields): - # extract field names - field_names = [e.name if type(e) is _Field else e for e in fields] - - # Some dictionary keys are Python keywords and cannot be used as field names, e.g. `from`. - # Get around by appending a '_', e.g. dict['from'] => namedtuple.from_ - keymap = [(k.rstrip('_'), k) for k in filter(lambda e: e in ['from_'], field_names)] - - # extract (non-simple) fields that need conversions - conversions = [(e.name, e.constructor) for e in fields if type(e) is _Field and e.constructor is not None] - - # extract default values - defaults = [e.default if type(e) is _Field else None for e in fields] - - # Create the base tuple class, with defaults. - base = collections.namedtuple(typename, field_names) - base.__new__.__defaults__ = tuple(defaults) - - class sub(base): - def __new__(cls, **kwargs): - # Map keys. - for oldkey, newkey in keymap: - if oldkey in kwargs: - kwargs[newkey] = kwargs[oldkey] - del kwargs[oldkey] - - # Any unexpected arguments? - unexpected = set(kwargs.keys()) - set(super(sub, cls)._fields) - - # Remove unexpected arguments and issue warning. - if unexpected: - for k in unexpected: - del kwargs[k] - - s = ('Unexpected fields: ' + ', '.join(unexpected) + '' - '\nBot API seems to have added new fields to the returned data.' - ' This version of namedtuple is not able to capture them.' - '\n\nPlease upgrade telepot by:' - '\n sudo pip install telepot --upgrade' - '\n\nIf you still see this message after upgrade, that means I am still working to bring the code up-to-date.' - ' Please try upgrade again a few days later.' - ' In the meantime, you can access the new fields the old-fashioned way, through the raw dictionary.') - - warnings.warn(s, UserWarning) - - # Convert non-simple values to namedtuples. - for key, func in conversions: - if key in kwargs: - if type(kwargs[key]) is dict: - kwargs[key] = func(**kwargs[key]) - elif type(kwargs[key]) is list: - kwargs[key] = func(kwargs[key]) - else: - raise RuntimeError('Can only convert dict or list') - - return super(sub, cls).__new__(cls, **kwargs) - - # https://bugs.python.org/issue24931 - # Python 3.4 bug: namedtuple subclass does not inherit __dict__ properly. - # Fix it manually. - if sys.version_info >= (3,4): - def _asdict(self): - return collections.OrderedDict(zip(self._fields, self)) - sub._asdict = _asdict - - sub.__name__ = typename - - return sub - -""" -Different treatments for incoming and outgoing namedtuples: - -- Incoming ones require type declarations for certain fields for deeper parsing. -- Outgoing ones need no such declarations because users are expected to put the correct object in place. -""" - -# Namedtuple class will reference other namedtuple classes. Due to circular -# dependencies, it is impossible to have all class definitions ready at -# compile time. We have to dynamically obtain class reference at runtime. -# For example, the following function acts like a constructor for `Message` -# so any class can reference the Message namedtuple even before the Message -# namedtuple is defined. -def _Message(**kwargs): - return getattr(sys.modules[__name__], 'Message')(**kwargs) - -# incoming -User = _create_class('User', [ - 'id', - 'is_bot', - 'first_name', - 'last_name', - 'username', - 'language_code' - ]) - -def UserArray(data): - return [User(**p) for p in data] - -# incoming -ChatPhoto = _create_class('ChatPhoto', [ - 'small_file_id', - 'big_file_id', - ]) - -# incoming -Chat = _create_class('Chat', [ - 'id', - 'type', - 'title', - 'username', - 'first_name', - 'last_name', - 'all_members_are_administrators', - _Field('photo', constructor=ChatPhoto), - 'description', - 'invite_link', - _Field('pinned_message', constructor=_Message), - 'sticker_set_name', - 'can_set_sticker_set', - ]) - -# incoming -PhotoSize = _create_class('PhotoSize', [ - 'file_id', - 'width', - 'height', - 'file_size', - 'file_path', # undocumented - ]) - -# incoming -Audio = _create_class('Audio', [ - 'file_id', - 'duration', - 'performer', - 'title', - 'mime_type', - 'file_size' - ]) - -# incoming -Document = _create_class('Document', [ - 'file_id', - _Field('thumb', constructor=PhotoSize), - 'file_name', - 'mime_type', - 'file_size', - 'file_path', # undocumented - ]) - -# incoming and outgoing -MaskPosition = _create_class('MaskPosition', [ - 'point', - 'x_shift', - 'y_shift', - 'scale', - ]) - -# incoming -Sticker = _create_class('Sticker', [ - 'file_id', - 'width', - 'height', - _Field('thumb', constructor=PhotoSize), - 'emoji', - 'set_name', - _Field('mask_position', constructor=MaskPosition), - 'file_size', - ]) - -def StickerArray(data): - return [Sticker(**p) for p in data] - -# incoming -StickerSet = _create_class('StickerSet', [ - 'name', - 'title', - 'contains_masks', - _Field('stickers', constructor=StickerArray), - ]) - -# incoming -Video = _create_class('Video', [ - 'file_id', - 'width', - 'height', - 'duration', - _Field('thumb', constructor=PhotoSize), - 'mime_type', - 'file_size', - 'file_path', # undocumented - ]) - -# incoming -Voice = _create_class('Voice', [ - 'file_id', - 'duration', - 'mime_type', - 'file_size' - ]) - -# incoming -VideoNote = _create_class('VideoNote', [ - 'file_id', - 'length', - 'duration', - _Field('thumb', constructor=PhotoSize), - 'file_size' - ]) - -# incoming -Contact = _create_class('Contact', [ - 'phone_number', - 'first_name', - 'last_name', - 'user_id' - ]) - -# incoming -Location = _create_class('Location', [ - 'longitude', - 'latitude' - ]) - -# incoming -Venue = _create_class('Venue', [ - _Field('location', constructor=Location), - 'title', - 'address', - 'foursquare_id', - ]) - -# incoming -File = _create_class('File', [ - 'file_id', - 'file_size', - 'file_path' - ]) - -def PhotoSizeArray(data): - return [PhotoSize(**p) for p in data] - -def PhotoSizeArrayArray(data): - return [[PhotoSize(**p) for p in array] for array in data] - -# incoming -UserProfilePhotos = _create_class('UserProfilePhotos', [ - 'total_count', - _Field('photos', constructor=PhotoSizeArrayArray) - ]) - -# incoming -ChatMember = _create_class('ChatMember', [ - _Field('user', constructor=User), - 'status', - 'until_date', - 'can_be_edited', - 'can_change_info', - 'can_post_messages', - 'can_edit_messages', - 'can_delete_messages', - 'can_invite_users', - 'can_restrict_members', - 'can_pin_messages', - 'can_promote_members', - 'can_send_messages', - 'can_send_media_messages', - 'can_send_other_messages', - 'can_add_web_page_previews', - ]) - -def ChatMemberArray(data): - return [ChatMember(**p) for p in data] - -# outgoing -ReplyKeyboardMarkup = _create_class('ReplyKeyboardMarkup', [ - 'keyboard', - 'resize_keyboard', - 'one_time_keyboard', - 'selective', - ]) - -# outgoing -KeyboardButton = _create_class('KeyboardButton', [ - 'text', - 'request_contact', - 'request_location', - ]) - -# outgoing -ReplyKeyboardRemove = _create_class('ReplyKeyboardRemove', [ - _Field('remove_keyboard', default=True), - 'selective', - ]) - -# outgoing -ForceReply = _create_class('ForceReply', [ - _Field('force_reply', default=True), - 'selective', - ]) - -# outgoing -InlineKeyboardButton = _create_class('InlineKeyboardButton', [ - 'text', - 'url', - 'callback_data', - 'switch_inline_query', - 'switch_inline_query_current_chat', - 'callback_game', - 'pay', - ]) - -# outgoing -InlineKeyboardMarkup = _create_class('InlineKeyboardMarkup', [ - 'inline_keyboard', - ]) - -# incoming -MessageEntity = _create_class('MessageEntity', [ - 'type', - 'offset', - 'length', - 'url', - _Field('user', constructor=User), - ]) - -# incoming -def MessageEntityArray(data): - return [MessageEntity(**p) for p in data] - -# incoming -GameHighScore = _create_class('GameHighScore', [ - 'position', - _Field('user', constructor=User), - 'score', - ]) - -# incoming -Animation = _create_class('Animation', [ - 'file_id', - _Field('thumb', constructor=PhotoSize), - 'file_name', - 'mime_type', - 'file_size', - ]) - -# incoming -Game = _create_class('Game', [ - 'title', - 'description', - _Field('photo', constructor=PhotoSizeArray), - 'text', - _Field('text_entities', constructor=MessageEntityArray), - _Field('animation', constructor=Animation), - ]) - -# incoming -Invoice = _create_class('Invoice', [ - 'title', - 'description', - 'start_parameter', - 'currency', - 'total_amount', - ]) - -# outgoing -LabeledPrice = _create_class('LabeledPrice', [ - 'label', - 'amount', - ]) - -# outgoing -ShippingOption = _create_class('ShippingOption', [ - 'id', - 'title', - 'prices', - ]) - -# incoming -ShippingAddress = _create_class('ShippingAddress', [ - 'country_code', - 'state', - 'city', - 'street_line1', - 'street_line2', - 'post_code', - ]) - -# incoming -OrderInfo = _create_class('OrderInfo', [ - 'name', - 'phone_number', - 'email', - _Field('shipping_address', constructor=ShippingAddress), - ]) - -# incoming -ShippingQuery = _create_class('ShippingQuery', [ - 'id', - _Field('from_', constructor=User), - 'invoice_payload', - _Field('shipping_address', constructor=ShippingAddress), - ]) - -# incoming -PreCheckoutQuery = _create_class('PreCheckoutQuery', [ - 'id', - _Field('from_', constructor=User), - 'currency', - 'total_amount', - 'invoice_payload', - 'shipping_option_id', - _Field('order_info', constructor=OrderInfo), - ]) - -# incoming -SuccessfulPayment = _create_class('SuccessfulPayment', [ - 'currency', - 'total_amount', - 'invoice_payload', - 'shipping_option_id', - _Field('order_info', constructor=OrderInfo), - 'telegram_payment_charge_id', - 'provider_payment_charge_id', - ]) - -# incoming -Message = _create_class('Message', [ - 'message_id', - _Field('from_', constructor=User), - 'date', - _Field('chat', constructor=Chat), - _Field('forward_from', constructor=User), - _Field('forward_from_chat', constructor=Chat), - 'forward_from_message_id', - 'forward_signature', - 'forward_date', - _Field('reply_to_message', constructor=_Message), - 'edit_date', - 'author_signature', - 'text', - _Field('entities', constructor=MessageEntityArray), - _Field('caption_entities', constructor=MessageEntityArray), - _Field('audio', constructor=Audio), - _Field('document', constructor=Document), - _Field('game', constructor=Game), - _Field('photo', constructor=PhotoSizeArray), - _Field('sticker', constructor=Sticker), - _Field('video', constructor=Video), - _Field('voice', constructor=Voice), - _Field('video_note', constructor=VideoNote), - _Field('new_chat_members', constructor=UserArray), - 'caption', - _Field('contact', constructor=Contact), - _Field('location', constructor=Location), - _Field('venue', constructor=Venue), - _Field('new_chat_member', constructor=User), - _Field('left_chat_member', constructor=User), - 'new_chat_title', - _Field('new_chat_photo', constructor=PhotoSizeArray), - 'delete_chat_photo', - 'group_chat_created', - 'supergroup_chat_created', - 'channel_chat_created', - 'migrate_to_chat_id', - 'migrate_from_chat_id', - _Field('pinned_message', constructor=_Message), - _Field('invoice', constructor=Invoice), - _Field('successful_payment', constructor=SuccessfulPayment), - 'connected_website', - ]) - -# incoming -InlineQuery = _create_class('InlineQuery', [ - 'id', - _Field('from_', constructor=User), - _Field('location', constructor=Location), - 'query', - 'offset', - ]) - -# incoming -ChosenInlineResult = _create_class('ChosenInlineResult', [ - 'result_id', - _Field('from_', constructor=User), - _Field('location', constructor=Location), - 'inline_message_id', - 'query', - ]) - -# incoming -CallbackQuery = _create_class('CallbackQuery', [ - 'id', - _Field('from_', constructor=User), - _Field('message', constructor=Message), - 'inline_message_id', - 'chat_instance', - 'data', - 'game_short_name', - ]) - -# incoming -Update = _create_class('Update', [ - 'update_id', - _Field('message', constructor=Message), - _Field('edited_message', constructor=Message), - _Field('channel_post', constructor=Message), - _Field('edited_channel_post', constructor=Message), - _Field('inline_query', constructor=InlineQuery), - _Field('chosen_inline_result', constructor=ChosenInlineResult), - _Field('callback_query', constructor=CallbackQuery), - ]) - -# incoming -def UpdateArray(data): - return [Update(**u) for u in data] - -# incoming -WebhookInfo = _create_class('WebhookInfo', [ - 'url', - 'has_custom_certificate', - 'pending_update_count', - 'last_error_date', - 'last_error_message', - ]) - -# outgoing -InputTextMessageContent = _create_class('InputTextMessageContent', [ - 'message_text', - 'parse_mode', - 'disable_web_page_preview', - ]) - -# outgoing -InputLocationMessageContent = _create_class('InputLocationMessageContent', [ - 'latitude', - 'longitude', - 'live_period', - ]) - -# outgoing -InputVenueMessageContent = _create_class('InputVenueMessageContent', [ - 'latitude', - 'longitude', - 'title', - 'address', - 'foursquare_id', - ]) - -# outgoing -InputContactMessageContent = _create_class('InputContactMessageContent', [ - 'phone_number', - 'first_name', - 'last_name', - ]) - -# outgoing -InlineQueryResultArticle = _create_class('InlineQueryResultArticle', [ - _Field('type', default='article'), - 'id', - 'title', - 'input_message_content', - 'reply_markup', - 'url', - 'hide_url', - 'description', - 'thumb_url', - 'thumb_width', - 'thumb_height', - ]) - -# outgoing -InlineQueryResultPhoto = _create_class('InlineQueryResultPhoto', [ - _Field('type', default='photo'), - 'id', - 'photo_url', - 'thumb_url', - 'photo_width', - 'photo_height', - 'title', - 'description', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultGif = _create_class('InlineQueryResultGif', [ - _Field('type', default='gif'), - 'id', - 'gif_url', - 'gif_width', - 'gif_height', - 'gif_duration', - 'thumb_url', - 'title', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultMpeg4Gif = _create_class('InlineQueryResultMpeg4Gif', [ - _Field('type', default='mpeg4_gif'), - 'id', - 'mpeg4_url', - 'mpeg4_width', - 'mpeg4_height', - 'mpeg4_duration', - 'thumb_url', - 'title', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultVideo = _create_class('InlineQueryResultVideo', [ - _Field('type', default='video'), - 'id', - 'video_url', - 'mime_type', - 'thumb_url', - 'title', - 'caption', - 'parse_mode', - 'video_width', - 'video_height', - 'video_duration', - 'description', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultAudio = _create_class('InlineQueryResultAudio', [ - _Field('type', default='audio'), - 'id', - 'audio_url', - 'title', - 'caption', - 'parse_mode', - 'performer', - 'audio_duration', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultVoice = _create_class('InlineQueryResultVoice', [ - _Field('type', default='voice'), - 'id', - 'voice_url', - 'title', - 'caption', - 'parse_mode', - 'voice_duration', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultDocument = _create_class('InlineQueryResultDocument', [ - _Field('type', default='document'), - 'id', - 'title', - 'caption', - 'parse_mode', - 'document_url', - 'mime_type', - 'description', - 'reply_markup', - 'input_message_content', - 'thumb_url', - 'thumb_width', - 'thumb_height', - ]) - -# outgoing -InlineQueryResultLocation = _create_class('InlineQueryResultLocation', [ - _Field('type', default='location'), - 'id', - 'latitude', - 'longitude', - 'title', - 'live_period', - 'reply_markup', - 'input_message_content', - 'thumb_url', - 'thumb_width', - 'thumb_height', - ]) - -# outgoing -InlineQueryResultVenue = _create_class('InlineQueryResultVenue', [ - _Field('type', default='venue'), - 'id', - 'latitude', - 'longitude', - 'title', - 'address', - 'foursquare_id', - 'reply_markup', - 'input_message_content', - 'thumb_url', - 'thumb_width', - 'thumb_height', - ]) - -# outgoing -InlineQueryResultContact = _create_class('InlineQueryResultContact', [ - _Field('type', default='contact'), - 'id', - 'phone_number', - 'first_name', - 'last_name', - 'reply_markup', - 'input_message_content', - 'thumb_url', - 'thumb_width', - 'thumb_height', - ]) - -# outgoing -InlineQueryResultGame = _create_class('InlineQueryResultGame', [ - _Field('type', default='game'), - 'id', - 'game_short_name', - 'reply_markup', - ]) - -# outgoing -InlineQueryResultCachedPhoto = _create_class('InlineQueryResultCachedPhoto', [ - _Field('type', default='photo'), - 'id', - 'photo_file_id', - 'title', - 'description', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedGif = _create_class('InlineQueryResultCachedGif', [ - _Field('type', default='gif'), - 'id', - 'gif_file_id', - 'title', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedMpeg4Gif = _create_class('InlineQueryResultCachedMpeg4Gif', [ - _Field('type', default='mpeg4_gif'), - 'id', - 'mpeg4_file_id', - 'title', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedSticker = _create_class('InlineQueryResultCachedSticker', [ - _Field('type', default='sticker'), - 'id', - 'sticker_file_id', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedDocument = _create_class('InlineQueryResultCachedDocument', [ - _Field('type', default='document'), - 'id', - 'title', - 'document_file_id', - 'description', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedVideo = _create_class('InlineQueryResultCachedVideo', [ - _Field('type', default='video'), - 'id', - 'video_file_id', - 'title', - 'description', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedVoice = _create_class('InlineQueryResultCachedVoice', [ - _Field('type', default='voice'), - 'id', - 'voice_file_id', - 'title', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InlineQueryResultCachedAudio = _create_class('InlineQueryResultCachedAudio', [ - _Field('type', default='audio'), - 'id', - 'audio_file_id', - 'caption', - 'parse_mode', - 'reply_markup', - 'input_message_content', - ]) - -# outgoing -InputMediaPhoto = _create_class('InputMediaPhoto', [ - _Field('type', default='photo'), - 'media', - 'caption', - 'parse_mode', - ]) - -# outgoing -InputMediaVideo = _create_class('InputMediaVideo', [ - _Field('type', default='video'), - 'media', - 'caption', - 'parse_mode', - 'width', - 'height', - 'duration', - 'supports_streaming', - ]) - -# incoming -ResponseParameters = _create_class('ResponseParameters', [ - 'migrate_to_chat_id', - 'retry_after', - ]) diff --git a/lib/support/telepot2/routing.py b/lib/support/telepot2/routing.py deleted file mode 100644 index 41a141c..0000000 --- a/lib/support/telepot2/routing.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -This module has a bunch of key function factories and routing table factories -to facilitate the use of :class:`.Router`. - -Things to remember: - -1. A key function takes one argument - the message, and returns a key, optionally - followed by positional arguments and keyword arguments. - -2. A routing table is just a dictionary. After obtaining one from a factory - function, you can customize it to your liking. -""" - -import re -from . import glance, _isstring, all_content_types - -def by_content_type(): - """ - :return: - A key function that returns a 2-tuple (content_type, (msg[content_type],)). - In plain English, it returns the message's *content type* as the key, - and the corresponding content as a positional argument to the handler - function. - """ - def f(msg): - content_type = glance(msg, flavor='chat')[0] - return content_type, (msg[content_type],) - return f - -def by_command(extractor, prefix=('/',), separator=' ', pass_args=False): - """ - :param extractor: - a function that takes one argument (the message) and returns a portion - of message to be interpreted. To extract the text of a chat message, - use ``lambda msg: msg['text']``. - - :param prefix: - a list of special characters expected to indicate the head of a command. - - :param separator: - a command may be followed by arguments separated by ``separator``. - - :type pass_args: bool - :param pass_args: - If ``True``, arguments following a command will be passed to the handler - function. - - :return: - a key function that interprets a specific part of a message and returns - the embedded command, optionally followed by arguments. If the text is - not preceded by any of the specified ``prefix``, it returns a 1-tuple - ``(None,)`` as the key. This is to distinguish with the special - ``None`` key in routing table. - """ - if not isinstance(prefix, (tuple, list)): - prefix = (prefix,) - - def f(msg): - text = extractor(msg) - for px in prefix: - if text.startswith(px): - chunks = text[len(px):].split(separator) - return chunks[0], (chunks[1:],) if pass_args else () - return (None,), # to distinguish with `None` - return f - -def by_chat_command(prefix=('/',), separator=' ', pass_args=False): - """ - :param prefix: - a list of special characters expected to indicate the head of a command. - - :param separator: - a command may be followed by arguments separated by ``separator``. - - :type pass_args: bool - :param pass_args: - If ``True``, arguments following a command will be passed to the handler - function. - - :return: - a key function that interprets a chat message's text and returns - the embedded command, optionally followed by arguments. If the text is - not preceded by any of the specified ``prefix``, it returns a 1-tuple - ``(None,)`` as the key. This is to distinguish with the special - ``None`` key in routing table. - """ - return by_command(lambda msg: msg['text'], prefix, separator, pass_args) - -def by_text(): - """ - :return: - a key function that returns a message's ``text`` field. - """ - return lambda msg: msg['text'] - -def by_data(): - """ - :return: - a key function that returns a message's ``data`` field. - """ - return lambda msg: msg['data'] - -def by_regex(extractor, regex, key=1): - """ - :param extractor: - a function that takes one argument (the message) and returns a portion - of message to be interpreted. To extract the text of a chat message, - use ``lambda msg: msg['text']``. - - :type regex: str or regex object - :param regex: the pattern to look for - - :param key: the part of match object to be used as key - - :return: - a key function that returns ``match.group(key)`` as key (where ``match`` - is the match object) and the match object as a positional argument. - If no match is found, it returns a 1-tuple ``(None,)`` as the key. - This is to distinguish with the special ``None`` key in routing table. - """ - if _isstring(regex): - regex = re.compile(regex) - - def f(msg): - text = extractor(msg) - match = regex.search(text) - if match: - index = key if isinstance(key, tuple) else (key,) - return match.group(*index), (match,) - else: - return (None,), # to distinguish with `None` - return f - -def process_key(processor, fn): - """ - :param processor: - a function to process the key returned by the supplied key function - - :param fn: - a key function - - :return: - a function that wraps around the supplied key function to further - process the key before returning. - """ - def f(*aa, **kw): - k = fn(*aa, **kw) - if isinstance(k, (tuple, list)): - return (processor(k[0]),) + tuple(k[1:]) - else: - return processor(k) - return f - -def lower_key(fn): - """ - :param fn: a key function - - :return: - a function that wraps around the supplied key function to ensure - the returned key is in lowercase. - """ - def lower(key): - try: - return key.lower() - except AttributeError: - return key - return process_key(lower, fn) - -def upper_key(fn): - """ - :param fn: a key function - - :return: - a function that wraps around the supplied key function to ensure - the returned key is in uppercase. - """ - def upper(key): - try: - return key.upper() - except AttributeError: - return key - return process_key(upper, fn) - -def make_routing_table(obj, keys, prefix='on_'): - """ - :return: - a dictionary roughly equivalent to ``{'key1': obj.on_key1, 'key2': obj.on_key2, ...}``, - but ``obj`` does not have to define all methods. It may define the needed ones only. - - :param obj: the object - - :param keys: a list of keys - - :param prefix: a string to be prepended to keys to make method names - """ - def maptuple(k): - if isinstance(k, tuple): - if len(k) == 2: - return k - elif len(k) == 1: - return k[0], lambda *aa, **kw: getattr(obj, prefix+k[0])(*aa, **kw) - else: - raise ValueError() - else: - return k, lambda *aa, **kw: getattr(obj, prefix+k)(*aa, **kw) - # Use `lambda` to delay evaluation of `getattr`. - # I don't want to require definition of all methods. - # Let users define only the ones he needs. - - return dict([maptuple(k) for k in keys]) - -def make_content_type_routing_table(obj, prefix='on_'): - """ - :return: - a dictionary covering all available content types, roughly equivalent to - ``{'text': obj.on_text, 'photo': obj.on_photo, ...}``, - but ``obj`` does not have to define all methods. It may define the needed ones only. - - :param obj: the object - - :param prefix: a string to be prepended to content types to make method names - """ - return make_routing_table(obj, all_content_types, prefix) diff --git a/lib/support/telepot2/text.py b/lib/support/telepot2/text.py deleted file mode 100644 index d4b3cb7..0000000 --- a/lib/support/telepot2/text.py +++ /dev/null @@ -1,88 +0,0 @@ -def _apply_entities(text, entities, escape_map, format_map): - def inside_entities(i): - return any(map(lambda e: - e['offset'] <= i < e['offset']+e['length'], - entities)) - - # Split string into char sequence and escape in-place to - # preserve index positions. - seq = list(map(lambda c,i: - escape_map[c] # escape special characters - if c in escape_map and not inside_entities(i) - else c, - list(text), # split string to char sequence - range(0,len(text)))) # along with each char's index - - # Ensure smaller offsets come first - sorted_entities = sorted(entities, key=lambda e: e['offset']) - offset = 0 - result = '' - - for e in sorted_entities: - f,n,t = e['offset'], e['length'], e['type'] - - result += ''.join(seq[offset:f]) - - if t in format_map: - # apply format - result += format_map[t](''.join(seq[f:f+n]), e) - else: - result += ''.join(seq[f:f+n]) - - offset = f + n - - result += ''.join(seq[offset:]) - return result - - -def apply_entities_as_markdown(text, entities): - """ - Format text as Markdown. Also take care of escaping special characters. - Returned value can be passed to :meth:`.Bot.sendMessage` with appropriate - ``parse_mode``. - - :param text: - plain text - - :param entities: - a list of `MessageEntity `_ objects - """ - escapes = {'*': '\\*', - '_': '\\_', - '[': '\\[', - '`': '\\`',} - - formatters = {'bold': lambda s,e: '*'+s+'*', - 'italic': lambda s,e: '_'+s+'_', - 'text_link': lambda s,e: '['+s+']('+e['url']+')', - 'text_mention': lambda s,e: '['+s+'](tg://user?id='+str(e['user']['id'])+')', - 'code': lambda s,e: '`'+s+'`', - 'pre': lambda s,e: '```text\n'+s+'```'} - - return _apply_entities(text, entities, escapes, formatters) - - -def apply_entities_as_html(text, entities): - """ - Format text as HTML. Also take care of escaping special characters. - Returned value can be passed to :meth:`.Bot.sendMessage` with appropriate - ``parse_mode``. - - :param text: - plain text - - :param entities: - a list of `MessageEntity `_ objects - """ - escapes = {'<': '<', - '>': '>', - '&': '&',} - - formatters = {'bold': lambda s,e: ''+s+'', - 'italic': lambda s,e: ''+s+'', - 'text_link': lambda s,e: ''+s+'', - 'text_mention': lambda s,e: ''+s+'', - 'code': lambda s,e: ''+s+'', - 'pre': lambda s,e: '
'+s+'
'} - - return _apply_entities(text, entities, escapes, formatters) diff --git a/lib/system/logic_auth.py b/lib/system/logic_auth.py index f680e61..3411b0a 100644 --- a/lib/system/logic_auth.py +++ b/lib/system/logic_auth.py @@ -1,25 +1,27 @@ # -*- coding: utf-8 -*- ######################################################### # python -import os -import traceback -import random -import json -import string import codecs +import json +import os +import random +import string +import traceback # third-party import requests -from flask import Blueprint, request, Response, send_file, render_template, redirect, jsonify +from flask import (Blueprint, Response, jsonify, redirect, render_template, + request, send_file) +from framework import app, frame, path_app_root +from framework.util import Util + +from .model import ModelSetting +# 패키지 +from .plugin import logger, package_name # sjva 공용 -from framework import frame, path_app_root, app -from framework.util import Util -# 패키지 -from .plugin import package_name, logger -from .model import ModelSetting class SystemLogicAuth(object): @staticmethod @@ -78,7 +80,7 @@ class SystemLogicAuth(object): @staticmethod def check_auth_status(value=None): try: - from support.base.aes import SupportAES + from support import SupportAES mykey=(codecs.encode(SystemLogicAuth.get_ip().encode(), 'hex').decode() + codecs.encode(ModelSetting.get('auth_apikey').encode(), 'hex').decode()).zfill(32)[:32].encode() logger.debug(mykey) tmp = SupportAES.decrypt(value, mykey=mykey) diff --git a/lib/system/logic_env.py b/lib/system/logic_env.py index 27bd71a..16877a6 100644 --- a/lib/system/logic_env.py +++ b/lib/system/logic_env.py @@ -1,30 +1,31 @@ # -*- coding: utf-8 -*- ######################################################### # python -import os -import traceback import logging +import os import platform -import time import threading +import time +import traceback # third-party -from flask import Blueprint, request, Response, send_file, render_template, redirect, jsonify +from flask import (Blueprint, Response, jsonify, redirect, render_template, + request, send_file) +from framework import F, app, celery, path_app_root, path_data +from .model import ModelSetting +# 패키지 +from .plugin import logger, package_name # sjva 공용 -from framework import F, path_app_root, path_data, celery, app -# 패키지 -from .plugin import logger, package_name -from .model import ModelSetting class SystemLogicEnv(object): @staticmethod def load_export(): try: - from support.base.file import SupportFile + from support import SupportFile f = os.path.join(path_app_root, 'export.sh') if os.path.exists(f): return SupportFile.read_file(f) @@ -89,7 +90,7 @@ class SystemLogicEnv(object): def celery_test(): if F.config['use_celery']: from celery import Celery - from celery.exceptions import TimeoutError, NotRegistered + from celery.exceptions import NotRegistered, TimeoutError data = {} try: @@ -126,4 +127,4 @@ class SystemLogicEnv(object): return data except Exception as exception: logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) \ No newline at end of file + logger.error(traceback.format_exc()) diff --git a/lib/system/logic_plugin.py b/lib/system/logic_plugin.py index 664f2a6..d0f1231 100644 --- a/lib/system/logic_plugin.py +++ b/lib/system/logic_plugin.py @@ -14,7 +14,7 @@ import requests # sjva 공용 from framework import app, frame, logger, path_data from framework.util import Util -from support.base.process import SupportProcess +from support import SupportProcess import system diff --git a/lib/system/mod_home.py b/lib/system/mod_home.py index c8d77fb..496eead 100644 --- a/lib/system/mod_home.py +++ b/lib/system/mod_home.py @@ -1,6 +1,6 @@ import platform -from support.base.util import SupportUtil +from support import SupportUtil from .setup import * diff --git a/lib/system/mod_plugin.py b/lib/system/mod_plugin.py new file mode 100644 index 0000000..42a331e --- /dev/null +++ b/lib/system/mod_plugin.py @@ -0,0 +1,42 @@ +from support import SupportFile + +from .setup import * + +name = 'plugin' + +class ModulePlugin(PluginModuleBase): + db_default = { + 'plugin_dev_path': os.path.join(F.config['path_data'], 'dev'), + } + + def __init__(self, P): + super(ModulePlugin, self).__init__(P, name=name, first_menu='list') + + + def process_menu(self, page, req): + arg = P.ModelSetting.to_dict() + try: + return render_template(f'{__package__}_{name}_{page}.html', arg=arg) + except Exception as e: + P.logger.error(f'Exception:{str(e)}') + P.logger.error(traceback.format_exc()) + return render_template('sample.html', title=f"{__package__}/{name}/{page}") + + + def process_command(self, command, arg1, arg2, arg3, req): + ret = {'ret':'success'} + if command == 'plugin_install': + ret = F.PluginManager.plugin_install(arg1) + + return jsonify(ret) + + + def plugin_load(self): + try: + pass + except Exception as e: + P.logger.error(f'Exception:{str(e)}') + P.logger.error(traceback.format_exc()) + + + \ No newline at end of file diff --git a/lib/system/mod_setting.py b/lib/system/mod_setting.py index f15aa3f..18bbd8c 100644 --- a/lib/system/mod_setting.py +++ b/lib/system/mod_setting.py @@ -1,7 +1,7 @@ import random import string -from support.base.file import SupportFile +from support import SupportDiscord, SupportFile, SupportTelegram from .setup import * @@ -19,9 +19,9 @@ class ModuleSetting(PluginModuleBase): 'use_apikey': 'False', 'apikey': ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)), f'restart_interval': f'{random.randint(0,59)} {random.randint(1,23)} * * *', + 'restart_notify': 'False', 'theme' : 'Cerulean', 'log_level' : '20', - 'plugin_dev_path': os.path.join(F.config['path_data'], 'dev'), 'system_start_time': '', # notify 'notify_telegram_use' : 'False', @@ -31,6 +31,7 @@ class ModuleSetting(PluginModuleBase): 'notify_discord_use' : 'False', 'notify_discord_webhook' : '', 'notify_advaned_use' : 'False', + 'notify.yaml': '', #직접 사용하지 않으나 저장 편의상. } def __init__(self, P): @@ -52,12 +53,17 @@ class ModuleSetting(PluginModuleBase): elif page == 'menu': arg['menu_yaml_filepath'] = F.config['menu_yaml_filepath'] arg['menu.yaml'] = SupportFile.read_file(arg['menu_yaml_filepath']) + elif page == 'notify': + arg['notify_yaml_filepath'] = F.config['notify_yaml_filepath'] + arg['notify.yaml'] = SupportFile.read_file(arg['notify_yaml_filepath']) + return render_template(f'{__package__}_{name}_{page}.html', arg=arg) except Exception as e: P.logger.error(f'Exception:{str(e)}') P.logger.error(traceback.format_exc()) return render_template('sample.html', title=f"{__package__}/{name}/{page}") + def process_command(self, command, arg1, arg2, arg3, req): ret = {'ret':'success'} if command == 'apikey_generate': @@ -80,11 +86,34 @@ class ModuleSetting(PluginModuleBase): F.socketio.emit("refresh", {}, namespace='/framework', broadcast=True) elif command == 'notify_test': if arg1 == 'telegram': - pass - + token, chatid, sound, text = arg2.split('||') + sound = True if sound == 'true' else False + SupportTelegram.send_telegram_message(text, image_url=None, bot_token=token, chat_id=chatid, disable_notification=sound) + ret['msg'] = '메시지를 전송했습니다.' + elif arg1 == 'discord': + SupportDiscord.send_discord_message(arg3, webhook_url=arg2) + ret['msg'] = '메시지를 전송했습니다.' + elif arg1 == 'advanced': + from tool import ToolNotify + ToolNotify.send_advanced_message(arg3, message_id=arg2) + ret['msg'] = '메시지를 전송했습니다.' + elif command == 'ddns_test': + try: + import requests + url = arg1 + '/version' + res = requests.get(url) + data = res.text + ret['msg'] = f"버전: {data}" + except Exception as e: + P.logger.error(f'Exception:{str(e)}') + P.logger.error(traceback.format_exc()) + ret['msg'] = str(e) + ret['type'] = 'warning' + elif command == 'command_run': + ret['msg'] = arg1 + pass return jsonify(ret) - def plugin_load(self): try: @@ -99,18 +128,29 @@ class ModuleSetting(PluginModuleBase): self.__set_scheduler_check_scheduler() F.get_recent_version() + notify_yaml_filepath = os.path.join(F.config['path_data'], 'db', 'notify.yaml') + if os.path.exists(notify_yaml_filepath) == False: + import shutil + shutil.copy( + os.path.join(F.config['path_app'], 'files', 'notify.yaml.template'), + notify_yaml_filepath + ) + if SystemModelSetting.get_bool('restart_notify'): + from tool import ToolNotify + msg = f"시스템이 시작되었습니다.\n재시작: {F.config['arg_repeat']}" + ToolNotify.send_message(msg, message_id='system_start') except Exception as e: P.logger.error(f'Exception:{str(e)}') P.logger.error(traceback.format_exc()) - + + def setting_save_after(self, change_list): if 'theme' in change_list: F.socketio.emit("refresh", {}, namespace='/framework', broadcast=True) - + elif 'notify.yaml' in change_list: + SupportFile.write_file(F.config['notify_yaml_filepath'], SystemModelSetting.get('notify.yaml')) - - def __set_restart_scheduler(self): name = f'{__package__}_restart' if F.scheduler.is_include(name): @@ -132,4 +172,3 @@ class ModuleSetting(PluginModuleBase): scheduler.add_job_instance(job_instance, run=False) - diff --git a/lib/system/plugin.py b/lib/system/plugin.py index 8f872fd..5c6db16 100644 --- a/lib/system/plugin.py +++ b/lib/system/plugin.py @@ -1,21 +1,23 @@ # -*- coding: utf-8 -*- ######################################################### # python -import os, platform -import traceback +import json import logging +import os +import platform import threading import time -import json +import traceback # third-party import requests -from flask import Blueprint, request, Response, send_file, render_template, redirect, jsonify, stream_with_context - -# sjva 공용 -from framework import frame, app, scheduler, socketio, check_api, path_app_root, path_data, get_logger#, celery -from support.base.util import SingletonClass +from flask import (Blueprint, Response, jsonify, redirect, render_template, + request, send_file, stream_with_context) from flask_login import login_required +# sjva 공용 +from framework import (app, check_api, frame, get_logger, # , celery + path_app_root, path_data, scheduler, socketio) +from support import SingletonClass # 로그 package_name = __name__.split('.')[0] @@ -23,19 +25,19 @@ logger = get_logger(__package__) # 패키지 from .logic import SystemLogic -from .model import ModelSetting -from .logic_plugin import LogicPlugin -from .logic_selenium import SystemLogicSelenium +from .logic_auth import SystemLogicAuth from .logic_command import SystemLogicCommand from .logic_command2 import SystemLogicCommand2 -from .logic_notify import SystemLogicNotify -from .logic_telegram_bot import SystemLogicTelegramBot -from .logic_auth import SystemLogicAuth -from .logic_tool_crypt import SystemLogicToolDecrypt -from .logic_terminal import SystemLogicTerminal # celery 때문에 import from .logic_env import SystemLogicEnv +from .logic_notify import SystemLogicNotify +from .logic_plugin import LogicPlugin +from .logic_selenium import SystemLogicSelenium from .logic_site import SystemLogicSite +from .logic_telegram_bot import SystemLogicTelegramBot +from .logic_terminal import SystemLogicTerminal +from .logic_tool_crypt import SystemLogicToolDecrypt +from .model import ModelSetting ######################################################### diff --git a/lib/system/setup.py b/lib/system/setup.py index 0d94bc3..927f3ca 100644 --- a/lib/system/setup.py +++ b/lib/system/setup.py @@ -16,7 +16,15 @@ __menu = { ], }, - {'uri': 'plugin', 'name': '플러그인'}, + { + 'uri': 'plugin', + 'name': '플러그인', + 'list': [ + {'uri': 'setting', 'name': '개발 설정'}, + {'uri': 'list', 'name': '플러그인 목록'}, + ], + }, + { 'uri': 'tool', 'name': '시스템 툴', @@ -25,6 +33,7 @@ __menu = { {'uri': 'python', 'name': 'Python'}, {'uri': 'db', 'name': 'DB'}, {'uri': 'crypt', 'name': '암호화'}, + {'uri': 'upload', 'name': '업로드'}, ] }, { @@ -64,10 +73,11 @@ try: SystemModelSetting = P.ModelSetting from .mod_home import ModuleHome + from .mod_plugin import ModulePlugin from .mod_route import ModuleRoute from .mod_setting import ModuleSetting - P.set_module_list([ModuleSetting, ModuleHome, ModuleRoute]) + P.set_module_list([ModuleSetting, ModuleHome, ModuleRoute, ModulePlugin]) except Exception as e: P.logger.error(f'Exception:{str(e)}') diff --git a/lib/system/templates/system_plugin.html b/lib/system/templates/system_plugin_list.html similarity index 100% rename from lib/system/templates/system_plugin.html rename to lib/system/templates/system_plugin_list.html diff --git a/lib/system/templates/system_plugin_setting.html b/lib/system/templates/system_plugin_setting.html new file mode 100644 index 0000000..2838e82 --- /dev/null +++ b/lib/system/templates/system_plugin_setting.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block content %} + +
+ {{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']])}} + {{ macros.m_row_start('5') }} + {{ macros.m_row_end() }} + {{ macros.m_hr() }} +
+ {{ macros.setting_input_text_and_buttons('plugin_dev_path', '개발용 플러그인 경로', [['select_btn', '폴더 선택']], value=arg['plugin_dev_path'], desc=['개발용 플러그인 패키지 폴더가 있는 경로']) }} + {{ macros.m_hr() }} + {{ macros.setting_input_text_and_buttons('_plugin_git', '플러그인 설치', [['plugin_install_btn', '설치']], value='https://github.com/', desc=['플러그인 Git 주소']) }} +
+
+ + +{% endblock %} diff --git a/lib/system/templates/system_setting_basic copy.html b/lib/system/templates/system_setting_basic copy.html deleted file mode 100644 index b468cbe..0000000 --- a/lib/system/templates/system_setting_basic copy.html +++ /dev/null @@ -1,420 +0,0 @@ -{% extends "base.html" %} - - -{% block content %} - - -
- - - -
- - -{{ macros.m_modal_start('link_edit_modal', '링크', 'modal-lg') }} - -{{ macros.m_modal_end() }} - - - -{% endblock %} diff --git a/lib/system/templates/system_setting_basic.html b/lib/system/templates/system_setting_basic.html index fd49496..e0381bc 100644 --- a/lib/system/templates/system_setting_basic.html +++ b/lib/system/templates/system_setting_basic.html @@ -11,126 +11,22 @@ {{ macros.setting_input_int('port', 'Port', value=arg['port'], min='1', placeholder='Port', desc=['포트 번호입니다.', '네이티브 설치 혹은 도커 네트워크 타입이 호스트일 경우 반영됩니다.', '도커 브릿지 모드인 경우는 docker run -p 옵션에서 변경하시기 바랍니다.', '경고 : -p 브릿지 모드로 사용중 일 경우 9999번을 변경하지 마세요.']) }} {{ macros.setting_input_text_and_buttons('ddns', 'DDNS', [['ddns_test_btn', '테스트']], value=arg['ddns'], desc=['외부에서 접근시 사용할 DDNS. http:// 나 https:// 로 시작해야합니다.', 'URL생성시 사용합니다.', '테스트 버튼 클릭 후 버전을 확인 할 수 있어야 합니다.']) }} {{ macros.setting_input_text('restart_interval', '자동 재시작 시간', value=arg['restart_interval'], col='3', desc=['자동 재시작 간격(시간단위)이나 Cron 설정을 입력합니다.', '0이면 재시작 안함.']) }} + {{ macros.setting_checkbox('restart_notify', '시작시 알림', value=arg['restart_notify'], desc=['메시지 ID: system_start']) }} {{ macros.setting_select('log_level', '로그 레벨', [['10', 'DEBUG'],['20', 'INFO'],['30', 'WARNING'],['40', 'ERROR'], ['50', 'CRITICAL'] ], value=arg['log_level'], col='3') }} {{ macros.m_hr() }} + {{ macros.setting_input_text_and_buttons('command_text', 'Command', [['command_run_btn', 'Run']], value='', desc='') }} {% endblock %} diff --git a/lib/system/templates/system_setting_notify.html b/lib/system/templates/system_setting_notify.html index 168f61d..4d6559c 100644 --- a/lib/system/templates/system_setting_notify.html +++ b/lib/system/templates/system_setting_notify.html @@ -18,24 +18,27 @@
{{ macros.setting_input_text('notify_telegram_token', 'Bot Token', value=arg['notify_telegram_token']) }} {{ macros.setting_input_text('notify_telegram_chat_id', 'My Chat ID', value=arg['notify_telegram_chat_id'], col='3') }} - {{ macros.setting_input_text_and_buttons('tmp_text_telegram', 'Test', [['tmp_telegram_test_btn', '전송']], value='테스트 메시지입니다.', col='9', desc=['사용자가 먼저 봇과 대화를 시작하여 대화창이 생성된 상태여야 합니다.', '(대화창이 있을 경우에만 알림 수신)']) }} {{ macros.setting_checkbox('notify_telegram_disable_notification', '알람 Disable', value=arg['notify_telegram_disable_notification'], desc='On : 알람 소리 없이 메시지를 수신합니다.') }} + {{ macros.setting_input_text_and_buttons('tmp_text_telegram', 'Test', [['tmp_telegram_test_btn', '전송']], value='테스트 메시지입니다.', col='9', desc=['사용자가 먼저 봇과 대화를 시작하여 대화창이 생성된 상태여야 합니다.', '(대화창이 있을 경우에만 알림 수신)']) }}
+ {{ macros.m_hr() }} + {{ macros.setting_checkbox('notify_discord_use', '디스코드 사용', value=arg['notify_discord_use']) }}
{{ macros.setting_input_text('notify_discord_webhook', '웹훅', value=arg['notify_discord_webhook']) }} {{ macros.setting_input_text_and_buttons('tmp_text_discord', 'Test', [['tmp_discord_test_btn', '전송']], value='테스트 메시지입니다.', col='9') }} -
{{ macros.m_tab_content_end() }} {{ macros.m_tab_content_start('advanced', false) }} - {{ macros.setting_checkbox('notify_advaned_use', '사용', value=arg['notify_advaned_use'], desc=['충분히 내용 숙지하고 사용하세요.', '사용시 기본설정은 무시됩니다.']) }} + {{ macros.setting_checkbox('notify_advaned_use', '사용', value=arg['notify_advaned_use'], desc=['사용시 기본설정은 무시됩니다.']) }}
- {{ macros.setting_input_textarea('_notify_advaned_policy', '정책', value=arg['notify_advaned_policy'], row='30') }} - {{ macros.setting_input_text_and_buttons('tmp_text_advanced', 'Test', [['tmp_advanced_test_btn', '전송']], value='테스트 메시지입니다.', col='9', desc=['메시지 ID = 형식', '형식의 구분자 |', '텔레그램 : bot_token,chat_id | 디스코드 : 웹훅 URL', '예) DEFAULT = 794150118:AAEAAAAAAAAAAAAAAA,186485141|https://discordapp.com/api/webhooks/626295849....', '모든 알림을 텔레그램과 디스코드에 보냄']) }} + {{ macros.info_text_and_buttons('notify_yaml_filepath', '파일 위치', [['globalEditBtn', '편집기에서 열기', [('file',arg['notify_yaml_filepath'])]]], value=arg['notify_yaml_filepath']) }} + {{ macros.setting_input_textarea('notify.yaml', '정책', value=arg['notify.yaml'], row='20') }} {{ macros.setting_input_text('tmp_message_id', 'Test Message ID', value='DEFAULT') }} + {{ macros.setting_input_text_and_buttons('tmp_text_advanced', 'Test', [['tmp_advanced_test_btn', '전송']], value='테스트 메시지입니다.', col='9', desc=['저장 후 적용됩니다.']) }} +
{{ macros.m_tab_content_end() }} @@ -46,8 +49,6 @@ {% endblock %} diff --git a/lib/tool/__init__.py b/lib/tool/__init__.py new file mode 100644 index 0000000..f1b33fa --- /dev/null +++ b/lib/tool/__init__.py @@ -0,0 +1,3 @@ +from framework import logger + +from .notify import ToolNotify diff --git a/lib/tool/notify.py b/lib/tool/notify.py index e578b38..168d88f 100644 --- a/lib/tool/notify.py +++ b/lib/tool/notify.py @@ -1,125 +1,48 @@ -# -*- coding: utf-8 -*- -######################################################### - -import os import traceback +from datetime import datetime -from discord_webhook import DiscordEmbed, DiscordWebhook from framework import F -from telepot2 import Bot, glance -from telepot2.loop import MessageLoop +from support import SupportDiscord, SupportTelegram, SupportYaml from . import logger class ToolNotify(object): - @classmethod + @classmethod def send_message(cls, text, message_id=None, image_url=None): if F.SystemModelSetting.get_bool('notify_advaned_use'): return cls.send_advanced_message(text, image_url=image_url, message_id=message_id) else: if F.SystemModelSetting.get_bool('notify_telegram_use'): - cls.send_telegram_message(text, image_url=image_url, bot_token=F.SystemModelSetting.get('notify_telegram_token'), chat_id=F.SystemModelSetting.get('notify_telegram_chat_id')) + SupportTelegram.send_telegram_message(text, image_url=image_url, bot_token=F.SystemModelSetting.get('notify_telegram_token'), chat_id=F.SystemModelSetting.get('notify_telegram_chat_id')) if F.SystemModelSetting.get_bool('notify_discord_use'): cls.send_discord_message(text, image_url=image_url, webhook_url=F.SystemModelSetting.get('notify_discord_webhook')) - @classmethod - def send_advanced_message(cls, text, image_url=None, policy=None, message_id=None): - from system.model import ModelSetting as SystemModelSetting + def send_advanced_message(cls, text, image_url=None, policy=None, message_id=None): try: - if policy is None: - policy = SystemModelSetting.get('notify_advaned_policy') - - if message_id is None: + message_id = message_id.strip() + policy = SupportYaml.read_yaml(F.config['notify_yaml_filepath']) + if message_id is None or message_id not in policy: message_id = 'DEFAULT' - - policy_list = cls._make_policy_dict(policy) - #logger.debug(policy_list) - #logger.debug(message_id) - if message_id.strip() not in policy_list: - message_id = 'DEFAULT' - - for tmp in policy_list[message_id.strip()]: - if tmp.startswith('http'): - cls.send_discord_message(text, image_url=image_url, webhook_url=tmp) - elif tmp.find(',') != -1: - tmp2 = tmp.split(',') - cls.send_telegram_message(text, image_url=image_url, bot_token=tmp2[0], chat_id=tmp2[1]) - return True - except Exception as exception: - logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) - #logger.debug('Chatid:%s', chat_id) - return False - - @classmethod - def _make_policy_dict(cls, policy): - try: - ret = {} - for t in policy.split('\n'): - t = t.strip() - if t == '' or t.startswith('#'): - continue - else: - tmp2 = t.split('=') - if len(tmp2) != 2: + now = datetime.now() + for item in policy[message_id]: + if item.get('enable_time') != None: + tmp = item.get('enable_time').split('-') + if now.hour < int(tmp[0]) or now.hour > int(tmp[1]): continue - ret[tmp2[0].strip()] = [x.strip() for x in tmp2[1].split('|')] - return ret - except Exception as exception: - logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) - return False - - @classmethod - def send_discord_message(cls, text, image_url=None, webhook_url=None): - from system.model import ModelSetting as SystemModelSetting - try: - if webhook_url is None: - webhook_url = SystemModelSetting.get('notify_discord_webhook') - - webhook = DiscordWebhook(url=webhook_url, content=text) - if image_url is not None: - embed = DiscordEmbed() - embed.set_timestamp() - embed.set_image(url=image_url) - webhook.add_embed(embed) - response = webhook.execute() - #discord = response.json() - #logger.debug(discord) + if item.get('type') == 'telegram': + if item.get('token', '') == '' or item.get('chat_id', '') == '': + continue + SupportTelegram.send_telegram_message(text, image_url=image_url, bot_token=item.get('token'), chat_id=item.get('chat_id'), disable_notification=item.get('disable_notification', False)) + elif item.get('type') == 'discord': + if item.get('webhook', '') == '': + continue + SupportDiscord.send_discord_message(text, webhook_url=item.get('webhook')) return True except Exception as exception: logger.error('Exception:%s', exception) logger.error(traceback.format_exc()) return False - - @classmethod - def send_telegram_message(cls, text, bot_token=None, chat_id=None, image_url=None, disable_notification=None): - from system.model import ModelSetting as SystemModelSetting - try: - if bot_token is None: - bot_token = SystemModelSetting.get('notify_telegram_token') - - if chat_id is None: - chat_id = SystemModelSetting.get('notify_telegram_chat_id') - - if disable_notification is None: - disable_notification = SystemModelSetting.get_bool('notify_telegram_disable_notification') - - bot = Bot(bot_token) - if image_url is not None: - #bot.sendPhoto(chat_id, text, caption=caption, disable_notification=disable_notification) - bot.sendPhoto(chat_id, image_url, disable_notification=disable_notification) - bot.sendMessage(chat_id, text, disable_web_page_preview=True, disable_notification=disable_notification) - #elif mime == 'video': - # bot.sendVideo(chat_id, text, disable_notification=disable_notification) - return True - except Exception as exception: - logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) - logger.debug('Chatid:%s', chat_id) - return False - diff --git a/lib/tool_base/__init__.py b/lib/tool_base/__init__.py index b8a5461..275b9b9 100644 --- a/lib/tool_base/__init__.py +++ b/lib/tool_base/__init__.py @@ -1,20 +1 @@ from framework import logger -from .notify import ToolBaseNotify -from .file import ToolBaseFile -from .aes_cipher import ToolAESCipher -from .celery_shutil import ToolShutil -from .subprocess import ToolSubprocess -from .rclone import ToolRclone -from .ffmpeg import ToolFfmpeg -from .util import ToolUtil -from .hangul import ToolHangul -from .os_command import ToolOSCommand -from .util_yaml import ToolUtilYaml - - -def d(data): - if type(data) in [type({}), type([])]: - import json - return '\n' + json.dumps(data, indent=4, ensure_ascii=False) - else: - return str(data) diff --git a/lib/tool_base/ffmpeg.py b/lib/tool_base/ffmpeg.py index f79a418..6e8add3 100644 --- a/lib/tool_base/ffmpeg.py +++ b/lib/tool_base/ffmpeg.py @@ -2,7 +2,7 @@ ######################################################### import os, sys, traceback, subprocess, json, platform from framework import app, logger, path_data -from .subprocess import ToolSubprocess +from ..support.base.subprocess import ToolSubprocess class ToolFfmpeg(object): diff --git a/lib/tool_base/file.py b/lib/tool_base/file.py index 8843883..ecfe8a8 100644 --- a/lib/tool_base/file.py +++ b/lib/tool_base/file.py @@ -1,45 +1,18 @@ # -*- coding: utf-8 -*- ######################################################### -import os, traceback, io, re, json, codecs +import codecs +import io +import json +import os +import re +import traceback + from . import logger + class ToolBaseFile(object): - @classmethod - def read(cls, filepath, mode='r'): - try: - import codecs - ifp = codecs.open(filepath, mode, encoding='utf8') - data = ifp.read() - ifp.close() - if isinstance(data, bytes): - data = data.decode('utf-8') - return data - except Exception as exception: - logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) - - - @classmethod - def download(cls, url, filepath): - try: - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36', - 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', - 'Accept-Language' : 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7', - 'Connection': 'Keep-Alive', - } - - import requests - with open(filepath, "wb") as file_is: # open in binary mode - response = requests.get(url, headers=headers) # get request - file_is.write(response.content) # write to file - return True - except Exception as exception: - logger.debug('Exception:%s', exception) - logger.debug(traceback.format_exc()) - return False - + @classmethod def write(cls, data, filepath, mode='w'): @@ -80,7 +53,8 @@ class ToolBaseFile(object): @classmethod def file_move(cls, source_path, target_dir, target_filename): try: - import time, shutil + import shutil + import time if os.path.exists(target_dir) == False: os.makedirs(target_dir) target_path = os.path.join(target_dir, target_filename) @@ -196,7 +170,8 @@ class ToolBaseFile(object): @classmethod def makezip_simple(cls, zip_path, zip_extension='cbz', remove_zip_path=True): - import zipfile, shutil + import shutil + import zipfile try: if os.path.exists(zip_path) == False: return False diff --git a/lib/tool_base/rclone.py b/lib/tool_base/rclone.py index 351c48b..ad28fe1 100644 --- a/lib/tool_base/rclone.py +++ b/lib/tool_base/rclone.py @@ -1,8 +1,16 @@ # -*- coding: utf-8 -*- ######################################################### -import os, sys, traceback, subprocess, json, platform +import json +import os +import platform +import subprocess +import sys +import traceback + from framework import app, logger, path_data -from .subprocess import ToolSubprocess + +from ..support.base.subprocess import ToolSubprocess + class ToolRclone(object): @@ -132,4 +140,4 @@ class ToolRclone(object): return ret except Exception as exception: logger.error('Exception:%s', exception) - logger.error(traceback.format_exc()) \ No newline at end of file + logger.error(traceback.format_exc())