Skip to content

Commit

Permalink
inital commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikita Meshaninov committed Feb 22, 2020
0 parents commit 6118de5
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env/
.vscode/
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env/
.vscode/
env
36 changes: 36 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM python:3.8-alpine

ARG TOKEN
ARG ACCESS
ARG TRANSMISSION_URL=localhost
ARG TRANSMISSION_LOGIN
ARG TRANSMISSION_PASSWORD
ARG TRANSMISSION_PORT
ARG TIME_SHEDULE_SEC=300
ARG SOCKS5_LOGIN
ARG SOCKS5_PASSWORD
ARG SOCKS5_ADDRESS

ENV TOKEN=${TOKEN}
ENV ACCESS=${ACCESS}
ENV TRANSMISSION_URL=${TRANSMISSION_URL}
ENV TRANSMISSION_LOGIN=${TRANSMISSION_LOGIN}
ENV TRANSMISSION_PASSWORD=${TRANSMISSION_PASSWORD}
ENV TRANSMISSION_PORT=${TRANSMISSION_PORT}
ENV TIME_SHEDULE_SEC=${TIME_SHEDULE_SEC}
ENV SOCKS5_LOGIN=${SOCKS5_LOGIN}
ENV SOCKS5_PASSWORD=${SOCKS5_PASSWORD}
ENV SOCKS5_ADDRESS=${SOCKS5_ADDRESS}

WORKDIR /transmission_telegram

COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

COPY . /transmission_telegram/

ENTRYPOINT ["python", "bot.py", "&"]
CMD ["python ", "shedule.py", "&"]

EXPOSE 443
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Telegram-transmission-bot

Телеграм бот, для личного использования.
Если ему отправить torrent file или magnet ссылку, то через rpc подает запросы на transmission сервер.
Также можно управлять процессом раздачи каждого трекера

При изменении статуса раздачи уведомляет

Рекомендую использовать вместие с [докер контейнером transmission](https://hub.docker.com/r/linuxserver/transmission)

### Список задач:
![Список раздач](screenshots/image1.png)
### Изменение статуса:
![Изменение статуса](screenshots/image2.png)
### Добавление файла:
![Добавление файла](screenshots/image3.png)

**По сути это упрощенный клиент transmission для **телеграмма****

## Сборка
```bash
git clone https://github.com/meshchaninov/transmission-telegram-private-bot.git
cd transmission-telegram-private-bot-master
docker build -t transmission-bot . \
--build-arg TOKEN=<token> \
--build-arg ACCESS=<access> \
--build-arg TRANSMISSION_URL=<transmission_url> \
--build-arg TRANSMISSION_LOGIN=<login> \
--build-arg TRANSMISSION_PASSWORD=<password> \
--build-arg TRANSMISSION_PORT=<transmission_port> \
--build-arg TIME_SHEDULE_SEC=<time_shedule_sec> \
--build-arg SOCKS5_LOGIN=<socks5_login> \
--build-arg SOCKS5_PASSWORD=<socks5_password> \
--build-arg SOCKS5_ADDRESS=<socks5_address> \
--restart unless-stopped
```

Каждое из значений:
- TOKEN – токен из botFather телеграмма
- ACCESS - id пользователей у которых есть доступ к телеграмму (Их может быть несколько Пример: ```ACCESS=1111:2222:3333```)
- TRANSMISSION_URL – адресс где хостится transmission
- TRANSMISSION_LOGIN – логин в клиенте transmission (надеюсь у вас он есть, как и пароль. В опасное время живем:))
- TRANSMISSION_PASSWORD - пароль в клиенте transmission
- TRANSMISSION_PORT - порт клиента transmission. Обычно 9091
- TIME_SHEDULE_SEC – период через которое бот будет проаерять изменение статуса у трекера в секундах (Пример: ```TIME_SHEDULE_SEC=300```, уведомлять о изменениях, если они есть, раз в 5 минут)
- SOCKS5_LOGIN - логин socks5 сервера (Если ты из России, то по другому бот и не запустить, обязталеьно нужен прокси)
- SOCKS5_PASSWORD - пароль socks5 сервера
- SOCKS5_ADDRESS - адресс socks5 сервера

**Опять же повторю, делалось для личного использования. Из-за этого реализация топорна и отсутствует гипкость.**
98 changes: 98 additions & 0 deletions TransmissionConnection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from dataclasses import dataclass
from enum import Enum, auto
from typing import Dict, List

