This commit is contained in:
flaskfarm
2022-10-07 01:48:42 +09:00
parent 4b72b7dc65
commit cde69d4d8a
55 changed files with 523 additions and 7703 deletions

View 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: ''

View File

@@ -19,4 +19,5 @@ pytz
requests==2.26.0 requests==2.26.0
discord-webhook discord-webhook
pyyaml pyyaml
pycryptodome pycryptodome
telepot-mod

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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({

View File

@@ -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

View File

@@ -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

View File

@@ -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가 되면 기본 경로로 로그파일을

View File

@@ -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
"""

View File

@@ -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:

View File

@@ -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())

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}}])

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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']

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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',
])

View File

@@ -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)

View File

@@ -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 = {'<': '&lt;',
'>': '&gt;',
'&': '&amp;',}
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)

View File

@@ -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)

View File

@@ -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())

View File

@@ -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

View File

@@ -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
View 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())

View File

@@ -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)

View File

@@ -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
######################################################### #########################################################

View File

@@ -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)}')

View 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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
View File

@@ -0,0 +1,3 @@
from framework import logger
from .notify import ToolNotify

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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())