├── img ├── synobot_cfg.png ├── synobot_config1.png ├── synobot_config2.png ├── synobot_config3.png ├── synobot_config4.png ├── synobot_config5.png ├── synobot_config6.png ├── synobot_config7.png ├── synobot_config8.png ├── synobot_config9.png └── synobot_config10.png ├── requirements.txt ├── single.py ├── OtpHandler.py ├── CommonUtil.py ├── LogManager.py ├── ThreadTimer.py ├── Dockerfile ├── .github └── workflows │ ├── main.yml │ └── codeql-analysis.yml ├── main.py ├── synobotLang.py ├── ko_kr.json ├── en_us.json ├── taskmgr.py ├── BotConfig.py ├── dbmgr.py ├── README.md ├── bothandler.py └── synods.py /img/synobot_cfg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_cfg.png -------------------------------------------------------------------------------- /img/synobot_config1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config1.png -------------------------------------------------------------------------------- /img/synobot_config2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config2.png -------------------------------------------------------------------------------- /img/synobot_config3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config3.png -------------------------------------------------------------------------------- /img/synobot_config4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config4.png -------------------------------------------------------------------------------- /img/synobot_config5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config5.png -------------------------------------------------------------------------------- /img/synobot_config6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config6.png -------------------------------------------------------------------------------- /img/synobot_config7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config7.png -------------------------------------------------------------------------------- /img/synobot_config8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config8.png -------------------------------------------------------------------------------- /img/synobot_config9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config9.png -------------------------------------------------------------------------------- /img/synobot_config10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidpop/synobot/HEAD/img/synobot_config10.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | certifi==2022.12.7 3 | cffi==1.14.6 4 | chardet==3.0.4 5 | cryptography==39.0.1 6 | future==0.18.3 7 | idna==2.8 8 | pycparser==2.19 9 | pyotp==2.6.0 10 | python-telegram-bot==12.0.0b1 11 | requests==2.31.0 12 | six==1.12.0 13 | tornado==6.3.2 14 | urllib3==1.26.5 15 | 16 | -------------------------------------------------------------------------------- /single.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SingletonInstane: 4 | __instance = None 5 | 6 | @classmethod 7 | def __getInstance(cls): 8 | return cls.__instance 9 | 10 | @classmethod 11 | def instance(cls, *args, **kargs): 12 | cls.__instance = cls(*args, **kargs) 13 | cls.instance = cls.__getInstance 14 | return cls.__instance 15 | 16 | -------------------------------------------------------------------------------- /OtpHandler.py: -------------------------------------------------------------------------------- 1 | #PyOtp 2 | 3 | import single 4 | import pyotp 5 | from LogManager import log 6 | 7 | class OtpHandler(single.SingletonInstane): 8 | TimeOtp = None 9 | SecretKey = '' 10 | 11 | def __init__(self, *args, **kwargs): 12 | self.TimeOtp = pyotp.TOTP(self.SecretKey) 13 | 14 | def InitOtp(self, secretKey): 15 | self.SecretKey = secretKey 16 | self.TimeOtp = pyotp.TOTP(self.SecretKey) 17 | 18 | def GetOtp(self): 19 | if self.SecretKey == '': 20 | return '' 21 | 22 | try: 23 | retVal = self.TimeOtp.now() 24 | except Exception as e: 25 | retVal = '' 26 | log.info('otp except.') 27 | return retVal 28 | 29 | -------------------------------------------------------------------------------- /CommonUtil.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | # byte to Human Readable convert - MAX : TB 4 | def hbytes(num): 5 | for x in ['bytes','KB','MB','GB']: 6 | if num < 1024.0: 7 | return "%3.2f%s" % (num, x) 8 | num /= 1024.0 9 | return "%3.2f%s" % (num, 'TB') 10 | 11 | 12 | def dequote(text): 13 | if (text[0] == text[-1]) and text.startswith(("'", '"')): 14 | return text[1:-1] 15 | return text 16 | 17 | 18 | def GetDSMMajorVersion(): 19 | parseVars = {} 20 | with open("/etc/VERSION") as versionFile: 21 | for line in versionFile: 22 | key, value = line.partition("=")[::2] 23 | parseVars[key.strip()] = dequote(value.strip()) 24 | 25 | return parseVars['majorversion'] 26 | 27 | 28 | -------------------------------------------------------------------------------- /LogManager.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import logging 4 | import logging.handlers 5 | import BotConfig 6 | 7 | cfg = BotConfig.BotConfig().instance() 8 | 9 | LOGGER_NAME = 'synobot' 10 | 11 | LOG_NAME = 'synobot.log' 12 | LOG_SIZE = cfg.GetLogSize() * 1024 * 1024 # 50MB 13 | LOG_COUNT = cfg.GetLogCount() 14 | 15 | log = logging.getLogger(LOGGER_NAME) 16 | log.setLevel(logging.DEBUG) 17 | 18 | 19 | log_handler = logging.handlers.RotatingFileHandler(LOG_NAME, maxBytes=LOG_SIZE, backupCount=LOG_COUNT) 20 | log.addHandler(log_handler) 21 | 22 | if cfg.GetLogPrint() == True: 23 | log.addHandler( logging.StreamHandler(sys.stdout)) 24 | 25 | formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 26 | 27 | log_handler.setFormatter(formatter) 28 | 29 | -------------------------------------------------------------------------------- /ThreadTimer.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | from threading import Timer,Thread,Event 4 | 5 | # Usage : 6 | # variable = ThreadTimer(time_value(second), Handler Function) 7 | # variable.start() 8 | # Stopped timer -> t.cance() 9 | # t = ThreadTimer(3,printer) 10 | #t2 = ThreadTimer(1,printer2) 11 | #t.start() 12 | #t2.start() 13 | 14 | class ThreadTimer(): 15 | 16 | def __init__(self,t,hFunction): 17 | self.t=t 18 | self.hFunction = hFunction 19 | self.thread = Timer(self.t,self.handle_function) 20 | 21 | def handle_function(self): 22 | self.hFunction() 23 | self.thread = Timer(self.t,self.handle_function) 24 | self.thread.start() 25 | 26 | def start(self): 27 | self.thread.start() 28 | 29 | def cancel(self): 30 | self.thread.cancel() 31 | 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Synobot Docker build 2 | # docker build -t synobot:0.13 . 3 | 4 | FROM python:3.9.6-buster 5 | MAINTAINER Acidpop 6 | 7 | WORKDIR /synobot 8 | 9 | ENV TG_NOTY_ID 12345678,87654321 10 | ENV TG_BOT_TOKEN 186547547:AAEXOA9ld1tlsJXvEVBt4MZYq3bHA1EsJow 11 | ENV TG_VALID_USER 12345678,87654321 12 | ENV TG_DSM_PW_ID 12345678 13 | ENV DSM_ID your_dsm_id 14 | #ENV DSM_PW your_dsm_password 15 | ENV LOG_MAX_SIZE 50 16 | ENV LOG_COUNT 5 17 | ENV DSM_URL https://DSM_IP_OR_URL 18 | ENV DS_PORT 8000 19 | ENV DSM_CERT 1 20 | ENV DSM_RETRY_LOGIN 10 21 | ENV DSM_AUTO_DEL 0 22 | ENV TG_LANG ko_kr 23 | ENV DSM_WATCH torrent_watch_path 24 | ENV DSM_PW="" 25 | ENV DSM_OTP_SECRET="" 26 | ENV TZ Asia/Seoul 27 | ENV DOCKER_LOG 1 28 | 29 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 30 | 31 | #RUN apt-get python3-dev libffi-dev gcc && pip3 install --upgrade pip 32 | RUN apt-get update && apt-get install -y libffi-dev gcc 33 | 34 | COPY requirements.txt ./ 35 | RUN pip install --no-cache-dir -r requirements.txt 36 | 37 | COPY ./*.py ./ 38 | COPY ./*.json ./ 39 | 40 | CMD [ "python", "./main.py" ] 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Synobot build 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "master" branch 8 | push: 9 | tags: 10 | - '**' 11 | 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@v2 20 | - 21 | name: Docker meta 22 | id: docker_meta 23 | uses: crazy-max/ghaction-docker-meta@v1 24 | with: 25 | images: acidpop/synobot 26 | tag-semver: | 27 | {{version}} 28 | {{major}}.{{minor}} 29 | - 30 | name: Set up Docker Buildx 31 | uses: docker/setup-buildx-action@v1 32 | - 33 | name: Login to DockerHub 34 | uses: docker/login-action@v1 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | - 39 | name: Build and push 40 | uses: docker/build-push-action@v2 41 | with: 42 | context: . 43 | file: ./Dockerfile 44 | platforms: linux/amd64 45 | push: true 46 | tags: ${{ steps.docker_meta.outputs.tags }} 47 | labels: ${{ steps.docker_meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | 2 | #-*- coding: utf-8 -*- 3 | 4 | # 설치 할 패키지 목록 5 | # python-telegram-bot 6 | # requests 7 | 8 | 9 | import os 10 | import sys 11 | import time 12 | import traceback 13 | import signal 14 | 15 | import BotConfig 16 | import bothandler 17 | import synods 18 | import taskmgr 19 | 20 | from LogManager import log 21 | 22 | SIGNALS_TO_NAMES_DICT = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n ) 23 | 24 | def signal_handler(sig, frame): 25 | log.info('recv signal : %s[%d]', SIGNALS_TO_NAMES_DICT[sig], sig) 26 | 27 | def signal_term_handler(sig, frame): 28 | log.info('recv signal : %s[%d]', SIGNALS_TO_NAMES_DICT[sig], sig) 29 | log.info('SIGTERM signal ignore') 30 | taskmgr.TaskMgr().instance().SaveTask() 31 | #botConfig.SetLoop(False) 32 | 33 | def exception_hook(exc_type, exc_value, exc_traceback): 34 | log.error( 35 | "Uncaught exception", 36 | exc_info=(exc_type, exc_value, exc_traceback) 37 | ) 38 | 39 | 40 | def main(): 41 | 42 | # Signal 예외 처리 43 | # signal Register 44 | signal.signal(signal.SIGTERM, signal_term_handler) 45 | signal.signal(signal.SIGABRT, signal_handler) 46 | signal.signal(signal.SIGSEGV, signal_handler) 47 | signal.signal(signal.SIGHUP, signal_handler) 48 | 49 | # BotConfig Init 50 | BotConfig.BotConfig().instance() 51 | 52 | #synods.SynoDownloadStation().instance().Login() 53 | 54 | bot = bothandler.BotHandler().instance() 55 | 56 | bot.InitBot() 57 | 58 | log.info("Bot Exit") 59 | 60 | 61 | if __name__ == '__main__': 62 | main() 63 | 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '26 2 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /synobotLang.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | import os 3 | import single 4 | import json 5 | 6 | import BotConfig 7 | from LogManager import log 8 | 9 | 10 | class synobotLang(single.SingletonInstane): 11 | _instance = None 12 | lang_json = None 13 | 14 | #cfg = BotConfig.BotConfig().instance() 15 | #cfg = BotConfig() 16 | cfg = None 17 | 18 | @classmethod 19 | def _getInstance(cls): 20 | return cls._instance 21 | 22 | @classmethod 23 | def instance(cls, *args, **kars): 24 | cls._instance = cls(*args, **kars) 25 | cls.instance = cls._getInstance 26 | return cls._instance 27 | 28 | def __init__(self): 29 | self.cfg = BotConfig.BotConfig().instance() 30 | 31 | self.LoadLangFile() 32 | 33 | def LoadLangFile(self): 34 | cur_path = os.getcwd() 35 | lang_name = self.cfg.GetSynobotLang() 36 | 37 | lang_path = cur_path + '/' + lang_name + '.json' 38 | 39 | # 사용자 지정한 로컬라이징 파일이 없으면 기본값으로 ko_kr.json 으로 작동 40 | if os.path.exists(lang_path) == False: 41 | lang_path = cur_path + '/ko_kr.json' 42 | 43 | log.info('Localing language : %s', lang_path) 44 | 45 | try: 46 | with open( lang_path) as json_file: 47 | self.lang_json = json.load(json_file) 48 | except: 49 | #log.info("synobot Language file loading fail") 50 | #print("load lang fail") 51 | log.info('synobot localizing file load fail, file path:%s', lang_path) 52 | 53 | def GetJson(self): 54 | return self.lang_json 55 | 56 | def GetBotHandlerLang(self, key): 57 | if self.lang_json["bothandler"].get(key): 58 | return self.lang_json["bothandler"].get(key) 59 | 60 | msg = "Unknown language key : %s" % (key) 61 | return msg 62 | 63 | def GetSynoDsLang(self, key): 64 | if self.lang_json["synods"].get(key): 65 | return self.lang_json["synods"][key] 66 | 67 | msg = "Unknown language key : %s" % (key) 68 | return msg 69 | 70 | def GetSynoErrorLang(self, key): 71 | if self.lang_json["syno_error"].get(key): 72 | return self.lang_json["syno_error"].get(key) 73 | 74 | msg = "Unknown language error key : %s" % (key) 75 | return msg 76 | 77 | def GetSynoAuthErrorLang(self, key): 78 | if self.lang_json["syno_auth_error"].get(key): 79 | return self.lang_json["syno_auth_error"].get(key) 80 | 81 | if self.lang_json["syno_error"].get(key): 82 | return self.lang_json["syno_error"].get(key) 83 | 84 | errstr = 'Unknown Auth Error Code : %s' % (key) 85 | return errstr 86 | 87 | def GetSynoTaskErrorLang(self, key): 88 | if self.lang_json["syno_task_error"].get(key): 89 | return self.lang_json["syno_task_error"].get(key) 90 | 91 | if self.lang_json["syno_error"].get(key): 92 | return self.lang_json["syno_error"].get(key) 93 | 94 | errstr = 'Unknown Auth Error Code : %s' % (key) 95 | return errstr 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ko_kr.json: -------------------------------------------------------------------------------- 1 | { 2 | "bothandler" : { 3 | "input_login_id" : "DSM 로그인 ID를 입력하세요", 4 | "input_login_pw" : "DSM 로그인 비밀번호를 입력하세요\n비밀번호는 수신 후 삭제합니다", 5 | "input_login_otp" : "DSM OTP에 표시된 숫자를 입력하세요", 6 | "dsm_login_api_fail" : "로그인 요청 실패\n, 응답 코드 : %d", 7 | "dsm_login_fail_msg" : "DSM 로그인 실패\n, %s", 8 | "dsm_rest_api_fail" : "DSM API 요청 실패\n, 응답 코드 : %s\n%s", 9 | "dsm_api_res_empty" : "DSM API 응답 데이터가 없습니다", 10 | "dsm_session_expire" : "DSM Login 실패\n세션이 만료되었습니다.", 11 | "dsm_invalid_id_pw" : "DSM Login 실패\nID 또는 비밀번호가 다릅니다.", 12 | "dsm_account_disable" : "DSM Login 실패\n비활성화 된 계정입니다.", 13 | "dsm_login_succ" : "DS Login 성공\nSynobot을 시작합니다.", 14 | "dsm_login_fail_exit" : "DSM Login에 실패 하였습니다.\n프로그램이 종료됩니다", 15 | "dsm_try_login" : "DSM 로그인을 시도합니다.", 16 | "dsm_not_login" : "DSM 에 로그인 되어 있지 않습니다", 17 | "dsm_task_list" : "다운로드 스테이션 작업 목록을 가져옵니다.", 18 | "dsm_statistic" : "다운로드 스테이션의 네트워크 정보를 가져옵니다.", 19 | "noti_delete_pw" : "입력 된 암호 메시지를 삭제 하였습니다", 20 | "noti_delete_otp" : "입력 된 OTP 메시지를 삭제 하였습니다", 21 | "noti_magnet_link" : "마그넷 링크를 등록하였습니다", 22 | "noti_torrent_file" : "토렌트 파일(%s)을 등록 하였습니다", 23 | "noti_torrent_file_fail" : "토렌트 파일(%s) 등록에 실패하였습니다", 24 | "noti_not_support_cmd" : "지원 되지 않는 명령입니다", 25 | "noti_not_torrent_file" : "torrent 파일만 지원합니다, 파일 : %s", 26 | "noti_torrent_status" : "상태 : %s\n이름 : %s\n크기 : %s\n사용자 : %s", 27 | "noti_task_list" : "TaskID : %s\n이름 : %s\n크기 : %s\n상태 : %s\n다운로드 됨 : %s\n업로드 됨 : %s\n다운로드 속도 : %s/s\n업로드 속도 : %s/s", 28 | "noti_statistic" : "다운로드 스테이션 정보\n다운로드 속도:%s/s\n업로드 속도 : %s/s", 29 | "noti_torrent_watch_nothing" : "Torrent Watch 경로가 설정 되지 않았습니다.\nDSM_WATCH 환경 변수 및 볼륨 마운트가 필요합니다.", 30 | "noti_torrent_watch_mv_fail" : "Torrent Watch 경로에 파일 이동이 실패 하였습니다" 31 | }, 32 | "synods" :{ 33 | "task_status" : "상태", 34 | "task_name" : "파일 이름", 35 | "task_size" : "파일 크기", 36 | "task_user" : "사용자", 37 | "waiting": "대기 중", 38 | "downloading": "다운로드 중", 39 | "paused": "일시 중지", 40 | "finishing": "완료 중", 41 | "finished": "다운로드 완료", 42 | "hash_checking": "해쉬 검사 중", 43 | "seeding": "보내기", 44 | "filehosting_waiting": "파일 호스팅 대기 중", 45 | "extracting": "압축 해제 중", 46 | "error": "에러", 47 | "delete": "다운로드 취소" 48 | }, 49 | "syno_error" : { 50 | "100" : "100 - 알 수 없는 에러", 51 | "101" : "101 - 파라미터에 오류가 있습니다", 52 | "102" : "102 - 요청한 API가 존재하지 않습니다", 53 | "103" : "103 - 요청한 기능이 존재하지 않습니다.", 54 | "104" : "104 - 요청한 버전이 기능을 지원하지 않습니다.", 55 | "105" : "105 - 로그인 한 세션에 권한이 없습니다", 56 | "106" : "106 - 세션이 만료되었습니다", 57 | "107" : "107 - 중복 로그인으로 인해 세션이 중단되었습니다." 58 | }, 59 | "syno_auth_error" : { 60 | "400" : "400 - 계정 또는 패스워드가 일치하지 않습니다", 61 | "401" : "401 - 비활성화 된 계정입니다.", 62 | "402" : "402 - 권한이 없습니다.", 63 | "403" : "403 - 2단계 인증 코드가 필요합니다.", 64 | "404" : "404 - 2단계 인증 코드가 실패하였습니다." 65 | }, 66 | "syno_task_error" : { 67 | "400" : "400 - 알 수 없는 작업 에러", 68 | "401" : "401 - 파라미터에 오류가 있습니다.", 69 | "402" : "402 - 사용자 설정을 파싱하지 못했습니다.", 70 | "403" : "403 - 카테고리를 가져오지 못했습니다.", 71 | "404" : "404 - DB에서 검색 결과를 얻지 못했습니다.", 72 | "405" : "405 - 사용자 설정에 실패하였습니다." 73 | } 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "bothandler" : { 3 | "input_login_id" : "Input DSM Login ID", 4 | "input_login_pw" : "Input DSM Login Password\nDelete password after received message", 5 | "input_login_otp" : "Input DSM OTP Number", 6 | "dsm_login_api_fail" : "Login fail\n, error code : %d", 7 | "dsm_login_fail_msg" : "DSM Login fail\n, %s", 8 | "dsm_rest_api_fail" : "Request Login fail\n, response code : %d", 9 | "dsm_api_res_empty" : "DSM API response is empty", 10 | "dsm_session_expire" : "DSM Login Fail\nsession expired.", 11 | "dsm_invalid_id_pw" : "DSM Login Fail\nInvalid ID or Password", 12 | "dsm_account_disable" : "DSM Login Fail\nAccount Disabled", 13 | "dsm_login_succ" : "DS Login Success\nStarting Synobot...", 14 | "dsm_login_fail_exit" : "DSM Login Fail.\nExit synobot.", 15 | "dsm_try_login" : "Try DSM Login...", 16 | "dsm_not_login" : "DSM Not Login...", 17 | "dsm_task_list" : "Get Download station task list.", 18 | "dsm_statistic" : "Get Download station statistic.", 19 | "noti_delete_pw" : "Deleteed Password Message.", 20 | "noti_delete_otp" : "Deleted OTP Message", 21 | "noti_magnet_link" : "Added task magnet link", 22 | "noti_torrent_file" : "Added task torrent file(%s).", 23 | "noti_torrent_file_fail" : "Added task torrent file(%s) Fail.", 24 | "noti_not_support_cmd" : "Not support command", 25 | "noti_not_torrent_file" : "Only torrent files are supported, File : %s", 26 | "noti_torrent_status" : "*Status* : %s\n*File* : %s\n*Size* : %s\n*User* : %s", 27 | "noti_task_list" : "TaskID : %s\nFile : %s\nTotal Size : %s\nStatus : %s\nDownload Size : %s\nUpload Size : %s\ndownload Speed : %s/s\nUpload Speed : %s/s", 28 | "noti_statistic" : "Info Download Station\nDownload Speed:%s/s\nUpload Speed : %s/s", 29 | "noti_torrent_watch_nothing" : "Torrent Watch path nothing set.\nDSM_WATCH enviroment and Docker volume mount need.", 30 | "noti_torrent_watch_mv_fail" : "Torrent Watch Path file move fail." 31 | }, 32 | "synods" :{ 33 | "task_status" : "Status", 34 | "task_name" : "File Name", 35 | "task_size" : "File Size", 36 | "task_user" : "User", 37 | "waiting": "waiting", 38 | "downloading": "downloading", 39 | "paused": "paused", 40 | "finishing": "finishing", 41 | "finished": "finished", 42 | "hash_checking": "hash checking", 43 | "seeding": "seeding", 44 | "filehosting_waiting": "file hosting waiting", 45 | "extracting": "extracting", 46 | "error": "error", 47 | "delete": "delete" 48 | }, 49 | "syno_error" : { 50 | "100" : "", 51 | "101" : "Invalid parameter", 52 | "102" : "The requested API does not exist", 53 | "103" : "The requested method does not exist", 54 | "104" : "The requested version does not support the functionality", 55 | "105" : "The logged in session does not have permission", 56 | "106" : "Session timeout", 57 | "107" : "Session interrupted by duplicate login" 58 | }, 59 | "syno_auth_error" : { 60 | "400" : "No such account or incorrect password", 61 | "401" : "Account disabled", 62 | "402" : "Permission denied", 63 | "403" : "2-step verification code required", 64 | "404" : "Failed to authenticate 2-step verification code" 65 | }, 66 | "syno_task_error" : { 67 | "400" : "Unknown Error", 68 | "401" : "Invalid parameter", 69 | "402" : "Parse the user setting failed", 70 | "403" : "Get category failed", 71 | "404" : "Get the search result from DB failed", 72 | "405" : "Get the user setting failed" 73 | } 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /taskmgr.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | import json 3 | import enum 4 | 5 | import single 6 | from LogManager import log 7 | 8 | task_list_file = "taskdata.json" 9 | 10 | 11 | class TASK_TYPE(enum.IntEnum): 12 | TITLE = 0 13 | SIZE = 1 14 | USER = 2 15 | STATUS = 3 16 | 17 | class TaskMgr(single.SingletonInstane): 18 | 19 | # DS Download Task Dictionary 20 | # Structure { task_id : [title, size, user, status] } 21 | task_data = {} 22 | noti_callback = None 23 | 24 | def AddNotiCallback(self, fn): 25 | self.noti_callback = fn 26 | 27 | # 28 | def LoadFile(self): 29 | # Json 문자열을 가져와 Dictionary 로 변환 30 | #json_data = None 31 | try: 32 | if not self.task_data: 33 | with open( task_list_file ) as json_file: 34 | self.task_data = json.load(json_file) 35 | #self.task_data = json.loads(json_data) 36 | except: 37 | log.info('taskdata.json file open error') 38 | 39 | def SaveTask(self): 40 | # Dictionary 를 json으로 변환 후 json 문자열을 저장 41 | #json_val = json.dumps(self.task_data) 42 | 43 | with open( task_list_file, 'w' ) as json_file: 44 | json.dump(self.task_data, json_file, ensure_ascii=False, sort_keys=True) 45 | 46 | 47 | def InsertOrUpdateTask(self, task_id, title, size, user, status): 48 | task_list = [] 49 | #log.info("Insert or update task") 50 | 51 | if task_id not in self.task_data : 52 | # 최초 등록 53 | task_list = [title, size, user, status] 54 | self.task_data[task_id] = task_list 55 | log.info("insert task : %s, %s", task_id, title) 56 | 57 | if self.noti_callback != None: 58 | self.noti_callback(task_id, title, size, user, status) 59 | 60 | self.SaveTask() 61 | 62 | else: 63 | ## 64 | task_list = self.task_data[task_id] 65 | 66 | old_status = task_list[TASK_TYPE.STATUS] 67 | 68 | if old_status != status: 69 | log.info("status change (%s -> %s, update task", old_status, status) 70 | task_list = [title, size, user, status] 71 | self.task_data[task_id] = task_list 72 | log.info("update task : %s, %s", task_id, title) 73 | # 기존 상태 값과 현재 상태값이 다른 경우 콜백 호출 74 | if self.noti_callback != None: 75 | self.noti_callback(task_id, title, size, user, status) 76 | 77 | self.SaveTask() 78 | 79 | # 작업 중 삭제 된 Task 에 대한 예외 처리 필요 80 | def CheckRemoveTest(self, task_list): 81 | temp_task = {} 82 | # self.task_data 를 임시 dict 로 저장, 구조는 key : exists flag (true, flase) 83 | for key in self.task_data.keys(): 84 | temp_task[key] = False 85 | 86 | # 인자로 넘어온 task_list 를 loop 돌면서 temp_task 에 True 세팅 87 | for task_id in task_list: 88 | temp_task[task_id] = True 89 | 90 | # temp_task 를 loop 돌면서 Value 가 False 이면서 status 가 finished 가 아닌 Task 는 다운로드 취소 콜백 호출 후 task_data 에서 삭제한다 91 | delete_task_list = [] 92 | for key, value in temp_task.items(): 93 | if value == False: 94 | remove_task_list = self.task_data[key] 95 | 96 | if remove_task_list != None and self.noti_callback != None and remove_task_list[TASK_TYPE.STATUS] != 'finished': 97 | self.noti_callback(key, remove_task_list[TASK_TYPE.TITLE], remove_task_list[TASK_TYPE.SIZE], remove_task_list[TASK_TYPE.USER], 'delete') 98 | 99 | delete_task_list.append(key) 100 | 101 | for task_id in delete_task_list: 102 | log.info("delete task : %s", task_id) 103 | del self.task_data[task_id] 104 | 105 | self.SaveTask() 106 | 107 | 108 | -------------------------------------------------------------------------------- /BotConfig.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | import socket 6 | import single 7 | 8 | class BotConfig(single.SingletonInstane): 9 | 10 | # 알림을 받을 Telegram 사용자의 Chat ID리스트 (, 기호로 구분) 11 | notify_chat_id_list = None 12 | dsm_pw_chat_id = "" 13 | # DSM 로그인 ID 14 | dsm_id = "" 15 | # DSM 로그인 PW 16 | dsm_pw = "" 17 | # Telegram Bot Token 18 | bot_token = "" 19 | # BOT 명령에 유효한 Telegram 사용자 Chat ID 20 | valid_user_list = None 21 | # Log Size (단위:MB) 22 | log_size = 0 23 | # Log Rotation 개수 24 | log_count = 0 25 | # Synlogy DSM 접속 URL 또는 IP 26 | dsm_url = '' 27 | # Synology Download Station 의 포트 28 | ds_download_port = 80 29 | # Https SSL 인증서 불일치 무시 여부 30 | dsm_cert = True 31 | # 로그인 재시도 횟수 32 | dsm_retry_login = 10 33 | # 작업 완료시 자동 삭제 여부 34 | dsm_task_auto_delete = False 35 | # 로컬라이징 36 | synobot_lang = 'ko_kr' 37 | # Torrent Watch Direcotry 38 | tor_watch_path = '' 39 | 40 | execute_path = "" 41 | host_name = '' 42 | 43 | # OTP Secret Key 44 | otp_secret = '' 45 | 46 | # Docker Log print option 47 | log_print = False 48 | 49 | 50 | def __init__(self, *args, **kwargs): 51 | 52 | temp_notify_list = os.environ.get('TG_NOTY_ID', '12345678') 53 | if temp_notify_list.find(',') == -1: 54 | temp_notify_list += ', ' 55 | self.notify_chat_id_list = eval(temp_notify_list) 56 | 57 | self.dsm_pw_chat_id = os.environ.get('TG_DSM_PW_ID', '12345678') 58 | 59 | self.dsm_id = os.environ.get('DSM_ID', '') 60 | self.bot_token = os.environ.get('TG_BOT_TOKEN', '186547547:AAEXOA9ld1tlsJXvEVBt4MZYq3bHA1EsJow') 61 | temp_valid_user = str(os.environ.get('TG_VALID_USER', '12345678,87654321')) 62 | if temp_valid_user.find(',') == -1: 63 | temp_valid_user += ', ' 64 | self.valid_user_list = eval(temp_valid_user) 65 | 66 | self.log_size = int( os.environ.get('LOG_MAX_SIZE', '50') ) 67 | self.log_count = int( os.environ.get('LOG_COUNT', '5') ) 68 | 69 | self.dsm_url = os.environ.get('DSM_URL', 'https://DSM_IP_OR_URL') 70 | self.ds_download_port = os.environ.get('DS_PORT', '8000') 71 | 72 | # Https SSL 인증서 불일치 무시 여부 73 | temp_val = os.environ.get('DSM_CERT', '1') 74 | if temp_val == '0': 75 | self.dsm_cert = False 76 | 77 | self.dsm_retry_login = os.environ.get('DSM_RETRY_LOGIN', 10) 78 | 79 | temp_val = os.environ.get('DSM_AUTO_DEL', '0') 80 | if temp_val == '1': 81 | self.dsm_task_auto_delete = True 82 | 83 | self.synobot_lang = os.environ.get('TG_LANG', 'ko_kr') 84 | 85 | self.tor_watch_path = os.environ.get('DSM_WATCH', '') 86 | 87 | temp_path = os.path.split(sys.argv[0]) 88 | self.execute_path = temp_path[0] 89 | 90 | self.host_name = socket.gethostname() 91 | 92 | # DSM_PW 환경변수가 있는 경우에는 Telegram 을 통해 암호를 입력 받는 과정을 생략 한다. 93 | self.dsm_pw = os.environ.get('DSM_PW', '') 94 | 95 | # DSM_OTP_SECRET 환경변수가 있으면 OTP Code 를 자동으로 생성하여 로그인한다. 96 | self.otp_secret = os.environ.get('DSM_OTP_SECRET', '') 97 | 98 | temp_val = os.environ.get('DOCKER_LOG', '1') 99 | if temp_val == '1': 100 | self.log_print = True 101 | 102 | def GetNotifyList(self): 103 | return self.notify_chat_id_list 104 | 105 | def GetDsmPwId(self): 106 | return self.dsm_pw_chat_id 107 | 108 | def GetDsmId(self): 109 | return self.dsm_id 110 | 111 | def GetBotToken(self): 112 | return self.bot_token 113 | 114 | def GetValidUser(self): 115 | return self.valid_user_list 116 | 117 | def GetLogSize(self): 118 | return self.log_size 119 | 120 | def GetLogCount(self): 121 | return self.log_count 122 | 123 | def GetDSDownloadUrl(self): 124 | return self.dsm_url + ":" + self.ds_download_port 125 | 126 | def GetExecutePath(self): 127 | return self.execute_path 128 | 129 | def GetHostName(self): 130 | return self.host_name 131 | 132 | def GetDsmPW(self): 133 | return self.dsm_pw 134 | 135 | def SetDsmPW(self, pw): 136 | self.dsm_pw = pw 137 | 138 | def IsUseCert(self): 139 | return self.dsm_cert 140 | 141 | def GetDsmRetryLoginCnt(self): 142 | return int(self.dsm_retry_login) 143 | 144 | def IsTaskAutoDel(self): 145 | return self.dsm_task_auto_delete 146 | 147 | def GetSynobotLang(self): 148 | return self.synobot_lang 149 | 150 | def GetTorWatch(self): 151 | return self.tor_watch_path 152 | 153 | def GetLogPrint(self): 154 | return self.log_print 155 | 156 | def GetOtpSecret(self): 157 | return self.otp_secret 158 | -------------------------------------------------------------------------------- /dbmgr.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | import sqlite3 4 | from threading import Lock 5 | import json 6 | from datetime import datetime 7 | import single 8 | from LogManager import log 9 | 10 | # SynoBot Sqlite database manager 11 | 12 | # SQlite DB File Name 13 | dbfile = "synobot.db" 14 | 15 | # Table Schema 16 | dsdownload_table = """ 17 | CREATE TABLE IF NOT EXISTS dsdownload(task_id TEXT, title TEXT, size INTEGER, user TEXT, status TEXT, workdate TEXT, PRIMARY KEY(task_id) ); 18 | """ 19 | 20 | dsdownload_event_table = """ 21 | CREATE TABLE IF NOT EXISTS dsdownload_event(task_id TEXT, title TEXT, size INTEGER, user TEXT, status TEXT, isread INTEGER, PRIMARY KEY(task_id) ); 22 | """ 23 | 24 | # 1. 최초 insert 시에는 use_yn 을 0으로 세팅 25 | # 2. insert or replace 구문으로 수행시 다음 조건에 해당 하면 26 | # dsdownload_event table 에 Task 데이터를 insert 하고 isread 값을 0으로 한다. 27 | # 조건 1 : Task ID 가 존재 28 | # 조건 2 : Size 가 0 이상 29 | # 조건 3 : status 값이 기존 데이터와 다른 경우 30 | 31 | # 3. dsdownload_event 콜백에서 status 가 finished 가 되면 dsdownload 테이블의 데이터를 delete 한다 32 | dsdownload_insert_trigger = """ 33 | CREATE TRIGGER IF NOT EXISTS ds_chk 34 | BEFORE INSERT ON dsdownload 35 | WHEN 36 | NEW.status <> (SELECT status FROM dsdownload WHERE task_id = NEW.task_id) 37 | BEGIN 38 | INSERT INTO dsdownload_event VALUES(NEW.task_id, NEW.title, NEW.size, NEW.user, NEW.status, 0); 39 | END; 40 | """ 41 | 42 | 43 | dsdownload_delete_trigger = """ 44 | CREATE TRIGGER IF NOT EXISTS ds_delete 45 | AFTER DELETE on dsdownload 46 | BEGIN 47 | DELETE FROM dsdownload_event WHERE task_id = OLD.task_id; 48 | END; 49 | """ 50 | 51 | class DBMgr(single.SingletonInstane): 52 | # 53 | con = None 54 | cur = None 55 | lock = None 56 | 57 | def Init(self): 58 | self.lock = Lock() 59 | self.con = sqlite3.connect(":memory:", check_same_thread=False) 60 | #self.con = sqlite3.connect(dbfile, check_same_thread=False) 61 | 62 | # 컬럼명으로 접근 하기 위한 세팅 63 | self.con.row_factory = sqlite3.Row 64 | 65 | #self.RegiTriggerFunction() 66 | 67 | self.cur = self.con.cursor() 68 | self.CreateSynobotTable() 69 | 70 | log.info("SQLite DB Init") 71 | 72 | 73 | def ChkDBConnection(self): 74 | if self.con == None: 75 | self.Init() 76 | 77 | #def RegiTriggerFunction(self): 78 | # self.con.create_function("ds_update_event", 5, self.ds_update_event) 79 | 80 | def CreateSynobotTable(self): 81 | 82 | self.ChkDBConnection() 83 | 84 | # Create Table 85 | self.cur.execute(dsdownload_table) 86 | self.cur.execute(dsdownload_event_table) 87 | 88 | # Create Trigger 89 | self.cur.execute(dsdownload_insert_trigger) 90 | self.cur.execute(dsdownload_delete_trigger) 91 | 92 | self.con.commit() 93 | 94 | return True 95 | 96 | def InsertTask(self, task_id, title, size, user, status): 97 | self.ChkDBConnection() 98 | 99 | log.info('Insert Task : %s, %s' % (task_id, title) ) 100 | insert_time = datetime.now().strftime("%B %d, %Y %I:%M%p") 101 | insert_query = "INSERT OR REPLACE INTO dsdownload values (?, ?, ?, ?, ?, ?);" 102 | 103 | self.cur.execute(insert_query, (task_id, title, size, user, status, insert_time)) 104 | 105 | insert_event_query = "INSERT OR IGNORE INTO dsdownload_event VALUES ('%s', '%s', '%s', '%s', '%s', 0);" % (task_id, title, size, user, status) 106 | self.cur.execute(insert_event_query) 107 | 108 | self.con.commit() 109 | 110 | def SetUseTask(self, task_id): 111 | self.ChkDBConnection() 112 | 113 | update_query = "UPDATE dsdownload_event SET isread = 1 WHERE task_id = '%s';" % (task_id) 114 | log.info(update_query) 115 | self.cur.execute(update_query) 116 | self.con.commit() 117 | 118 | def DeleteTask(self, task_id): 119 | self.ChkDBConnection() 120 | 121 | log.info('Delete Task : %s' % (task_id) ) 122 | delete_query = "DELETE FROM dsdownload WHERE task_id = '%s';" % (task_id) 123 | self.cur.execute(delete_query) 124 | self.con.commit() 125 | 126 | def DeleteTaskNotInList(self, data_list): 127 | task_list = [] 128 | 129 | log.info("Delete Task Not In List") 130 | 131 | if len(data_list) <= 0: 132 | log.info("Delete Task data list is 0") 133 | return 134 | 135 | for item in data_list: 136 | task_list.append(item['task_id']) 137 | 138 | delete_query = "DELETE FROM dsdownload WHERE task_id NOT IN ({seq})".format(seq=','.join(['?']*len(task_list))) 139 | #task_list_str = ','.join("'{0}'".format(e) for e in task_list) 140 | #delete_query = "DELETE FROM dsdownload WHERE task_id NOT IN (%s);" % (task_list_str) 141 | log.info(delete_query) 142 | 143 | self.cur.execute(delete_query, (task_list) ) 144 | self.con.commit() 145 | 146 | def GetTaskList(self): 147 | self.ChkDBConnection() 148 | 149 | with self.lock: 150 | data_list = [] 151 | task_query = "SELECT * FROM dsdownload_event WHERE isread = 0;" 152 | self.cur.execute(task_query) 153 | 154 | #if self.cur.rowcount <= 0: 155 | # print('no data') 156 | # return None 157 | 158 | rows = self.cur.fetchall() 159 | # task_id 160 | # title 161 | # size 162 | # user 163 | # status 164 | # workdate 165 | for row in rows: 166 | data = dict() 167 | data['task_id'] = row['task_id'] 168 | data['title'] = row['title'] 169 | data['size'] = row['size'] 170 | data['user'] = row['user'] 171 | data['status'] = row['status'] 172 | data_list.append(data) 173 | 174 | return data_list 175 | 176 | #def ds_update_event(self, task_id, title, user, size, status): 177 | # return True 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *** 2 | 3 | ### 0.14 (2022/06/15) 4 | - OTP Secret Key 에 잘못된 값이 입력 되는 경우 오류 수정 5 | 6 | ### 0.13 (2021/10/05) 7 | - OTP 자동입력 기능 추가 [설정 방법](#Otp-설정하기) 8 | 9 | - 로그인 풀리는 경우 자동으로 다시 로그인(ID, PW 까지는 자동 입력, 2단계 인증이 설정 되어 있고 DSM_OTP_SECRET 비밀키까지 입력이 되어 있다면 자동으로 로그인) 10 | 11 | - DSM 연결 실패시 실패 메시지 전송 기능 추가 12 | 13 | 14 | ### 0.12 (2021/07/10) 15 | - DSM 7.0 에서 OTP 로그인 안되는 오류 수정 16 | 17 | 18 | ### 0.11 (2020/10/23) 19 | - DSM 7.0 또는 Download Station 을 2020년 10월 20일 전후로 업데이트 한 경우 에 대한 기능 변경 20 | 21 | - 텔레그램으로 파일 전송시 watch_dir 로 업로드 하도록 기능 변경 22 | 23 | - DSM_WATCH 환경 변수 추가, Torrent Watch 경로를 마운트 한 전체 경로명 [설정 방법](#Torrent-Watch-경로-설정하기) 24 | 25 | - "알수 없는 에러" 알림 보내는 부분 수정 26 | 27 | 28 | ### 0.10 (2020/09/11) 29 | - DSM 7.0 에서 작동하지 않는 문제 수정 30 | 31 | - 현재 DSM 7.0 과 synobot 0.10 버전에서 토렌트 파일을 텔레그램으로 전달시 작업 등록이 되지 않는 이슈가 있습니다. 32 | 33 | - /task 명령 추가 (다운로드 스테이션에 작동중인 작업 목록 조회, ID, 이름, 크기, 상태, 다운로드 된 크기, 업로드 된 크기, 다운로드 속도, 업로드 속도) 34 | 35 | - /stat 명령 추가 (다운로드 스테이션의 네트워크 속도 정보 조회, 다운로드 속도, 업로드 속도) 36 | 37 | ### 0.9 (2020/06/02) 38 | - Download Station 상태 일림 메시지에서 Torrent 제목에 Markdown 문법이 포함 되는 경우 오류가 많아 Markdown 형식 삭제 39 | 40 | ### 0.8 (2020/05/22) 41 | - 사설 인증서 사용시 전달 받은 토렌트 파일 등록이 실패 하는 오류 수정 42 | 43 | - 사설 인증서 사용시 warning 로그가 계속 출력 되는 부분 수정 44 | 45 | - 작업 삭제시 삭제 된 작업 목록을 계속 가지고 있었던 오류 수정 46 | 47 | - TG_LANG 환경변수 추가로 로컬라이징 지원, (ko_kr, en_us), 따로 설정하지 않으면 ko_kr 로 작동 48 | 49 | ### 0.7 (2020/05/19) 50 | - DSM_AUTO_DEL 환경 변수 추가, 기본값은 0이며 작업 완료시 자동 삭제가 필요하면 1 로 변경해서 사용 51 | 52 | - Download Station 자동 삭제 기능 추가 53 | 54 | ### 0.6 (2020/05/18) 55 | - DSM_PW 환경 변수가 없는 경우 Docker 가 시작 하지 못 하는 오류 수정 56 | 57 | - 텔레그램 봇에 /dslogin 명령 추가 (재 로그인 시도 명령) 58 | 59 | - 2단계 인증 지원 (OTP) 60 | 61 | - 로그인 코드 전체 변경 62 | 63 | - DSM_CERT 환경 변수 추가 (https 사용시 인증서 불일치인 경우 해당 값을 0으로 변경 해서 사용) 64 | 65 | ### 0.5 (2019/09/03) 66 | - DS Download 알림 시 Markdown 문법 제거 67 | 68 | - LOG 관련 환경 변수 제거 69 | 70 | ### 0.4 (2019/08/30) 71 | - Torrent 제목에 [] 중괄호가 포함 되어 있는 경우 오류 수정 72 | 73 | - 다운로드 중 삭제시 오류 케이스 수정 74 | 75 | ### 0.3 76 | - 언어팩 관련 준비 (SYNO_LANG 환경 설정에 따라 언어팩 로딩) 77 | 78 | - 다운로드 취소시 중복 메시지 발생 오류 수정 79 | 80 | ### 0.2 81 | - request 예외 처리 82 | 83 | ### 0.1 84 | 85 | - synobot 최초 버전 86 | 87 | *** 88 | 89 | # Synology DSM Download Station Task Monitor and Create Task Magnet, Torrent File 90 | 91 | ## **synobot 기능** 92 | 93 | synobot 은 다음과 같이 간단한 기능만 제공 합니다. 94 | 95 | 1. Download Station 작업 목록 Telegram 알림, 96 | - 알림 기준 97 | - 최초 작업 등록 후 받을 사이즈가 0이상인 경우 98 | - 상태 값이 바뀌는 경우 (다운로드중 -> 완료) 99 | - 작업 목록이 삭제 되는 경우 100 | 101 | 2. Magnet 링크 지원 102 | - Telegram BOT 에 magnet 링크를 보내면 Download Station 에 등록 합니다. 103 | 104 | 3. Torrent 파일 지원 105 | - Telegram BOT 에 Torrent 파일을 전송 하면 Download Station 에 등록 합니다. 106 | 107 | 4. /dslogin 명령 지원 108 | - Telegram BOT 에 /dslogin 커맨드를 전송 하면 로그인을 재시도 합니다. 109 | 110 | 111 | ## **도커 설치시 환경 변수에 다음 값을 설정 해야 합니다.** 112 | 113 | 텔레그램 알람을 받을 사용자 chat id (여러명을 사용해야 하는 경우 ,콤마로 구분되며 공백이 없어야 합니다) 114 | 115 | **TG_NOTY_ID** *12345678,87654321* 116 | 117 | 텔레그램 봇 토큰을 입력합니다 118 | 119 | **TG_BOT_TOKEN** *186547547:AAEXOA9ld1tlsJXvEVBt4MZYq3bHA1EsJow* 120 | 121 | 텔레그램 봇 명령 사용시 허용 할 사용자의 chat id, 여러명 허용시 ,콤마로 구분되며 공백이 없어야 합니다 122 | 123 | **TG_VALID_USER** *12345678,87654321* 124 | 125 | DSM 로그인 할 사용자 ID를 입력합니다. 126 | 127 | **DSM_ID** *your_dsm_id* 128 | 129 | DSM 에 로그인 할 ID 를 입력합니다 130 | 131 | **TG_DSM_PW_ID** *12345678* 132 | 133 | Download Station 에 로그인시 암호를 물어 볼 Telegram 사용자의 chat id를 입력합니다. 134 | 135 | **DSM_URL** *https://DSM_IP_OR_URL* 136 | 137 | DSM URL, http 또는 https 까지 모두 포함하여 입력하고 포트는 제외해야 합니다 138 | 139 | 예시) 올바른 예 - https://www.dsm.com 140 | 141 | 잘못 된 예 - https://www.dsm.com:8000 142 | 143 | **DS_PORT** *8000* 144 | 145 | DSM 제어판 -> 응용 프로그램 포털 -> Download Station 의 포트를 입력합니다. 146 | 147 | DSM_URL 에 입력한 http 또는 https 에 맞춰서 입력하셔야 합니다. 148 | 149 | **SYNO_LANG** *ko_kr* 150 | 151 | synobot 언어팩 설정입니다. 현재 버전에는 ko_kr 버전만 지원 합니다. 152 | 153 | **DSM_CERT** *1* 154 | 155 | DSM_URL 에 https 사용시 DSM 인증서가 도메인과 일치하지 않는 경우에 사설 인증서를 사용 가능하도록 하는 환경 변수입니다. 156 | 157 | 사설 인증서를 사용 한다면 값을 0 으로 사용하시면 됩니다. 158 | 159 | **DSM_OTP_SECRET** "VFGW" 160 | 161 | DSM 2단계 인증 설정시 비밀 키의 값을 입력 162 | 163 | [설정 방법](#Otp-설정하기) 164 | 165 | **TZ** *Asia/Seoul* 166 | 167 | Synobot 도커가 작동하는 지역의 시간대 설정 168 | 169 | 링크(https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) 의 *TZ database name* 에 해당 하는 값을 사용 170 | 171 | **DOCKER_LOG** 172 | 173 | Synobot 도커의 로그를 컨테이너 상세보기의 로그 탭에서 확인 할 수 있도록 설정 174 | 175 | 1 : 컨테이너 로그 화면에서 확인 가능 176 | 177 | 0 : 도커 내부의 로그 파일에만 기록 178 | 179 | *** 180 | 181 | **DSM_PW 환경 변수가 없는 경우에는 synobot 도커가 재시작 될 때마다 텔레그램 봇이 DSM 로그인 암호를 요청합니다.** 182 | 183 | **synobot 은 사용자 암호를 어느곳에도 저장하지 않습니다.** 184 | 185 | **텔레그램 봇으로 전달 받은 암호 메시지는 수신 즉시 봇이 삭제 합니다.** 186 | 187 | 매번 암호 입력하는것이 번거러운 경우 DSM_PW 환경 변수에 DSM 로그인 암호를 추가 하면 해당 암호를 이용하여 DSM 에 로그인합니다. 188 | 189 | DSM_PW 환경 변수를 + 버튼을 클릭하여 직접 입력해주셔야 합니다 190 | 191 | **DSM_PW** *your_dsm_password* 192 | 193 | *** 194 | 195 | 196 | Synology Docker 에서 다음과 같이 화면에서 세팅하면 됩니다. 197 | 198 | ![synobot_config_1](https://raw.githubusercontent.com/acidpop/synobot_public/master/img/synobot_config1.png) 199 | 200 | ![synobot_config_2](https://raw.githubusercontent.com/acidpop/synobot_public/master/img/synobot_config2.png) 201 | 202 | ![synobot_config_3](https://raw.githubusercontent.com/acidpop/synobot_public/master/img/synobot_config3.png) 203 | 204 | *** 205 | 206 | - Tip 207 | 208 | Telegram 사용자의 chat id 알아내기 209 | 210 | chat id 알아내기 211 | 212 | *** 213 | 214 | synobot 안내 문구 커스터마이징 하기 215 | 216 | 1. DSM 의 터미널에 접속 한 후 sudo -i 명령으로 root 권한으로 로그인. 217 | 218 | 2. docker ps -a 명령으로 현재 실행 되어 있는 synobot의 Container ID 값을 알아낸다. 219 | 220 | 3. docker exec -it {Container ID} /bin/bash 명령으로 Docker 내부에 접속한다. 221 | 222 | 4. apt-get update 223 | 224 | 5. apt-get install vim 225 | 226 | 6. vim 으로 ko_kr.json 파일을 열어 문구를 알맞게 수정 한다. 227 | 228 | 7. synobot 도커 재시작 229 | 230 | *** 231 | 232 | 233 | ## Torrent Watch 경로 설정하기 234 | 235 | 0.11 버전에서 신규 추가 된 DSM_WATCH 환경 변수를 설정 하는 방법은 다음과 같다. 236 | 237 | 1. 고급 설정 버튼 클릭 238 | 239 | ![synobot_config_4](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config4.png) 240 | 241 | 2. 볼륨 탭 선택 후 폴더 추가 버튼 클릭 242 | 243 | ![synobot_config_5](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config5.png) 244 | 245 | 3. Download Station 에서 설정한 watch 경로를 선택 246 | 247 | ![synobot_config_6](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config6.png) 248 | 249 | 4. 마운트 경로 부분에 **/tor_watch** 입력 250 | 251 | ![synobot_config_7](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config7.png) 252 | 253 | 5. 환경 변수 설정 탭에서 DSM_WATCH 변수의 값에 **/tor_watch** 입력 254 | 255 | ![synobot_config_8](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config8.png) 256 | 257 | 258 | ## #Otp 설정하기 259 | 260 | 0.13 버전에서 신규 추가 된 OTP 자동 입력 기능 설정 방법은 다음과 같다. 261 | 262 | 1. 게정 설정에서 2단계 인증 추가를 클릭한다. 263 | 264 | 2. QR 코드 스캔 화면이 나오면 "스캔 할 수 없습니까" 를 클릭한다. 265 | 266 | ![synobot_config_9](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config9.png) 267 | 268 | 3. 비밀 키에 해당 하는 값을 기록 해 둔다. 269 | 270 | ![synobot_config_10](https://raw.githubusercontent.com/acidpop/synobot/master/img/synobot_config10.png) 271 | 272 | 4. Synobot 의 Docker 세팅시 DSM_OTP_SECRET 환경 변수에 해당 비밀키를 입력한다. 273 | 274 | ---- 275 | 276 | 문의 사항은 github synobot Repository 를 이용해 주세요 277 | 278 | synobot github 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /bothandler.py: -------------------------------------------------------------------------------- 1 | 2 | #-*- coding: utf-8 -*- 3 | import sys 4 | import time 5 | import json 6 | from telegram.ext import Updater, CommandHandler, MessageHandler, Filters 7 | from telegram.error import (TelegramError, Unauthorized, BadRequest, TimedOut, ChatMigrated, NetworkError) 8 | 9 | import single 10 | import synods 11 | import ThreadTimer 12 | #import systemutil 13 | import BotConfig 14 | import synobotLang 15 | import OtpHandler 16 | 17 | from LogManager import log 18 | 19 | # Valid User 구현 20 | 21 | class BotHandler(single.SingletonInstane): 22 | 23 | # Task Monitor Timer Instance 24 | dsdown_task_monitor = None 25 | ds = None 26 | BotUpdater = None 27 | bot = None 28 | 29 | cur_mode = '' 30 | 31 | cfg = None 32 | valid_users = None 33 | 34 | lang = None 35 | 36 | otp_input = False 37 | otp_code = '' 38 | 39 | try_login_cnt = 0 40 | 41 | otp_handler = None 42 | 43 | # SynoBot Command 44 | """ 45 | /dslogin 46 | /cancel 47 | /task 48 | /stat 49 | / 50 | """ 51 | synobot_cmd_list = "/dslogin\n/task\n/stat" 52 | 53 | def InitBot(self): 54 | self.cfg = BotConfig.BotConfig().instance() 55 | self.valid_users = self.cfg.GetValidUser() 56 | 57 | self.lang = synobotLang.synobotLang().instance() 58 | 59 | self.otp_handler = OtpHandler.OtpHandler().instance() 60 | 61 | self.otp_handler.InitOtp(self.cfg.GetOtpSecret()) 62 | 63 | log.info("Bot Token : %s", self.cfg.GetBotToken()) 64 | updater = Updater(self.cfg.GetBotToken(), use_context=True) 65 | 66 | self.BotUpdater = updater 67 | self.bot = updater.bot 68 | 69 | # Get the dispatcher to register handlers 70 | dp = updater.dispatcher 71 | 72 | # on different commands - answer in Telegram 73 | dp.add_handler(CommandHandler("start", self.start)) 74 | dp.add_handler(CommandHandler("help", self.help)) 75 | #dp.add_handler(CommandHandler("systeminfo", self.systeminfo)) 76 | dp.add_handler(CommandHandler("dslogin", self.dslogin)) 77 | dp.add_handler(CommandHandler("task", self.TaskList)) 78 | dp.add_handler(CommandHandler("stat", self.Statistic)) 79 | dp.add_handler(CommandHandler("otp", self.GetOtp)) 80 | 81 | # on noncommand i.e message - echo the message on Telegram 82 | dp.add_handler(MessageHandler(Filters.text, self.msg_handler)) 83 | 84 | dp.add_handler(MessageHandler(Filters.document, self.file_handler)) 85 | 86 | # log all errors 87 | dp.add_error_handler(self.error) 88 | 89 | self.ds = synods.SynoDownloadStation().instance() 90 | 91 | # Start the Bot 92 | updater.start_polling() 93 | 94 | # Download Station Task Monitor 95 | self.dsdown_task_monitor = ThreadTimer.ThreadTimer(10, self.ds.GetTaskList) 96 | 97 | self.dsdown_task_monitor.start() 98 | 99 | self.StartDsmLogin() 100 | 101 | # Run the bot until you press Ctrl-C or the process receives SIGINT, 102 | # SIGTERM or SIGABRT. This should be used most of the time, since 103 | # start_polling() is non-blocking and will stop the bot gracefully. 104 | updater.idle() 105 | 106 | self.dsdown_task_monitor.cancel() 107 | 108 | def StopTaskMonitor(self): 109 | self.dsdown_task_monitor.cancel() 110 | 111 | def CheckValidUser(self, chat_id): 112 | if not chat_id in self.valid_users: 113 | log.info("Invalid User : %s", chat_id) 114 | return False 115 | return True 116 | 117 | def StartInputLoginId(self): 118 | log.info('Start Input ID Flow') 119 | self.cur_mode = 'input_id' 120 | bot = self.BotUpdater.bot 121 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM 로그인 ID를 입력하세요") 122 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('input_login_id')) 123 | 124 | def StartInputPW(self): 125 | log.info("Start Input PW Flow") 126 | self.cur_mode = 'input_pw' 127 | bot = self.BotUpdater.bot 128 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM 로그인 비밀번호를 입력하세요\n비밀번호 메시지를 메시지 수신 후 삭제합니다") 129 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('input_login_pw')) 130 | # log.info('lang : %s', self.lang.GetBotHandlerLang('input_login_pw')) 131 | 132 | def StartInputOTP(self): 133 | log.info("Start Input OTP Flow") 134 | self.cur_mode = 'input_otp' 135 | bot = self.BotUpdater.bot 136 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM OTP에 표시된 숫자를 입력하세요") 137 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('input_login_otp')) 138 | 139 | def StartDsmLogin(self, msg_silent = False): 140 | retry_cnt = self.cfg.GetDsmRetryLoginCnt() 141 | 142 | bot = self.BotUpdater.bot 143 | 144 | retry_msg = '' 145 | 146 | log.info('StartDsmLogin, try_login_cnt:%d, retry_cnt:%d', self.try_login_cnt, retry_cnt) 147 | 148 | # 재시도 횟수 까지 로그인을 시도하며, 횟수 초과시 프로그램 종료 149 | while self.try_login_cnt < retry_cnt: 150 | # 로그인 Flow 151 | # 1. dsm_id 값이 있는지 확인 152 | # 2. dsm_pw 값이 있는지 확인 153 | # 3. DS API Login 시도 154 | # 4. 결과 코드 조회 155 | # 4-1 반환된 rest api 의 content 값을 보고 판단. 156 | 157 | retry_msg = ' [%d/%d]' % ( self.try_login_cnt + 1, retry_cnt) 158 | 159 | # step 1 160 | # dsm_id 가 비어 있는 경우 161 | if not self.cfg.GetDsmId(): 162 | log.info('DSM ID is empty') 163 | self.StartInputLoginId() 164 | return False 165 | 166 | # step 2 167 | if not self.cfg.GetDsmPW(): 168 | log.info('DSM PW is empty') 169 | self.StartInputPW() 170 | return False 171 | 172 | # step 3 173 | id = self.cfg.GetDsmId() 174 | pw = self.cfg.GetDsmPW() 175 | 176 | # otp_code = self.otp_code 177 | # res, content = self.ds.DsmLogin(id, pw, otp_code) 178 | 179 | # otp_code 저장된게 없다면 OtpHandler 에서 OTP Code 를 가져와본다. 180 | # OtpHandler 에서도 값이 없다면 그대로 진행 181 | if self.otp_code == None or len(self.otp_code) == 0: 182 | log.info('none otp') 183 | l_otp_code = self.otp_handler.GetOtp() 184 | log.info('GetOtp : [%s]', l_otp_code) 185 | else: 186 | log.info('exist otp') 187 | l_otp_code = self.otp_code 188 | res, content = self.ds.DsmLogin(id, pw, l_otp_code) 189 | 190 | # step 4 191 | if res == False: 192 | log.info('DSM Login fail, API request fail') 193 | msg = self.lang.GetBotHandlerLang('dsm_login_fail_msg') % (content) 194 | 195 | msg += retry_msg 196 | self.try_login_cnt += 1 197 | 198 | bot.sendMessage(self.cfg.GetDsmPwId(), msg) 199 | # 3초 대기 후 재시도 200 | time.sleep(3) 201 | continue 202 | 203 | log.info('DSM Login check content json data') 204 | # step 4-1 205 | # rest api 의 내용 중 http status code 가 200이 아닌 경우 예외처리 206 | # status code 가 200 이면 수신 된 json 데이터를 가지고 예외 처리 207 | if content.status_code != 200: 208 | log.warn("DSM Login Request fail") 209 | # msg = '로그인 요청 실패\n, 응답 코드 : %d' % (res.status_code) 210 | msg = self.lang.GetBotHandlerLang('dsm_login_api_fail') % (res.status_code) 211 | 212 | msg += retry_msg 213 | self.try_login_cnt += 1 214 | 215 | bot.sendMessage(self.cfg.GetDsmPwId(), msg) 216 | time.sleep(3) 217 | continue 218 | 219 | # content 에서 json 데이터 파싱 220 | json_data = json.loads(content.content.decode('utf-8')) 221 | 222 | # json_data 가 None 이라면 DS API 이상, 재시도 223 | if json_data == None: 224 | log.info('DS API Response content is none') 225 | msg = self.lang.GetBotHandlerLang('dsm_api_res_empty') 226 | msg += retry_msg 227 | self.try_login_cnt += 1 228 | #bot.sendMessage(self.cfg.GetDsmPwId(), 'DS API 응답 데이터가 비어있습니다') 229 | bot.sendMessage(self.cfg.GetDsmPwId(), msg) 230 | time.sleep(3) 231 | continue 232 | 233 | if json_data['success'] == False: 234 | log.info('DS API Response false') 235 | errcode = json_data['error']['code'] 236 | 237 | # 105 세션 만료 238 | # 400 id 또는 암호 오류 239 | # 401 계정 비활성화 240 | # 402, 403, 404 2단계 인증 실패 241 | 242 | if errcode == 105: 243 | log.info('105 error, session expired') 244 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM Login 실패\n세션이 만료되었습니다.") 245 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('dsm_session_expire')) 246 | return False 247 | elif errcode == 400: 248 | log.info('400 error, id or pw invalid') 249 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM Login 실패\nID 또는 비밀번호가 다릅니다.") 250 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('dsm_invalid_id_pw')) 251 | self.StartInputPW() 252 | return False 253 | elif errcode == 401: 254 | log.info('401 error, account disabled') 255 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM Login 실패\n비활성화 된 계정입니다.") 256 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('dsm_account_disable')) 257 | return False 258 | elif errcode == 402 or errcode == 403 or errcode == 404: 259 | log.info('%d error, permission denied, try otp auth login', errcode) 260 | if self.otp_handler.GetOtp() != '': 261 | log.info('otp handler instance exist, retry login. [%d/%d]', iself.try_login_cnt+1, retry_cnt) 262 | self.try_login_cnt += 1 263 | continue 264 | 265 | self.StartInputOTP() 266 | return False 267 | 268 | if json_data['success'] == True: 269 | log.info('DSM Login success') 270 | self.ds.auth_cookie = content.cookies 271 | self.ds.dsm_login_flag = True 272 | self.try_login_cnt = 0 273 | 274 | # Login 성공시 id, pw 메모리에 저장 275 | self.ds.dsm_id = id 276 | self.ds.dsm_pw = pw 277 | self.ds.DSMOtpHandler = self.otp_handler 278 | 279 | # bot.sendMessage(self.cfg.GetDsmPwId(), 'DS Login 성공\nSynobot을 시작합니다.') 280 | if msg_silent == False: 281 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('dsm_login_succ')) 282 | return True 283 | 284 | log.info('retry login... %d/%d', self.try_login_cnt, retry_cnt) 285 | self.try_login_cnt += 1 286 | 287 | 288 | #bot.sendMessage(self.cfg.GetDsmPwId(), "DSM Login에 실패 하였습니다.\n프로그램이 종료됩니다") 289 | bot.sendMessage(self.cfg.GetDsmPwId(), self.lang.GetBotHandlerLang('dsm_login_fail_exit')) 290 | log.info('DSM Login Fail!!') 291 | sys.exit() 292 | 293 | return False 294 | 295 | 296 | # Define a few command handlers. These usually take the two arguments bot and 297 | # update. Error handlers also receive the raised TelegramError object in error. 298 | def start(self, update, context): 299 | # Bot Start Message 300 | if self.CheckValidUser(update.message.from_user.id) == False: 301 | return 302 | 303 | update.message.reply_text('Hi!') 304 | 305 | 306 | def help(self, update, context): 307 | """Send a message when the command /help is issued.""" 308 | if self.CheckValidUser(update.message.from_user.id) == False: 309 | return 310 | 311 | update.message.reply_text(self.synobot_cmd_list) 312 | 313 | #def systeminfo(self, update, context): 314 | # if self.CheckValidUser(update.message.from_user.id) == False: 315 | # return 316 | # # /systeminfo Command 317 | # sys_info = systemutil.GetTopProcess() 318 | # 319 | # update.message.reply_markdown(sys_info) 320 | 321 | def dslogin(self, update, context): 322 | if self.CheckValidUser(update.message.from_user.id) == False: 323 | return 324 | 325 | log.info("DSM Login Mode") 326 | 327 | #update.message.reply_text('로그인을 시도합니다') 328 | update.message.reply_text(self.lang.GetBotHandlerLang('dsm_try_login')) 329 | 330 | #self.cur_mode = 'input_id' 331 | #update.message.reply_text('로그인 ID를 입력하세요') 332 | #update.message.reply_text(self.lang.GetBotHandlerLang('input_login_id')) 333 | self.StartDsmLogin() 334 | 335 | return 336 | 337 | def TaskList(self, update, context): 338 | if self.CheckValidUser(update.message.from_user.id) == False: 339 | return 340 | 341 | if self.ds.auth_cookie == None: 342 | update.message.reply_text(self.lang.GetBotHandlerLang('dsm_not_login')) 343 | return 344 | 345 | #update.message.reply_text('다운로드 스테이션 작업 목록을 가져옵니다') 346 | update.message.reply_text(self.lang.GetBotHandlerLang('dsm_task_list')) 347 | self.ds.GetTaskDetail() 348 | 349 | return 350 | 351 | def Statistic(self, update, context): 352 | if self.CheckValidUser(update.message.from_user.id) == False: 353 | return 354 | 355 | if self.ds.auth_cookie == None: 356 | update.message.reply_text(self.lang.GetBotHandlerLang('dsm_not_login')) 357 | return 358 | 359 | #update.message.reply_text('다운로드 스테이션의 네트워크 정보를 가져옵니다.') 360 | update.message.reply_text(self.lang.GetBotHandlerLang('dsm_statistic')) 361 | self.ds.GetStatistic() 362 | 363 | return 364 | 365 | def GetOtp(self, update, context): 366 | if self.CheckValidUser(update.message.from_user.id) == False: 367 | return 368 | 369 | otp_msg = self.otp_handler.GetOtp() 370 | update.message.reply_text(otp_msg) 371 | return 372 | 373 | def current_mode_handle(self, update, context): 374 | command = update.message.text 375 | 376 | if self.cur_mode == 'input_id': 377 | self.ds.dsm_id = command 378 | self.cur_mode = 'input_pw' 379 | #update.message.reply_text('로그인 비밀번호를 입력하세요') 380 | update.message.reply_text(self.lang.GetBotHandlerLang('input_login_pw')) 381 | elif self.cur_mode == 'input_pw': 382 | self.cfg.SetDsmPW(command) 383 | self.ds.dsm_pw = command 384 | self.cur_mode = '' 385 | 386 | update.message.delete() 387 | #update.message.reply_text('입력 된 암호 메시지를 삭제 하였습니다') 388 | update.message.reply_text(self.lang.GetBotHandlerLang('noti_delete_pw')) 389 | 390 | #self.ds.Login() 391 | self.StartDsmLogin() 392 | elif self.cur_mode == 'input_otp': 393 | self.ds.dsm_otp = command 394 | self.otp_code = command 395 | self.cur_mode = '' 396 | self.otp_input = True 397 | update.message.delete() 398 | #update.message.reply_text('입력 된 OTP 메시지를 삭제 하였습니다') 399 | update.message.reply_text(self.lang.GetBotHandlerLang('noti_delete_otp')) 400 | 401 | #self.ds.Login() 402 | self.StartDsmLogin() 403 | else: 404 | log.info('unknown current mode : %s', self.cur_mode) 405 | self.cur_mode = '' 406 | 407 | 408 | def msg_handler(self, update, context): 409 | 410 | log.info('current mode : %s', self.cur_mode) 411 | 412 | if self.cur_mode != 'input_pw' and self.cur_mode != 'input_otp': 413 | log.info("msg : %s", update.message.text) 414 | 415 | if self.CheckValidUser(update.message.from_user.id) == False: 416 | return 417 | 418 | command = update.message.text 419 | 420 | if self.cur_mode: 421 | log.info('try current mode handle') 422 | self.current_mode_handle(update, context) 423 | elif( command[0:8] == 'magnet:?'): 424 | log.info("Detected magnet link, Create Task URL for magnet") 425 | self.ds.CreateTaskForUrl(command) 426 | #update.message.reply_text('마그넷 링크를 등록하였습니다') 427 | update.message.reply_text(self.lang.GetBotHandlerLang('noti_magnet_link')) 428 | else: 429 | # Send Help Message 430 | #update.message.reply_text('지원 되지 않는 명령입니다') 431 | update.message.reply_text(self.lang.GetBotHandlerLang('noti_not_support_cmd')) 432 | #synobot_cmd_list 433 | update.message.reply_text(self.synobot_cmd_list) 434 | 435 | 436 | def file_handler(self, update, context): 437 | if self.CheckValidUser(update.message.from_user.id) == False: 438 | return 439 | 440 | # 1. Get File Name 441 | file_name = update.message.document.file_name 442 | file_mime = update.message.document.mime_type 443 | file_path = file_name 444 | 445 | # 2. .torrent 파일인지 확인 446 | if( file_mime == 'application/x-bittorrent'): 447 | tor_file = update.message.document.get_file(timeout=5) 448 | tor_file.download(custom_path=file_path, timeout=5) 449 | 450 | #ret = self.ds.CreateTaskForFile(file_path) 451 | ret = self.ds.CreateTaskForFileToWatchDir(file_path) 452 | 453 | log.info("File Received, file name : %s", file_path) 454 | 455 | bot = self.BotUpdater.bot 456 | #msg = 'Torrent 파일(%s)을 등록 하였습니다' % (file_name) 457 | if ret == True: 458 | msg = self.lang.GetBotHandlerLang('noti_torrent_file') % (file_name) 459 | else: 460 | msg = self.lang.GetBotHandlerLang('noti_torrent_file_fail') % (file_name) 461 | 462 | bot.sendMessage(self.cfg.GetDsmPwId(), msg) 463 | else: 464 | bot = self.BotUpdater.bot 465 | 466 | #msg = 'torrent 파일만 지원합니다, File : %s' % (file_name) 467 | msg = self.lang.GetBotHandlerLang('noti_not_torrent_file') % (file_name) 468 | 469 | bot.sendMessage(self.cfg.GetDsmPwId(), msg) 470 | 471 | 472 | def error(self, update, context): 473 | """Log Errors caused by Updates.""" 474 | log.error('Update "%s" caused error "%s"', update, context.error) 475 | log.error("error") 476 | 477 | -------------------------------------------------------------------------------- /synods.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | import os 3 | import shutil 4 | import requests 5 | import json 6 | import telegram 7 | import urllib3 8 | 9 | import single 10 | import CommonUtil 11 | import bothandler 12 | import taskmgr 13 | import BotConfig 14 | import synobotLang 15 | import OtpHandler 16 | from LogManager import log 17 | 18 | 19 | 20 | # Synology Download Station Handler 21 | 22 | # sqlite Structure 23 | 24 | #import os 25 | #print(os.environ.get('DSM_')) 26 | 27 | # Task Status : waiting, downloading, paused, finishing, finished, hash_checking, seeding, filehosting_waiting, extracting, error 28 | 29 | # 1. Login 30 | # 2. Task List 31 | # 3. Create (File, Magnet) 32 | # 4. Auto Delete (Optional) 33 | # 5. SQLite insert or replace -> Trigger 34 | 35 | class SynoDownloadStation(single.SingletonInstane): 36 | 37 | auth_cookie = None 38 | theTaskMgr = taskmgr.TaskMgr().instance() 39 | dsm_id = '' 40 | dsm_pw = '' 41 | dsm_otp = '' 42 | cfg = None 43 | lang = None 44 | dsm_login_flag = False 45 | DSMOtpHandler = None 46 | # 구현 대상 47 | # 1. Task List 가져오기 48 | # 2. Magnet 등록 하기 49 | # 3. Torrent File 등록 하기 50 | def __init__(self, *args, **kwars): 51 | self.theTaskMgr.AddNotiCallback(self.TaskNotiCallback) 52 | self.theTaskMgr.LoadFile() 53 | self.cfg = BotConfig.BotConfig().instance() 54 | self.lang = synobotLang.synobotLang().instance() 55 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 56 | 57 | return 58 | 59 | def SendNotifyMessage(self, msg, ParseMode = None): 60 | noti_list = self.cfg.GetNotifyList() 61 | 62 | bot = bothandler.BotHandler().instance().bot 63 | 64 | for chat_id in noti_list: 65 | if ParseMode == 'mark': 66 | bot.sendMessage(chat_id, msg, parse_mode = telegram.ParseMode.MARKDOWN) 67 | elif ParseMode == 'html': 68 | bot.sendMessage(chat_id, msg, parse_mode = telegram.ParseMode.HTML) 69 | else: 70 | bot.sendMessage(chat_id, msg) 71 | 72 | def ChkTaskResponse(self, result_json, log_body, msg_silent = False): 73 | if not result_json: 74 | log.info('check response fail, result json data is empty') 75 | return False 76 | 77 | try: 78 | if result_json['success'] == True: 79 | return True 80 | 81 | errcode = result_json['error']['code'] 82 | 83 | errstr = self.GetErrorTaskCode(errcode) 84 | msg = self.lang.GetBotHandlerLang('dsm_rest_api_fail') % (errstr, log_body) 85 | # msg = 'DSM 작업 실패\n%s\n%s' % (errstr, log_body) 86 | 87 | log.info(msg) 88 | log.info(result_json) 89 | 90 | if errcode != 100 and msg_silent == False: 91 | self.SendNotifyMessage(msg) 92 | 93 | return False 94 | except: 95 | log.info('ChkTaskResponse Exception') 96 | return False 97 | 98 | return False 99 | 100 | def ChkAPIResponse(self, result_json, log_body): 101 | if not result_json: 102 | log.info('check API response fail, result json data is empty') 103 | return False 104 | 105 | try: 106 | if result_json['success'] == True: 107 | return True 108 | 109 | errcode = result_json['error']['code'] 110 | 111 | errstr = self.GetErrorTaskCode(errcode) 112 | msg = self.lang.GetBotHandlerLang('dsm_rest_api_fail') % (errstr, log_body) 113 | # msg = 'DSM 작업 실패\n%s\n%s' % (errstr, log_body) 114 | 115 | log.info(msg) 116 | log.info(result_json) 117 | self.SendNotifyMessage(msg) 118 | 119 | return False 120 | except: 121 | log.info('ChkAPIResponse Exception') 122 | return False 123 | 124 | return False 125 | 126 | def DsmLogin(self, id, pw, otp_code = None): 127 | url = self.cfg.GetDSDownloadUrl() + '/webapi/auth.cgi' 128 | if otp_code == None or len(otp_code) == 0: 129 | # Not Use OTP Code 130 | log.info('without otp') 131 | params = {'api' : 'SYNO.API.Auth', 'version' : '3', 'method' : 'login' , 'account' : id, 'passwd' : pw, 'session' : 'DownloadStation', 'format' : 'cookie'} 132 | else: 133 | log.info('with otp') 134 | params = {'api' : 'SYNO.API.Auth', 'version' : '3', 'method' : 'login' , 'account' : id, 'passwd' : pw, 'session' : 'DownloadStation', 'format' : 'cookie', 'otp_code' : otp_code} 135 | 136 | log.info('Request url : %s', url) 137 | 138 | try: 139 | res = requests.get(url, params=params, verify=self.cfg.IsUseCert(), timeout=30) 140 | except requests.ConnectionError: 141 | log.error('Login|synology rest api request Connection Error') 142 | return False, 'Connection error' 143 | except requests.exceptions.Timeout: 144 | log.error('Login|synology rest api request timeout') 145 | return False, 'Connection timeout' 146 | except: 147 | log.error('Login|synology requests fail') 148 | return False, 'Unknown Connect Error' 149 | 150 | log.info('auth url requests succ') 151 | 152 | return True, res 153 | 154 | 155 | def GetTaskList(self): 156 | #url = 'https://downloadstation_dsm_url:9999/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method=list' 157 | 158 | if self.auth_cookie == None: 159 | return False 160 | 161 | params = {'api' : 'SYNO.DownloadStation.Task', 'version' : '3', 'method' : 'list'} 162 | 163 | url = self.cfg.GetDSDownloadUrl() + '/webapi/DownloadStation/task.cgi' 164 | 165 | try: 166 | res = requests.get(url, params=params, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 167 | except requests.ConnectionError: 168 | log.error('GetTaskList|synology rest api request Connection Error') 169 | return False 170 | except: 171 | log.error('GetTaskList|synology requests fail') 172 | return False 173 | 174 | 175 | 176 | if res.status_code != 200: 177 | log.warn("Get Task List Request fail") 178 | return False 179 | 180 | #print(res.content) 181 | 182 | json_data = json.loads(res.content.decode('utf-8')) 183 | 184 | msg_silent = False 185 | 186 | if self.dsm_login_flag == True: 187 | msg_silent = True 188 | 189 | if self.ChkTaskResponse(json_data, "GetTaskList Download station api fail", msg_silent) == False: 190 | self.auth_cookie = None 191 | log.info('bothandler StartDsmLogin call') 192 | bothandler.BotHandler().instance().StartDsmLogin(msg_silent) 193 | return False 194 | 195 | 196 | exists_task_list = [] 197 | for item in json_data['data']['tasks']: 198 | # log.info('GetTaskList : %s, %s, %s, %s, %s' % (item['id'], item['title'], CommonUtil.hbytes(item['size']), item['username'], item['status']) ) 199 | # size 가 0 보다 큰 값인 경우에만 Torrent 정보가 정상적으로 확인 된다. 200 | exists_task_list.append(item['id']) 201 | tor_size = int(item['size']) 202 | if tor_size > 0: 203 | self.theTaskMgr.InsertOrUpdateTask(item['id'], item['title'], item['size'], item['username'], item['status'] ) 204 | 205 | self.theTaskMgr.CheckRemoveTest(exists_task_list) 206 | 207 | if self.cfg.IsTaskAutoDel() == True: 208 | self.TaskAutoDelete(json_data) 209 | 210 | return True 211 | 212 | def GetTaskDetail(self): 213 | #url = 'https://downloadstation_dsm_url:9999/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method=list&additional=detail,file,transfer' 214 | 215 | if self.auth_cookie == None: 216 | return False 217 | 218 | log.info('try task list detail') 219 | 220 | params = {'api' : 'SYNO.DownloadStation.Task', 'version' : '3', 'method' : 'list', 'additional' : 'detail,file,transfer'} 221 | 222 | url = self.cfg.GetDSDownloadUrl() + '/webapi/DownloadStation/task.cgi' 223 | 224 | try: 225 | res = requests.get(url, params=params, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 226 | except requests.ConnectionError: 227 | log.error('GetTaskList|synology rest api request Connection Error') 228 | return False 229 | except: 230 | log.error('GetTaskList|synology requests fail') 231 | return False 232 | 233 | log.info('complete task list detail') 234 | 235 | if res.status_code != 200: 236 | log.warn("Get Task List Request fail") 237 | return False 238 | 239 | json_data = json.loads(res.content.decode('utf-8')) 240 | 241 | if self.ChkAPIResponse(json_data, "Download station api fail") == False: 242 | self.auth_cookie = None 243 | log.info('ChkAPIResponse fail') 244 | return False 245 | 246 | # Json 분석 후 다운로드 리스트 보내기 247 | # 보낼 데이터, Task ID, 파일이름, 파일 크기, 다운로드 된 크기, 진행 상태, 업스피드, 다운스피드, 상태 248 | log.info("Task : %s", json_data['data']['tasks']) 249 | 250 | for item in json_data['data']['tasks']: 251 | # size 가 0 보다 큰 값인 경우에만 Torrent 정보가 정상적으로 확인 된다. 252 | tor_size = int(item['size']) 253 | # item 정보 254 | # id : Task ID 255 | # size : 파일 전체 크기 256 | # status : 진행 상태 257 | # title : 토렌트 제목 258 | 259 | # item['additional']['transfer'] 260 | # size_downloaded : 다운로드 된 크기 261 | # size_uploaded : 업로드 된 크기 262 | # speed_download : 다운로드 속도(단위/s) 263 | # speed_upload : 업로드 속도 (단위/s) 264 | tor_id = item['id'] 265 | tor_status = item['status'] 266 | tor_title = item['title'] 267 | 268 | tor_size_download = 0 269 | tor_size_upload = 0 270 | tor_speed_down = 0 271 | tor_speed_up = 0 272 | 273 | if tor_size <= 0: 274 | self.SendTaskList(tor_id, tor_size, tor_status, tor_title, tor_size_download, tor_size_upload, tor_speed_down, tor_speed_up) 275 | return True 276 | 277 | # additional 아이템이 없다면 0 정보 전송 278 | if 'additional' in item == False: 279 | self.SendTaskList(tor_id, tor_size, tor_status, tor_title, tor_size_download, tor_size_upload, tor_speed_down, tor_speed_up) 280 | return True 281 | 282 | # transfer 아이템이 없다면 0 정보 전송 283 | if 'transfer' in item['additional'] == False: 284 | self.SendTaskList(tor_id, tor_size, tor_status, tor_title, tor_size_download, tor_size_upload, tor_speed_down, tor_speed_up) 285 | return True 286 | 287 | transfer_item = item['additional']['transfer'] 288 | tor_size_download = transfer_item['size_downloaded'] 289 | tor_size_upload = transfer_item['size_uploaded'] 290 | tor_speed_down = transfer_item['speed_download'] 291 | tor_speed_up = transfer_item['speed_upload'] 292 | 293 | self.SendTaskList(tor_id, tor_size, tor_status, tor_title, tor_size_download, tor_size_upload, tor_speed_down, tor_speed_up) 294 | 295 | log.info('success Task List') 296 | 297 | return True 298 | 299 | def GetStatistic(self): 300 | # param = {'api' : 'SYNO.DownloadStation.Statistic', 'version' : '1', 'method' : 'getinfo'} 301 | # url = 'https://downloadstation_dsm_url:9999/webapi/DownloadStation/statistic.cgi' 302 | if self.auth_cookie == None: 303 | return False 304 | 305 | log.info('try get statistic') 306 | 307 | params = {'api' : 'SYNO.DownloadStation.Statistic', 'version' : '1', 'method' : 'getinfo'} 308 | 309 | url = self.cfg.GetDSDownloadUrl() + '/webapi/DownloadStation/statistic.cgi' 310 | 311 | try: 312 | res = requests.get(url, params=params, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 313 | except requests.ConnectionError: 314 | log.error('GetStatistic|synology rest api request Connection Error') 315 | return False 316 | except: 317 | log.error('GetStatistic|synology requests fail') 318 | return False 319 | 320 | log.info('GetStatistic|complete get statistic') 321 | 322 | if res.status_code != 200: 323 | log.warn("GetStatistic|Get statistic Request fail") 324 | return False 325 | 326 | json_data = json.loads(res.content.decode('utf-8')) 327 | 328 | if self.ChkAPIResponse(json_data, "Download station api fail") == False: 329 | self.auth_cookie = None 330 | log.info('GetStatistic|ChkAPIResponse fail') 331 | return False 332 | 333 | # Data sample : {"data":{"speed_download":3496632,"speed_upload":0},"success":true} 334 | item = json_data.get('data') 335 | if item != None: 336 | download_speed = item['speed_download'] 337 | upload_speed = item['speed_upload'] 338 | self.SendStatistic(download_speed, upload_speed) 339 | else: 340 | log.info('GetStatistic|not found data, %s', res.content) 341 | 342 | return True 343 | 344 | def TaskAutoDelete(self, task_items): 345 | delete_task_id_arr = [] 346 | 347 | for item in task_items['data']['tasks']: 348 | status = item['status'] 349 | 350 | if status == 'finished' or status == 'seeding' or status == 'filehosting_waiting': 351 | # 완료, 보내기, 파일 호스팅 상태인 경우에만 작업 삭제 명령. 352 | log.info('Task Auto Delete, id:%s, title:%s, status:%s', item['id'], item['title'], item['status']) 353 | delete_task_id_arr.append(item['id']) 354 | 355 | # task array 에 아이템이 있다면 API 호출 356 | if len(delete_task_id_arr) > 0: 357 | task_id_list = ','.join(delete_task_id_arr) 358 | log.info('DS Delete API Call, Task ID : %s', task_id_list) 359 | self.DeleteTask(task_id_list) 360 | 361 | 362 | def CreateTaskForFile(self, file_path): 363 | create_url = self.cfg.GetDSDownloadUrl() + '/webapi/DownloadStation/task.cgi' 364 | 365 | params2 = {'api' : 'SYNO.DownloadStation.Task', 'version' : '3', 'method' : 'create' } 366 | 367 | files = {'file' : open(file_path, 'rb')} 368 | 369 | try: 370 | log.info("url:%s, data:%s, files:%s, cookies:%s", create_url, params2, files, self.auth_cookie) 371 | res = requests.post(create_url, data=params2, files=files, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 372 | except requests.ConnectionError: 373 | log.error('CreateTaskForFile|synology rest api request Connection Error') 374 | return False 375 | except: 376 | log.error('CreateTaskForFile|synology requests fail') 377 | return False 378 | 379 | if res.status_code != 200: 380 | # print('request fail') 381 | log.warn("Create Task For File Request fail") 382 | return False 383 | 384 | json_data = json.loads(res.content.decode('utf-8')) 385 | if self.ChkAPIResponse(json_data, "Download station Create Task for file") == False: 386 | return False 387 | 388 | # Remove Torrent File 389 | files['file'].close() 390 | os.remove(file_path) 391 | log.info('Torrent File removed, file:%s', file_path) 392 | 393 | return True 394 | 395 | 396 | def CreateTaskForFileToWatchDir(self, file_path): 397 | watch_path = self.cfg.GetTorWatch() 398 | 399 | if len(watch_path) <= 0: 400 | log.info("watch path is not define") 401 | # DSM_WATCH 환경 변수 등록 알림, noti_torrent_watch_nothing 402 | 403 | msg = self.lang.GetBotHandlerLang('noti_torrent_watch_nothing') 404 | 405 | self.SendNotifyMessage(msg) 406 | 407 | return False 408 | 409 | try: 410 | shutil.move(file_path, watch_path) 411 | except FileNotFoundError: 412 | log.info('Torrent file move fail, No such file or directory') 413 | 414 | msg = self.lang.GetBotHandlerLang('noti_torrent_watch_mv_fail') 415 | self.SendNotifyMessage(msg) 416 | return False 417 | except: 418 | log.info('Torrent file move fail, error') 419 | 420 | msg = self.lang.GetBotHandlerLang('noti_torrent_watch_mv_fail') 421 | self.SendNotifyMessage(msg) 422 | return False 423 | 424 | log.info("Torrent file move to torrent watch directory, watch:%s", watch_path) 425 | 426 | return True 427 | 428 | 429 | # HTTP/FTP/magnet/ED2K link 430 | def CreateTaskForUrl(self, url): 431 | create_url = self.cfg.GetDSDownloadUrl() + '/webapi/DownloadStation/task.cgi' 432 | 433 | params = {'api' : 'SYNO.DownloadStation.Task', 'version' : '3', 'method' : 'create' , 'uri' : url} 434 | 435 | try: 436 | res = requests.get(create_url, params=params, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 437 | except requests.ConnectionError: 438 | log.error('CreateTaskForUrl|synology rest api request Connection Error') 439 | return False 440 | except: 441 | log.error('CreateTaskForUrl|synology requests fail') 442 | return False 443 | 444 | if res.status_code != 200: 445 | log.warn("Create Task For Url Request fail") 446 | return False 447 | 448 | json_data = json.loads(res.content.decode('utf-8')) 449 | if self.ChkAPIResponse(json_data, "Download station Create Task for url") == False: 450 | return False 451 | 452 | return True 453 | 454 | # Download Station Delete 455 | def DeleteTask(self, task_id): 456 | delete_url = self.cfg.GetDSDownloadUrl() + '/webapi/DownloadStation/task.cgi' 457 | 458 | params = {'api' : 'SYNO.DownloadStation.Task', 'version' : '3', 'method' : 'delete' , 'id' : task_id} 459 | 460 | try: 461 | res = requests.get(delete_url, params=params, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 462 | except requests.ConnectionError: 463 | log.error('DeleteTask|synology rest api request Connection Error') 464 | return False 465 | except: 466 | log.error('DeleteTask|synology requests fail') 467 | return False 468 | 469 | if res.status_code != 200: 470 | log.warn("Delete Task Request fail") 471 | return False 472 | 473 | json_data = json.loads(res.content.decode('utf-8')) 474 | if self.ChkAPIResponse(json_data, "Download station Delete Task") == False: 475 | return False 476 | 477 | return True 478 | 479 | def TaskNotiCallback(self, task_id, title, size, user, status): 480 | log.info("Task Noti") 481 | bot = bothandler.BotHandler().instance().bot 482 | 483 | if bot == None: 484 | log.info("Bot instance is none") 485 | return 486 | 487 | log.info('Task Monitor : %s, %s, %s, %s, %s' % (task_id, title, CommonUtil.hbytes(size), user, status) ) 488 | #msg = '*상태* : %s\n*이름* : %s\n*크기* : %s\n*사용자* : %s' % ( self.StatusTranslate(status), title, CommonUtil.hbytes(size), user) 489 | msg = self.lang.GetBotHandlerLang('noti_torrent_status') % ( self.StatusTranslate(status), title, CommonUtil.hbytes(size), user) 490 | #self.SendNotifyMessage(msg, ParseMode = 'mark') 491 | self.SendNotifyMessage(msg) 492 | 493 | def SendTaskList(self, task_id, task_size, task_status, task_title, download_size, upload_size, download_speed, upload_speed): 494 | log.info("Task Noti") 495 | bot = bothandler.BotHandler().instance().bot 496 | 497 | if bot == None: 498 | log.info("Bot instance is none") 499 | return 500 | 501 | log.info('Task Detail : %s, %s, %s, %s, %s, %s, %s, %s' % ( task_id, task_title, CommonUtil.hbytes(task_size), self.StatusTranslate(task_status), CommonUtil.hbytes(download_size), CommonUtil.hbytes(upload_size), CommonUtil.hbytes(download_speed), CommonUtil.hbytes(upload_speed) ) ) 502 | msg = self.lang.GetBotHandlerLang('noti_task_list') % ( task_id, task_title, CommonUtil.hbytes(task_size), self.StatusTranslate(task_status), CommonUtil.hbytes(download_size), CommonUtil.hbytes(upload_size), CommonUtil.hbytes(download_speed), CommonUtil.hbytes(upload_speed) ) 503 | self.SendNotifyMessage(msg) 504 | 505 | def SendStatistic(self, download_speed, upload_speed): 506 | log.info("Statistic Noti") 507 | bot = bothandler.BotHandler().instance().bot 508 | 509 | if bot == None: 510 | log.info("Bot instance is none") 511 | return 512 | 513 | log.info('Statistic : %s, %s' % ( CommonUtil.hbytes(download_speed), CommonUtil.hbytes(upload_speed) ) ) 514 | msg = self.lang.GetBotHandlerLang('noti_statistic') % ( CommonUtil.hbytes(download_speed), CommonUtil.hbytes(upload_speed) ) 515 | self.SendNotifyMessage(msg) 516 | return 517 | 518 | 519 | def StatusTranslate(self, status): 520 | status_msg = '' 521 | try: 522 | status_msg = self.lang.GetSynoDsLang(status) 523 | except: 524 | status_msg = status 525 | 526 | return status_msg 527 | 528 | def GetErrorAuthCode(self, code): 529 | return self.lang.GetSynoAuthErrorLang( str(code) ) 530 | 531 | def GetErrorTaskCode(self, code): 532 | return self.lang.GetSynoTaskErrorLang( str(code) ) 533 | 534 | """ 535 | # 해당 코드는 현재 작동 안함. 536 | def CreateTaskForFileDSM7(self, file_path): 537 | create_url = self.cfg.GetDSDownloadUrl() + '/webapi/entry.cgi' 538 | 539 | stat = os.stat(file_path) 540 | 541 | #torrent_file = open(file_path, 'rb') 542 | 543 | params2 = {'api' : 'SYNO.DownloadStation2.Task', 'version' : '2', 'method' : 'create' , 'type' : 'file', 'create_list' : True, 'destination' : '/volume1/download/torrent', 'username' : 'userid' , 'password' : 'userpassword', 'mtime' : stat.st_mtime, 'size' : stat.st_size} 544 | 545 | torrent_file = {'file' : open(file_path, 'rb')} 546 | 547 | 548 | try: 549 | log.info("url:%s, data:%s, files:%s, cookies:%s", create_url, params2, file_path, self.auth_cookie) 550 | res = requests.post(create_url, data=params2, files=torrent_file, cookies=self.auth_cookie, verify=self.cfg.IsUseCert()) 551 | except requests.ConnectionError: 552 | log.error('CreateTaskForFile|synology rest api request Connection Error') 553 | return False 554 | except: 555 | log.error('CreateTaskForFile|synology requests fail') 556 | return False 557 | 558 | if res.status_code != 200: 559 | # print('request fail') 560 | log.warn("Create Task For File Request fail") 561 | return False 562 | 563 | json_data = json.loads(res.content.decode('utf-8')) 564 | if self.ChkAPIResponse(json_data, "Download station Create Task for file") == False: 565 | return False 566 | 567 | # Remove Torrent File 568 | torrent_file['file'].close() 569 | #torrent_file.close() 570 | os.remove(file_path) 571 | log.info('Torrent File removed, file:%s', file_path) 572 | 573 | return True 574 | """ 575 | 576 | --------------------------------------------------------------------------------