import transmissionrpc as trpc


class TorrentStatus(Enum):
STOPPED = auto()
SEEDING = auto()
CHECKING = auto()
DOWNLOADING = auto()
UNKNOWN = auto()

@dataclass
class Torrent:
name: str
status: TorrentStatus
hashStr: str

def str_to_torrent_status(status: str) -> TorrentStatus:
if status == 'stopped':
return TorrentStatus.STOPPED
elif status == 'seeding':
return TorrentStatus.SEEDING
elif status == 'checking':
return TorrentStatus.CHECKING
elif status == 'downloading':
return TorrentStatus.DOWNLOADING
else:
return TorrentStatus.UNKNOWN

def torrent_status_to_emoji(status: TorrentStatus) -> str:
if status == TorrentStatus.CHECKING:
return '🔎'
elif status == TorrentStatus.SEEDING:
return '🔋'
elif status == TorrentStatus.STOPPED:
return '⛔'
elif status == TorrentStatus.DOWNLOADING:
return '⏳'
elif status == TorrentStatus.UNKNOWN:
return '❓'

class TransmissionConnection:
def __init__(self, address='localhost', login=None, password=None, port=9091):
try:
if login and password:
self._conn: trpc.Client = trpc.Client(address=address, user=login, password=password, port=port)
else:
self._conn: trpc.Client = trpc.Client(address=address, port=port)
self._torrents, self._torrents_dict = self.get_torrents()
except Exception as e:
raise e

def get_torrents(self) -> (List[Torrent], Dict[str, Torrent]):
l = [Torrent(t.name, str_to_torrent_status(t.status), t.hashString) for t in self._conn.get_torrents()]
d = {le.hashStr: le for le in l}
return l, d

def _refresh_torrents(self):
self._torrents, self._torrents_dict = self.get_torrents()

def add_torrent(self, url):
self._conn.add_torrent(url)

def stop_torrent(self, torrent: Torrent):
self._conn.stop_torrent(torrent.hashStr)

def start_torrent(self, torrent: Torrent):
self._conn.start_torrent(torrent.hashStr)

def del_torrent(self, torrent: Torrent, delete_data=False):
self.stop_torrent(torrent)
self._conn.remove_torrent(torrent.hashStr, delete_data=delete_data)

def __getitem__(self, key):
self._refresh_torrents()
if isinstance(key, int):
return self._torrents[key]
if isinstance(key, str):
return self._torrents_dict[key]

def __len__(self):
self._refresh_torrents()
return len(self._torrents)

def __delitem__(self, key):
self._refresh_torrents()
self.del_torrent(key)

def __iter__(self):
self._refresh_torrents()
return iter(self._torrents)

def __list__(self):
self._refresh_torrents()
return self._torrents
102 changes: 102 additions & 0 deletions bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import base64
import os
import time
from enum import Enum

import telebot
from telebot import types

from consts import TOKKEN, ACCESS, LIST_BUTTON_TEXT, list_keyboard
from TransmissionConnection import (TorrentStatus, TransmissionConnection,
torrent_status_to_emoji)

bot = telebot.TeleBot(TOKKEN)


tc = TransmissionConnection(os.environ.get('TRANSMISSION_URL', 'localhost'), os.environ.get('TRANSMISSION_LOGIN', None), os.environ.get('TRANSMISSION_PASSWORD', None), os.environ.get('TRANSMISSION_PORT', 9091))

class AccessDeniedException(Exception):
pass

def access(message):
if message.from_user.id not in ACCESS:
raise AccessDeniedException

def torrent_info(hashStr: str) -> (str, types.InlineKeyboardMarkup):
torrent = tc[hashStr]
keyboard = types.InlineKeyboardMarkup()
if torrent.status == TorrentStatus.STOPPED:
key_start = types.InlineKeyboardButton(text=f'Раздавать', callback_data=f'start_{torrent.hashStr}')
keyboard.add(key_start)
if torrent.status == TorrentStatus.SEEDING:
key_pause = types.InlineKeyboardButton(text=f'Поставить на паузу', callback_data=f'pause_{torrent.hashStr}')
keyboard.add(key_pause)
key_del = types.InlineKeyboardButton(text=f'Удалить {torrent.name}', callback_data=f'del_{torrent.hashStr}')
keyboard.add(key_del)
text = f'{torrent.name}\nstatus: {torrent_status_to_emoji(torrent.status)}'
return text, keyboard


