update
This commit is contained in:
23
files/notify.yaml.template
Normal file
23
files/notify.yaml.template
Normal file
@@ -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: ''
|
||||||
@@ -19,4 +19,5 @@ pytz
|
|||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
discord-webhook
|
discord-webhook
|
||||||
pyyaml
|
pyyaml
|
||||||
pycryptodome
|
pycryptodome
|
||||||
|
telepot-mod
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
VERSION="4.0.0"
|
VERSION="4.0.0"
|
||||||
from support import d
|
|
||||||
|
|
||||||
from .init_main import Framework
|
from .init_main import Framework
|
||||||
|
|
||||||
frame = Framework.get_instance()
|
frame = Framework.get_instance()
|
||||||
@@ -14,8 +12,8 @@ socketio = frame.socketio
|
|||||||
path_app_root = frame.path_app_root
|
path_app_root = frame.path_app_root
|
||||||
path_data = frame.path_data
|
path_data = frame.path_data
|
||||||
get_logger = frame.get_logger
|
get_logger = frame.get_logger
|
||||||
|
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
from support import d
|
||||||
|
|
||||||
from .init_declare import User, check_api
|
from .init_declare import User, check_api
|
||||||
from .scheduler import Job
|
from .scheduler import Job
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class CustomFormatter(logging.Formatter):
|
|||||||
green = "\x1B[32m"
|
green = "\x1B[32m"
|
||||||
# pathname filename
|
# pathname filename
|
||||||
#format = "[%(asctime)s|%(name)s|%(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
|
#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 = {
|
FORMATS = {
|
||||||
logging.DEBUG: format.format(color=grey, reset=reset, yellow=yellow, green=green),
|
logging.DEBUG: format.format(color=grey, reset=reset, yellow=yellow, green=green),
|
||||||
@@ -86,7 +86,7 @@ class User:
|
|||||||
return str(r)
|
return str(r)
|
||||||
|
|
||||||
def can_login(self, passwd_hash):
|
def can_login(self, passwd_hash):
|
||||||
from support.base.aes import SupportAES
|
from support import SupportAES
|
||||||
tmp = SupportAES.decrypt(self.passwd_hash)
|
tmp = SupportAES.decrypt(self.passwd_hash)
|
||||||
return passwd_hash == tmp
|
return passwd_hash == tmp
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class Framework:
|
|||||||
|
|
||||||
|
|
||||||
def __initialize(self):
|
def __initialize(self):
|
||||||
|
os.environ['FF'] = "true"
|
||||||
self.__config_initialize("first")
|
self.__config_initialize("first")
|
||||||
self.__make_default_dir()
|
self.__make_default_dir()
|
||||||
|
|
||||||
@@ -155,13 +156,13 @@ class Framework:
|
|||||||
self.logger.error('CRITICAL db.create_all()!!!')
|
self.logger.error('CRITICAL db.create_all()!!!')
|
||||||
self.logger.error(f'Exception:{str(e)}')
|
self.logger.error(f'Exception:{str(e)}')
|
||||||
self.logger.error(traceback.format_exc())
|
self.logger.error(traceback.format_exc())
|
||||||
|
self.SystemModelSetting = SystemInstance.ModelSetting
|
||||||
SystemInstance.plugin_load()
|
SystemInstance.plugin_load()
|
||||||
self.app.register_blueprint(SystemInstance.blueprint)
|
self.app.register_blueprint(SystemInstance.blueprint)
|
||||||
self.config['flag_system_loading'] = True
|
self.config['flag_system_loading'] = True
|
||||||
self.__config_initialize('member')
|
self.__config_initialize('member')
|
||||||
self.__config_initialize('system_loading_after')
|
self.__config_initialize('system_loading_after')
|
||||||
self.SystemModelSetting = SystemInstance.ModelSetting
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_plugin(self):
|
def initialize_plugin(self):
|
||||||
from system.setup import P as SP
|
from system.setup import P as SP
|
||||||
@@ -222,6 +223,7 @@ class Framework:
|
|||||||
self.__load_config()
|
self.__load_config()
|
||||||
self.__init_define()
|
self.__init_define()
|
||||||
self.config['menu_yaml_filepath'] = os.path.join(self.config['path_data'], 'db', 'menu.yaml')
|
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":
|
elif mode == "flask":
|
||||||
self.app.secret_key = os.urandom(24)
|
self.app.secret_key = os.urandom(24)
|
||||||
#self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data/db/system.db?check_same_thread=False'
|
#self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data/db/system.db?check_same_thread=False'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from support.base.yaml import SupportYaml
|
from support import SupportYaml
|
||||||
|
|
||||||
from framework import F
|
from framework import F
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from support import SupportFile, SupportSubprocess, SupportYaml
|
||||||
|
|
||||||
from framework import F
|
from framework import F
|
||||||
|
|
||||||
@@ -139,7 +145,7 @@ class PluginManager:
|
|||||||
#logger.error(traceback.format_exc())
|
#logger.error(traceback.format_exc())
|
||||||
|
|
||||||
#mod_plugin_info = getattr(mod, 'setup')
|
#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:
|
if mod_plugin_info == None:
|
||||||
try:
|
try:
|
||||||
mod = __import__(f'{plugin_name}.setup', fromlist=['setup'])
|
mod = __import__(f'{plugin_name}.setup', fromlist=['setup'])
|
||||||
@@ -296,3 +302,96 @@ class PluginManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
F.logger.error(f'Exception:{str(e)}')
|
F.logger.error(f'Exception:{str(e)}')
|
||||||
F.logger.error(traceback.format_exc())
|
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'] = '<br>'.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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from support.base.util import SingletonClass
|
from support import SingletonClass
|
||||||
|
|
||||||
from framework import F
|
from framework import F
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from random import randint
|
|||||||
from apscheduler.jobstores.base import JobLookupError
|
from apscheduler.jobstores.base import JobLookupError
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
from support.base.util import pt
|
|
||||||
|
|
||||||
|
|
||||||
class Scheduler(object):
|
class Scheduler(object):
|
||||||
@@ -27,7 +26,6 @@ class Scheduler(object):
|
|||||||
self.sched.start()
|
self.sched.start()
|
||||||
self.logger.info('SCHEDULER start..')
|
self.logger.info('SCHEDULER start..')
|
||||||
|
|
||||||
@pt
|
|
||||||
def first_run_check_thread_function(self):
|
def first_run_check_thread_function(self):
|
||||||
try:
|
try:
|
||||||
#time.sleep(60)
|
#time.sleep(60)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ $("body").on('click', '#globalEditBtn', function(e) {
|
|||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
|
|
||||||
function globalSendCommand(command, arg1, arg2, arg3, modal_title, callback) {
|
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');
|
console.log('/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command');
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import os, traceback
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from framework import F
|
from framework import F
|
||||||
from support.base.yaml import SupportYaml
|
from support import SupportYaml
|
||||||
from . import get_model_setting, Logic, default_route, default_route_single_module
|
|
||||||
|
from . import (Logic, default_route, default_route_single_module,
|
||||||
|
get_model_setting)
|
||||||
|
|
||||||
|
|
||||||
class PluginBase(object):
|
class PluginBase(object):
|
||||||
package_name = None
|
package_name = None
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# python
|
|
||||||
import traceback, os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
# third-party
|
from flask import jsonify, redirect, render_template, request
|
||||||
from flask import Blueprint, request, render_template, redirect, jsonify
|
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from flask_socketio import SocketIO, emit, send
|
from framework import F
|
||||||
|
from support import AlchemyEncoder
|
||||||
# sjva 공용
|
|
||||||
from framework import socketio, check_api
|
|
||||||
from support.base.util import AlchemyEncoder
|
|
||||||
# 패키지
|
|
||||||
|
|
||||||
#########################################################
|
|
||||||
|
|
||||||
|
|
||||||
def default_route(P):
|
def default_route(P):
|
||||||
@@ -178,7 +170,7 @@ def default_route(P):
|
|||||||
#########################################################
|
#########################################################
|
||||||
# 단일 모듈인 경우 모듈이름을 붙이기 불편하여 추가.
|
# 단일 모듈인 경우 모듈이름을 붙이기 불편하여 추가.
|
||||||
@P.blueprint.route('/api/<sub2>', methods=['GET', 'POST'])
|
@P.blueprint.route('/api/<sub2>', methods=['GET', 'POST'])
|
||||||
@check_api
|
@F.check_api
|
||||||
def api_first(sub2):
|
def api_first(sub2):
|
||||||
try:
|
try:
|
||||||
for module in P.module_list:
|
for module in P.module_list:
|
||||||
@@ -188,7 +180,7 @@ def default_route(P):
|
|||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
|
||||||
@P.blueprint.route('/api/<sub>/<sub2>', methods=['GET', 'POST'])
|
@P.blueprint.route('/api/<sub>/<sub2>', methods=['GET', 'POST'])
|
||||||
@check_api
|
@F.check_api
|
||||||
def api(sub, sub2):
|
def api(sub, sub2):
|
||||||
try:
|
try:
|
||||||
for module in P.module_list:
|
for module in P.module_list:
|
||||||
@@ -284,7 +276,7 @@ def default_route_single_module(P):
|
|||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
|
||||||
@P.blueprint.route('/api/<sub>', methods=['GET', 'POST'])
|
@P.blueprint.route('/api/<sub>', methods=['GET', 'POST'])
|
||||||
@check_api
|
@F.check_api
|
||||||
def api(sub):
|
def api(sub):
|
||||||
try:
|
try:
|
||||||
return P.module_list[0].process_api(sub, request)
|
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:
|
if module.socketio_list is None:
|
||||||
module.socketio_list = []
|
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():
|
def connect():
|
||||||
try:
|
try:
|
||||||
P.logger.debug(f'socket_connect : {P.package_name} - {module.name}')
|
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())
|
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():
|
def disconnect():
|
||||||
try:
|
try:
|
||||||
P.logger.debug('socket_disconnect : %s - %s', P.package_name, module.name)
|
P.logger.debug('socket_disconnect : %s - %s', P.package_name, module.name)
|
||||||
@@ -358,7 +350,7 @@ def default_route_socketio_module(module):
|
|||||||
if encoding:
|
if encoding:
|
||||||
data = json.dumps(data, cls=AlchemyEncoder)
|
data = json.dumps(data, cls=AlchemyEncoder)
|
||||||
data = json.loads(data)
|
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
|
module.socketio_callback = socketio_callback
|
||||||
|
|
||||||
@@ -420,4 +412,4 @@ def default_route_socketio_page(page):
|
|||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}/{page.name}', broadcast=True)
|
socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}/{page.name}', broadcast=True)
|
||||||
|
|
||||||
page.socketio_callback = socketio_callback
|
page.socketio_callback = socketio_callback
|
||||||
|
|||||||
@@ -5,12 +5,42 @@ def d(data):
|
|||||||
else:
|
else:
|
||||||
return str(data)
|
return str(data)
|
||||||
|
|
||||||
from .logger import get_logger
|
def load():
|
||||||
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
|
||||||
|
|
||||||
def set_logger(l):
|
import os
|
||||||
global logger
|
|
||||||
logger = l
|
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 사용 겸용이다.
|
# 일반 cli 사용 겸용이다.
|
||||||
# set_logger 로 인한 진입이 아니고 import가 되면 기본 경로로 로그파일을
|
# set_logger 로 인한 진입이 아니고 import가 되면 기본 경로로 로그파일을
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from support import logger
|
from support import d, logger
|
||||||
"""
|
|
||||||
from support import d, get_logger, logger
|
from .aes import SupportAES
|
||||||
from .discord import SupportDiscord
|
from .discord import SupportDiscord
|
||||||
from .ffmpeg import SupportFfmpeg
|
from .ffmpeg import SupportFfmpeg
|
||||||
from .file import SupportFile
|
from .file import SupportFile
|
||||||
from .image import SupportImage
|
from .image import SupportImage
|
||||||
from .process import SupportProcess
|
from .process import SupportProcess
|
||||||
from .string import SupportString
|
from .string import SupportString
|
||||||
from .util import SupportUtil, pt, default_headers, SingletonClass, AlchemyEncoder
|
from .util import (AlchemyEncoder, SingletonClass, SupportUtil,
|
||||||
from .aes import SupportAES
|
default_headers, pt)
|
||||||
from .yaml import SupportYaml
|
from .yaml import SupportYaml
|
||||||
"""
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import os, traceback, re, json, codecs
|
import codecs
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
class SupportFile(object):
|
class SupportFile(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_file(cls, filename):
|
def read_file(cls, filename, mode='r'):
|
||||||
try:
|
try:
|
||||||
ifp = codecs.open(filename, 'r', encoding='utf8')
|
ifp = codecs.open(filename, mode, encoding='utf8')
|
||||||
data = ifp.read()
|
data = ifp.read()
|
||||||
ifp.close()
|
ifp.close()
|
||||||
return data
|
return data
|
||||||
@@ -15,10 +21,10 @@ class SupportFile(object):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_file(cls, filename, data):
|
def write_file(cls, filename, data, mode='w'):
|
||||||
try:
|
try:
|
||||||
import codecs
|
import codecs
|
||||||
ofp = codecs.open(filename, 'w', encoding='utf8')
|
ofp = codecs.open(filename, mode, encoding='utf8')
|
||||||
ofp.write(data)
|
ofp.write(data)
|
||||||
ofp.close()
|
ofp.close()
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
@@ -26,25 +32,8 @@ class SupportFile(object):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, url, filepath):
|
def download_file(cls, url, filepath):
|
||||||
try:
|
try:
|
||||||
headers = {
|
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',
|
'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
|
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
|
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
|
file_is.write(response.content) # write to file
|
||||||
return True
|
return True
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
@@ -67,6 +53,47 @@ class SupportFile(object):
|
|||||||
return False
|
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
|
@classmethod
|
||||||
def write(cls, data, filepath, mode='w'):
|
def write(cls, data, filepath, mode='w'):
|
||||||
try:
|
try:
|
||||||
@@ -83,14 +110,7 @@ class SupportFile(object):
|
|||||||
return False
|
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
|
@classmethod
|
||||||
@@ -106,7 +126,8 @@ class SupportFile(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def file_move(cls, source_path, target_dir, target_filename):
|
def file_move(cls, source_path, target_dir, target_filename):
|
||||||
try:
|
try:
|
||||||
import time, shutil
|
import shutil
|
||||||
|
import time
|
||||||
if os.path.exists(target_dir) == False:
|
if os.path.exists(target_dir) == False:
|
||||||
os.makedirs(target_dir)
|
os.makedirs(target_dir)
|
||||||
target_path = os.path.join(target_dir, target_filename)
|
target_path = os.path.join(target_dir, target_filename)
|
||||||
@@ -217,7 +238,8 @@ class SupportFile(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def makezip(cls, zip_path, zip_extension='zip', remove_zip_path=True):
|
def makezip(cls, zip_path, zip_extension='zip', remove_zip_path=True):
|
||||||
import zipfile, shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
try:
|
try:
|
||||||
if os.path.exists(zip_path) == False:
|
if os.path.exists(zip_path) == False:
|
||||||
return False
|
return False
|
||||||
@@ -248,7 +270,8 @@ class SupportFile(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def makezip_all(cls, zip_path, zip_filepath=None, zip_extension='zip', remove_zip_path=True):
|
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
|
from pathlib import Path
|
||||||
try:
|
try:
|
||||||
if os.path.exists(zip_path) == False:
|
if os.path.exists(zip_path) == False:
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import os, traceback, io, re, json, codecs
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
class SupportString(object):
|
class SupportString(object):
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cate_char_by_first(cls, title): # get_first
|
def get_cate_char_by_first(cls, title): # get_first
|
||||||
value = ord(title[0].upper())
|
value = ord(title[0].upper())
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
# -*- coding: utf-8 -*-
|
import json
|
||||||
#########################################################
|
import os
|
||||||
import os, traceback, subprocess, json
|
import platform
|
||||||
from framework import frame
|
import subprocess
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
class ToolSubprocess(object):
|
class SupportSubprocess(object):
|
||||||
|
|
||||||
|
# 2021-10-25
|
||||||
|
# timeout 적용
|
||||||
@classmethod
|
@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:
|
try:
|
||||||
#logger.debug('execute_command_return : %s', ' '.join(command))
|
if platform.system() == 'Windows':
|
||||||
if frame.config['running_type'] == 'windows':
|
|
||||||
tmp = []
|
tmp = []
|
||||||
if type(command) == type([]):
|
if type(command) == type([]):
|
||||||
for x in command:
|
for x in command:
|
||||||
@@ -21,60 +30,7 @@ class ToolSubprocess(object):
|
|||||||
command = ' '.join(tmp)
|
command = ' '.join(tmp)
|
||||||
|
|
||||||
iter_arg = ''
|
iter_arg = ''
|
||||||
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8')
|
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')
|
|
||||||
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':
|
|
||||||
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')
|
||||||
else:
|
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')
|
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:
|
except Exception as exception:
|
||||||
logger.error('Exception:%s', exception)
|
logger.error('Exception:%s', exception)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
logger.error('command : %s', command)
|
logger.error('command : %s', command)
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from telepot_mod import Bot
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
@@ -5,24 +9,11 @@ class SupportTelegram:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_telegram_message(cls, text, bot_token=None, chat_id=None, image_url=None, disable_notification=None):
|
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:
|
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)
|
bot = Bot(bot_token)
|
||||||
if image_url is not None:
|
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.sendPhoto(chat_id, image_url, disable_notification=disable_notification)
|
||||||
bot.sendMessage(chat_id, text, disable_web_page_preview=True, 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
|
return True
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
logger.error('Exception:%s', exception)
|
logger.error('Exception:%s', exception)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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 <https://core.telegram.org/bots/api#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 <https://core.telegram.org/bots/api#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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 <https://core.telegram.org/bots/api#inlinequeryresult>`_
|
|
||||||
- a *tuple* whose first element is a list of `InlineQueryResult <https://core.telegram.org/bots/api#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}}])
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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']
|
|
||||||
@@ -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))
|
|
||||||
@@ -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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 <https://core.telegram.org/bots/api#update>`_ object.
|
|
||||||
- a ``dict`` representing an Update object.
|
|
||||||
"""
|
|
||||||
update = _dictify(data)
|
|
||||||
self._orderer.input_queue.put(update)
|
|
||||||
@@ -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',
|
|
||||||
])
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 <https://core.telegram.org/bots/api#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 <https://core.telegram.org/bots/api#messageentity>`_ objects
|
|
||||||
"""
|
|
||||||
escapes = {'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'&': '&',}
|
|
||||||
|
|
||||||
formatters = {'bold': lambda s,e: '<b>'+s+'</b>',
|
|
||||||
'italic': lambda s,e: '<i>'+s+'</i>',
|
|
||||||
'text_link': lambda s,e: '<a href="'+e['url']+'">'+s+'</a>',
|
|
||||||
'text_mention': lambda s,e: '<a href="tg://user?id='+str(e['user']['id'])+'">'+s+'</a>',
|
|
||||||
'code': lambda s,e: '<code>'+s+'</code>',
|
|
||||||
'pre': lambda s,e: '<pre>'+s+'</pre>'}
|
|
||||||
|
|
||||||
return _apply_entities(text, entities, escapes, formatters)
|
|
||||||
@@ -1,25 +1,27 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#########################################################
|
#########################################################
|
||||||
# python
|
# python
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
import random
|
|
||||||
import json
|
|
||||||
import string
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import traceback
|
||||||
|
|
||||||
# third-party
|
# third-party
|
||||||
import requests
|
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 공용
|
# 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):
|
class SystemLogicAuth(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -78,7 +80,7 @@ class SystemLogicAuth(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def check_auth_status(value=None):
|
def check_auth_status(value=None):
|
||||||
try:
|
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()
|
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)
|
logger.debug(mykey)
|
||||||
tmp = SupportAES.decrypt(value, mykey=mykey)
|
tmp = SupportAES.decrypt(value, mykey=mykey)
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#########################################################
|
#########################################################
|
||||||
# python
|
# python
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import time
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
# third-party
|
# 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 공용
|
# 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):
|
class SystemLogicEnv(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_export():
|
def load_export():
|
||||||
try:
|
try:
|
||||||
from support.base.file import SupportFile
|
from support import SupportFile
|
||||||
f = os.path.join(path_app_root, 'export.sh')
|
f = os.path.join(path_app_root, 'export.sh')
|
||||||
if os.path.exists(f):
|
if os.path.exists(f):
|
||||||
return SupportFile.read_file(f)
|
return SupportFile.read_file(f)
|
||||||
@@ -89,7 +90,7 @@ class SystemLogicEnv(object):
|
|||||||
def celery_test():
|
def celery_test():
|
||||||
if F.config['use_celery']:
|
if F.config['use_celery']:
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.exceptions import TimeoutError, NotRegistered
|
from celery.exceptions import NotRegistered, TimeoutError
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
try:
|
try:
|
||||||
@@ -126,4 +127,4 @@ class SystemLogicEnv(object):
|
|||||||
return data
|
return data
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
logger.error('Exception:%s', exception)
|
logger.error('Exception:%s', exception)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import requests
|
|||||||
# sjva 공용
|
# sjva 공용
|
||||||
from framework import app, frame, logger, path_data
|
from framework import app, frame, logger, path_data
|
||||||
from framework.util import Util
|
from framework.util import Util
|
||||||
from support.base.process import SupportProcess
|
from support import SupportProcess
|
||||||
|
|
||||||
import system
|
import system
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import platform
|
import platform
|
||||||
|
|
||||||
from support.base.util import SupportUtil
|
from support import SupportUtil
|
||||||
|
|
||||||
from .setup import *
|
from .setup import *
|
||||||
|
|
||||||
|
|||||||
42
lib/system/mod_plugin.py
Normal file
42
lib/system/mod_plugin.py
Normal file
@@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from support.base.file import SupportFile
|
from support import SupportDiscord, SupportFile, SupportTelegram
|
||||||
|
|
||||||
from .setup import *
|
from .setup import *
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ class ModuleSetting(PluginModuleBase):
|
|||||||
'use_apikey': 'False',
|
'use_apikey': 'False',
|
||||||
'apikey': ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)),
|
'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)} * * *',
|
f'restart_interval': f'{random.randint(0,59)} {random.randint(1,23)} * * *',
|
||||||
|
'restart_notify': 'False',
|
||||||
'theme' : 'Cerulean',
|
'theme' : 'Cerulean',
|
||||||
'log_level' : '20',
|
'log_level' : '20',
|
||||||
'plugin_dev_path': os.path.join(F.config['path_data'], 'dev'),
|
|
||||||
'system_start_time': '',
|
'system_start_time': '',
|
||||||
# notify
|
# notify
|
||||||
'notify_telegram_use' : 'False',
|
'notify_telegram_use' : 'False',
|
||||||
@@ -31,6 +31,7 @@ class ModuleSetting(PluginModuleBase):
|
|||||||
'notify_discord_use' : 'False',
|
'notify_discord_use' : 'False',
|
||||||
'notify_discord_webhook' : '',
|
'notify_discord_webhook' : '',
|
||||||
'notify_advaned_use' : 'False',
|
'notify_advaned_use' : 'False',
|
||||||
|
'notify.yaml': '', #직접 사용하지 않으나 저장 편의상.
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, P):
|
def __init__(self, P):
|
||||||
@@ -52,12 +53,17 @@ class ModuleSetting(PluginModuleBase):
|
|||||||
elif page == 'menu':
|
elif page == 'menu':
|
||||||
arg['menu_yaml_filepath'] = F.config['menu_yaml_filepath']
|
arg['menu_yaml_filepath'] = F.config['menu_yaml_filepath']
|
||||||
arg['menu.yaml'] = SupportFile.read_file(arg['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)
|
return render_template(f'{__package__}_{name}_{page}.html', arg=arg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error(f'Exception:{str(e)}')
|
P.logger.error(f'Exception:{str(e)}')
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
return render_template('sample.html', title=f"{__package__}/{name}/{page}")
|
return render_template('sample.html', title=f"{__package__}/{name}/{page}")
|
||||||
|
|
||||||
|
|
||||||
def process_command(self, command, arg1, arg2, arg3, req):
|
def process_command(self, command, arg1, arg2, arg3, req):
|
||||||
ret = {'ret':'success'}
|
ret = {'ret':'success'}
|
||||||
if command == 'apikey_generate':
|
if command == 'apikey_generate':
|
||||||
@@ -80,11 +86,34 @@ class ModuleSetting(PluginModuleBase):
|
|||||||
F.socketio.emit("refresh", {}, namespace='/framework', broadcast=True)
|
F.socketio.emit("refresh", {}, namespace='/framework', broadcast=True)
|
||||||
elif command == 'notify_test':
|
elif command == 'notify_test':
|
||||||
if arg1 == 'telegram':
|
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)
|
return jsonify(ret)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_load(self):
|
def plugin_load(self):
|
||||||
try:
|
try:
|
||||||
@@ -99,18 +128,29 @@ class ModuleSetting(PluginModuleBase):
|
|||||||
self.__set_scheduler_check_scheduler()
|
self.__set_scheduler_check_scheduler()
|
||||||
F.get_recent_version()
|
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:
|
except Exception as e:
|
||||||
P.logger.error(f'Exception:{str(e)}')
|
P.logger.error(f'Exception:{str(e)}')
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
def setting_save_after(self, change_list):
|
def setting_save_after(self, change_list):
|
||||||
if 'theme' in change_list:
|
if 'theme' in change_list:
|
||||||
F.socketio.emit("refresh", {}, namespace='/framework', broadcast=True)
|
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):
|
def __set_restart_scheduler(self):
|
||||||
name = f'{__package__}_restart'
|
name = f'{__package__}_restart'
|
||||||
if F.scheduler.is_include(name):
|
if F.scheduler.is_include(name):
|
||||||
@@ -132,4 +172,3 @@ class ModuleSetting(PluginModuleBase):
|
|||||||
scheduler.add_job_instance(job_instance, run=False)
|
scheduler.add_job_instance(job_instance, run=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#########################################################
|
#########################################################
|
||||||
# python
|
# python
|
||||||
import os, platform
|
import json
|
||||||
import traceback
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import json
|
import traceback
|
||||||
|
|
||||||
# third-party
|
# third-party
|
||||||
import requests
|
import requests
|
||||||
from flask import Blueprint, request, Response, send_file, render_template, redirect, jsonify, stream_with_context
|
from flask import (Blueprint, Response, jsonify, redirect, render_template,
|
||||||
|
request, send_file, 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_login import login_required
|
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]
|
package_name = __name__.split('.')[0]
|
||||||
@@ -23,19 +25,19 @@ logger = get_logger(__package__)
|
|||||||
|
|
||||||
# 패키지
|
# 패키지
|
||||||
from .logic import SystemLogic
|
from .logic import SystemLogic
|
||||||
from .model import ModelSetting
|
from .logic_auth import SystemLogicAuth
|
||||||
from .logic_plugin import LogicPlugin
|
|
||||||
from .logic_selenium import SystemLogicSelenium
|
|
||||||
from .logic_command import SystemLogicCommand
|
from .logic_command import SystemLogicCommand
|
||||||
from .logic_command2 import SystemLogicCommand2
|
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
|
# celery 때문에 import
|
||||||
from .logic_env import SystemLogicEnv
|
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_site import SystemLogicSite
|
||||||
|
from .logic_telegram_bot import SystemLogicTelegramBot
|
||||||
|
from .logic_terminal import SystemLogicTerminal
|
||||||
|
from .logic_tool_crypt import SystemLogicToolDecrypt
|
||||||
|
from .model import ModelSetting
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ __menu = {
|
|||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{'uri': 'plugin', 'name': '플러그인'},
|
{
|
||||||
|
'uri': 'plugin',
|
||||||
|
'name': '플러그인',
|
||||||
|
'list': [
|
||||||
|
{'uri': 'setting', 'name': '개발 설정'},
|
||||||
|
{'uri': 'list', 'name': '플러그인 목록'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
'uri': 'tool',
|
'uri': 'tool',
|
||||||
'name': '시스템 툴',
|
'name': '시스템 툴',
|
||||||
@@ -25,6 +33,7 @@ __menu = {
|
|||||||
{'uri': 'python', 'name': 'Python'},
|
{'uri': 'python', 'name': 'Python'},
|
||||||
{'uri': 'db', 'name': 'DB'},
|
{'uri': 'db', 'name': 'DB'},
|
||||||
{'uri': 'crypt', 'name': '암호화'},
|
{'uri': 'crypt', 'name': '암호화'},
|
||||||
|
{'uri': 'upload', 'name': '업로드'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -64,10 +73,11 @@ try:
|
|||||||
|
|
||||||
SystemModelSetting = P.ModelSetting
|
SystemModelSetting = P.ModelSetting
|
||||||
from .mod_home import ModuleHome
|
from .mod_home import ModuleHome
|
||||||
|
from .mod_plugin import ModulePlugin
|
||||||
from .mod_route import ModuleRoute
|
from .mod_route import ModuleRoute
|
||||||
from .mod_setting import ModuleSetting
|
from .mod_setting import ModuleSetting
|
||||||
|
|
||||||
P.set_module_list([ModuleSetting, ModuleHome, ModuleRoute])
|
P.set_module_list([ModuleSetting, ModuleHome, ModuleRoute, ModulePlugin])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error(f'Exception:{str(e)}')
|
P.logger.error(f'Exception:{str(e)}')
|
||||||
|
|||||||
30
lib/system/templates/system_plugin_setting.html
Normal file
30
lib/system/templates/system_plugin_setting.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장']])}}
|
||||||
|
{{ macros.m_row_start('5') }}
|
||||||
|
{{ macros.m_row_end() }}
|
||||||
|
{{ macros.m_hr() }}
|
||||||
|
<form id='setting' name='setting'>
|
||||||
|
{{ 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 주소']) }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$("body").on('click', '#select_btn', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
selectLocalFolder("폴더 선택", $('#plugin_dev_path').val(), function(ret) {
|
||||||
|
$('#plugin_dev_path').val(ret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("body").on('click', '#plugin_install_btn', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
globalSendCommand('plugin_install', $('#_plugin_git').val());
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<style type="text/css">
|
|
||||||
.my_hover:hover{
|
|
||||||
background-color: #ffff00;
|
|
||||||
transition: all 0.01s ease-in-out;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<nav>
|
|
||||||
{{ macros.m_tab_head_start() }}
|
|
||||||
{{ macros.m_tab_head2('normal', '일반', true) }}
|
|
||||||
{{ macros.m_tab_head2('web', '웹', false) }}
|
|
||||||
{{ macros.m_tab_head2('menu', '메뉴', false) }}
|
|
||||||
{{ macros.m_tab_head2('link', '링크', false) }}
|
|
||||||
{{ macros.m_tab_head2('download', '다운로드', false) }}
|
|
||||||
{{ macros.m_tab_head_end() }}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="tab-content" id="nav-tabContent">
|
|
||||||
{{ macros.m_tab_content_start('normal', true) }}
|
|
||||||
<form id='setting' name='setting'>
|
|
||||||
{{ 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:// 로 시작해야합니다.', 'RSS, Plex Callback, KLive 등에서 URL생성시 사용합니다.', '테스트 버튼 클릭 후 버전을 확인 할 수 있어야 합니다.']) }}
|
|
||||||
{{ macros.setting_input_text('auto_restart_hour', '자동 재시작 시간', value=arg['auto_restart_hour'], col='3', desc=['자동 재시작 간격(시간단위)이나 Cron 설정을 입력합니다.', '0이면 재시작 안함.']) }}
|
|
||||||
{{ macros.setting_select('log_level', '로그 레벨', [['10', 'DEBUG'],['20', 'INFO'],['30', 'WARNING'],['40', 'ERROR'], ['50', 'CRITICAL'] ], value=arg['log_level'], col='3') }}
|
|
||||||
{{ macros.setting_button([['setting_save', '저장']]) }}
|
|
||||||
</form>
|
|
||||||
</form>
|
|
||||||
{{ macros.m_hr() }}
|
|
||||||
{{ macros.setting_input_text_and_buttons('command_text', 'Command', [['command_run_btn', 'Run']], value='', desc='') }}
|
|
||||||
{{ macros.m_tab_content_end() }}
|
|
||||||
|
|
||||||
|
|
||||||
{{ macros.m_tab_content_start('web', false) }}
|
|
||||||
<form id='setting2' name='setting2'>
|
|
||||||
{{ macros.setting_select('theme', '테마 선택', [['Default','Default'], ['Cerulean','Cerulean'], ['Cosmo','Cosmo'], ['Cyborg','Cyborg'], ['Darkly','Darkly'], ['Flatly','Flatly'], ['Journal','Journal'], ['Litera','Litera'], ['Lumen','Lumen'], ['Lux','Lux'], ['Materia','Materia'], ['Minty','Minty'], ['Morph','Morph'],['Pulse','Pulse'], ['Quartz','Quartz'], ['Sandstone','Sandstone'], ['Simplex','Simplex'], ['Sketchy','Sketchy'], ['Slate','Slate'], ['Solar','Solar'], ['Spacelab','Spacelab'], ['Superhero','Superhero'], ['United','United'], ['Vapor','Vapor'], ['Yeti','Yeti'], ['Zephyr','Zephyr']], value=arg['theme'], desc=['https://bootswatch.com'], col='6') }}
|
|
||||||
{{ macros.setting_input_text('web_title', '웹 타이틀', value=arg['web_title']) }}
|
|
||||||
{{ macros.setting_button([['setting_save2', '저장']]) }}
|
|
||||||
</form>
|
|
||||||
{{ macros.m_tab_content_end() }}
|
|
||||||
|
|
||||||
|
|
||||||
{{ macros.m_tab_content_start('menu', false) }}
|
|
||||||
<form id='setting3' name='setting3'>
|
|
||||||
{% if arg['use_category_vod'] == 'True' %}
|
|
||||||
{{ macros.m_hr() }}
|
|
||||||
{{ macros.setting_button_with_info([['menu_toggle_btn', 'Toggle', [{'key':'category', 'value':'vod'}]]], left='VOD', desc=None) }}
|
|
||||||
<div id="menu_vod_div" class="collapse">
|
|
||||||
{{ macros.setting_checkbox('use_plugin_ffmpeg', 'FFMPEG', value=arg['use_plugin_ffmpeg']) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if arg['use_category_file_process'] == 'True' %}
|
|
||||||
{{ macros.m_hr() }}
|
|
||||||
{{ macros.setting_button_with_info([['menu_toggle_btn', 'Toggle', [{'key':'category', 'value':'file_process'}]]], left='파일처리', desc=None) }}
|
|
||||||
<div id="menu_file_process_div" class="collapse">
|
|
||||||
{{ macros.setting_checkbox('use_plugin_ktv', '국내TV', value=arg['use_plugin_ktv']) }}
|
|
||||||
{{ macros.setting_checkbox('use_plugin_fileprocess_movie', '영화', value=arg['use_plugin_fileprocess_movie']) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if arg['use_category_plex'] == 'True' %}
|
|
||||||
{{ macros.m_hr() }}
|
|
||||||
{{ macros.setting_button_with_info([['menu_toggle_btn', 'Toggle', [{'key':'category', 'value':'plex'}]]], left='PLEX', desc=None) }}
|
|
||||||
<div id="menu_plex_div" class="collapse">
|
|
||||||
{{ macros.setting_checkbox('use_plugin_plex', 'PLEX', value=arg['use_plugin_plex']) }}
|
|
||||||
{{ macros.setting_checkbox('use_plugin_gdrive_scan', 'GDrive 스캔', value=arg['use_plugin_gdrive_scan']) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if arg['use_category_tool'] == 'True' %}
|
|
||||||
{{ macros.m_hr() }}
|
|
||||||
{{ macros.setting_button_with_info([['menu_toggle_btn', 'Toggle', [{'key':'category', 'value':'tool'}]]], left='툴', desc=None) }}
|
|
||||||
<div id="menu_tool_div" class="collapse">
|
|
||||||
{{ macros.setting_checkbox('use_plugin_rclone', 'RClone', value=arg['use_plugin_rclone']) }}
|
|
||||||
{{ macros.setting_checkbox('use_plugin_daum_tv', 'Daum TV', value=arg['use_plugin_daum_tv']) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{{ macros.setting_button([['setting_save3', '저장']]) }}
|
|
||||||
</form>
|
|
||||||
{{ macros.m_tab_content_end() }}
|
|
||||||
|
|
||||||
|
|
||||||
{{ macros.m_tab_content_start('link', false) }}
|
|
||||||
{{ macros.m_button_group([['link_add_btn', '추가'], ['link_add_divider_btn', 'Divider Line 추가'], ['link_save_btn', '저장'], ['link_reset_btn', '초기화']])}}
|
|
||||||
{{ macros.m_row_start('0') }}
|
|
||||||
{{ macros.m_row_end() }}
|
|
||||||
|
|
||||||
{{ macros.m_hr_head_top() }}
|
|
||||||
{{ macros.m_row_start('0') }}
|
|
||||||
{{ macros.m_col(1, macros.m_strong('Idx')) }}
|
|
||||||
{{ macros.m_col(4, macros.m_strong('Title')) }}
|
|
||||||
{{ macros.m_col(4, macros.m_strong('URL')) }}
|
|
||||||
{{ macros.m_col(3, macros.m_strong('Action')) }}
|
|
||||||
{{ macros.m_row_end() }}
|
|
||||||
{{ macros.m_hr_head_bottom() }}
|
|
||||||
<form id="link_form" name="link_form">
|
|
||||||
<div id="link_list_div"></div>
|
|
||||||
</form>
|
|
||||||
{{ macros.m_tab_content_end() }}
|
|
||||||
|
|
||||||
{{ macros.m_tab_content_start('download', false) }}
|
|
||||||
{{ macros.setting_button_with_info([['global_link_btn', '다운로드', [{'key':'url', 'value':'https://github.com/soju6jan/soju6jan.github.io/blob/master/etc/hdhomerun_scan_191214.zip'}]], ['global_link_btn', '매뉴얼', [{'key':'url', 'value':'.'}]]], left='HDHomerun Scan Tool', desc=['HDHomerun 스캔하여 TVH용 프리셋 파일을 만들어주는 Windows용 프로그램', '8VSB 지원 케이블용']) }}
|
|
||||||
<!--
|
|
||||||
{{ macros.setting_button_with_info([['global_link_btn', '다운로드', [{'key':'url', 'value':'https://github.com/soju6jan/soju6jan.github.io/raw/master/etc/sjva_lc_0.1.1.apk'}]], ['global_link_btn', '매뉴얼', [{'key':'url', 'value':'.'}]]], left='SJVA for Live Channels', desc=['Android TV Live Channels 앱에 채널 소스를 제공하는 앱.', 'Klive, Plex 지원']) }}
|
|
||||||
{{ macros.setting_button_with_info([['global_link_btn', '티빙 애드온', [{'key':'url', 'value':'https://github.com/soju6jan/soju6jan.github.io/blob/master/kodi_plugin/plugin.video.tving.zip'}]]], left='KODI', desc=None) }}
|
|
||||||
-->
|
|
||||||
{{ macros.m_tab_content_end() }}
|
|
||||||
|
|
||||||
</div><!--tab-content-->
|
|
||||||
</div> <!--전체-->
|
|
||||||
|
|
||||||
<!-- 링크 모달 -->
|
|
||||||
{{ macros.m_modal_start('link_edit_modal', '링크', 'modal-lg') }}
|
|
||||||
<form id="link_form">
|
|
||||||
<input type="hidden" id="link_edit_index" name="link_edit_index"/>
|
|
||||||
{{ macros.setting_input_text('link_edit_title', '제목') }}
|
|
||||||
{{ macros.setting_input_text('link_edit_url', 'URL') }}
|
|
||||||
{{ macros.setting_button([['link_edit_confirm_btn', '확인'], ['link_edit_cancel_btn', '취소']]) }}
|
|
||||||
</form>
|
|
||||||
{{ macros.m_modal_end() }}
|
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var package_name = 'system';
|
|
||||||
var current_data;
|
|
||||||
var link_data;
|
|
||||||
|
|
||||||
$(document).ready(function(){
|
|
||||||
$(function() {
|
|
||||||
});
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/get_link_list',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data: {},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
link_data = data
|
|
||||||
make_link_data();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function setting_save_func(formData, noti) {
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/setting_save_system',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data: formData,
|
|
||||||
dataType: "json",
|
|
||||||
success: function (ret) {
|
|
||||||
if (ret) {
|
|
||||||
if (noti) {
|
|
||||||
$.notify('<strong>설정을 저장하였습니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.location.href = "/"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>설정 저장에 실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//설정 저장
|
|
||||||
$("#setting_save").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting');
|
|
||||||
setting_save_func(formData, true)
|
|
||||||
});
|
|
||||||
|
|
||||||
//설정 저장
|
|
||||||
$("#setting_save2").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting2');
|
|
||||||
setting_save_func(formData, false)
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#setting_save4").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting4');
|
|
||||||
setting_save_func(formData, true)
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#setting_save3").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting3');
|
|
||||||
setting_save_func(formData, true)
|
|
||||||
$.notify('<strong>재시작해야 적용됩니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#ddns_test_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
ddns = document.getElementById('ddns').value;
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/ddns_test',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{ddns:ddns},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
console.log(data)
|
|
||||||
if (data == 'fail') {
|
|
||||||
$.notify('<strong>접속에 실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>Version:'+ data+'</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#menu_toggle_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
category = $(this).data('category')
|
|
||||||
var div_name = '#menu_'+category+'_div'
|
|
||||||
$(div_name).collapse('toggle')
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#command_run_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
command_text = document.getElementById('command_text').value;
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/command_run',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{command_text:command_text},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
if (data.ret == 'success') {
|
|
||||||
$.notify('<strong>성공 : '+ data.log +'</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>실패 : ' + data.log+'</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
|
||||||
// 링크
|
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// 화면 상단 버튼 START
|
|
||||||
$("body").on('click', '#link_add_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
document.getElementById("link_edit_index").value = -1;
|
|
||||||
document.getElementById('link_edit_title').value = '';
|
|
||||||
document.getElementById('link_edit_url').value = '';
|
|
||||||
$('#link_edit_modal').modal();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_add_divider_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
tmp = {}
|
|
||||||
tmp['type'] = 'divider'
|
|
||||||
link_data.splice(link_data.length, 0, tmp);
|
|
||||||
make_link_data()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_save_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/link_save',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{link_data:JSON.stringify(link_data)},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
if (data) {
|
|
||||||
$.notify('<strong>저장 후 적용하였습니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_reset_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
link_data = []
|
|
||||||
make_link_data()
|
|
||||||
});
|
|
||||||
// 화면 상단 버튼 END
|
|
||||||
|
|
||||||
// 리스트 각 항목 별 버튼 START
|
|
||||||
$("body").on('click', '#link_item_up_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
target_id = $(this).data('index')
|
|
||||||
target = link_data[target_id]
|
|
||||||
if (target_id != 0) {
|
|
||||||
link_data.splice(target_id, 1);
|
|
||||||
link_data.splice(target_id-1, 0, target);
|
|
||||||
}
|
|
||||||
make_link_data()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_item_down_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
target_id = $(this).data('index')
|
|
||||||
target = link_data[target_id]
|
|
||||||
if (link_data.length -1 != target_id) {
|
|
||||||
link_data.splice(target_id, 1);
|
|
||||||
link_data.splice(target_id+1, 0, target);
|
|
||||||
}
|
|
||||||
make_link_data()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_item_delete_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
target_id = $(this).data('index')
|
|
||||||
target = link_data[target_id]
|
|
||||||
link_data.splice(target_id, 1);
|
|
||||||
make_link_data()
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_item_edit_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
target_id = $(this).data('index')
|
|
||||||
target = link_data[target_id]
|
|
||||||
document.getElementById('link_edit_index').value = target_id
|
|
||||||
document.getElementById('link_edit_title').value = target.title
|
|
||||||
document.getElementById('link_edit_url').value = target.url
|
|
||||||
$('#link_edit_modal').modal();
|
|
||||||
});
|
|
||||||
// 리스트 각 항목 별 버튼 END
|
|
||||||
|
|
||||||
// START 모달 버튼
|
|
||||||
$("body").on('click', '#link_edit_confirm_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
edit_index = parseInt(document.getElementById('link_edit_index').value)
|
|
||||||
tmp = {}
|
|
||||||
tmp['type'] = 'link'
|
|
||||||
tmp['title'] = document.getElementById('link_edit_title').value
|
|
||||||
tmp['url'] = document.getElementById('link_edit_url').value
|
|
||||||
if (edit_index == -1) {
|
|
||||||
link_data.splice(link_data.length, 0, tmp);
|
|
||||||
} else {
|
|
||||||
link_data.splice(target_id, 1);
|
|
||||||
link_data.splice(target_id, 0, tmp);
|
|
||||||
}
|
|
||||||
make_link_data()
|
|
||||||
$('#link_edit_modal').modal('hide');
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#link_edit_cancel_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
$('#link_edit_modal').modal('hide');
|
|
||||||
});
|
|
||||||
// END 모달 버튼
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function make_link_data() {
|
|
||||||
str = ''
|
|
||||||
for (i in link_data) {
|
|
||||||
//console.log(link_data[i])
|
|
||||||
str += m_row_start_hover();
|
|
||||||
str += m_col(1, parseInt(i)+1);
|
|
||||||
|
|
||||||
if (link_data[i].type == 'link') {
|
|
||||||
str += m_col(4, link_data[i].title)
|
|
||||||
str += m_col(4, link_data[i].url)
|
|
||||||
} else {
|
|
||||||
str += m_col(8, '---Divider Line---')
|
|
||||||
}
|
|
||||||
tmp = ''
|
|
||||||
tmp += m_button('link_item_up_btn', 'UP', [{'key':'index', 'value':i}]);
|
|
||||||
tmp += m_button('link_item_down_btn', 'DOWN', [{'key':'index', 'value':i}]);
|
|
||||||
tmp += m_button('link_item_delete_btn', '삭제', [{'key':'index', 'value':i}]);
|
|
||||||
if (link_data[i].type == 'link') {
|
|
||||||
tmp += m_button('link_item_edit_btn', '편집', [{'key':'index', 'value':i}]);
|
|
||||||
tmp += m_button('global_link_btn', 'Go', [{'key':'url', 'value':link_data[i].url}]);
|
|
||||||
}
|
|
||||||
tmp = m_button_group(tmp)
|
|
||||||
str += m_col(3, tmp)
|
|
||||||
str += m_row_end();
|
|
||||||
if (i != link_data.length -1) str += m_hr(0);
|
|
||||||
}
|
|
||||||
document.getElementById("link_list_div").innerHTML = str;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#go_filebrowser_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
url = document.getElementById('url_filebrowser').value
|
|
||||||
window.open(url, "_blank");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -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_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_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_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.setting_select('log_level', '로그 레벨', [['10', 'DEBUG'],['20', 'INFO'],['30', 'WARNING'],['40', 'ERROR'], ['50', 'CRITICAL'] ], value=arg['log_level'], col='3') }}
|
||||||
{{ macros.m_hr() }}
|
{{ macros.m_hr() }}
|
||||||
|
{{ macros.setting_input_text_and_buttons('command_text', 'Command', [['command_run_btn', 'Run']], value='', desc='') }}
|
||||||
</form>
|
</form>
|
||||||
</div><!--전체-->
|
</div><!--전체-->
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
$(document).ready(function(){
|
|
||||||
});
|
|
||||||
|
|
||||||
$("body").on('click', '#ddns_test_btn', function(e){
|
$("body").on('click', '#ddns_test_btn', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
globalSendCommand('ddns_test', $('#ddns').val());
|
globalSendCommand('ddns_test', $('#ddns').val());
|
||||||
|
|
||||||
ddns = document.getElementById('ddns').value;
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/ddns_test',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{ddns:ddns},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
console.log(data)
|
|
||||||
if (data == 'fail') {
|
|
||||||
$.notify('<strong>접속에 실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>Version:'+ data+'</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function setting_save_func(formData, noti) {
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/setting_save_system',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data: formData,
|
|
||||||
dataType: "json",
|
|
||||||
success: function (ret) {
|
|
||||||
if (ret) {
|
|
||||||
if (noti) {
|
|
||||||
$.notify('<strong>설정을 저장하였습니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.location.href = "/"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>설정 저장에 실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//설정 저장
|
|
||||||
$("#setting_save").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting');
|
|
||||||
setting_save_func(formData, true)
|
|
||||||
});
|
|
||||||
|
|
||||||
//설정 저장
|
|
||||||
$("#setting_save2").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting2');
|
|
||||||
setting_save_func(formData, false)
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#setting_save4").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting4');
|
|
||||||
setting_save_func(formData, true)
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#setting_save3").click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var formData = get_formdata('#setting3');
|
|
||||||
setting_save_func(formData, true)
|
|
||||||
$.notify('<strong>재시작해야 적용됩니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#command_run_btn', function(e){
|
$("body").on('click', '#command_run_btn', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
command_text = document.getElementById('command_text').value;
|
globalSendCommand('command_run', $('#command_text').val());
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/command_run',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{command_text:command_text},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
if (data.ret == 'success') {
|
|
||||||
$.notify('<strong>성공 : '+ data.log +'</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>실패 : ' + data.log+'</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,24 +18,27 @@
|
|||||||
<div id="notify_telegram_use_div" class="collapse">
|
<div id="notify_telegram_use_div" class="collapse">
|
||||||
{{ macros.setting_input_text('notify_telegram_token', 'Bot Token', value=arg['notify_telegram_token']) }}
|
{{ 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('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_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=['사용자가 먼저 봇과 대화를 시작하여 대화창이 생성된 상태여야 합니다.', '(대화창이 있을 경우에만 알림 수신)']) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ macros.m_hr() }}
|
{{ macros.m_hr() }}
|
||||||
|
|
||||||
{{ macros.setting_checkbox('notify_discord_use', '디스코드 사용', value=arg['notify_discord_use']) }}
|
{{ macros.setting_checkbox('notify_discord_use', '디스코드 사용', value=arg['notify_discord_use']) }}
|
||||||
<div id="notify_discord_use_div" class="collapse">
|
<div id="notify_discord_use_div" class="collapse">
|
||||||
{{ macros.setting_input_text('notify_discord_webhook', '웹훅', value=arg['notify_discord_webhook']) }}
|
{{ 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.setting_input_text_and_buttons('tmp_text_discord', 'Test', [['tmp_discord_test_btn', '전송']], value='테스트 메시지입니다.', col='9') }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{ macros.m_tab_content_end() }}
|
{{ macros.m_tab_content_end() }}
|
||||||
|
|
||||||
{{ macros.m_tab_content_start('advanced', false) }}
|
{{ 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=['사용시 기본설정은 무시됩니다.']) }}
|
||||||
<div id="notify_advaned_use_div" class="collapse">
|
<div id="notify_advaned_use_div" class="collapse">
|
||||||
{{ macros.setting_input_textarea('_notify_advaned_policy', '정책', value=arg['notify_advaned_policy'], row='30') }}
|
{{ macros.info_text_and_buttons('notify_yaml_filepath', '파일 위치', [['globalEditBtn', '편집기에서 열기', [('file',arg['notify_yaml_filepath'])]]], value=arg['notify_yaml_filepath']) }}
|
||||||
{{ 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.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('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=['저장 후 적용됩니다.']) }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{ macros.m_tab_content_end() }}
|
{{ macros.m_tab_content_end() }}
|
||||||
|
|
||||||
@@ -46,8 +49,6 @@
|
|||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var package_name = "{{arg['package_name']}}";
|
|
||||||
var sub = "{{arg['sub'] }}";
|
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
use_collapse("notify_telegram_use");
|
use_collapse("notify_telegram_use");
|
||||||
@@ -70,139 +71,19 @@ $('#notify_advaned_use').change(function() {
|
|||||||
|
|
||||||
$("body").on('click', '#tmp_telegram_test_btn', function(e){
|
$("body").on('click', '#tmp_telegram_test_btn', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
bot_token = document.getElementById("notify_telegram_token").value;
|
param = $('#notify_telegram_token').val() + '||' + $('#notify_telegram_chat_id').val() + '||' + $('#notify_telegram_disable_notification').is(":checked") + '||' + $('#tmp_text_telegram').val();
|
||||||
chat_id = document.getElementById("notify_telegram_chat_id").value;
|
globalSendCommand('notify_test', 'telegram', param);
|
||||||
text = document.getElementById("tmp_text_telegram").value;
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/'+sub+'/telegram_test',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{bot_token:bot_token, chat_id:chat_id, text:text},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (ret) {
|
|
||||||
if (ret) {
|
|
||||||
$.notify('<strong>전송 하였습니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>전송에 실패하였습니다.<br>'+ret+'</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("body").on('click', '#tmp_discord_test_btn', function(e){
|
$("body").on('click', '#tmp_discord_test_btn', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
url = document.getElementById("notify_discord_webhook").value;
|
globalSendCommand('notify_test', 'discord', $('#notify_discord_webhook').val(), $('#tmp_text_discord').val());
|
||||||
text = document.getElementById("tmp_text_discord").value;
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/'+sub+'/discord_test',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{url:url, text:text},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (ret) {
|
|
||||||
if (ret) {
|
|
||||||
$.notify('<strong>전송 하였습니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>전송에 실패하였습니다.<br>'+ret+'</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("body").on('click', '#tmp_advanced_test_btn', function(e){
|
$("body").on('click', '#tmp_advanced_test_btn', function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
policy = document.getElementById("notify_advaned_policy").value;
|
globalSendCommand('notify_test', 'advanced', $('#tmp_message_id').val(), $('#tmp_text_advanced').val());
|
||||||
text = document.getElementById("tmp_text_advanced").value;
|
|
||||||
message_id = document.getElementById("tmp_message_id").value;
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/'+sub+'/advanced_test',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data:{policy:policy, text:text, message_id:message_id},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (ret) {
|
|
||||||
if (ret) {
|
|
||||||
$.notify('<strong>전송 하였습니다.</strong>', {
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>전송에 실패하였습니다.<br>'+ret+'</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$("body").on('click', '#capture_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
url = document.getElementById('tmp_go_url').value
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/'+sub+'/capture',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data: {url:url},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
if (data.ret == 'success') {
|
|
||||||
tmp = '<img src="' + data.data + '" class="img-fluid">'
|
|
||||||
document.getElementById("image_div").innerHTML = tmp;
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//, ['full_capture_btn', '전체 캡처 이미지 다운']
|
|
||||||
$("body").on('click', '#full_capture_btn', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
url = document.getElementById('tmp_go_url').value
|
|
||||||
$.ajax({
|
|
||||||
url: '/' + package_name + '/ajax/'+sub+'/full_capture',
|
|
||||||
type: "POST",
|
|
||||||
cache: false,
|
|
||||||
data: {url:url},
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data) {
|
|
||||||
if (data.ret == 'success') {
|
|
||||||
console.log('xxx')
|
|
||||||
tmp = '<img src="' + data.data + '" class="img-fluid">'
|
|
||||||
document.getElementById("image_div").innerHTML = tmp;
|
|
||||||
} else {
|
|
||||||
$.notify('<strong>실패하였습니다.</strong>', {
|
|
||||||
type: 'warning'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
3
lib/tool/__init__.py
Normal file
3
lib/tool/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from framework import logger
|
||||||
|
|
||||||
|
from .notify import ToolNotify
|
||||||
@@ -1,125 +1,48 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#########################################################
|
|
||||||
|
|
||||||
import os
|
|
||||||
import traceback
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from discord_webhook import DiscordEmbed, DiscordWebhook
|
|
||||||
from framework import F
|
from framework import F
|
||||||
from telepot2 import Bot, glance
|
from support import SupportDiscord, SupportTelegram, SupportYaml
|
||||||
from telepot2.loop import MessageLoop
|
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
class ToolNotify(object):
|
class ToolNotify(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_message(cls, text, message_id=None, image_url=None):
|
def send_message(cls, text, message_id=None, image_url=None):
|
||||||
if F.SystemModelSetting.get_bool('notify_advaned_use'):
|
if F.SystemModelSetting.get_bool('notify_advaned_use'):
|
||||||
return cls.send_advanced_message(text, image_url=image_url, message_id=message_id)
|
return cls.send_advanced_message(text, image_url=image_url, message_id=message_id)
|
||||||
else:
|
else:
|
||||||
if F.SystemModelSetting.get_bool('notify_telegram_use'):
|
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'):
|
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'))
|
cls.send_discord_message(text, image_url=image_url, webhook_url=F.SystemModelSetting.get('notify_discord_webhook'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_advanced_message(cls, text, image_url=None, policy=None, message_id=None):
|
def send_advanced_message(cls, text, image_url=None, policy=None, message_id=None):
|
||||||
from system.model import ModelSetting as SystemModelSetting
|
|
||||||
try:
|
try:
|
||||||
if policy is None:
|
message_id = message_id.strip()
|
||||||
policy = SystemModelSetting.get('notify_advaned_policy')
|
policy = SupportYaml.read_yaml(F.config['notify_yaml_filepath'])
|
||||||
|
if message_id is None or message_id not in policy:
|
||||||
if message_id is None:
|
|
||||||
message_id = 'DEFAULT'
|
message_id = 'DEFAULT'
|
||||||
|
now = datetime.now()
|
||||||
policy_list = cls._make_policy_dict(policy)
|
for item in policy[message_id]:
|
||||||
#logger.debug(policy_list)
|
if item.get('enable_time') != None:
|
||||||
#logger.debug(message_id)
|
tmp = item.get('enable_time').split('-')
|
||||||
if message_id.strip() not in policy_list:
|
if now.hour < int(tmp[0]) or now.hour > int(tmp[1]):
|
||||||
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:
|
|
||||||
continue
|
continue
|
||||||
ret[tmp2[0].strip()] = [x.strip() for x in tmp2[1].split('|')]
|
if item.get('type') == 'telegram':
|
||||||
return ret
|
if item.get('token', '') == '' or item.get('chat_id', '') == '':
|
||||||
except Exception as exception:
|
continue
|
||||||
logger.error('Exception:%s', exception)
|
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))
|
||||||
logger.error(traceback.format_exc())
|
elif item.get('type') == 'discord':
|
||||||
return False
|
if item.get('webhook', '') == '':
|
||||||
|
continue
|
||||||
@classmethod
|
SupportDiscord.send_discord_message(text, webhook_url=item.get('webhook'))
|
||||||
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)
|
|
||||||
return True
|
return True
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
logger.error('Exception:%s', exception)
|
logger.error('Exception:%s', exception)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
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
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1 @@
|
|||||||
from framework import logger
|
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)
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#########################################################
|
#########################################################
|
||||||
import os, sys, traceback, subprocess, json, platform
|
import os, sys, traceback, subprocess, json, platform
|
||||||
from framework import app, logger, path_data
|
from framework import app, logger, path_data
|
||||||
from .subprocess import ToolSubprocess
|
from ..support.base.subprocess import ToolSubprocess
|
||||||
|
|
||||||
class ToolFfmpeg(object):
|
class ToolFfmpeg(object):
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,18 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
from . import logger
|
||||||
|
|
||||||
|
|
||||||
class ToolBaseFile(object):
|
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
|
@classmethod
|
||||||
def write(cls, data, filepath, mode='w'):
|
def write(cls, data, filepath, mode='w'):
|
||||||
@@ -80,7 +53,8 @@ class ToolBaseFile(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def file_move(cls, source_path, target_dir, target_filename):
|
def file_move(cls, source_path, target_dir, target_filename):
|
||||||
try:
|
try:
|
||||||
import time, shutil
|
import shutil
|
||||||
|
import time
|
||||||
if os.path.exists(target_dir) == False:
|
if os.path.exists(target_dir) == False:
|
||||||
os.makedirs(target_dir)
|
os.makedirs(target_dir)
|
||||||
target_path = os.path.join(target_dir, target_filename)
|
target_path = os.path.join(target_dir, target_filename)
|
||||||
@@ -196,7 +170,8 @@ class ToolBaseFile(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def makezip_simple(cls, zip_path, zip_extension='cbz', remove_zip_path=True):
|
def makezip_simple(cls, zip_path, zip_extension='cbz', remove_zip_path=True):
|
||||||
import zipfile, shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
try:
|
try:
|
||||||
if os.path.exists(zip_path) == False:
|
if os.path.exists(zip_path) == False:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 framework import app, logger, path_data
|
||||||
from .subprocess import ToolSubprocess
|
|
||||||
|
from ..support.base.subprocess import ToolSubprocess
|
||||||
|
|
||||||
|
|
||||||
class ToolRclone(object):
|
class ToolRclone(object):
|
||||||
|
|
||||||
@@ -132,4 +140,4 @@ class ToolRclone(object):
|
|||||||
return ret
|
return ret
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
logger.error('Exception:%s', exception)
|
logger.error('Exception:%s', exception)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|||||||
Reference in New Issue
Block a user