diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/__init__.py | 0 | ||||
| -rw-r--r-- | server/aoprotocol.py | 807 | ||||
| -rw-r--r-- | server/area_manager.py | 412 | ||||
| -rw-r--r-- | server/ban_manager.py | 54 | ||||
| -rw-r--r-- | server/client_manager.py | 457 | ||||
| -rw-r--r-- | server/commands.py | 1255 | ||||
| -rw-r--r-- | server/constants.py | 11 | ||||
| -rw-r--r-- | server/districtclient.py | 79 | ||||
| -rw-r--r-- | server/evidence.py | 100 | ||||
| -rw-r--r-- | server/exceptions.py | 32 | ||||
| -rw-r--r-- | server/fantacrypt.py | 45 | ||||
| -rw-r--r-- | server/logger.py | 78 | ||||
| -rw-r--r-- | server/masterserverclient.py | 89 | ||||
| -rw-r--r-- | server/tsuserver.py | 305 | ||||
| -rw-r--r-- | server/websocket.py | 215 |
15 files changed, 3939 insertions, 0 deletions
diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/server/__init__.py diff --git a/server/aoprotocol.py b/server/aoprotocol.py new file mode 100644 index 00000000..2cf6fb43 --- /dev/null +++ b/server/aoprotocol.py @@ -0,0 +1,807 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import asyncio +import re +from time import localtime, strftime +from enum import Enum + +from . import commands +from . import logger +from .exceptions import ClientError, AreaError, ArgumentError, ServerError +from .fantacrypt import fanta_decrypt +from .evidence import EvidenceList +from .websocket import WebSocket +import unicodedata + + +class AOProtocol(asyncio.Protocol): + """ + The main class that deals with the AO protocol. + """ + + class ArgType(Enum): + STR = 1, + STR_OR_EMPTY = 2, + INT = 3 + + def __init__(self, server): + super().__init__() + self.server = server + self.client = None + self.buffer = '' + self.ping_timeout = None + self.websocket = None + + def data_received(self, data): + """ Handles any data received from the network. + + Receives data, parses them into a command and passes it + to the command handler. + + :param data: bytes of data + """ + + + if self.websocket is None: + self.websocket = WebSocket(self.client, self) + if not self.websocket.handshake(data): + self.websocket = False + else: + self.client.websocket = self.websocket + + buf = data + + if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid): + self.client.transport.close() + else: + self.client.is_checked = True + + if self.websocket: + buf = self.websocket.handle(data) + + if buf is None: + buf = b'' + + if not isinstance(buf, str): + # try to decode as utf-8, ignore any erroneous characters + self.buffer += buf.decode('utf-8', 'ignore') + else: + self.buffer = buf + + if len(self.buffer) > 8192: + self.client.disconnect() + for msg in self.get_messages(): + if len(msg) < 2: + continue + # general netcode structure is not great + if msg[0] in ('#', '3', '4'): + if msg[0] == '#': + msg = msg[1:] + spl = msg.split('#', 1) + msg = '#'.join([fanta_decrypt(spl[0])] + spl[1:]) + logger.log_debug('[INC][RAW]{}'.format(msg), self.client) + try: + cmd, *args = msg.split('#') + self.net_cmd_dispatcher[cmd](self, args) + except KeyError: + logger.log_debug('[INC][UNK]{}'.format(msg), self.client) + + def connection_made(self, transport): + """ Called upon a new client connecting + + :param transport: the transport object + """ + self.client = self.server.new_client(transport) + self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect) + asyncio.get_event_loop().call_later(0.25, self.client.send_command, 'decryptor', 34) # just fantacrypt things) + + def connection_lost(self, exc): + """ User disconnected + + :param exc: reason + """ + self.server.remove_client(self.client) + self.ping_timeout.cancel() + + def get_messages(self): + """ Parses out full messages from the buffer. + + :return: yields messages + """ + while '#%' in self.buffer: + spl = self.buffer.split('#%', 1) + self.buffer = spl[1] + yield spl[0] + # exception because bad netcode + askchar2 = '#615810BC07D12A5A#' + if self.buffer == askchar2: + self.buffer = '' + yield askchar2 + + def validate_net_cmd(self, args, *types, needs_auth=True): + """ Makes sure the net command's arguments match expectations. + + :param args: actual arguments to the net command + :param types: what kind of data types are expected + :param needs_auth: whether you need to have chosen a character + :return: returns True if message was validated + """ + if needs_auth and self.client.char_id == -1: + return False + if len(args) != len(types): + return False + for i, arg in enumerate(args): + if len(arg) == 0 and types[i] != self.ArgType.STR_OR_EMPTY: + return False + if types[i] == self.ArgType.INT: + try: + args[i] = int(arg) + except ValueError: + return False + return True + + def net_cmd_hi(self, args): + """ Handshake. + + HI#<hdid:string>#% + + :param args: a list containing all the arguments + """ + if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False): + return + self.client.hdid = args[0] + if self.client.hdid not in self.client.server.hdid_list: + self.client.server.hdid_list[self.client.hdid] = [] + if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]: + self.client.server.hdid_list[self.client.hdid].append(self.client.ipid) + self.client.server.dump_hdids() + for ipid in self.client.server.hdid_list[self.client.hdid]: + if self.server.ban_manager.is_banned(ipid): + self.client.send_command('BD') + self.client.disconnect() + return + logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client) + self.client.send_command('ID', self.client.id, self.server.software, self.server.get_version_string()) + self.client.send_command('PN', self.server.get_player_count() - 1, self.server.config['playerlimit']) + + def net_cmd_id(self, args): + """ Client version and PV + + ID#<pv:int>#<software:string>#<version:string>#% + + """ + + self.client.is_ao2 = False + + if len(args) < 2: + return + + version_list = args[1].split('.') + + if len(version_list) < 3: + return + + release = int(version_list[0]) + major = int(version_list[1]) + minor = int(version_list[2]) + + if args[0] != 'AO2': + return + if release < 2: + return + elif release == 2: + if major < 2: + return + elif major == 2: + if minor < 5: + return + + self.client.is_ao2 = True + + self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts') + + def net_cmd_ch(self, _): + """ Periodically checks the connection. + + CHECK#% + + """ + self.client.send_command('CHECK') + self.ping_timeout.cancel() + self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect) + + def net_cmd_askchaa(self, _): + """ Ask for the counts of characters/evidence/music + + askchaa#% + + """ + char_cnt = len(self.server.char_list) + evi_cnt = 0 + music_cnt = sum([len(x) for x in self.server.music_pages_ao1]) + self.client.send_command('SI', char_cnt, evi_cnt, music_cnt) + + def net_cmd_askchar2(self, _): + """ Asks for the character list. + + askchar2#% + + """ + self.client.send_command('CI', *self.server.char_pages_ao1[0]) + + def net_cmd_an(self, args): + """ Asks for specific pages of the character list. + + AN#<page:int>#% + + """ + if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False): + return + if len(self.server.char_pages_ao1) > args[0] >= 0: + self.client.send_command('CI', *self.server.char_pages_ao1[args[0]]) + else: + self.client.send_command('EM', *self.server.music_pages_ao1[0]) + + def net_cmd_ae(self, _): + """ Asks for specific pages of the evidence list. + + AE#<page:int>#% + + """ + pass # todo evidence maybe later + + def net_cmd_am(self, args): + """ Asks for specific pages of the music list. + + AM#<page:int>#% + + """ + if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False): + return + if len(self.server.music_pages_ao1) > args[0] >= 0: + self.client.send_command('EM', *self.server.music_pages_ao1[args[0]]) + else: + self.client.send_done() + self.client.send_area_list() + self.client.send_motd() + + def net_cmd_rc(self, _): + """ Asks for the whole character list(AO2) + + AC#% + + """ + + self.client.send_command('SC', *self.server.char_list) + + def net_cmd_rm(self, _): + """ Asks for the whole music list(AO2) + + AM#% + + """ + + self.client.send_command('SM', *self.server.music_list_ao2) + + + def net_cmd_rd(self, _): + """ Asks for server metadata(charscheck, motd etc.) and a DONE#% signal(also best packet) + + RD#% + + """ + + self.client.send_done() + self.client.send_area_list() + self.client.send_motd() + + def net_cmd_cc(self, args): + """ Character selection. + + CC#<client_id:int>#<char_id:int>#<hdid:string>#% + + """ + if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR, needs_auth=False): + return + cid = args[1] + try: + self.client.change_character(cid) + except ClientError: + return + + def net_cmd_ms(self, args): + """ IC message. + + Refer to the implementation for details. + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.area.can_send_message(self.client): + return + + target_area = [] + + if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): + # Vanilla validation monstrosity. + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args + showname = "" + charid_pair = -1 + offset_pair = 0 + nonint_pre = 0 + elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY): + # 1.3.0 validation monstrosity. + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args + charid_pair = -1 + offset_pair = 0 + nonint_pre = 0 + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): + # 1.3.5 validation monstrosity. + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args + nonint_pre = 0 + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): + # 1.4.0 validation monstrosity. + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + else: + return + if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): + self.client.send_host_message("Iniswap is blocked in this area") + return + if len(self.client.charcurse) > 0 and folder != self.client.get_char_name(): + self.client.send_host_message("You may not iniswap while you are charcursed!") + return + if not self.client.area.blankposting_allowed: + if text == ' ': + self.client.send_host_message("Blankposting is forbidden in this area!") + return + if text.isspace(): + self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") + return + if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': + self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") + return + if text.startswith('/a '): + part = text.split(' ') + try: + aid = int(part[1]) + if self.client in self.server.area_manager.get_area_by_id(aid).owners: + target_area.append(aid) + if not target_area: + self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name)) + return + text = ' '.join(part[2:]) + except ValueError: + self.client.send_host_message("That does not look like a valid area ID!") + return + elif text.startswith('/s '): + part = text.split(' ') + for a in self.server.area_manager.areas: + if self.client in a.owners: + target_area.append(a.id) + if not target_area: + self.client.send_host_message('You don\'t any areas!') + return + text = ' '.join(part[1:]) + if msg_type not in ('chat', '0', '1'): + return + if anim_type not in (0, 1, 2, 5, 6): + return + if cid != self.client.char_id: + return + if sfx_delay < 0: + return + if button not in (0, 1, 2, 3, 4): + return + if evidence < 0: + return + if ding not in (0, 1): + return + if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8): + return + if len(showname) > 15: + self.client.send_host_message("Your IC showname is way too long!") + return + if nonint_pre == 1: + if button in (1, 2, 3, 4, 23): + if anim_type == 1 or anim_type == 2: + anim_type = 0 + elif anim_type == 6: + anim_type = 5 + if self.client.area.non_int_pres_only: + if anim_type == 1 or anim_type == 2: + anim_type = 0 + nonint_pre = 1 + elif anim_type == 6: + anim_type = 5 + nonint_pre = 1 + if not self.client.area.shouts_allowed: + # Old clients communicate the objecting in anim_type. + if anim_type == 2: + anim_type = 1 + elif anim_type == 6: + anim_type = 5 + # New clients do it in a specific objection message area. + button = 0 + # Turn off the ding. + ding = 0 + if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners): + color = 0 + if color == 6: + text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse + if len(text.strip( ' ' )) == 1: + color = 0 + else: + if text.strip( ' ' ) in ('<num>', '<percent>', '<dollar>', '<and>'): + color = 0 + if self.client.pos: + pos = self.client.pos + else: + if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): + return + msg = text[:256] + if self.client.shaken: + msg = self.client.shake_message(msg) + if self.client.disemvowel: + msg = self.client.disemvowel_message(msg) + self.client.pos = pos + if evidence: + if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all': + self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all' + self.client.area.broadcast_evidence_list() + + # Here, we check the pair stuff, and save info about it to the client. + # Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset + # and an other offset. + self.client.charid_pair = charid_pair + self.client.offset_pair = offset_pair + if anim_type not in (5, 6): + self.client.last_sprite = anim + self.client.flip = flip + self.client.claimed_folder = folder + other_offset = 0 + other_emote = '' + other_flip = 0 + other_folder = '' + + confirmed = False + if charid_pair > -1: + for target in self.client.area.clients: + if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos: + confirmed = True + other_offset = target.offset_pair + other_emote = target.last_sprite + other_flip = target.flip + other_folder = target.claimed_folder + break + + if not confirmed: + charid_pair = -1 + offset_pair = 0 + + self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) + + self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid, + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) + + self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) + + self.client.area.set_next_msg_delay(len(msg)) + logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) + + if (self.client.area.is_recording): + self.client.area.recorded_messages.append(args) + + def net_cmd_ct(self, args): + """ OOC Message + + CT#<name:string>#<message:string>#% + + """ + if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR): + return + if self.client.name != args[0] and self.client.fake_name != args[0]: + if self.client.is_valid_name(args[0]): + self.client.name = args[0] + self.client.fake_name = args[0] + else: + self.client.fake_name = args[0] + if self.client.name == '': + self.client.send_host_message('You must insert a name with at least one letter') + return + if len(self.client.name) > 30: + self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.') + return + for c in self.client.name: + if unicodedata.category(c) == 'Cf': + self.client.send_host_message('You cannot use format characters in your name!') + return + if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('<dollar>G') or self.client.name.startswith('<dollar>M'): + self.client.send_host_message('That name is reserved!') + return + if args[1].startswith(' /'): + self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.') + return + if args[1].startswith('/'): + spl = args[1][1:].split(' ', 1) + cmd = spl[0].lower() + arg = '' + if len(spl) == 2: + arg = spl[1][:256] + try: + called_function = 'ooc_cmd_{}'.format(cmd) + getattr(commands, called_function)(self.client, arg) + except AttributeError: + print('Attribute error with ' + called_function) + self.client.send_host_message('Invalid command.') + except (ClientError, AreaError, ArgumentError, ServerError) as ex: + self.client.send_host_message(ex) + else: + if self.client.shaken: + args[1] = self.client.shake_message(args[1]) + if self.client.disemvowel: + args[1] = self.client.disemvowel_message(args[1]) + self.client.area.send_command('CT', self.client.name, args[1]) + self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1]) + logger.log_server( + '[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), + args[1]), self.client) + + def net_cmd_mc(self, args): + """ Play music. + + MC#<song_name:int>#<???:int>#% + + """ + try: + area = self.server.area_manager.get_area_by_name(args[0]) + self.client.change_area(area) + except AreaError: + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.is_dj: + self.client.send_host_message('You were blockdj\'d by a moderator.') + return + if self.client.area.cannot_ic_interact(self.client): + self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!") + return + if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): + return + if args[1] != self.client.char_id: + return + if self.client.change_music_cd(): + self.client.send_host_message('You changed song too many times. Please try again after {} seconds.'.format(int(self.client.change_music_cd()))) + return + try: + name, length = self.server.get_song_data(args[0]) + + if self.client.area.jukebox: + showname = '' + if len(args) > 2: + showname = args[2] + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + self.client.area.add_jukebox_vote(self.client, name, length, showname) + logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) + else: + if len(args) > 2: + showname = args[2] + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) + self.client.area.add_music_playing_shownamed(self.client, showname, name) + else: + self.client.area.play_music(name, self.client.char_id, length) + self.client.area.add_music_playing(self.client, name) + logger.log_server('[{}][{}]Changed music to {}.' + .format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) + except ServerError: + return + except ClientError as ex: + self.client.send_host_message(ex) + + def net_cmd_rt(self, args): + """ Plays the Testimony/CE animation. + + RT#<type:string>#% + + """ + if not self.client.area.shouts_allowed: + self.client.send_host_message("You cannot use the testimony buttons here!") + return + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.can_wtce: + self.client.send_host_message('You were blocked from using judge signs by a moderator.') + return + if self.client.area.cannot_ic_interact(self.client): + self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!") + return + if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): + return + if args[0] == 'testimony1': + sign = 'WT' + elif args[0] == 'testimony2': + sign = 'CE' + elif args[0] == 'judgeruling': + sign = 'JR' + else: + return + if self.client.wtce_mute(): + self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute()))) + return + if len(args) == 1: + self.client.area.send_command('RT', args[0]) + elif len(args) == 2: + self.client.area.send_command('RT', args[0], args[1]) + self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) + logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client) + + def net_cmd_hp(self, args): + """ Sets the penalty bar. + + HP#<type:int>#<new_value:int>#% + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if self.client.area.cannot_ic_interact(self.client): + self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!") + return + if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT): + return + try: + self.client.area.change_hp(args[0], args[1]) + self.client.area.add_to_judgelog(self.client, 'changed the penalties') + logger.log_server('[{}]{} changed HP ({}) to {}' + .format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client) + except AreaError: + return + + def net_cmd_pe(self, args): + """ Adds a piece of evidence. + + PE#<name: string>#<description: string>#<image: string>#% + + """ + if len(args) < 3: + return + #evi = Evidence(args[0], args[1], args[2], self.client.pos) + self.client.area.evi_list.add_evidence(self.client, args[0], args[1], args[2], 'all') + self.client.area.broadcast_evidence_list() + + def net_cmd_de(self, args): + """ Deletes a piece of evidence. + + DE#<id: int>#% + + """ + + self.client.area.evi_list.del_evidence(self.client, self.client.evi_list[int(args[0])]) + self.client.area.broadcast_evidence_list() + + def net_cmd_ee(self, args): + """ Edits a piece of evidence. + + EE#<id: int>#<name: string>#<description: string>#<image: string>#% + + """ + + if len(args) < 4: + return + + evi = (args[1], args[2], args[3], 'all') + + self.client.area.evi_list.edit_evidence(self.client, self.client.evi_list[int(args[0])], evi) + self.client.area.broadcast_evidence_list() + + + def net_cmd_zz(self, args): + """ Sent on mod call. + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + + if not self.client.can_call_mod(): + self.client.send_host_message("You must wait 30 seconds between mod calls.") + return + + current_time = strftime("%H:%M", localtime()) + + if len(args) < 1: + self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)' + .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod) + self.client.set_mod_call_delay() + logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client) + else: + self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}' + .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod) + self.client.set_mod_call_delay() + logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client) + + def net_cmd_opKICK(self, args): + self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])]) + + def net_cmd_opBAN(self, args): + self.net_cmd_ct(['opban', '/ban {}'.format(args[0])]) + + net_cmd_dispatcher = { + 'HI': net_cmd_hi, # handshake + 'ID': net_cmd_id, # client version + 'CH': net_cmd_ch, # keepalive + 'askchaa': net_cmd_askchaa, # ask for list lengths + 'askchar2': net_cmd_askchar2, # ask for list of characters + 'AN': net_cmd_an, # character list + 'AE': net_cmd_ae, # evidence list + 'AM': net_cmd_am, # music list + 'RC': net_cmd_rc, # AO2 character list + 'RM': net_cmd_rm, # AO2 music list + 'RD': net_cmd_rd, # AO2 done request, charscheck etc. + 'CC': net_cmd_cc, # select character + 'MS': net_cmd_ms, # IC message + 'CT': net_cmd_ct, # OOC message + 'MC': net_cmd_mc, # play song + 'RT': net_cmd_rt, # WT/CE buttons + 'HP': net_cmd_hp, # penalties + 'PE': net_cmd_pe, # add evidence + 'DE': net_cmd_de, # delete evidence + 'EE': net_cmd_ee, # edit evidence + 'ZZ': net_cmd_zz, # call mod button + 'opKICK': net_cmd_opKICK, # /kick with guard on + 'opBAN': net_cmd_opBAN, # /ban with guard on + } diff --git a/server/area_manager.py b/server/area_manager.py new file mode 100644 index 00000000..cfb2be0d --- /dev/null +++ b/server/area_manager.py @@ -0,0 +1,412 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import asyncio +import random + +import time +import yaml + +from server.exceptions import AreaError +from server.evidence import EvidenceList +from enum import Enum + + +class AreaManager: + class Area: + def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False): + self.iniswap_allowed = iniswap_allowed + self.clients = set() + self.invite_list = {} + self.id = area_id + self.name = name + self.background = background + self.bg_lock = bg_lock + self.server = server + self.music_looper = None + self.next_message_time = 0 + self.hp_def = 10 + self.hp_pro = 10 + self.doc = 'No document.' + self.status = 'IDLE' + self.judgelog = [] + self.current_music = '' + self.current_music_player = '' + self.current_music_player_ipid = -1 + self.evi_list = EvidenceList() + self.is_recording = False + self.recorded_messages = [] + self.evidence_mod = evidence_mod + self.locking_allowed = locking_allowed + self.showname_changes_allowed = showname_changes_allowed + self.shouts_allowed = shouts_allowed + self.abbreviation = abbreviation + self.cards = dict() + + """ + #debug + self.evidence_list.append(Evidence("WOW", "desc", "1.png")) + self.evidence_list.append(Evidence("wewz", "desc2", "2.png")) + self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png")) + """ + + self.is_locked = self.Locked.FREE + self.blankposting_allowed = True + self.non_int_pres_only = non_int_pres_only + self.jukebox = jukebox + self.jukebox_votes = [] + self.jukebox_prev_char_id = -1 + + self.owners = [] + + class Locked(Enum): + FREE = 1, + SPECTATABLE = 2, + LOCKED = 3 + + def new_client(self, client): + self.clients.add(client) + self.server.area_manager.send_arup_players() + + def remove_client(self, client): + self.clients.remove(client) + if len(self.clients) == 0: + self.change_status('IDLE') + + def unlock(self): + self.is_locked = self.Locked.FREE + self.blankposting_allowed = True + self.invite_list = {} + self.server.area_manager.send_arup_lock() + self.send_host_message('This area is open now.') + + def spectator(self): + self.is_locked = self.Locked.SPECTATABLE + for i in self.clients: + self.invite_list[i.id] = None + for i in self.owners: + self.invite_list[i.id] = None + self.server.area_manager.send_arup_lock() + self.send_host_message('This area is spectatable now.') + + def lock(self): + self.is_locked = self.Locked.LOCKED + for i in self.clients: + self.invite_list[i.id] = None + for i in self.owners: + self.invite_list[i.id] = None + self.server.area_manager.send_arup_lock() + self.send_host_message('This area is locked now.') + + def is_char_available(self, char_id): + return char_id not in [x.char_id for x in self.clients] + + def get_rand_avail_char_id(self): + avail_set = set(range(len(self.server.char_list))) - set([x.char_id for x in self.clients]) + if len(avail_set) == 0: + raise AreaError('No available characters.') + return random.choice(tuple(avail_set)) + + def send_command(self, cmd, *args): + for c in self.clients: + c.send_command(cmd, *args) + + def send_owner_command(self, cmd, *args): + for c in self.owners: + if not c in self.clients: + c.send_command(cmd, *args) + + def send_host_message(self, msg): + self.send_command('CT', self.server.config['hostname'], msg, '1') + self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1') + + def set_next_msg_delay(self, msg_length): + delay = min(3000, 100 + 60 * msg_length) + self.next_message_time = round(time.time() * 1000.0 + delay) + + def is_iniswap(self, client, anim1, anim2, char): + if self.iniswap_allowed: + return False + if '..' in anim1 or '..' in anim2: + return True + for char_link in self.server.allowed_iniswaps: + if client.get_char_name() in char_link and char in char_link: + return False + return True + + def add_jukebox_vote(self, client, music_name, length=-1, showname=''): + if not self.jukebox: + return + if length <= 0: + self.remove_jukebox_vote(client, False) + else: + self.remove_jukebox_vote(client, True) + self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname)) + client.send_host_message('Your song was added to the jukebox.') + if len(self.jukebox_votes) == 1: + self.start_jukebox() + + def remove_jukebox_vote(self, client, silent): + if not self.jukebox: + return + for current_vote in self.jukebox_votes: + if current_vote.client.id == client.id: + self.jukebox_votes.remove(current_vote) + if not silent: + client.send_host_message('You removed your song from the jukebox.') + + def get_jukebox_picked(self): + if not self.jukebox: + return + if len(self.jukebox_votes) == 0: + return None + elif len(self.jukebox_votes) == 1: + return self.jukebox_votes[0] + else: + weighted_votes = [] + for current_vote in self.jukebox_votes: + i = 0 + while i < current_vote.chance: + weighted_votes.append(current_vote) + i += 1 + return random.choice(weighted_votes) + + def start_jukebox(self): + # There is a probability that the jukebox feature has been turned off since then, + # we should check that. + # We also do a check if we were the last to play a song, just in case. + if not self.jukebox: + if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID': + self.current_music = '' + return + + vote_picked = self.get_jukebox_picked() + + if vote_picked is None: + self.current_music = '' + return + + if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: + self.jukebox_prev_char_id = vote_picked.client.char_id + if vote_picked.showname == '': + self.send_command('MC', vote_picked.name, vote_picked.client.char_id) + else: + self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) + else: + self.send_command('MC', vote_picked.name, -1) + + self.current_music_player = 'The Jukebox' + self.current_music_player_ipid = 'has no IPID' + self.current_music = vote_picked.name + + for current_vote in self.jukebox_votes: + # Choosing the same song will get your votes down to 0, too. + # Don't want the same song twice in a row! + if current_vote.name == vote_picked.name: + current_vote.chance = 0 + else: + current_vote.chance += 1 + + if self.music_looper: + self.music_looper.cancel() + self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox()) + + def play_music(self, name, cid, length=-1): + self.send_command('MC', name, cid) + if self.music_looper: + self.music_looper.cancel() + if length > 0: + self.music_looper = asyncio.get_event_loop().call_later(length, + lambda: self.play_music(name, -1, length)) + + def play_music_shownamed(self, name, cid, showname, length=-1): + self.send_command('MC', name, cid, showname) + if self.music_looper: + self.music_looper.cancel() + if length > 0: + self.music_looper = asyncio.get_event_loop().call_later(length, + lambda: self.play_music(name, -1, length)) + + + def can_send_message(self, client): + if self.cannot_ic_interact(client): + client.send_host_message('This is a locked area - ask the CM to speak.') + return False + return (time.time() * 1000.0 - self.next_message_time) > 0 + + def cannot_ic_interact(self, client): + return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list + + def change_hp(self, side, val): + if not 0 <= val <= 10: + raise AreaError('Invalid penalty value.') + if not 1 <= side <= 2: + raise AreaError('Invalid penalty side.') + if side == 1: + self.hp_def = val + elif side == 2: + self.hp_pro = val + self.send_command('HP', side, val) + + def change_background(self, bg): + if bg.lower() not in (name.lower() for name in self.server.backgrounds): + raise AreaError('Invalid background name.') + self.background = bg + self.send_command('BN', self.background) + + def change_status(self, value): + allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming') + if value.lower() not in allowed_values: + raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values))) + if value.lower() == 'lfp': + value = 'looking-for-players' + self.status = value.upper() + self.server.area_manager.send_arup_status() + + def change_doc(self, doc='No document.'): + self.doc = doc + + def add_to_judgelog(self, client, msg): + if len(self.judgelog) >= 10: + self.judgelog = self.judgelog[1:] + self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(), client.get_ip(), msg)) + + def add_music_playing(self, client, name): + self.current_music_player = client.get_char_name() + self.current_music_player_ipid = client.ipid + self.current_music = name + + def add_music_playing_shownamed(self, client, showname, name): + self.current_music_player = showname + " (" + client.get_char_name() + ")" + self.current_music_player_ipid = client.ipid + self.current_music = name + + def get_evidence_list(self, client): + client.evi_list, evi_list = self.evi_list.create_evi_list(client) + return evi_list + + def broadcast_evidence_list(self): + """ + LE#<name>&<desc>&<img>#<name> + + """ + for client in self.clients: + client.send_command('LE', *self.get_evidence_list(client)) + + def get_cms(self): + msg = '' + for i in self.owners: + msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', ' + if len(msg) > 2: + msg = msg[:-2] + return msg + + class JukeboxVote: + def __init__(self, client, name, length, showname): + self.client = client + self.name = name + self.length = length + self.chance = 1 + self.showname = showname + + def __init__(self, server): + self.server = server + self.cur_id = 0 + self.areas = [] + self.load_areas() + + def load_areas(self): + with open('config/areas.yaml', 'r') as chars: + areas = yaml.load(chars) + for item in areas: + if 'evidence_mod' not in item: + item['evidence_mod'] = 'FFA' + if 'locking_allowed' not in item: + item['locking_allowed'] = False + if 'iniswap_allowed' not in item: + item['iniswap_allowed'] = True + if 'showname_changes_allowed' not in item: + item['showname_changes_allowed'] = False + if 'shouts_allowed' not in item: + item['shouts_allowed'] = True + if 'jukebox' not in item: + item['jukebox'] = False + if 'noninterrupting_pres' not in item: + item['noninterrupting_pres'] = False + if 'abbreviation' not in item: + item['abbreviation'] = self.get_generated_abbreviation(item['area']) + self.areas.append( + self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres'])) + self.cur_id += 1 + + def default_area(self): + return self.areas[0] + + def get_area_by_name(self, name): + for area in self.areas: + if area.name == name: + return area + raise AreaError('Area not found.') + + def get_area_by_id(self, num): + for area in self.areas: + if area.id == num: + return area + raise AreaError('Area not found.') + + def get_generated_abbreviation(self, name): + if name.lower().startswith("courtroom"): + return "CR" + name.split()[-1] + elif name.lower().startswith("area"): + return "A" + name.split()[-1] + elif len(name.split()) > 1: + return "".join(item[0].upper() for item in name.split()) + elif len(name) > 3: + return name[:3].upper() + else: + return name.upper() + + def send_remote_command(self, area_ids, cmd, *args): + for a_id in area_ids: + self.get_area_by_id(a_id).send_command(cmd, *args) + self.get_area_by_id(a_id).send_owner_command(cmd, *args) + + def send_arup_players(self): + players_list = [0] + for area in self.areas: + players_list.append(len(area.clients)) + self.server.send_arup(players_list) + + def send_arup_status(self): + status_list = [1] + for area in self.areas: + status_list.append(area.status) + self.server.send_arup(status_list) + + def send_arup_cms(self): + cms_list = [2] + for area in self.areas: + cm = 'FREE' + if len(area.owners) > 0: + cm = area.get_cms() + cms_list.append(cm) + self.server.send_arup(cms_list) + + def send_arup_lock(self): + lock_list = [3] + for area in self.areas: + lock_list.append(area.is_locked.name) + self.server.send_arup(lock_list) diff --git a/server/ban_manager.py b/server/ban_manager.py new file mode 100644 index 00000000..20c186f9 --- /dev/null +++ b/server/ban_manager.py @@ -0,0 +1,54 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json + +from server.exceptions import ServerError + + +class BanManager: + def __init__(self): + self.bans = [] + self.load_banlist() + + def load_banlist(self): + try: + with open('storage/banlist.json', 'r') as banlist_file: + self.bans = json.load(banlist_file) + except FileNotFoundError: + return + + def write_banlist(self): + with open('storage/banlist.json', 'w') as banlist_file: + json.dump(self.bans, banlist_file) + + def add_ban(self, ip): + if ip not in self.bans: + self.bans.append(ip) + else: + raise ServerError('This IPID is already banned.') + self.write_banlist() + + def remove_ban(self, ip): + if ip in self.bans: + self.bans.remove(ip) + else: + raise ServerError('This IPID is not banned.') + self.write_banlist() + + def is_banned(self, ipid): + return (ipid in self.bans)
\ No newline at end of file diff --git a/server/client_manager.py b/server/client_manager.py new file mode 100644 index 00000000..432c39d4 --- /dev/null +++ b/server/client_manager.py @@ -0,0 +1,457 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from server import fantacrypt +from server import logger +from server.exceptions import ClientError, AreaError +from enum import Enum +from server.constants import TargetType +from heapq import heappop, heappush + +import time +import re + + + +class ClientManager: + class Client: + def __init__(self, server, transport, user_id, ipid): + self.is_checked = False + self.transport = transport + self.hdid = '' + self.pm_mute = False + self.id = user_id + self.char_id = -1 + self.area = server.area_manager.default_area() + self.server = server + self.name = '' + self.fake_name = '' + self.is_mod = False + self.is_dj = True + self.can_wtce = True + self.pos = '' + self.evi_list = [] + self.disemvowel = False + self.shaken = False + self.charcurse = [] + self.muted_global = False + self.muted_adverts = False + self.is_muted = False + self.is_ooc_muted = False + self.pm_mute = False + self.mod_call_time = 0 + self.in_rp = False + self.ipid = ipid + self.websocket = None + + # Pairing stuff + self.charid_pair = -1 + self.offset_pair = 0 + self.last_sprite = '' + self.flip = 0 + self.claimed_folder = '' + + # Casing stuff + self.casing_cm = False + self.casing_cases = "" + self.casing_def = False + self.casing_pro = False + self.casing_jud = False + self.casing_jur = False + self.casing_steno = False + self.case_call_time = 0 + + #flood-guard stuff + self.mus_counter = 0 + self.mus_mute_time = 0 + self.mus_change_time = [x * self.server.config['music_change_floodguard']['interval_length'] for x in range(self.server.config['music_change_floodguard']['times_per_interval'])] + self.wtce_counter = 0 + self.wtce_mute_time = 0 + self.wtce_time = [x * self.server.config['wtce_floodguard']['interval_length'] for x in range(self.server.config['wtce_floodguard']['times_per_interval'])] + + def send_raw_message(self, msg): + if self.websocket: + self.websocket.send_text(msg.encode('utf-8')) + else: + self.transport.write(msg.encode('utf-8')) + + def send_command(self, command, *args): + if args: + if command == 'MS': + for evi_num in range(len(self.evi_list)): + if self.evi_list[evi_num] == args[11]: + lst = list(args) + lst[11] = evi_num + args = tuple(lst) + break + self.send_raw_message('{}#{}#%'.format(command, '#'.join([str(x) for x in args]))) + else: + self.send_raw_message('{}#%'.format(command)) + + def send_host_message(self, msg): + self.send_command('CT', self.server.config['hostname'], msg, '1') + + def send_motd(self): + self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd'])) + + def send_player_count(self): + self.send_host_message('{}/{} players online.'.format( + self.server.get_player_count(), + self.server.config['playerlimit'])) + + def is_valid_name(self, name): + name_ws = name.replace(' ', '') + if not name_ws or name_ws.isdigit(): + return False + for client in self.server.client_manager.clients: + print(client.name == name) + if client.name == name: + return False + return True + + def disconnect(self): + self.transport.close() + + def change_character(self, char_id, force=False): + if not self.server.is_valid_char_id(char_id): + raise ClientError('Invalid Character ID.') + if len(self.charcurse) > 0: + if not char_id in self.charcurse: + raise ClientError('Character not available.') + force = True + if not self.area.is_char_available(char_id): + if force: + for client in self.area.clients: + if client.char_id == char_id: + client.char_select() + else: + raise ClientError('Character not available.') + old_char = self.get_char_name() + self.char_id = char_id + self.pos = '' + self.send_command('PV', self.id, 'CID', self.char_id) + self.area.send_command('CharsCheck', *self.get_available_char_list()) + logger.log_server('[{}]Changed character from {} to {}.' + .format(self.area.abbreviation, old_char, self.get_char_name()), self) + + def change_music_cd(self): + if self.is_mod or self in self.area.owners: + return 0 + if self.mus_mute_time: + if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']: + return self.server.config['music_change_floodguard']['mute_length'] - (time.time() - self.mus_mute_time) + else: + self.mus_mute_time = 0 + times_per_interval = self.server.config['music_change_floodguard']['times_per_interval'] + interval_length = self.server.config['music_change_floodguard']['interval_length'] + if time.time() - self.mus_change_time[(self.mus_counter - times_per_interval + 1) % times_per_interval] < interval_length: + self.mus_mute_time = time.time() + return self.server.config['music_change_floodguard']['mute_length'] + self.mus_counter = (self.mus_counter + 1) % times_per_interval + self.mus_change_time[self.mus_counter] = time.time() + return 0 + + def wtce_mute(self): + if self.is_mod or self in self.area.owners: + return 0 + if self.wtce_mute_time: + if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']: + return self.server.config['wtce_floodguard']['mute_length'] - (time.time() - self.wtce_mute_time) + else: + self.wtce_mute_time = 0 + times_per_interval = self.server.config['wtce_floodguard']['times_per_interval'] + interval_length = self.server.config['wtce_floodguard']['interval_length'] + if time.time() - self.wtce_time[(self.wtce_counter - times_per_interval + 1) % times_per_interval] < interval_length: + self.wtce_mute_time = time.time() + return self.server.config['music_change_floodguard']['mute_length'] + self.wtce_counter = (self.wtce_counter + 1) % times_per_interval + self.wtce_time[self.wtce_counter] = time.time() + return 0 + + def reload_character(self): + try: + self.change_character(self.char_id, True) + except ClientError: + raise + + def change_area(self, area): + if self.area == area: + raise ClientError('User already in specified area.') + if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list: + raise ClientError("That area is locked!") + if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list: + self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.') + + if self.area.jukebox: + self.area.remove_jukebox_vote(self, True) + + old_area = self.area + if not area.is_char_available(self.char_id): + try: + new_char_id = area.get_rand_avail_char_id() + except AreaError: + raise ClientError('No available characters in that area.') + + self.change_character(new_char_id) + self.send_host_message('Character taken, switched to {}.'.format(self.get_char_name())) + + self.area.remove_client(self) + self.area = area + area.new_client(self) + + self.send_host_message('Changed area to {}.[{}]'.format(area.name, self.area.status)) + logger.log_server( + '[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id, + self.area.name, self.area.id), self) + self.area.send_command('CharsCheck', *self.get_available_char_list()) + self.send_command('HP', 1, self.area.hp_def) + self.send_command('HP', 2, self.area.hp_pro) + self.send_command('BN', self.area.background) + self.send_command('LE', *self.area.get_evidence_list(self)) + + def send_area_list(self): + msg = '=== Areas ===' + for i, area in enumerate(self.server.area_manager.areas): + owner = 'FREE' + if len(area.owners) > 0: + owner = 'CM: {}'.format(area.get_cms()) + lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} + msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) + if self.area == area: + msg += ' [*]' + self.send_host_message(msg) + + def get_area_info(self, area_id, mods): + info = '\r\n' + try: + area = self.server.area_manager.get_area_by_id(area_id) + except AreaError: + raise + info += '=== {} ==='.format(area.name) + info += '\r\n' + + lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} + info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked]) + + sorted_clients = [] + for client in area.clients: + if (not mods) or client.is_mod: + sorted_clients.append(client) + for owner in area.owners: + if not (mods or owner in area.clients): + sorted_clients.append(owner) + if not sorted_clients: + return '' + sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) + for c in sorted_clients: + info += '\r\n' + if c in area.owners: + if not c in area.clients: + info += '[RCM]' + else: + info +='[CM]' + info += '[{}] {}'.format(c.id, c.get_char_name()) + if self.is_mod: + info += ' ({})'.format(c.ipid) + info += ': {}'.format(c.name) + + return info + + def send_area_info(self, area_id, mods): + #if area_id is -1 then return all areas. If mods is True then return only mods + info = '' + if area_id == -1: + # all areas info + cnt = 0 + info = '\n== Area List ==' + for i in range(len(self.server.area_manager.areas)): + if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0: + cnt += len(self.server.area_manager.areas[i].clients) + info += '{}'.format(self.get_area_info(i, mods)) + info = 'Current online: {}'.format(cnt) + info + else: + try: + info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) + except AreaError: + raise + self.send_host_message(info) + + def send_area_hdid(self, area_id): + try: + info = self.get_area_hdid(area_id) + except AreaError: + raise + self.send_host_message(info) + + def send_all_area_hdid(self): + info = '== HDID List ==' + for i in range (len(self.server.area_manager.areas)): + if len(self.server.area_manager.areas[i].clients) > 0: + info += '\r\n{}'.format(self.get_area_hdid(i)) + self.send_host_message(info) + + def send_all_area_ip(self): + info = '== IP List ==' + for i in range (len(self.server.area_manager.areas)): + if len(self.server.area_manager.areas[i].clients) > 0: + info += '\r\n{}'.format(self.get_area_ip(i)) + self.send_host_message(info) + + def send_done(self): + self.send_command('CharsCheck', *self.get_available_char_list()) + self.send_command('HP', 1, self.area.hp_def) + self.send_command('HP', 2, self.area.hp_pro) + self.send_command('BN', self.area.background) + self.send_command('LE', *self.area.get_evidence_list(self)) + self.send_command('MM', 1) + + self.server.area_manager.send_arup_players() + self.server.area_manager.send_arup_status() + self.server.area_manager.send_arup_cms() + self.server.area_manager.send_arup_lock() + + self.send_command('DONE') + + def char_select(self): + self.char_id = -1 + self.send_done() + + def get_available_char_list(self): + if len(self.charcurse) > 0: + avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse) + else: + avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) + char_list = [-1] * len(self.server.char_list) + for x in avail_char_ids: + char_list[x] = 0 + return char_list + + def auth_mod(self, password): + if self.is_mod: + raise ClientError('Already logged in.') + if password == self.server.config['modpass']: + self.is_mod = True + else: + raise ClientError('Invalid password.') + + def get_ip(self): + return self.ipid + + + + def get_char_name(self): + if self.char_id == -1: + return 'CHAR_SELECT' + return self.server.char_list[self.char_id] + + def change_position(self, pos=''): + if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): + raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.') + self.pos = pos + + def set_mod_call_delay(self): + self.mod_call_time = round(time.time() * 1000.0 + 30000) + + def can_call_mod(self): + return (time.time() * 1000.0 - self.mod_call_time) > 0 + + def set_case_call_delay(self): + self.case_call_time = round(time.time() * 1000.0 + 60000) + + def can_call_case(self): + return (time.time() * 1000.0 - self.case_call_time) > 0 + + def disemvowel_message(self, message): + message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) + return re.sub(r"\s+", " ", message) + + def shake_message(self, message): + import random + parts = message.split() + random.shuffle(parts) + return ' '.join(parts) + + + def __init__(self, server): + self.clients = set() + self.server = server + self.cur_id = [i for i in range(self.server.config['playerlimit'])] + self.clients_list = [] + + def new_client(self, transport): + c = self.Client(self.server, transport, heappop(self.cur_id), self.server.get_ipid(transport.get_extra_info('peername')[0])) + self.clients.add(c) + return c + + + def remove_client(self, client): + if client.area.jukebox: + client.area.remove_jukebox_vote(client, True) + for a in self.server.area_manager.areas: + if client in a.owners: + a.owners.remove(client) + client.server.area_manager.send_arup_cms() + if len(a.owners) == 0: + if a.is_locked != a.Locked.FREE: + a.unlock() + heappush(self.cur_id, client.id) + self.clients.remove(client) + + def get_targets(self, client, key, value, local = False): + #possible keys: ip, OOC, id, cname, ipid, hdid + areas = None + if local: + areas = [client.area] + else: + areas = client.server.area_manager.areas + targets = [] + if key == TargetType.ALL: + for nkey in range(6): + targets += self.get_targets(client, nkey, value, local) + for area in areas: + for client in area.clients: + if key == TargetType.IP: + if value.lower().startswith(client.get_ip().lower()): + targets.append(client) + elif key == TargetType.OOC_NAME: + if value.lower().startswith(client.name.lower()) and client.name: + targets.append(client) + elif key == TargetType.CHAR_NAME: + if value.lower().startswith(client.get_char_name().lower()): + targets.append(client) + elif key == TargetType.ID: + if client.id == value: + targets.append(client) + elif key == TargetType.IPID: + if client.ipid == value: + targets.append(client) + return targets + + + def get_muted_clients(self): + clients = [] + for client in self.clients: + if client.is_muted: + clients.append(client) + return clients + + def get_ooc_muted_clients(self): + clients = [] + for client in self.clients: + if client.is_ooc_muted: + clients.append(client) + return clients diff --git a/server/commands.py b/server/commands.py new file mode 100644 index 00000000..d02eff25 --- /dev/null +++ b/server/commands.py @@ -0,0 +1,1255 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#possible keys: ip, OOC, id, cname, ipid, hdid +import random +import re +import hashlib +import string +from server.constants import TargetType + +from server import logger +from server.exceptions import ClientError, ServerError, ArgumentError, AreaError + +def ooc_cmd_a(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify an area.') + arg = arg.split(' ') + + try: + area = client.server.area_manager.get_area_by_id(int(arg[0])) + except AreaError: + raise + + message_areas_cm(client, [area], ' '.join(arg[1:])) + +def ooc_cmd_s(client, arg): + areas = [] + for a in client.server.area_manager.areas: + if client in a.owners: + areas.append(a) + if not areas: + client.send_host_message('You aren\'t a CM in any area!') + return + message_areas_cm(client, areas, arg) + +def message_areas_cm(client, areas, message): + for a in areas: + if not client in a.owners: + client.send_host_message('You are not a CM in {}!'.format(a.name)) + return + a.send_command('CT', client.name, message) + a.send_owner_command('CT', client.name, message) + +def ooc_cmd_switch(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify a character name.') + try: + cid = client.server.get_char_id_by_name(arg) + except ServerError: + raise + try: + client.change_character(cid, client.is_mod) + except ClientError: + raise + client.send_host_message('Character changed.') + +def ooc_cmd_bg(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify a name. Use /bg <background>.') + if not client.is_mod and client.area.bg_lock == "true": + raise AreaError("This area's background is locked") + try: + client.area.change_background(arg) + except AreaError: + raise + client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg)) + logger.log_server('[{}][{}]Changed background to {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_bglock(client,arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if client.area.bg_lock == "true": + client.area.bg_lock = "false" + else: + client.area.bg_lock = "true" + client.area.send_host_message('{} [{}] has set the background lock to {}.'.format(client.get_char_name(), client.id, client.area.bg_lock)) + logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.bg_lock), client) + +def ooc_cmd_evidence_mod(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if not arg: + client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) + return + if arg in ['FFA', 'Mods', 'CM', 'HiddenCM']: + if arg == client.area.evidence_mod: + client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) + return + if client.area.evidence_mod == 'HiddenCM': + for i in range(len(client.area.evi_list.evidences)): + client.area.evi_list.evidences[i].pos = 'all' + client.area.evidence_mod = arg + client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) + return + else: + raise ArgumentError('Wrong Argument. Use /evidence_mod <MOD>. Possible values: FFA, CM, Mods, HiddenCM') + return + +def ooc_cmd_allow_iniswap(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + client.area.iniswap_allowed = not client.area.iniswap_allowed + answer = {True: 'allowed', False: 'forbidden'} + client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed])) + return + +def ooc_cmd_allow_blankposting(client, arg): + if not client.is_mod and not client in client.area.owners: + raise ClientError('You must be authorized to do that.') + client.area.blankposting_allowed = not client.area.blankposting_allowed + answer = {True: 'allowed', False: 'forbidden'} + client.area.send_host_message('{} [{}] has set blankposting in the area to {}.'.format(client.get_char_name(), client.id, answer[client.area.blankposting_allowed])) + return + +def ooc_cmd_force_nonint_pres(client, arg): + if not client.is_mod and not client in client.area.owners: + raise ClientError('You must be authorized to do that.') + client.area.non_int_pres_only = not client.area.non_int_pres_only + answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} + client.area.send_host_message('{} [{}] has set pres in the area to be {}.'.format(client.get_char_name(), client.id, answer[client.area.non_int_pres_only])) + return + +def ooc_cmd_roll(client, arg): + roll_max = 11037 + if len(arg) != 0: + try: + val = list(map(int, arg.split(' '))) + if not 1 <= val[0] <= roll_max: + raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) + except ValueError: + raise ArgumentError('Wrong argument. Use /roll [<max>] [<num of rolls>]') + else: + val = [6] + if len(val) == 1: + val.append(1) + if len(val) > 2: + raise ArgumentError('Too many arguments. Use /roll [<max>] [<num of rolls>]') + if val[1] > 20 or val[1] < 1: + raise ArgumentError('Num of rolls must be between 1 and 20') + roll = '' + for i in range(val[1]): + roll += str(random.randint(1, val[0])) + ', ' + roll = roll[:-2] + if val[1] > 1: + roll = '(' + roll + ')' + client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) + logger.log_server( + '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) + +def ooc_cmd_rollp(client, arg): + roll_max = 11037 + if len(arg) != 0: + try: + val = list(map(int, arg.split(' '))) + if not 1 <= val[0] <= roll_max: + raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) + except ValueError: + raise ArgumentError('Wrong argument. Use /rollp [<max>] [<num of rolls>]') + else: + val = [6] + if len(val) == 1: + val.append(1) + if len(val) > 2: + raise ArgumentError('Too many arguments. Use /rollp [<max>] [<num of rolls>]') + if val[1] > 20 or val[1] < 1: + raise ArgumentError('Num of rolls must be between 1 and 20') + roll = '' + for i in range(val[1]): + roll += str(random.randint(1, val[0])) + ', ' + roll = roll[:-2] + if val[1] > 1: + roll = '(' + roll + ')' + client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) + + client.area.send_host_message('{} rolled in secret.'.format(client.get_char_name())) + for c in client.area.owners: + c.send_host_message('[{}]{} secretly rolled {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0])) + + logger.log_server( + '[{}][{}]Used /rollp and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) + +def ooc_cmd_currentmusic(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if client.area.current_music == '': + raise ClientError('There is no music currently playing.') + if client.is_mod: + client.send_host_message('The current music is {} and was played by {} ({}).'.format(client.area.current_music, + client.area.current_music_player, client.area.current_music_player_ipid)) + else: + client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, + client.area.current_music_player)) + +def ooc_cmd_jukebox_toggle(client, arg): + if not client.is_mod and not client in client.area.owners: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + client.area.jukebox = not client.area.jukebox + client.area.jukebox_votes = [] + client.area.send_host_message('{} [{}] has set the jukebox to {}.'.format(client.get_char_name(), client.id, client.area.jukebox)) + +def ooc_cmd_jukebox_skip(client, arg): + if not client.is_mod and not client in client.area.owners: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if not client.area.jukebox: + raise ClientError('This area does not have a jukebox.') + if len(client.area.jukebox_votes) == 0: + raise ClientError('There is no song playing right now, skipping is pointless.') + client.area.start_jukebox() + if len(client.area.jukebox_votes) == 1: + client.area.send_host_message('{} [{}] has forced a skip, restarting the only jukebox song.'.format(client.get_char_name(), client.id)) + else: + client.area.send_host_message('{} [{}] has forced a skip to the next jukebox song.'.format(client.get_char_name(), client.id)) + logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.abbreviation, client.get_char_name()), client) + +def ooc_cmd_jukebox(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if not client.area.jukebox: + raise ClientError('This area does not have a jukebox.') + if len(client.area.jukebox_votes) == 0: + client.send_host_message('The jukebox has no songs in it.') + else: + total = 0 + songs = [] + voters = dict() + chance = dict() + message = '' + + for current_vote in client.area.jukebox_votes: + if songs.count(current_vote.name) == 0: + songs.append(current_vote.name) + voters[current_vote.name] = [current_vote.client] + chance[current_vote.name] = current_vote.chance + else: + voters[current_vote.name].append(current_vote.client) + chance[current_vote.name] += current_vote.chance + total += current_vote.chance + + for song in songs: + message += '\n- ' + song + '\n' + message += '-- VOTERS: ' + + first = True + for voter in voters[song]: + if first: + first = False + else: + message += ', ' + message += voter.get_char_name() + ' [' + str(voter.id) + ']' + if client.is_mod: + message += '(' + str(voter.ipid) + ')' + message += '\n' + + if total == 0: + message += '-- CHANCE: 100' + else: + message += '-- CHANCE: ' + str(round(chance[song] / total * 100)) + + client.send_host_message('The jukebox has the following songs in it:{}'.format(message)) + +def ooc_cmd_coinflip(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + coin = ['heads', 'tails'] + flip = random.choice(coin) + client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip)) + logger.log_server( + '[{}][{}]Used /coinflip and got {}.'.format(client.area.abbreviation, client.get_char_name(), flip), client) + +def ooc_cmd_motd(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.send_motd() + +def ooc_cmd_pos(client, arg): + if len(arg) == 0: + client.change_position() + client.send_host_message('Position reset.') + else: + try: + client.change_position(arg) + except ClientError: + raise + client.area.broadcast_evidence_list() + client.send_host_message('Position changed.') + +def ooc_cmd_forcepos(client, arg): + if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be authorized to do that.') + + args = arg.split() + + if len(args) < 1: + raise ArgumentError( + 'Not enough arguments. Use /forcepos <pos> <target>. Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".') + + targets = [] + + pos = args[0] + if len(args) > 1: + targets = client.server.client_manager.get_targets( + client, TargetType.CHAR_NAME, " ".join(args[1:]), True) + if len(targets) == 0 and args[1].isdigit(): + targets = client.server.client_manager.get_targets( + client, TargetType.ID, int(arg[1]), True) + if len(targets) == 0: + targets = client.server.client_manager.get_targets( + client, TargetType.OOC_NAME, " ".join(args[1:]), True) + if len(targets) == 0: + raise ArgumentError('No targets found.') + else: + for c in client.area.clients: + targets.append(c) + + + + for t in targets: + try: + t.change_position(pos) + t.area.broadcast_evidence_list() + t.send_host_message('Forced into /pos {}.'.format(pos)) + except ClientError: + raise + + client.area.send_host_message( + '{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos)) + logger.log_server( + '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.abbreviation, client.get_char_name(), pos, len(targets)), client) + +def ooc_cmd_help(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + help_url = 'http://casecafe.byethost14.com/commandlist' + help_msg = 'The commands available on this server can be found here: {}'.format(help_url) + client.send_host_message(help_msg) + +def ooc_cmd_kick(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /kick <ipid> <ipid> ...') + args = list(arg.split(' ')) + client.send_host_message('Attempting to kick {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + ipid = int(raw_ipid) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if targets: + for c in targets: + logger.log_server('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + logger.log_mod('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + client.send_host_message("{} was kicked.".format(c.get_char_name())) + c.send_command('KK', c.char_id) + c.disconnect() + else: + client.send_host_message("No targets with the IPID {} were found.".format(ipid)) + +def ooc_cmd_ban(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /ban <ipid> <ipid> ...') + args = list(arg.split(' ')) + client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + ipid = int(raw_ipid) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + try: + client.server.ban_manager.add_ban(ipid) + except ServerError: + raise + if ipid != None: + targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if targets: + for c in targets: + c.send_command('KB', c.char_id) + c.disconnect() + client.send_host_message('{} clients was kicked.'.format(len(targets))) + client.send_host_message('{} was banned.'.format(ipid)) + logger.log_server('Banned {}.'.format(ipid), client) + logger.log_mod('Banned {}.'.format(ipid), client) + +def ooc_cmd_unban(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /unban <ipid> <ipid> ...') + args = list(arg.split(' ')) + client.send_host_message('Attempting to unban {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + client.server.ban_manager.remove_ban(int(raw_ipid)) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + logger.log_server('Unbanned {}.'.format(raw_ipid), client) + logger.log_mod('Unbanned {}.'.format(raw_ipid), client) + client.send_host_message('Unbanned {}'.format(raw_ipid)) + +def ooc_cmd_play(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a song.') + client.area.play_music(arg, client.char_id, -1) + client.area.add_music_playing(client, arg) + logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_mute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /mute <ipid>.') + args = list(arg.split(' ')) + client.send_host_message('Attempting to mute {} IPIDs.'.format(len(args))) + for raw_ipid in args: + if raw_ipid.isdigit(): + ipid = int(raw_ipid) + clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if (clients): + msg = 'Muted the IPID ' + str(ipid) + '\'s following clients:' + for c in clients: + c.is_muted = True + logger.log_server('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + logger.log_mod('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' + msg = msg[:-1] + msg += '.' + client.send_host_message('{}'.format(msg)) + else: + client.send_host_message("No targets found. Use /mute <ipid> <ipid> ... for mute.") + else: + client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) + +def ooc_cmd_unmute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target.') + args = list(arg.split(' ')) + client.send_host_message('Attempting to unmute {} IPIDs.'.format(len(args))) + for raw_ipid in args: + if raw_ipid.isdigit(): + ipid = int(raw_ipid) + clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if (clients): + msg = 'Unmuted the IPID ' + str(ipid) + '\'s following clients::' + for c in clients: + c.is_muted = False + logger.log_server('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + logger.log_mod('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' + msg = msg[:-1] + msg += '.' + client.send_host_message('{}'.format(msg)) + else: + client.send_host_message("No targets found. Use /unmute <ipid> <ipid> ... for unmute.") + else: + client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) + +def ooc_cmd_login(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify the password.') + try: + client.auth_mod(arg) + except ClientError: + raise + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.send_host_message('Logged in as a moderator.') + logger.log_server('Logged in as moderator.', client) + logger.log_mod('Logged in as moderator.', client) + +def ooc_cmd_g(client, arg): + if client.muted_global: + raise ClientError('Global chat toggled off.') + if len(arg) == 0: + raise ArgumentError("You can't send an empty message.") + client.server.broadcast_global(client, arg) + logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_gm(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if client.muted_global: + raise ClientError('You have the global chat muted.') + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.server.broadcast_global(client, arg, True) + logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_m(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError("You can't send an empty message.") + client.server.send_modchat(client, arg) + logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_lm(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.area.send_command('CT', '{}[MOD][{}]' + .format(client.server.config['hostname'], client.get_char_name()), arg) + logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_announce(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), + '=== Announcement ===\r\n{}\r\n=================='.format(arg), '1') + logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_toggleglobal(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.muted_global = not client.muted_global + glob_stat = 'on' + if client.muted_global: + glob_stat = 'off' + client.send_host_message('Global chat turned {}.'.format(glob_stat)) + + +def ooc_cmd_need(client, arg): + if client.muted_adverts: + raise ClientError('You have advertisements muted.') + if len(arg) == 0: + raise ArgumentError("You must specify what you need.") + client.server.broadcast_need(client, arg) + logger.log_server('[{}][{}][NEED]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + +def ooc_cmd_toggleadverts(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.muted_adverts = not client.muted_adverts + adv_stat = 'on' + if client.muted_adverts: + adv_stat = 'off' + client.send_host_message('Advertisements turned {}.'.format(adv_stat)) + +def ooc_cmd_doc(client, arg): + if len(arg) == 0: + client.send_host_message('Document: {}'.format(client.area.doc)) + logger.log_server( + '[{}][{}]Requested document. Link: {}'.format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) + else: + client.area.change_doc(arg) + client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name())) + logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + +def ooc_cmd_cleardoc(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name())) + logger.log_server('[{}][{}]Cleared document. Old link: {}' + .format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) + client.area.change_doc() + + +def ooc_cmd_status(client, arg): + if len(arg) == 0: + client.send_host_message('Current status: {}'.format(client.area.status)) + else: + try: + client.area.change_status(arg) + client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status)) + logger.log_server( + '[{}][{}]Changed status to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.status), client) + except AreaError: + raise + + +def ooc_cmd_online(client, _): + client.send_player_count() + + +def ooc_cmd_area(client, arg): + args = arg.split() + if len(args) == 0: + client.send_area_list() + elif len(args) == 1: + try: + area = client.server.area_manager.get_area_by_id(int(args[0])) + client.change_area(area) + except ValueError: + raise ArgumentError('Area ID must be a number.') + except (AreaError, ClientError): + raise + else: + raise ArgumentError('Too many arguments. Use /area <id>.') + +def ooc_cmd_pm(client, arg): + args = arg.split() + key = '' + msg = None + if len(args) < 2: + raise ArgumentError('Not enough arguments. use /pm <target> <message>. Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".') + targets = client.server.client_manager.get_targets(client, TargetType.CHAR_NAME, arg, True) + key = TargetType.CHAR_NAME + if len(targets) == 0 and args[0].isdigit(): + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) + key = TargetType.ID + if len(targets) == 0: + targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, True) + key = TargetType.OOC_NAME + if len(targets) == 0: + raise ArgumentError('No targets found.') + try: + if key == TargetType.ID: + msg = ' '.join(args[1:]) + else: + if key == TargetType.CHAR_NAME: + msg = arg[len(targets[0].get_char_name()) + 1:] + if key == TargetType.OOC_NAME: + msg = arg[len(targets[0].name) + 1:] + except: + raise ArgumentError('Not enough arguments. Use /pm <target> <message>.') + c = targets[0] + if c.pm_mute: + raise ClientError('This user muted all pm conversation') + else: + if c.is_mod: + c.send_host_message('PM from {} (ID: {}, IPID: {}) in {} ({}): {}'.format(client.name, client.id, client.ipid, client.area.name, client.get_char_name(), msg)) + else: + c.send_host_message('PM from {} (ID: {}) in {} ({}): {}'.format(client.name, client.id, client.area.name, client.get_char_name(), msg)) + client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg)) + +def ooc_cmd_mutepm(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.pm_mute = not client.pm_mute + client.send_host_message({True:'You stopped receiving PMs', False:'You are now receiving PMs'}[client.pm_mute]) + +def ooc_cmd_charselect(client, arg): + if not arg: + client.char_select() + else: + if client.is_mod: + try: + client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0].char_select() + except: + raise ArgumentError('Wrong arguments. Use /charselect <target\'s id>') + +def ooc_cmd_reload(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + try: + client.reload_character() + except ClientError: + raise + client.send_host_message('Character reloaded.') + +def ooc_cmd_randomchar(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if len(client.charcurse) > 0: + free_id = random.choice(client.charcurse) + else: + try: + free_id = client.area.get_rand_avail_char_id() + except AreaError: + raise + try: + client.change_character(free_id) + except ClientError: + raise + client.send_host_message('Randomly switched to {}'.format(client.get_char_name())) + +def ooc_cmd_getarea(client, arg): + client.send_area_info(client.area.id, False) + +def ooc_cmd_getareas(client, arg): + client.send_area_info(-1, False) + +def ooc_cmd_mods(client, arg): + client.send_area_info(-1, True) + +def ooc_cmd_evi_swap(client, arg): + args = list(arg.split(' ')) + if len(args) != 2: + raise ClientError("you must specify 2 numbers") + try: + client.area.evi_list.evidence_swap(client, int(args[0]), int(args[1])) + client.area.broadcast_evidence_list() + except: + raise ClientError("you must specify 2 numbers") + +def ooc_cmd_cm(client, arg): + if 'CM' not in client.area.evidence_mod: + raise ClientError('You can\'t become a CM in this area') + if len(client.area.owners) == 0: + if len(arg) > 0: + raise ArgumentError('You cannot \'nominate\' people to be CMs when you are not one.') + client.area.owners.append(client) + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.server.area_manager.send_arup_cms() + client.area.send_host_message('{} [{}] is CM in this area now.'.format(client.get_char_name(), client.id)) + elif client in client.area.owners: + if len(arg) > 0: + arg = arg.split(' ') + for id in arg: + try: + id = int(id) + c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] + if c in client.area.owners: + client.send_host_message('{} [{}] is already a CM here.'.format(c.get_char_name(), c.id)) + else: + client.area.owners.append(c) + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.server.area_manager.send_arup_cms() + client.area.send_host_message('{} [{}] is CM in this area now.'.format(c.get_char_name(), c.id)) + except: + client.send_host_message('{} does not look like a valid ID.'.format(id)) + else: + raise ClientError('You must be authorized to do that.') + + +def ooc_cmd_uncm(client, arg): + if client in client.area.owners: + if len(arg) > 0: + arg = arg.split(' ') + else: + arg = [client.id] + for id in arg: + try: + id = int(id) + c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] + if c in client.area.owners: + client.area.owners.remove(c) + client.server.area_manager.send_arup_cms() + client.area.send_host_message('{} [{}] is no longer CM in this area.'.format(c.get_char_name(), c.id)) + else: + client.send_host_message('You cannot remove someone from CMing when they aren\'t a CM.') + except: + client.send_host_message('{} does not look like a valid ID.'.format(id)) + else: + raise ClientError('You must be authorized to do that.') + +def ooc_cmd_setcase(client, arg): + args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) + if len(args) == 0: + raise ArgumentError('Please do not call this command manually!') + else: + client.casing_cases = args[0] + client.casing_cm = args[1] == "1" + client.casing_def = args[2] == "1" + client.casing_pro = args[3] == "1" + client.casing_jud = args[4] == "1" + client.casing_jur = args[5] == "1" + client.casing_steno = args[6] == "1" + +def ooc_cmd_anncase(client, arg): + if client in client.area.owners: + if not client.can_call_case(): + raise ClientError('Please wait 60 seconds between case announcements!') + args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) + if len(args) == 0: + raise ArgumentError('Please do not call this command manually!') + elif len(args) == 1: + raise ArgumentError('You should probably announce the case to at least one person.') + else: + if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1" and not args[5] == "1": + raise ArgumentError('You should probably announce the case to at least one person.') + msg = '=== Case Announcement ===\r\n{} [{}] is hosting {}, looking for '.format(client.get_char_name(), client.id, args[0]) + + lookingfor = [] + + if args[1] == "1": + lookingfor.append("defence") + if args[2] == "1": + lookingfor.append("prosecutor") + if args[3] == "1": + lookingfor.append("judge") + if args[4] == "1": + lookingfor.append("juror") + if args[5] == "1": + lookingfor.append("stenographer") + + msg = msg + ', '.join(lookingfor) + '.\r\n==================' + + client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], args[5], '1') + + client.set_case_call_delay() + + logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}, STENO: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4], args[5]), client) + else: + raise ClientError('You cannot announce a case in an area where you are not a CM!') + +def ooc_cmd_unmod(client, arg): + client.is_mod = False + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.send_host_message('you\'re not a mod now') + +def ooc_cmd_area_lock(client, arg): + if not client.area.locking_allowed: + client.send_host_message('Area locking is disabled in this area.') + return + if client.area.is_locked == client.area.Locked.LOCKED: + client.send_host_message('Area is already locked.') + if client in client.area.owners: + client.area.lock() + return + else: + raise ClientError('Only CM can lock the area.') + +def ooc_cmd_area_spectate(client, arg): + if not client.area.locking_allowed: + client.send_host_message('Area locking is disabled in this area.') + return + if client.area.is_locked == client.area.Locked.SPECTATABLE: + client.send_host_message('Area is already spectatable.') + if client in client.area.owners: + client.area.spectator() + return + else: + raise ClientError('Only CM can make the area spectatable.') + +def ooc_cmd_area_unlock(client, arg): + if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area is already unlocked.') + if not client in client.area.owners: + raise ClientError('Only CM can unlock area.') + client.area.unlock() + client.send_host_message('Area is unlocked.') + +def ooc_cmd_invite(client, arg): + if not arg: + raise ClientError('You must specify a target. Use /invite <id>') + if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area isn\'t locked.') + if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] + client.area.invite_list[c.id] = None + client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) + c.send_host_message('You were invited and given access to {}.'.format(client.area.name)) + except: + raise ClientError('You must specify a target. Use /invite <id>') + +def ooc_cmd_uninvite(client, arg): + if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be authorized to do that.') + if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area isn\'t locked.') + if not arg: + raise ClientError('You must specify a target. Use /uninvite <id>') + arg = arg.split(' ') + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), True) + if targets: + try: + for c in targets: + client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) + c.send_host_message("You were removed from the area whitelist.") + if client.area.is_locked != client.area.Locked.FREE: + client.area.invite_list.pop(c.id) + except AreaError: + raise + except ClientError: + raise + else: + client.send_host_message("No targets found.") + +def ooc_cmd_area_kick(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area isn\'t locked.') + if not arg: + raise ClientError('You must specify a target. Use /area_kick <id> [destination #]') + arg = arg.split(' ') + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) + if targets: + try: + for c in targets: + if len(arg) == 1: + area = client.server.area_manager.get_area_by_id(int(0)) + output = 0 + else: + try: + area = client.server.area_manager.get_area_by_id(int(arg[1])) + output = arg[1] + except AreaError: + raise + client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output)) + c.change_area(area) + c.send_host_message("You were kicked from the area to area {}.".format(output)) + if client.area.is_locked != client.area.Locked.FREE: + client.area.invite_list.pop(c.id) + except AreaError: + raise + except ClientError: + raise + else: + client.send_host_message("No targets found.") + + +def ooc_cmd_ooc_mute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /ooc_mute <OOC-name>.') + targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, False) + if not targets: + raise ArgumentError('Targets not found. Use /ooc_mute <OOC-name>.') + for target in targets: + target.is_ooc_muted = True + client.send_host_message('Muted {} existing client(s).'.format(len(targets))) + +def ooc_cmd_ooc_unmute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /ooc_unmute <OOC-name>.') + targets = client.server.client_manager.get_ooc_muted_clients() + if not targets: + raise ArgumentError('Targets not found. Use /ooc_unmute <OOC-name>.') + for target in targets: + target.is_ooc_muted = False + client.send_host_message('Unmuted {} existing client(s).'.format(len(targets))) + +def ooc_cmd_disemvowel(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /disemvowel <id>.') + if targets: + for c in targets: + logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client) + logger.log_mod('Disemvowelling {}.'.format(c.get_ip()), client) + c.disemvowel = True + client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_undisemvowel(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /undisemvowel <id>.') + if targets: + for c in targets: + logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) + logger.log_mod('Undisemvowelling {}.'.format(c.get_ip()), client) + c.disemvowel = False + client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_shake(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /shake <id>.') + if targets: + for c in targets: + logger.log_server('Shaking {}.'.format(c.get_ip()), client) + logger.log_mod('Shaking {}.'.format(c.get_ip()), client) + c.shaken = True + client.send_host_message('Shook {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_unshake(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /unshake <id>.') + if targets: + for c in targets: + logger.log_server('Unshaking {}.'.format(c.get_ip()), client) + logger.log_mod('Unshaking {}.'.format(c.get_ip()), client) + c.shaken = False + client.send_host_message('Unshook {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_charcurse(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target (an ID) and at least one character ID. Consult /charids for the character IDs.') + elif len(arg) == 1: + raise ArgumentError('You must specific at least one character ID. Consult /charids for the character IDs.') + args = arg.split() + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) + except: + raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') + if targets: + for c in targets: + log_msg = ' ' + str(c.get_ip()) + ' to' + part_msg = ' [' + str(c.id) + '] to' + for raw_cid in args[1:]: + try: + cid = int(raw_cid) + c.charcurse.append(cid) + part_msg += ' ' + str(client.server.char_list[cid]) + ',' + log_msg += ' ' + str(client.server.char_list[cid]) + ',' + except: + ArgumentError('' + str(raw_cid) + ' does not look like a valid character ID.') + part_msg = part_msg[:-1] + part_msg += '.' + log_msg = log_msg[:-1] + log_msg += '.' + c.char_select() + logger.log_server('Charcursing' + log_msg, client) + logger.log_mod('Charcursing' + log_msg, client) + client.send_host_message('Charcursed' + part_msg) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_uncharcurse(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target (an ID).') + args = arg.split() + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) + except: + raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') + if targets: + for c in targets: + if len(c.charcurse) > 0: + c.charcurse = [] + logger.log_server('Uncharcursing {}.'.format(c.get_ip()), client) + logger.log_mod('Uncharcursing {}.'.format(c.get_ip()), client) + client.send_host_message('Uncharcursed [{}].'.format(c.id)) + c.char_select() + else: + client.send_host_message('[{}] is not charcursed.'.format(c.id)) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_charids(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + msg = 'Here is a list of all available characters on the server:' + for c in range(0, len(client.server.char_list)): + msg += '\n[' + str(c) + '] ' + client.server.char_list[c] + client.send_host_message(msg) + +def ooc_cmd_blockdj(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /blockdj <id>.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /blockdj <id>.') + if not targets: + raise ArgumentError('Target not found. Use /blockdj <id>.') + for target in targets: + target.is_dj = False + target.send_host_message('A moderator muted you from changing the music.') + logger.log_server('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + target.area.remove_jukebox_vote(target, True) + client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_unblockdj(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /unblockdj <id>.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /unblockdj <id>.') + if not targets: + raise ArgumentError('Target not found. Use /blockdj <id>.') + for target in targets: + target.is_dj = True + target.send_host_message('A moderator unmuted you from changing the music.') + logger.log_server('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_blockwtce(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /blockwtce <id>.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /blockwtce <id>.') + if not targets: + raise ArgumentError('Target not found. Use /blockwtce <id>.') + for target in targets: + target.can_wtce = False + target.send_host_message('A moderator blocked you from using judge signs.') + logger.log_server('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_unblockwtce(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /unblockwtce <id>.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /unblockwtce <id>.') + if not targets: + raise ArgumentError('Target not found. Use /unblockwtce <id>.') + for target in targets: + target.can_wtce = True + target.send_host_message('A moderator unblocked you from using judge signs.') + logger.log_server('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_notecard(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify the contents of the note card.') + client.area.cards[client.get_char_name()] = arg + client.area.send_host_message('{} wrote a note card.'.format(client.get_char_name())) + +def ooc_cmd_notecard_clear(client, arg): + try: + del client.area.cards[client.get_char_name()] + client.area.send_host_message('{} erased their note card.'.format(client.get_char_name())) + except KeyError: + raise ClientError('You do not have a note card.') + +def ooc_cmd_notecard_reveal(client, arg): + if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be a CM or moderator to reveal cards.') + if len(client.area.cards) == 0: + raise ClientError('There are no cards to reveal in this area.') + msg = 'Note cards have been revealed.\n' + for card_owner, card_msg in client.area.cards.items(): + msg += '{}: {}\n'.format(card_owner, card_msg) + client.area.cards.clear() + client.area.send_host_message(msg) + +def ooc_cmd_rolla_reload(client, arg): + if not client.is_mod: + raise ClientError('You must be a moderator to load the ability dice configuration.') + rolla_reload(client.area) + client.send_host_message('Reloaded ability dice configuration.') + +def rolla_reload(area): + try: + import yaml + with open('config/dice.yaml', 'r') as dice: + area.ability_dice = yaml.load(dice) + except: + raise ServerError('There was an error parsing the ability dice configuration. Check your syntax.') + +def ooc_cmd_rolla_set(client, arg): + if not hasattr(client.area, 'ability_dice'): + rolla_reload(client.area) + available_sets = ', '.join(client.area.ability_dice.keys()) + if len(arg) == 0: + raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets)) + if arg in client.area.ability_dice: + client.ability_dice_set = arg + client.send_host_message("Set ability set to {}.".format(arg)) + else: + raise ArgumentError('Invalid ability set \'{}\'.\nAvailable sets: {}'.format(arg, available_sets)) + +def ooc_cmd_rolla(client, arg): + if not hasattr(client.area, 'ability_dice'): + rolla_reload(client.area) + if not hasattr(client, 'ability_dice_set'): + raise ClientError('You must set your ability set using /rolla_set <name>.') + ability_dice = client.area.ability_dice[client.ability_dice_set] + max_roll = ability_dice['max'] if 'max' in ability_dice else 6 + roll = random.randint(1, max_roll) + ability = ability_dice[roll] if roll in ability_dice else "Nothing happens" + client.area.send_host_message( + '{} rolled a {} (out of {}): {}.'.format(client.get_char_name(), roll, max_roll, ability)) + +def ooc_cmd_refresh(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len (arg) > 0: + raise ClientError('This command does not take in any arguments!') + else: + try: + client.server.refresh() + client.send_host_message('You have reloaded the server.') + except ServerError: + raise + +def ooc_cmd_judgelog(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command does not take any arguments.') + jlog = client.area.judgelog + if len(jlog) > 0: + jlog_msg = '== Judge Log ==' + for x in jlog: + jlog_msg += '\r\n{}'.format(x) + client.send_host_message(jlog_msg) + else: + raise ServerError('There have been no judge actions in this area since start of session.') diff --git a/server/constants.py b/server/constants.py new file mode 100644 index 00000000..fa07e8ee --- /dev/null +++ b/server/constants.py @@ -0,0 +1,11 @@ +from enum import Enum + +class TargetType(Enum): + #possible keys: ip, OOC, id, cname, ipid, hdid + IP = 0 + OOC_NAME = 1 + ID = 2 + CHAR_NAME = 3 + IPID = 4 + HDID = 5 + ALL = 6
\ No newline at end of file diff --git a/server/districtclient.py b/server/districtclient.py new file mode 100644 index 00000000..c766ba5f --- /dev/null +++ b/server/districtclient.py @@ -0,0 +1,79 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import asyncio + +from server import logger + + +class DistrictClient: + def __init__(self, server): + self.server = server + self.reader = None + self.writer = None + self.message_queue = [] + + async def connect(self): + loop = asyncio.get_event_loop() + while True: + try: + self.reader, self.writer = await asyncio.open_connection(self.server.config['district_ip'], + self.server.config['district_port'], loop=loop) + await self.handle_connection() + except (ConnectionRefusedError, TimeoutError): + pass + except (ConnectionResetError, asyncio.IncompleteReadError): + self.writer = None + self.reader = None + finally: + logger.log_debug("Couldn't connect to the district, retrying in 30 seconds.") + await asyncio.sleep(30) + + async def handle_connection(self): + logger.log_debug('District connected.') + self.send_raw_message('AUTH#{}'.format(self.server.config['district_password'])) + while True: + data = await self.reader.readuntil(b'\r\n') + if not data: + return + raw_msg = data.decode()[:-2] + logger.log_debug('[DISTRICT][INC][RAW]{}'.format(raw_msg)) + cmd, *args = raw_msg.split('#') + if cmd == 'GLOBAL': + glob_name = '{}[{}:{}][{}]'.format('<dollar>G', args[1], args[2], args[3]) + if args[0] == '1': + glob_name += '[M]' + self.server.send_all_cmd_pred('CT', glob_name, args[4], pred=lambda x: not x.muted_global) + elif cmd == 'NEED': + need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \ + .format(args[1], args[0], args[2], args[3], args[4]) + self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1', + pred=lambda x: not x.muted_adverts) + + async def write_queue(self): + while self.message_queue: + msg = self.message_queue.pop(0) + try: + self.writer.write(msg) + await self.writer.drain() + except ConnectionResetError: + return + + def send_raw_message(self, msg): + if not self.writer: + return + self.message_queue.append('{}\r\n'.format(msg).encode()) + asyncio.ensure_future(self.write_queue(), loop=asyncio.get_event_loop()) diff --git a/server/evidence.py b/server/evidence.py new file mode 100644 index 00000000..b34172ad --- /dev/null +++ b/server/evidence.py @@ -0,0 +1,100 @@ +class EvidenceList: + limit = 35 + + class Evidence: + def __init__(self, name, desc, image, pos): + self.name = name + self.desc = desc + self.image = image + self.public = False + self.pos = pos + + def set_name(self, name): + self.name = name + + def set_desc(self, desc): + self.desc = desc + + def set_image(self, image): + self.image = image + + def to_string(self): + sequence = (self.name, self.desc, self.image) + return '&'.join(sequence) + + def __init__(self): + self.evidences = [] + self.poses = {'def':['def', 'hld'], + 'pro':['pro', 'hlp'], + 'wit':['wit', 'sea'], + 'sea':['sea', 'wit'], + 'hlp':['hlp', 'pro'], + 'hld':['hld', 'def'], + 'jud':['jud', 'jur'], + 'jur':['jur', 'jud'], + 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''], + 'pos':[]} + + def login(self, client): + if client.area.evidence_mod == 'FFA': + pass + if client.area.evidence_mod == 'Mods': + if not client in client.area.owners: + return False + if client.area.evidence_mod == 'CM': + if not client in client.area.owners and not client.is_mod: + return False + if client.area.evidence_mod == 'HiddenCM': + if not client in client.area.owners and not client.is_mod: + return False + return True + + def correct_format(self, client, desc): + if client.area.evidence_mod != 'HiddenCM': + return True + else: + #correct format: <owner = pos>\ndesc + if desc[:9] == '<owner = ' and desc[9:12] in self.poses and desc[12:14] == '>\n': + return True + return False + + + def add_evidence(self, client, name, description, image, pos = 'all'): + if self.login(client): + if client.area.evidence_mod == 'HiddenCM': + pos = 'pos' + if len(self.evidences) >= self.limit: + client.send_host_message('You can\'t have more than {} evidence items at a time.'.format(self.limit)) + else: + self.evidences.append(self.Evidence(name, description, image, pos)) + + def evidence_swap(self, client, id1, id2): + if self.login(client): + self.evidences[id1], self.evidences[id2] = self.evidences[id2], self.evidences[id1] + + def create_evi_list(self, client): + evi_list = [] + nums_list = [0] + for i in range(len(self.evidences)): + if client.area.evidence_mod == 'HiddenCM' and self.login(client): + nums_list.append(i + 1) + evi = self.evidences[i] + evi_list.append(self.Evidence(evi.name, '<owner = {}>\n{}'.format(evi.pos, evi.desc), evi.image, evi.pos).to_string()) + elif client.pos in self.poses[self.evidences[i].pos]: + nums_list.append(i + 1) + evi_list.append(self.evidences[i].to_string()) + return nums_list, evi_list + + def del_evidence(self, client, id): + if self.login(client): + self.evidences.pop(id) + + def edit_evidence(self, client, id, arg): + if self.login(client): + if client.area.evidence_mod == 'HiddenCM' and self.correct_format(client, arg[1]): + self.evidences[id] = self.Evidence(arg[0], arg[1][14:], arg[2], arg[1][9:12]) + return + if client.area.evidence_mod == 'HiddenCM': + client.send_host_message('You entered a wrong pos.') + return + self.evidences[id] = self.Evidence(arg[0], arg[1], arg[2], arg[3])
\ No newline at end of file diff --git a/server/exceptions.py b/server/exceptions.py new file mode 100644 index 00000000..d3503e91 --- /dev/null +++ b/server/exceptions.py @@ -0,0 +1,32 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +class ClientError(Exception): + pass + + +class AreaError(Exception): + pass + + +class ArgumentError(Exception): + pass + + +class ServerError(Exception): + pass diff --git a/server/fantacrypt.py b/server/fantacrypt.py new file mode 100644 index 00000000..e31548e7 --- /dev/null +++ b/server/fantacrypt.py @@ -0,0 +1,45 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# fantacrypt was a mistake, just hardcoding some numbers is good enough + +import binascii + +CRYPT_CONST_1 = 53761 +CRYPT_CONST_2 = 32618 +CRYPT_KEY = 5 + + +def fanta_decrypt(data): + data_bytes = [int(data[x:x + 2], 16) for x in range(0, len(data), 2)] + key = CRYPT_KEY + ret = '' + for byte in data_bytes: + val = byte ^ ((key & 0xffff) >> 8) + ret += chr(val) + key = ((byte + key) * CRYPT_CONST_1) + CRYPT_CONST_2 + return ret + + +def fanta_encrypt(data): + key = CRYPT_KEY + ret = '' + for char in data: + val = ord(char) ^ ((key & 0xffff) >> 8) + ret += binascii.hexlify(val.to_bytes(1, byteorder='big')).decode().upper() + key = ((val + key) * CRYPT_CONST_1) + CRYPT_CONST_2 + return ret diff --git a/server/logger.py b/server/logger.py new file mode 100644 index 00000000..fb1b8b36 --- /dev/null +++ b/server/logger.py @@ -0,0 +1,78 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +import time + + +def setup_logger(debug): + logging.Formatter.converter = time.gmtime + debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + + debug_log = logging.getLogger('debug') + debug_log.setLevel(logging.DEBUG) + + debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8') + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(debug_formatter) + debug_log.addHandler(debug_handler) + + if not debug: + debug_log.disabled = True + + server_log = logging.getLogger('server') + server_log.setLevel(logging.INFO) + + server_handler = logging.FileHandler('logs/server.log', encoding='utf-8') + server_handler.setLevel(logging.INFO) + server_handler.setFormatter(srv_formatter) + server_log.addHandler(server_handler) + + mod_log = logging.getLogger('mod') + mod_log.setLevel(logging.INFO) + + mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8') + mod_handler.setLevel(logging.INFO) + mod_handler.setFormatter(mod_formatter) + mod_log.addHandler(mod_handler) + + +def log_debug(msg, client=None): + msg = parse_client_info(client) + msg + logging.getLogger('debug').debug(msg) + + +def log_server(msg, client=None): + msg = parse_client_info(client) + msg + logging.getLogger('server').info(msg) + + +def log_mod(msg, client=None): + msg = parse_client_info(client) + msg + logging.getLogger('mod').info(msg) + + +def parse_client_info(client): + if client is None: + return '' + info = client.get_ip() + if client.is_mod: + return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name) + return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name) diff --git a/server/masterserverclient.py b/server/masterserverclient.py new file mode 100644 index 00000000..49af0435 --- /dev/null +++ b/server/masterserverclient.py @@ -0,0 +1,89 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import asyncio +import time +from server import logger + + +class MasterServerClient: + def __init__(self, server): + self.server = server + self.reader = None + self.writer = None + + async def connect(self): + loop = asyncio.get_event_loop() + while True: + try: + self.reader, self.writer = await asyncio.open_connection(self.server.config['masterserver_ip'], + self.server.config['masterserver_port'], + loop=loop) + await self.handle_connection() + except (ConnectionRefusedError, TimeoutError): + pass + except (ConnectionResetError, asyncio.IncompleteReadError): + self.writer = None + self.reader = None + finally: + logger.log_debug("Couldn't connect to the master server, retrying in 30 seconds.") + print("Couldn't connect to the master server, retrying in 30 seconds.") + await asyncio.sleep(30) + + async def handle_connection(self): + logger.log_debug('Master server connected.') + await self.send_server_info() + fl = False + lastping = time.time() - 20 + while True: + self.reader.feed_data(b'END') + full_data = await self.reader.readuntil(b'END') + full_data = full_data[:-3] + if len(full_data) > 0: + data_list = list(full_data.split(b'#%'))[:-1] + for data in data_list: + raw_msg = data.decode() + cmd, *args = raw_msg.split('#') + if cmd != 'CHECK' and cmd != 'PONG': + logger.log_debug('[MASTERSERVER][INC][RAW]{}'.format(raw_msg)) + elif cmd == 'CHECK': + await self.send_raw_message('PING#%') + elif cmd == 'PONG': + fl = False + elif cmd == 'NOSERV': + await self.send_server_info() + if time.time() - lastping > 5: + if fl: + return + lastping = time.time() + fl = True + await self.send_raw_message('PING#%') + await asyncio.sleep(1) + + async def send_server_info(self): + cfg = self.server.config + msg = 'SCC#{}#{}#{}#{}#%'.format(cfg['port'], cfg['masterserver_name'], cfg['masterserver_description'], + self.server.software) + await self.send_raw_message(msg) + + async def send_raw_message(self, msg): + try: + self.writer.write(msg.encode()) + await self.writer.drain() + except ConnectionResetError: + return diff --git a/server/tsuserver.py b/server/tsuserver.py new file mode 100644 index 00000000..5af8161d --- /dev/null +++ b/server/tsuserver.py @@ -0,0 +1,305 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import asyncio + +import yaml +import json + +from server import logger +from server.aoprotocol import AOProtocol +from server.area_manager import AreaManager +from server.ban_manager import BanManager +from server.client_manager import ClientManager +from server.districtclient import DistrictClient +from server.exceptions import ServerError +from server.masterserverclient import MasterServerClient + +class TsuServer3: + def __init__(self): + self.config = None + self.allowed_iniswaps = None + self.load_config() + self.load_iniswaps() + self.client_manager = ClientManager(self) + self.area_manager = AreaManager(self) + self.ban_manager = BanManager() + self.software = 'tsuserver3' + self.version = 'tsuserver3dev' + self.release = 3 + self.major_version = 1 + self.minor_version = 1 + self.ipid_list = {} + self.hdid_list = {} + self.char_list = None + self.char_pages_ao1 = None + self.music_list = None + self.music_list_ao2 = None + self.music_pages_ao1 = None + self.backgrounds = None + self.load_characters() + self.load_music() + self.load_backgrounds() + self.load_ids() + self.district_client = None + self.ms_client = None + self.rp_mode = False + logger.setup_logger(debug=self.config['debug']) + + def start(self): + loop = asyncio.get_event_loop() + + bound_ip = '0.0.0.0' + if self.config['local']: + bound_ip = '127.0.0.1' + + ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) + ao_server = loop.run_until_complete(ao_server_crt) + + if self.config['use_district']: + self.district_client = DistrictClient(self) + asyncio.ensure_future(self.district_client.connect(), loop=loop) + + if self.config['use_masterserver']: + self.ms_client = MasterServerClient(self) + asyncio.ensure_future(self.ms_client.connect(), loop=loop) + + logger.log_debug('Server started.') + + try: + loop.run_forever() + except KeyboardInterrupt: + pass + + logger.log_debug('Server shutting down.') + + ao_server.close() + loop.run_until_complete(ao_server.wait_closed()) + loop.close() + + def get_version_string(self): + return str(self.release) + '.' + str(self.major_version) + '.' + str(self.minor_version) + + def new_client(self, transport): + c = self.client_manager.new_client(transport) + if self.rp_mode: + c.in_rp = True + c.server = self + c.area = self.area_manager.default_area() + c.area.new_client(c) + return c + + def remove_client(self, client): + client.area.remove_client(client) + self.client_manager.remove_client(client) + + def get_player_count(self): + return len(self.client_manager.clients) + + def load_config(self): + with open('config/config.yaml', 'r', encoding = 'utf-8') as cfg: + self.config = yaml.load(cfg) + self.config['motd'] = self.config['motd'].replace('\\n', ' \n') + if 'music_change_floodguard' not in self.config: + self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} + if 'wtce_floodguard' not in self.config: + self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} + + def load_characters(self): + with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars: + self.char_list = yaml.load(chars) + self.build_char_pages_ao1() + + def load_music(self): + with open('config/music.yaml', 'r', encoding = 'utf-8') as music: + self.music_list = yaml.load(music) + self.build_music_pages_ao1() + self.build_music_list_ao2() + + def load_ids(self): + self.ipid_list = {} + self.hdid_list = {} + #load ipids + try: + with open('storage/ip_ids.json', 'r', encoding = 'utf-8') as whole_list: + self.ipid_list = json.loads(whole_list.read()) + except: + logger.log_debug('Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.') + #load hdids + try: + with open('storage/hd_ids.json', 'r', encoding = 'utf-8') as whole_list: + self.hdid_list = json.loads(whole_list.read()) + except: + logger.log_debug('Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.') + + def dump_ipids(self): + with open('storage/ip_ids.json', 'w') as whole_list: + json.dump(self.ipid_list, whole_list) + + def dump_hdids(self): + with open('storage/hd_ids.json', 'w') as whole_list: + json.dump(self.hdid_list, whole_list) + + def get_ipid(self, ip): + if not (ip in self.ipid_list): + self.ipid_list[ip] = len(self.ipid_list) + self.dump_ipids() + return self.ipid_list[ip] + + def load_backgrounds(self): + with open('config/backgrounds.yaml', 'r', encoding = 'utf-8') as bgs: + self.backgrounds = yaml.load(bgs) + + def load_iniswaps(self): + try: + with open('config/iniswaps.yaml', 'r', encoding = 'utf-8') as iniswaps: + self.allowed_iniswaps = yaml.load(iniswaps) + except: + logger.log_debug('cannot find iniswaps.yaml') + + + def build_char_pages_ao1(self): + self.char_pages_ao1 = [self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10)] + for i in range(len(self.char_list)): + self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format(i, self.char_list[i]) + + def build_music_pages_ao1(self): + self.music_pages_ao1 = [] + index = 0 + # add areas first + for area in self.area_manager.areas: + self.music_pages_ao1.append('{}#{}'.format(index, area.name)) + index += 1 + # then add music + for item in self.music_list: + self.music_pages_ao1.append('{}#{}'.format(index, item['category'])) + index += 1 + for song in item['songs']: + self.music_pages_ao1.append('{}#{}'.format(index, song['name'])) + index += 1 + self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)] + + def build_music_list_ao2(self): + self.music_list_ao2 = [] + # add areas first + for area in self.area_manager.areas: + self.music_list_ao2.append(area.name) + # then add music + for item in self.music_list: + self.music_list_ao2.append(item['category']) + for song in item['songs']: + self.music_list_ao2.append(song['name']) + + def is_valid_char_id(self, char_id): + return len(self.char_list) > char_id >= 0 + + def get_char_id_by_name(self, name): + for i, ch in enumerate(self.char_list): + if ch.lower() == name.lower(): + return i + raise ServerError('Character not found.') + + def get_song_data(self, music): + for item in self.music_list: + if item['category'] == music: + return item['category'], -1 + for song in item['songs']: + if song['name'] == music: + try: + return song['name'], song['length'] + except KeyError: + return song['name'], -1 + raise ServerError('Music not found.') + + def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): + for client in self.client_manager.clients: + if pred(client): + client.send_command(cmd, *args) + + def broadcast_global(self, client, msg, as_mod=False): + char_name = client.get_char_name() + ooc_name = '{}[{}][{}]'.format('<dollar>G', client.area.abbreviation, char_name) + if as_mod: + ooc_name += '[M]' + self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) + if self.config['use_district']: + self.district_client.send_raw_message( + 'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg)) + + def send_modchat(self, client, msg): + name = client.name + ooc_name = '{}[{}][{}]'.format('<dollar>M', client.area.abbreviation, name) + self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) + if self.config['use_district']: + self.district_client.send_raw_message( + 'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg)) + + def broadcast_need(self, client, msg): + char_name = client.get_char_name() + area_name = client.area.name + area_id = client.area.abbreviation + self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), + ['=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' + .format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts) + if self.config['use_district']: + self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) + + def send_arup(self, args): + """ Updates the area properties on the Case Café Custom Client. + + Playercount: + ARUP#0#<area1_p: int>#<area2_p: int>#... + Status: + ARUP#1##<area1_s: string>##<area2_s: string>#... + CM: + ARUP#2##<area1_cm: string>##<area2_cm: string>#... + Lockedness: + ARUP#3##<area1_l: string>##<area2_l: string>#... + + """ + if len(args) < 2: + # An argument count smaller than 2 means we only got the identifier of ARUP. + return + if args[0] not in (0,1,2,3): + return + + if args[0] == 0: + for part_arg in args[1:]: + try: + sanitised = int(part_arg) + except: + return + elif args[0] in (1, 2, 3): + for part_arg in args[1:]: + try: + sanitised = str(part_arg) + except: + return + + self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True) + + def refresh(self): + with open('config/config.yaml', 'r') as cfg: + self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') + with open('config/characters.yaml', 'r') as chars: + self.char_list = yaml.load(chars) + with open('config/music.yaml', 'r') as music: + self.music_list = yaml.load(music) + self.build_music_pages_ao1() + self.build_music_list_ao2() + with open('config/backgrounds.yaml', 'r') as bgs: + self.backgrounds = yaml.load(bgs) diff --git a/server/websocket.py b/server/websocket.py new file mode 100644 index 00000000..ba4258f3 --- /dev/null +++ b/server/websocket.py @@ -0,0 +1,215 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2017 argoneus <argoneuscze@gmail.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Partly authored by Johan Hanssen Seferidis (MIT license): +# https://github.com/Pithikos/python-websocket-server + +import asyncio +import re +import struct +from base64 import b64encode +from hashlib import sha1 + +from server import logger + + +class Bitmasks: + FIN = 0x80 + OPCODE = 0x0f + MASKED = 0x80 + PAYLOAD_LEN = 0x7f + PAYLOAD_LEN_EXT16 = 0x7e + PAYLOAD_LEN_EXT64 = 0x7f + + +class Opcode: + CONTINUATION = 0x0 + TEXT = 0x1 + BINARY = 0x2 + CLOSE_CONN = 0x8 + PING = 0x9 + PONG = 0xA + + +class WebSocket: + """ + State data for clients that are connected via a WebSocket that wraps + over a conventional TCP connection. + """ + + def __init__(self, client, protocol): + self.client = client + self.transport = client.transport + self.protocol = protocol + self.keep_alive = True + self.handshake_done = False + self.valid = False + + def handle(self, data): + if not self.handshake_done: + return self.handshake(data) + return self.parse(data) + + def parse(self, data): + b1, b2 = 0, 0 + if len(data) >= 2: + b1, b2 = data[0], data[1] + + fin = b1 & Bitmasks.FIN + opcode = b1 & Bitmasks.OPCODE + masked = b2 & Bitmasks.MASKED + payload_length = b2 & Bitmasks.PAYLOAD_LEN + + if not b1: + # Connection closed + self.keep_alive = 0 + return + if opcode == Opcode.CLOSE_CONN: + # Connection close requested + self.keep_alive = 0 + return + if not masked: + # Client was not masked (spec violation) + logger.log_debug("ws: client was not masked.", self.client) + self.keep_alive = 0 + print(data) + return + if opcode == Opcode.CONTINUATION: + # No continuation frames supported + logger.log_debug("ws: client tried to send continuation frame.", self.client) + return + elif opcode == Opcode.BINARY: + # No binary frames supported + logger.log_debug("ws: client tried to send binary frame.", self.client) + return + elif opcode == Opcode.TEXT: + def opcode_handler(s, msg): + return msg + elif opcode == Opcode.PING: + opcode_handler = self.send_pong + elif opcode == Opcode.PONG: + opcode_handler = lambda s, msg: None + else: + # Unknown opcode + logger.log_debug("ws: unknown opcode!", self.client) + self.keep_alive = 0 + return + + mask_offset = 2 + if payload_length == 126: + payload_length = struct.unpack(">H", data[2:4])[0] + mask_offset = 4 + elif payload_length == 127: + payload_length = struct.unpack(">Q", data[2:10])[0] + mask_offset = 10 + + masks = data[mask_offset:mask_offset + 4] + decoded = "" + for char in data[mask_offset + 4:payload_length + mask_offset + 4]: + char ^= masks[len(decoded) % 4] + decoded += chr(char) + + return opcode_handler(self, decoded) + + def send_message(self, message): + self.send_text(message) + + def send_pong(self, message): + self.send_text(message, Opcode.PONG) + + def send_text(self, message, opcode=Opcode.TEXT): + """ + Important: Fragmented (continuation) messages are not supported since + their usage cases are limited - when we don't know the payload length. + """ + + # Validate message + if isinstance(message, bytes): + message = message.decode("utf-8") + elif isinstance(message, str): + pass + else: + raise TypeError("Message must be either str or bytes") + + header = bytearray() + payload = message.encode("utf-8") + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(Bitmasks.FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(Bitmasks.FIN | opcode) + header.append(Bitmasks.PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < (1 << 64): + header.append(Bitmasks.FIN | opcode) + header.append(Bitmasks.PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big") + + self.transport.write(header + payload) + + def handshake(self, data): + try: + message = data[0:1024].decode().strip() + except UnicodeDecodeError: + return False + + upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower()) + if not upgrade: + self.keep_alive = False + return False + + key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message) + if key: + key = key.group(1) + else: + logger.log_debug("Client tried to connect but was missing a key", self.client) + self.keep_alive = False + return False + + response = self.make_handshake_response(key) + print(response.encode()) + self.transport.write(response.encode()) + self.handshake_done = True + self.valid = True + return True + + def make_handshake_response(self, key): + return \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: %s\r\n' \ + '\r\n' % self.calculate_response_key(key) + + def calculate_response_key(self, key): + GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + hash = sha1(key.encode() + GUID.encode()) + response_key = b64encode(hash.digest()).strip() + return response_key.decode('ASCII') + + def finish(self): + self.protocol.connection_lost(self) |
