Merge branch 'master' into node-build-flow

This commit is contained in:
Tyler Vigario
2020-05-29 21:12:22 -07:00
10 changed files with 240 additions and 124 deletions

View File

@ -55,7 +55,6 @@ tar -xzf botamusique.tar.gz
cd botamusique cd botamusique
python3 -m venv venv python3 -m venv venv
venv/bin/pip install wheel venv/bin/pip install wheel
venv/bin/pip install -r pymumble/requirements.txt
venv/bin/pip install -r requirements.txt venv/bin/pip install -r requirements.txt
``` ```

View File

@ -1,9 +1,10 @@
# coding=utf-8 # coding=utf-8
import logging import logging
import math import secrets
import datetime
import pymumble_py3 as pymumble import json
import re import re
import pymumble_py3 as pymumble
import constants import constants
import interface import interface
@ -21,12 +22,12 @@ log = logging.getLogger("bot")
def register_all_commands(bot): def register_all_commands(bot):
bot.register_command(constants.commands('joinme'), cmd_joinme, no_partial_match=False, access_outside_channel=True) bot.register_command(constants.commands('joinme'), cmd_joinme, access_outside_channel=True)
bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True) bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True) bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True) bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True) bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=True) bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('play'), cmd_play) bot.register_command(constants.commands('play'), cmd_play)
bot.register_command(constants.commands('pause'), cmd_pause) bot.register_command(constants.commands('pause'), cmd_pause)
bot.register_command(constants.commands('play_file'), cmd_play_file) bot.register_command(constants.commands('play_file'), cmd_play_file)
@ -42,8 +43,8 @@ def register_all_commands(bot):
bot.register_command(constants.commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True) bot.register_command(constants.commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True)
bot.register_command(constants.commands('stop'), cmd_stop) bot.register_command(constants.commands('stop'), cmd_stop)
bot.register_command(constants.commands('clear'), cmd_clear) bot.register_command(constants.commands('clear'), cmd_clear)
bot.register_command(constants.commands('kill'), cmd_kill) bot.register_command(constants.commands('kill'), cmd_kill, admin=True)
bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True) bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True, admin=True)
bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout) bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
bot.register_command(constants.commands('volume'), cmd_volume) bot.register_command(constants.commands('volume'), cmd_volume)
bot.register_command(constants.commands('ducking'), cmd_ducking) bot.register_command(constants.commands('ducking'), cmd_ducking)
@ -64,9 +65,13 @@ def register_all_commands(bot):
bot.register_command(constants.commands('search'), cmd_search_library) bot.register_command(constants.commands('search'), cmd_search_library)
bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist) bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist)
bot.register_command(constants.commands('delete_from_library'), cmd_delete_from_library) bot.register_command(constants.commands('delete_from_library'), cmd_delete_from_library)
bot.register_command(constants.commands('drop_database'), cmd_drop_database, no_partial_match=True) bot.register_command(constants.commands('drop_database'), cmd_drop_database, no_partial_match=True, admin=True)
bot.register_command(constants.commands('rescan'), cmd_refresh_cache, no_partial_match=True) bot.register_command(constants.commands('rescan'), cmd_refresh_cache, no_partial_match=True)
bot.register_command(constants.commands('requests_webinterface_access'), cmd_web_access) bot.register_command(constants.commands('requests_webinterface_access'), cmd_web_access)
bot.register_command(constants.commands('add_webinterface_user'), cmd_web_user_add, admin=True)
bot.register_command(constants.commands('remove_webinterface_user'), cmd_web_user_remove, admin=True)
bot.register_command(constants.commands('list_webinterface_user'), cmd_web_user_list, admin=True)
bot.register_command(constants.commands('change_user_password'), cmd_user_password, no_partial_match=True)
# Just for debug use # Just for debug use
bot.register_command('rtrms', cmd_real_time_rms, True) bot.register_command('rtrms', cmd_real_time_rms, True)
#bot.register_command('loop', cmd_loop_state, True) #bot.register_command('loop', cmd_loop_state, True)
@ -126,67 +131,47 @@ def cmd_joinme(bot, user, text, command, parameter):
def cmd_user_ban(bot, user, text, command, parameter): def cmd_user_ban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user): if parameter:
if parameter: bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
else:
bot.mumble.users[text.actor].send_text_message(util.get_user_ban())
else: else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) bot.mumble.users[text.actor].send_text_message(util.get_user_ban())
return
def cmd_user_unban(bot, user, text, command, parameter): def cmd_user_unban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user): if parameter:
if parameter: bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_ban(bot, user, text, command, parameter): def cmd_url_ban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user): if parameter:
if parameter: bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
id = item_id_generators['url'](url=parameter) id = item_id_generators['url'](url=parameter)
var.cache.free_and_delete(id) var.cache.free_and_delete(id)
var.playlist.remove_by_id(id) var.playlist.remove_by_id(id)
else:
if var.playlist.current_item() and var.playlist.current_item().type == 'url':
item = var.playlist.current_item().item()
bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(item.url)))
var.cache.free_and_delete(item.id)
var.playlist.remove_by_id(item.id)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
else: else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) if var.playlist.current_item() and var.playlist.current_item().type == 'url':
return item = var.playlist.current_item().item()
bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(item.url)))
var.cache.free_and_delete(item.id)
var.playlist.remove_by_id(item.id)
else:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
def cmd_url_ban_list(bot, user, text, command, parameter): def cmd_url_ban_list(bot, user, text, command, parameter):
if bot.is_admin(user): bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_unban(bot, user, text, command, parameter): def cmd_url_unban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user): if parameter:
if parameter: bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter)))
bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter)))
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_play(bot, user, text, command, parameter): def cmd_play(bot, user, text, command, parameter):
@ -584,12 +569,8 @@ def cmd_clear(bot, user, text, command, parameter):
def cmd_kill(bot, user, text, command, parameter): def cmd_kill(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user): bot.pause()
bot.pause() bot.exit = True
bot.exit = True
else:
bot.mumble.users[text.actor].send_text_message(
constants.strings('not_admin'))
def cmd_update(bot, user, text, command, parameter): def cmd_update(bot, user, text, command, parameter):
@ -1178,25 +1159,23 @@ def cmd_refresh_cache(bot, user, text, command, parameter):
def cmd_web_access(bot, user, text, command, parameter): def cmd_web_access(bot, user, text, command, parameter):
import secrets
import datetime
import json
auth_method = var.config.get("webinterface", "auth_method") auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'token': if auth_method == 'token':
interface.banned_ip = [] interface.banned_ip = []
interface.bad_access_count = {} interface.bad_access_count = {}
user_info = var.db.get("user", user, fallback=None) user_info = var.db.get("user", user, fallback='{}')
if user_info is not None: user_dict = json.loads(user_info)
user_dict = json.loads(user_info) if 'token' in user_dict:
token = user_dict['token'] var.db.remove_option("web_token", user_dict['token'])
else:
token = secrets.token_urlsafe(5)
var.db.set("web_token", token, user)
var.db.set("user", user, json.dumps({'token': token, 'datetime': str(datetime.datetime.now()), 'IP': ''})) token = secrets.token_urlsafe(5)
user_dict['token'] = token
user_dict['token_created'] = str(datetime.datetime.now())
user_dict['last_ip'] = ''
var.db.set("web_token", token, user)
var.db.set("user", user, json.dumps(user_dict))
access_address = var.config.get("webinterface", "access_address") + "/?token=" + token access_address = var.config.get("webinterface", "access_address") + "/?token=" + token
else: else:
@ -1205,6 +1184,64 @@ def cmd_web_access(bot, user, text, command, parameter):
bot.send_msg(constants.strings('webpage_address', address=access_address), text) bot.send_msg(constants.strings('webpage_address', address=access_address), text)
def cmd_user_password(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
return
user_info = var.db.get("user", user, fallback='{}')
user_dict = json.loads(user_info)
user_dict['password'], user_dict['salt'] = util.get_salted_password_hash(parameter)
var.db.set("user", user, json.dumps(user_dict))
bot.send_msg(constants.strings('user_password_set'), text)
def cmd_web_user_add(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
return
auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
if parameter not in web_users:
web_users.append(parameter)
var.db.set("privilege", "web_access", json.dumps(web_users))
bot.send_msg(constants.strings('web_user_list', users=", ". join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
def cmd_web_user_remove(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
return
auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
if parameter in web_users:
web_users.remove(parameter)
var.db.set("privilege", "web_access", json.dumps(web_users))
bot.send_msg(constants.strings('web_user_list', users=", ". join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
def cmd_web_user_list(bot, user, text, command, parameter):
auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
bot.send_msg(constants.strings('web_user_list', users=", ". join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
# Just for debug use # Just for debug use
def cmd_real_time_rms(bot, user, text, command, parameter): def cmd_real_time_rms(bot, user, text, command, parameter):
bot._display_rms = not bot._display_rms bot._display_rms = not bot._display_rms

View File

@ -96,7 +96,7 @@ listening_addr = 127.0.0.1
listening_port = 8181 listening_port = 8181
web_logfile = web_logfile =
auth_method = 'none' auth_method = none
user = user =
password = password =
max_attempts = 10 max_attempts = 10
@ -190,6 +190,10 @@ drop_database = dropdatabase
rescan = rescan rescan = rescan
requests_webinterface_access = web requests_webinterface_access = web
list_webinterface_user = webuserlist
add_webinterface_user = webuseradd
remove_webinterface_user = webuserdel
change_user_password = password
[strings] [strings]
current_volume = Current volume: {volume}. current_volume = Current volume: {volume}.
@ -241,7 +245,10 @@ paused = Music paused.
stopped = Music stopped. stopped = Music stopped.
cleared = Playlist emptied. cleared = Playlist emptied.
database_dropped = Database dropped. All records have gone. database_dropped = Database dropped. All records have gone.
new_version_found = <h3>Update Available!</h3> New version of botamusique is available, send <i>!update</i> to update! new_version_found = <h2>Update Available!</h2> Version {new_version} of botamusique is available! <hr />
<h3>Changelog</h3> {changelog} <hr /> Send <i>!update</i> to update!
update_successful = <h2>botamusique v{version} Installed!</h2><hr />
<h3>Changelog</h3> {changelog} <hr /> Visit <a href="https://github.com/azlux/botamusique">our github repo</a> for more details!
start_updating = Start updating... start_updating = Start updating...
file_missed = Music file '{file}' missed! This item has been removed from the playlist. file_missed = Music file '{file}' missed! This item has been removed from the playlist.
unknown_mode = Unknown playback mode '{mode}'. It should be one of <i>one-shot</i>, <i>repeat</i>, <i>random</i>. unknown_mode = Unknown playback mode '{mode}'. It should be one of <i>one-shot</i>, <i>repeat</i>, <i>random</i>.
@ -263,6 +270,9 @@ cleared_tags_from_all = Removed all tags from songs on the playlist.
shortlist_instruction = Use <i>!sl {indexes}</i> to play the item you want. shortlist_instruction = Use <i>!sl {indexes}</i> to play the item you want.
auto_paused = Use <i>!play</i> to resume music! auto_paused = Use <i>!play</i> to resume music!
webpage_address= Your own address to access the web interface is <a href="{address}">{address}</a> webpage_address= Your own address to access the web interface is <a href="{address}">{address}</a>
web_user_list = Following users has the privilege to access the web interface: <br /> {users}
user_password_set = Your password has been updated.
command_disabled = {command}: command disabled!
help = <h3>Commands</h3> help = <h3>Commands</h3>
<b>Control</b> <b>Control</b>

View File

@ -135,8 +135,8 @@ port = 64738
#auth_method = token #auth_method = token
#max_attempts = 10 #max_attempts = 10
# 'user', 'password': If auth_method set to 'password', you need to set the username and # 'user', 'password': If auth_method set to 'password', you need to set the default
# password. # username and password. You can add more users by '!webadduser'
#user = botamusique #user = botamusique
#password = mumble #password = mumble

View File

@ -66,7 +66,7 @@ class ReverseProxied(object):
web = Flask(__name__) web = Flask(__name__)
web.config['TEMPLATES_AUTO_RELOAD'] = True web.config['TEMPLATES_AUTO_RELOAD'] = True
log = logging.getLogger("bot") log = logging.getLogger("bot")
user = 'webuser' user = 'Remote Control'
def init_proxy(): def init_proxy():
@ -82,7 +82,18 @@ def check_auth(username, password):
"""This function is called to check if a username / """This function is called to check if a username /
password combination is valid. password combination is valid.
""" """
return username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password")
if username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password"):
return True
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
if username in web_users:
user_dict = json.loads(var.db.get("user", username, fallback='{}'))
if 'password' in user_dict and 'salt' in user_dict and \
util.verify_password(password, user_dict['password'], user_dict['salt']):
return True
return False
def authenticate(): def authenticate():
@ -109,8 +120,9 @@ def requires_auth(f):
if auth_method == 'password': if auth_method == 'password':
auth = request.authorization auth = request.authorization
if not auth or not check_auth(auth.username, auth.password): if auth:
if auth: user = auth.username
if not check_auth(auth.username, auth.password):
if request.remote_addr in bad_access_count: if request.remote_addr in bad_access_count:
bad_access_count[request.remote_addr] += 1 bad_access_count[request.remote_addr] += 1
log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}." log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}."
@ -122,6 +134,8 @@ def requires_auth(f):
else: else:
bad_access_count[request.remote_addr] = 1 bad_access_count[request.remote_addr] = 1
log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}.") log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}.")
return authenticate()
else:
return authenticate() return authenticate()
if auth_method == 'token': if auth_method == 'token':
if 'user' in session and 'token' not in request.args: if 'user' in session and 'token' not in request.args:

View File

@ -82,7 +82,7 @@ class BaseItem:
def add_tags(self, tags): def add_tags(self, tags):
for tag in tags: for tag in tags:
if tag not in self.tags: if tag and tag not in self.tags:
self.tags.append(tag) self.tags.append(tag)
self.version += 1 self.version += 1

View File

@ -10,8 +10,9 @@ from database import Condition
from media.item import ValidationFailedError, PreparationFailedError from media.item import ValidationFailedError, PreparationFailedError
def get_playlist(mode, _list=None, index=None): def get_playlist(mode, _list=None, _index=None):
if _list and index is None: index = -1
if _list and _index is None:
index = _list.current_index index = _list.current_index
if _list is None: if _list is None:

View File

@ -66,11 +66,8 @@ class MumbleBot:
self.read_pcm_size = 0 self.read_pcm_size = 0
# self.download_threads = [] # self.download_threads = []
self.wait_for_ready = False # flag for the loop are waiting for download to complete in the other thread self.wait_for_ready = False # flag for the loop are waiting for download to complete in the other thread
self.on_killing = threading.Lock() # lock to acquire when killing ffmpeg thread is asked but ffmpeg is not
if var.config.getboolean("bot", "auto_check_update"): # killed yet
th = threading.Thread(target=self.check_update, name="UpdateThread")
th.daemon = True
th.start()
if args.host: if args.host:
host = args.host host = args.host
@ -129,7 +126,7 @@ class MumbleBot:
self.ducking_release = time.time() self.ducking_release = time.time()
self.last_volume_cycle_time = time.time() self.last_volume_cycle_time = time.time()
if not var.db.has_option("bot", "ducking") and var.config.getboolean("bot", "ducking", fallback=False)\ if not var.db.has_option("bot", "ducking") and var.config.getboolean("bot", "ducking", fallback=False) \
or var.config.getboolean("bot", "ducking"): or var.config.getboolean("bot", "ducking"):
self.is_ducking = True self.is_ducking = True
self.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05) self.ducking_volume = var.config.getfloat("bot", "ducking_volume", fallback=0.05)
@ -144,8 +141,10 @@ class MumbleBot:
"Unknown action for when_nobody_in_channel" "Unknown action for when_nobody_in_channel"
if var.config.get("bot", "when_nobody_in_channel", fallback='') in ['pause', 'pause_resume', 'stop']: if var.config.get("bot", "when_nobody_in_channel", fallback='') in ['pause', 'pause_resume', 'stop']:
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERREMOVED, self.users_changed) user_change_callback = \
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERUPDATED, self.users_changed) lambda user, action: threading.Thread(target=self.users_changed, args=(user, action), daemon=True).start()
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERREMOVED, user_change_callback)
self.mumble.callbacks.set_callback(pymumble.constants.PYMUMBLE_CLBK_USERUPDATED, user_change_callback)
# Debug use # Debug use
self._loop_status = 'Idle' self._loop_status = 'Idle'
@ -154,40 +153,55 @@ class MumbleBot:
self.redirect_ffmpeg_log = var.config.getboolean('debug', 'redirect_ffmpeg_log', fallback=True) self.redirect_ffmpeg_log = var.config.getboolean('debug', 'redirect_ffmpeg_log', fallback=True)
if var.config.getboolean("bot", "auto_check_update"):
th = threading.Thread(target=self.check_update, name="UpdateThread")
th.daemon = True
th.start()
last_startup_version = var.db.get("bot", "version", fallback=None)
if not last_startup_version or version.parse(last_startup_version) < version.parse(self.version):
var.db.set("bot", "version", self.version)
changelog = util.fetch_changelog().replace("\n", "<br>")
self.send_channel_msg(constants.strings("update_successful", version=self.version, changelog=changelog))
# Set the CTRL+C shortcut # Set the CTRL+C shortcut
def ctrl_caught(self, signal, frame): def ctrl_caught(self, signal, frame):
self.log.info( self.log.info(
"\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit)) "\nSIGINT caught, quitting, {} more to kill".format(2 - self.nb_exit))
self.exit = True
self.pause()
if self.nb_exit > 1:
self.log.info("Forced Quit")
sys.exit(0)
self.nb_exit += 1
if var.config.getboolean('bot', 'save_playlist', fallback=True) \ if var.config.getboolean('bot', 'save_playlist', fallback=True) \
and var.config.get("bot", "save_music_library", fallback=True): and var.config.get("bot", "save_music_library", fallback=True):
self.log.info("bot: save playlist into database") self.log.info("bot: save playlist into database")
var.playlist.save() var.playlist.save()
if self.nb_exit > 1:
self.log.info("Forced Quit")
sys.exit(0)
self.nb_exit += 1
self.exit = True
def check_update(self): def check_update(self):
self.log.debug("update: checking for updates...") self.log.debug("update: checking for updates...")
new_version = util.new_release_version() new_version = util.new_release_version()
if version.parse(new_version) > version.parse(self.version): if version.parse(new_version) > version.parse(self.version):
changelog = util.fetch_changelog()
self.log.info("update: new version %s found, current installed version %s." % (new_version, self.version)) self.log.info("update: new version %s found, current installed version %s." % (new_version, self.version))
self.send_channel_msg(constants.strings('new_version_found')) self.log.info("update: changelog: " + changelog)
changelog = changelog.replace("\n", "<br>")
self.send_channel_msg(constants.strings('new_version_found', new_version=new_version, changelog=changelog))
else: else:
self.log.debug("update: no new version found.") self.log.debug("update: no new version found.")
def register_command(self, cmd, handle, no_partial_match=False, access_outside_channel=False): def register_command(self, cmd, handle, no_partial_match=False, access_outside_channel=False, admin=False):
cmds = cmd.split(",") cmds = cmd.split(",")
for command in cmds: for command in cmds:
command = command.strip() command = command.strip()
if command: if command:
self.cmd_handle[command] = {'handle': handle, self.cmd_handle[command] = {'handle': handle,
'partial_match': not no_partial_match, 'partial_match': not no_partial_match,
'access_outside_channel': access_outside_channel} 'access_outside_channel': access_outside_channel,
'admin': admin}
self.log.debug("bot: command added: " + command) self.log.debug("bot: command added: " + command)
def set_comment(self): def set_comment(self):
@ -275,6 +289,10 @@ class MumbleBot:
constants.strings('bad_command', command=command)) constants.strings('bad_command', command=command))
return return
if self.cmd_handle[command_exc]['admin'] and not self.is_admin(user):
self.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
if not self.cmd_handle[command_exc]['access_outside_channel'] \ if not self.cmd_handle[command_exc]['access_outside_channel'] \
and not self.is_admin(user) \ and not self.is_admin(user) \
and not var.config.getboolean('bot', 'allow_other_channel_message') \ and not var.config.getboolean('bot', 'allow_other_channel_message') \
@ -336,6 +354,9 @@ class MumbleBot:
# ======================= # =======================
def launch_music(self, music_wrapper, start_from=0): def launch_music(self, music_wrapper, start_from=0):
self.on_killing.acquire()
self.on_killing.release()
assert music_wrapper.is_ready() assert music_wrapper.is_ready()
uri = music_wrapper.uri() uri = music_wrapper.uri()
@ -469,7 +490,7 @@ class MumbleBot:
var.cache.free_and_delete(current.id) var.cache.free_and_delete(current.id)
# move to the next song. # move to the next song.
if not self.wait_for_ready: # if wait_for_ready flag is not true, move to the next song. if not self.wait_for_ready: # if wait_for_ready flag is not true, move to the next song.
if var.playlist.next(): if var.playlist.next():
current = var.playlist.current_item() current = var.playlist.current_item()
try: try:
@ -527,20 +548,22 @@ class MumbleBot:
if delta > 0.001: if delta > 0.001:
if self.is_ducking and self.on_ducking: if self.is_ducking and self.on_ducking:
self.volume = (self.volume - self.ducking_volume) * math.exp(- delta / 0.2) + self.ducking_volume self.volume = (self.volume - self.ducking_volume) * math.exp(- delta / 0.2) + self.ducking_volume
elif self.on_killing.locked():
self.volume = self.volume_set - (self.volume_set - self.volume) * math.exp(- delta / 0.05)
else: else:
self.volume = self.volume_set - (self.volume_set - self.volume) * math.exp(- delta / 0.5) self.volume = self.volume_set - (self.volume_set - self.volume) * math.exp(- delta / 0.5)
self.last_volume_cycle_time = time.time() self.last_volume_cycle_time = time.time()
def ducking_sound_received(self, user, sound): def ducking_sound_received(self, user, sound):
rms = audioop.rms(sound.pcm, 2) rms = audioop.rms(sound.pcm, 2)
self._max_rms = max(rms, self._max_rms) self._max_rms = max(rms, self._max_rms)
if self._display_rms: if self._display_rms:
if rms < self.ducking_threshold: if rms < self.ducking_threshold:
print('%6d/%6d ' % (rms, self._max_rms) + '-'*int(rms/200), end='\r') print('%6d/%6d ' % (rms, self._max_rms) + '-' * int(rms / 200), end='\r')
else: else:
print('%6d/%6d ' % (rms, self._max_rms) + '-'*int(self.ducking_threshold/200) print('%6d/%6d ' % (rms, self._max_rms) + '-' * int(self.ducking_threshold / 200)
+ '+'*int((rms - self.ducking_threshold)/200), end='\r') + '+' * int((rms - self.ducking_threshold) / 200), end='\r')
if rms > self.ducking_threshold: if rms > self.ducking_threshold:
if self.on_ducking is False: if self.on_ducking is False:
@ -582,22 +605,31 @@ class MumbleBot:
def interrupt(self): def interrupt(self):
# Kill the ffmpeg thread # Kill the ffmpeg thread
if self.thread: if not self.on_killing.locked():
self.thread.kill() self.on_killing.acquire()
self.thread = None if self.thread:
self.song_start_at = -1 volume_set = self.volume_set
self.read_pcm_size = 0 self.volume_set = 0
self.playhead = 0
while self.volume > 0.01 and self.thread: # Waiting for volume_cycle to gradually tune volume to 0.
time.sleep(0.01)
self.thread.kill()
self.thread = None
self.volume_set = volume_set
self.on_killing.release()
self.song_start_at = -1
self.read_pcm_size = 0
def pause(self): def pause(self):
# Kill the ffmpeg thread # Kill the ffmpeg thread
if self.thread: self.interrupt()
self.pause_at_id = var.playlist.current_item().id
self.thread.kill()
self.thread = None
self.is_pause = True self.is_pause = True
self.song_start_at = -1 self.song_start_at = -1
self.log.info("bot: music paused at %.2f seconds." % self.playhead) if len(var.playlist) > 0:
self.pause_at_id = var.playlist.current_item().id
self.log.info("bot: music paused at %.2f seconds." % self.playhead)
def resume(self): def resume(self):
self.is_pause = False self.is_pause = False
@ -698,7 +730,7 @@ if __name__ == '__main__':
bot_logger.setLevel(logging.ERROR) bot_logger.setLevel(logging.ERROR)
bot_logger.error("Starting in ERROR loglevel") bot_logger.error("Starting in ERROR loglevel")
logfile = util.solve_filepath(var.config.get('bot', 'logfile')) logfile = util.solve_filepath(var.config.get('bot', 'logfile').strip())
handler = None handler = None
if logfile: if logfile:
print(f"Redirecting stdout and stderr to log file: {logfile}") print(f"Redirecting stdout and stderr to log file: {logfile}")
@ -780,4 +812,3 @@ if __name__ == '__main__':
# Start the main loop. # Start the main loop.
var.bot.loop() var.bot.loop()

27
util.py
View File

@ -16,7 +16,7 @@ import youtube_dl
from importlib import reload from importlib import reload
from sys import platform from sys import platform
import traceback import traceback
import urllib.request import requests
from packaging import version from packaging import version
log = logging.getLogger("bot") log = logging.getLogger("bot")
@ -101,8 +101,15 @@ def get_user_ban():
def new_release_version(): def new_release_version():
v = urllib.request.urlopen(urllib.request.Request("https://packages.azlux.fr/botamusique/version")).read() r = requests.get("https://packages.azlux.fr/botamusique/version")
return v.rstrip().decode() v = r.text
return v.rstrip()
def fetch_changelog():
r = requests.get("https://packages.azlux.fr/botamusique/changelog")
c = r.text
return c
def update(current_version): def update(current_version):
@ -379,6 +386,20 @@ def parse_file_size(human):
raise ValueError("Invalid file size given.") raise ValueError("Invalid file size given.")
def get_salted_password_hash(password):
salt = os.urandom(10)
hashed = hashlib.pbkdf2_hmac('sha1', password.encode("utf-8"), salt, 100000)
return hashed.hex(), salt.hex()
def verify_password(password, salted_hash, salt):
hashed = hashlib.pbkdf2_hmac('sha1', password.encode("utf-8"), bytearray.fromhex(salt), 100000)
if hashed.hex() == salted_hash:
return True
return False
class LoggerIOWrapper(io.TextIOWrapper): class LoggerIOWrapper(io.TextIOWrapper):
def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer): def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer):
super().__init__(fallback_io_buffer, write_through=True) super().__init__(fallback_io_buffer, write_through=True)

