filebot/bot.py

448 lines
21 KiB
Python

# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
from wrapper.tox import Tox
import os
from settings import *
from wrapper.toxcore_enums_and_consts import *
from ctypes import *
from util import Singleton, folder_size
from file_transfers import *
from collections import defaultdict
global LOG
import logging
LOG = logging.getLogger(__name__)
class Bot(Singleton):
def __init__(self, tox):
"""
:param tox: tox instance
"""
super(Bot, self).__init__()
self._tox = tox
self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number)
self._downloads = defaultdict(int) # defaultdict of downloads count
# -----------------------------------------------------------------------------------------------------------------
# Edit current user's data
# -----------------------------------------------------------------------------------------------------------------
def set_name(self, value):
self._tox.self_set_name(value.encode('utf-8'))
def set_status_message(self, value):
self._tox.self_set_status_message(value.encode('utf-8'))
# -----------------------------------------------------------------------------------------------------------------
# Private messages
# -----------------------------------------------------------------------------------------------------------------
def send_message(self, number, message, message_type=TOX_MESSAGE_TYPE['NORMAL']):
"""
Send message with message splitting
:param number: friend's number
:param message: message text
:param message_type: type of message
"""
while len(message) > TOX_MAX_MESSAGE_LENGTH:
size = TOX_MAX_MESSAGE_LENGTH * 4 / 5
last_part = message[size:TOX_MAX_MESSAGE_LENGTH]
if ' ' in last_part:
index = last_part.index(' ')
elif ',' in last_part:
index = last_part.index(',')
elif '.' in last_part:
index = last_part.index('.')
else:
index = TOX_MAX_MESSAGE_LENGTH - size - 1
index += size + 1
self._tox.friend_send_message(number, message_type, message[:index])
message = message[index:]
self._tox.friend_send_message(number, message_type, message)
def new_message(self, friend_num, message):
"""
New message
:param friend_num: number of friend who sent message
:param message: text of message
"""
id = self._tox.friend_get_public_key(friend_num) # public key of user
settings = Settings.get_instance()
message = message.strip()
# message parsing
if message == 'files': # get file list
if id in settings['read']:
s = ''
for f in os.listdir(settings['folder']):
f = unicode(f)
if os.path.isfile(os.path.join(settings['folder'], f)):
s += u'{} ({} bytes)\n'.format(f, os.path.getsize(os.path.join(settings['folder'], f)))
if not s:
s = 'Nothing found'
self.send_message(friend_num, s.encode('utf-8'), TOX_MESSAGE_TYPE['NORMAL'])
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message.startswith('get '): # download file or all files
if id in settings['read']:
if '--all' not in message:
path = settings['folder'] + '/' + unicode(message[4:])
if os.path.exists(unicode(path)):
self.send_file(unicode(path), friend_num)
else:
self.send_message(friend_num, 'Wrong file name'.encode('utf-8'))
else:
for f in os.listdir(settings['folder']):
if os.path.isfile(os.path.join(settings['folder'], f)):
self.send_file(unicode(os.path.join(settings['folder'], f)), friend_num)
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message == 'help': # help
self.send_message(friend_num, """
help - list of commands\n
rights - get access rights\n
files - show list of files (get access)\n
id - get bot's id (get access)\n
share <ToxID> <file_name> - send file to friend (get access)\n
share --all <file_name> - send file to all friends (get access)\n
size <file_name> - get size of file (get access)\n
get <file_name> - get file with specified filename (get access)\n
get --all - get all files (get access)\n
stats - show statistics (write access)\n
del <file_name> - remove file with specified filename (delete access)\n
rename <file_name> --new <new_file_name> - rename file (delete access)\n
user <ToxID> <rights> - new rights (example: rwdm) for user (masters only)\n
status <new_status> - new status message (masters only)\n
name <new_name> - new name (masters only)\n
message <ToxID> <message_text> - send message to friend (masters only)\n
message --all <message_text> - send message to all friends (masters only)\n
stop - stop bot (masters only)\n
fsize <folder_size_in_MB> - set folder size in MB (masters only)\n
Users with write access can send files to bot.
""".encode('utf-8'))
elif message == 'rights': # get rights
self.send_message(friend_num, 'Read: {}\nWrite: {}\nDelete: {}\nMaster: {}'
.format('Yes' if id in settings['read'] else 'No, sorry',
'Yes' if id in settings['write'] else 'No',
'Yes' if id in settings['delete'] else 'No',
'Yes, sir!' if id in settings['master'] else 'No'))
elif message.startswith('del '): # delete file
if id in settings['delete']:
path = settings['folder'] + '/' + message[4:]
if os.path.exists(path):
os.remove(path)
self.send_message(friend_num, 'File was successfully deleted')
else:
self.send_message(friend_num, 'Wrong file name'.encode('utf-8'))
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message.startswith('user '): # new rights for user
if id not in settings['master']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
return
try:
rights = message.split(' ')[2]
except:
rights = ''
id = message.split(' ')[1][:TOX_PUBLIC_KEY_SIZE * 2]
if id in settings['read']:
settings['read'].remove(id)
if id in settings['write']:
settings['write'].remove(id)
if id in settings['delete']:
settings['delete'].remove(id)
if 'r' in rights:
settings['read'].append(id)
if 'w' in rights:
settings['write'].append(id)
if 'd' in rights:
settings['delete'].append(id)
if 'm' in rights:
settings['master'].append(id)
settings.save()
self.send_message(friend_num, 'Updated'.encode('utf-8'))
elif message.startswith('status '): # new status
if id not in settings['master']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
else:
self.set_status_message(message[7:])
elif message.startswith('name '): # new name
if id not in settings['master']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
else:
self.set_name(message[5:])
elif message.startswith('share '): # send file to friend (all friends)
if id in settings['read']:
if '--all' not in message:
fl = ' '.join(message.split(' ')[2:])
try:
num = self._tox.friend_by_public_key(message.split(' ')[1][:TOX_PUBLIC_KEY_SIZE * 2])
LOG.debug(num)
self.send_file(settings['folder'] + '/' + fl, num)
except Exception as ex:
LOG.warn(ex)
self.send_message(friend_num, 'Friend not found'.encode('utf-8'))
else:
fl = ' '.join(message.split(' ')[2:])
for num in self._tox.self_get_friend_list():
if self._tox.friend_get_connection_status(num):
self.send_file(settings['folder'] + '/' + fl, num)
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message.startswith('rename '): # rename existing file
ind = message.index(' --new ')
old = message[7:ind]
new = message[ind + 7:]
if id in settings['delete']:
if os.path.exists(settings['folder'] + '/' + old):
os.rename(settings['folder'] + '/' + old, settings['folder'] + '/' + new)
self.send_message(friend_num, 'Renamed'.encode('utf-8'))
else:
self.send_message(friend_num, 'File not found'.encode('utf-8'))
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message == 'id': # get TOX ID
if id in settings['read']:
tox_id = self._tox.self_get_address()
self.send_message(friend_num, tox_id.encode('utf-8'))
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message.startswith('size '): # get file size
path = unicode(settings['folder'] + '/' + message[5:])
if id not in settings['read']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif not os.path.exists(path):
self.send_message(friend_num, 'File not found'.encode('utf-8'))
else:
bytes_size = os.path.getsize(path)
if bytes_size < 1024:
size = u'{} B'.format(bytes_size)
elif bytes_size < 1024 * 1024:
size = u'{} KB'.format(bytes_size / 1024)
else:
size = u'{} MB'.format(bytes_size / (1024 * 1024))
s = u'Size: {} ({} bytes)'.format(size, bytes_size)
self.send_message(friend_num, s.encode('utf-8'))
elif message.startswith('message '): # send message to friend (all friends)
if id not in settings['master']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif '--all' not in message:
tox_id = message.split(' ')[1][:TOX_PUBLIC_KEY_SIZE * 2]
s = ' '.join(message.split(' ')[2:])
num = self._tox.friend_by_public_key(tox_id)
try:
self.send_message(num, s.encode('utf-8'))
except:
self.send_message(friend_num, 'Friend is not online'.encode('utf-8'))
else:
s = ' '.join(message.split(' ')[2:])
for num in self._tox.self_get_friend_list():
if self._tox.friend_get_connection_status(num):
self.send_message(num, s.encode('utf-8'))
elif message == 'stats': # get stats
if id not in settings['write']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
else:
s = ''
for f in os.listdir(settings['folder']):
f = unicode(f)
if os.path.isfile(os.path.join(settings['folder'], f)):
s += u'{} ({} downloads)\n'.format(f,
self._downloads[os.path.join(settings['folder'], f)])
if not s:
s = 'Nothing found'
else:
s += u'Downloads count: {}'.format(sum(self._downloads.values()))
count = 0
for num in self._tox.self_get_friend_list():
if self._tox.friend_get_connection_status(num):
count += 1
s = 'Friends: {}\nOnline friends: {}\nFiles:\n'.format(self._tox.self_get_friend_list_size(), count) + s
self.send_message(friend_num, s.encode('utf-8'), TOX_MESSAGE_TYPE['NORMAL'])
elif message == 'stop': # stop bot
if id in settings['master']:
settings.save()
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
del self._tox
raise SystemExit()
else:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
elif message.startswith('fsize '):
if id not in settings['master']:
self.send_message(friend_num, 'Not enough rights'.encode('utf-8'))
else:
try:
size = int(message[6:])
settings['size'] = max(size, 10)
settings.save()
self.send_message(friend_num, 'Size was set'.encode('utf-8'))
except:
self.send_message(friend_num, 'Wrong command'.encode('utf-8'))
else:
self.send_message(friend_num, 'Wrong command'.encode('utf-8'))
# -----------------------------------------------------------------------------------------------------------------
# Friend requests
# -----------------------------------------------------------------------------------------------------------------
def process_friend_request(self, tox_id, message):
"""
Accept or ignore friend request
:param tox_id: tox id of contact
:param message: message
"""
LOG.info('Friend request:' +message)
self._tox.friend_add_norequest(tox_id)
settings = Settings.get_instance()
# give friend default rights
if 'r' in settings['auto_rights'] and tox_id not in settings['read']:
settings['read'].append(tox_id)
if 'w' in settings['auto_rights'] and tox_id not in settings['write']:
settings['write'].append(tox_id)
if 'd' in settings['auto_rights'] and tox_id not in settings['delete']:
settings['delete'].append(tox_id)
if 'm' in settings['auto_rights'] and tox_id not in settings['master']:
settings['master'].append(tox_id)
settings.save()
data = self._tox.get_savedata()
ProfileHelper.save_profile(data)
# -----------------------------------------------------------------------------------------------------------------
# File transfers support
# -----------------------------------------------------------------------------------------------------------------
def incoming_file_transfer(self, friend_number, file_number, size, file_name):
"""
New transfer
:param friend_number: number of friend who sent file
:param file_number: file number
:param size: file size in bytes
:param file_name: file name without path
"""
id = self._tox.friend_get_public_key(friend_number)
settings = Settings.get_instance()
fsize = folder_size(settings['folder']) + size
max_folder_size = settings['size'] * 1024 * 1024
if id in settings['write'] and fsize <= max_folder_size:
path = settings['folder']
new_file_name, i = file_name, 1
while os.path.isfile(path + '/' + new_file_name): # file with same name already exists
if '.' in file_name: # has extension
d = file_name.rindex('.')
else: # no extension
d = len(file_name)
new_file_name = file_name[:d] + ' ({})'.format(i) + file_name[d:]
i += 1
self.accept_transfer(path + '/' + new_file_name, friend_number, file_number, size)
else:
self.cancel_transfer(friend_number, file_number, False)
def cancel_transfer(self, friend_number, file_number, already_cancelled=False):
"""
Stop transfer
:param friend_number: number of friend
:param file_number: file number
:param already_cancelled: was cancelled by friend
"""
if (friend_number, file_number) in self._file_transfers:
tr = self._file_transfers[(friend_number, file_number)]
if not already_cancelled:
tr.cancel()
else:
tr.cancelled()
del self._file_transfers[(friend_number, file_number)]
else:
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['CANCEL'])
def accept_transfer(self, path, friend_number, file_number, size):
"""
:param path: path for saving
:param friend_number: friend number
:param file_number: file number
:param size: file size
"""
rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number)
self._file_transfers[(friend_number, file_number)] = rt
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['RESUME'])
def send_file(self, path, friend_number):
"""
Send file to current active friend
:param path: file path
:param friend_number: friend_number
"""
self._downloads[path] += 1
st = SendTransfer(path, self._tox, friend_number)
self._file_transfers[(friend_number, st.get_file_number())] = st
def incoming_chunk(self, friend_number, file_number, position, data):
if (friend_number, file_number) in self._file_transfers:
transfer = self._file_transfers[(friend_number, file_number)]
transfer.write_chunk(position, data)
if transfer.state:
del self._file_transfers[(friend_number, file_number)]
def outgoing_chunk(self, friend_number, file_number, position, size):
if (friend_number, file_number) in self._file_transfers:
transfer = self._file_transfers[(friend_number, file_number)]
transfer.send_chunk(position, size)
if transfer.state:
del self._file_transfers[(friend_number, file_number)]
def tox_factory(data=None, settings=None):
"""
:param data: user data from .tox file. None = no saved data, create new profile
:param settings: current profile settings. None = default settings will be used
:return: new tox instance
"""
if settings is None:
if os.getenv('socks_proxy') != '':
settings = {
'ipv6_enabled': False,
'udp_enabled': False,
'proxy_type': 2,
'proxy_host': b'127.0.0.1',
'proxy_port': 9050,
'start_port': 0,
'end_port': 0,
'tcp_port': 0
}
else:
settings = {
'ipv6_enabled': True,
'udp_enabled': True,
'proxy_type': 0,
'proxy_host': '0',
'proxy_port': 0,
'start_port': 0,
'end_port': 0,
'tcp_port': 0
}
tox_options = Tox.options_new()
tox_options.contents.udp_enabled = settings['udp_enabled']
tox_options.contents.proxy_type = settings['proxy_type']
tox_options.contents.proxy_host = settings['proxy_host']
tox_options.contents.proxy_port = settings['proxy_port']
tox_options.contents.start_port = settings['start_port']
tox_options.contents.end_port = settings['end_port']
tox_options.contents.tcp_port = settings['tcp_port']
if data: # load existing profile
tox_options.contents.savedata_type = TOX_SAVEDATA_TYPE['TOX_SAVE']
tox_options.contents.savedata_data = c_char_p(data)
tox_options.contents.savedata_length = len(data)
else: # create new profile
tox_options.contents.savedata_type = TOX_SAVEDATA_TYPE['NONE']
tox_options.contents.savedata_data = None
tox_options.contents.savedata_length = 0
# overrides
tox_options.contents.local_discovery_enabled = False
tox_options.contents.ipv6_enabled = False
tox_options.contents.hole_punching_enabled = False
LOG.debug("wrapper.tox.Tox settings: " +repr(settings))
return Tox(tox_options)