def list_torrents(chat_id):
for torrent in tc:
text, keyboard = torrent_info(torrent.hashStr)
bot.send_message(chat_id, text=text, reply_markup=keyboard)

@bot.message_handler(content_types=['text'])
def get_text_message(message):
try:
access(message)
if message.text == '/help' or message.text == '/start':
bot.send_message(message.from_user.id, 'Перешли мне торрент файл или магнет ссылку и я начну скачивание. Введи /list – для просмотра списка торрентов. Этим ботом могут пользоваться только доверенные лица', reply_markup=list_keyboard)
elif message.text == '/list' or message.text == LIST_BUTTON_TEXT:
list_torrents(message.from_user.id)
elif message.text.startswith('magnet:'):
tc.add_torrent(message.text)
bot.send_message(message.from_user.id, f'{tc[-1].name} добавлен', reply_markup=list_keyboard)
except AccessDeniedException:
bot.send_message(message.from_user.id, 'Этот бот не для тебя')


@bot.message_handler(content_types=['document'])
def get_document_message(message):
try:
access(message)
if len(message.document.file_name) < 8 or message.document.file_name[-7:] != 'torrent':
bot.send_message(message.from_user.id, 'Можно мне отправить только torrent файл')
file_info = bot.get_file(message.document.file_id)
new_torrent = bot.download_file(file_info.file_path)
new_torrent = base64.b64encode(bytes(new_torrent)).decode('utf-8')
tc.add_torrent(new_torrent)
bot.send_message(message.from_user.id, f'{tc[-1].name} добавлен', reply_markup=list_keyboard)
except AccessDeniedException:
bot.send_message(message.from_user.id, 'Этот бот не для тебя')

@bot.callback_query_handler(func=lambda call: True)
def callback_worker(call):
for torrent in tc:
if call.data == f'del_{torrent.hashStr}':
keyboard = types.InlineKeyboardMarkup()
keyboard.add(types.InlineKeyboardButton(text=f'Да', callback_data=f'del_agree_{torrent.hashStr}'))
keyboard.add(types.InlineKeyboardButton(text=f'Нет', callback_data=f'del_disagree_{torrent.hashStr}'))
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=f"ТЫ УВЕРЕН?", reply_markup=keyboard)
elif call.data == f'del_disagree_{torrent.hashStr}':
info, keyboard = torrent_info(torrent.hashStr)
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=info, reply_markup=keyboard)
elif call.data == f'del_agree_{torrent.hashStr}':
tc.del_torrent(torrent, delete_data=True)


bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=f"{torrent.name} удален")
elif call.data == f'start_{torrent.hashStr}':
tc.start_torrent(torrent)
text, keyboard = torrent_info(torrent.hashStr)
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=text, reply_markup=keyboard)
elif call.data == f'pause_{torrent.hashStr}':
tc.stop_torrent(torrent)
text, keyboard = torrent_info(torrent.hashStr)
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=text, reply_markup=keyboard)
elif call.data == 'list':
list_torrents(call.message.chat.id)

if __name__ == "__main__":
bot.polling(none_stop=True)
16 changes: 16 additions & 0 deletions consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os

from telebot import apihelper, types

TOKKEN = os.environ.get('TOKEN')
ACCESS = set(int(i) for i in os.environ.get('ACCESS', '').split(':'))

LIST_BUTTON_TEXT = 'Список'
list_keyboard = types.ReplyKeyboardMarkup()
list_keyboard.add(types.KeyboardButton(text=LIST_BUTTON_TEXT))

TIME_SHEDULE_SEC = int(os.environ.get('TIME_SHEDULE_SEC', 600))

apihelper.proxy = {
'https': f'socks5://{os.environ.get("SOCKS5_LOGIN")}:{os.environ.get("SOCKS5_PASSWORD")}@{os.environ.get("SOCKS5_ADDRESS")}'
}
17 changes: 17 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
astroid==2.3.3
certifi==2019.11.28
chardet==3.0.4
gunicorn==20.0.4
idna==2.6
isort==4.3.21
lazy-object-proxy==1.4.3
mccabe==0.6.1
pylint==2.4.4
PySocks==1.7.1
pyTelegramBotAPI==3.6.7
requests==2.18.4
schedule==0.6.0
six==1.14.0
transmissionrpc==0.11
urllib3==1.22
wrapt==1.11.2
Binary file added screenshots/image1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/image2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/image3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6118de5

Please sign in to comment.