View File

@ -125,7 +125,10 @@ function displayPlaylist(data) {
playlist_loading.hide(); playlist_loading.hide();
$(".playlist-item").remove(); $(".playlist-item").remove();
let items = data.items; let items = data.items;
playlist_items = items; playlist_items = {};
for (let i in items){
playlist_items[items[i].index] = items[i]
}
let length = data.length; let length = data.length;
let start_from = data.start_from; let start_from = data.start_from;
playlist_range_from = start_from; playlist_range_from = start_from;
@ -156,7 +159,7 @@ function displayPlaylist(data) {
} }
displayActiveItem(data.current_index); displayActiveItem(data.current_index);
updatePlayerInfo(items[data.current_index - data.start_from]); updatePlayerInfo(playlist_items[data.current_index]);
bindPlaylistEvent(); bindPlaylistEvent();
playlist_table.animate({ opacity: 1 }, 200); playlist_table.animate({ opacity: 1 }, 200);
}); });
@ -236,7 +239,7 @@ function checkForPlaylistUpdate() {
updatePlaylist(); updatePlaylist();
} else { } else {
playlist_current_index = data.current_index; playlist_current_index = data.current_index;
updatePlayerInfo(playlist_items[data.current_index - data.start_from]); updatePlayerInfo(playlist_items[data.current_index]);
displayActiveItem(data.current_index); displayActiveItem(data.current_index);
} }
} }