Added the tsuserver3 files necessary to support this custom client.
This commit is contained in:
parent
f77381864e
commit
374e939ac4
2
.gitignore
vendored
2
.gitignore
vendored
@ -16,3 +16,5 @@ Makefile*
|
||||
object_script*
|
||||
/Attorney_Online_remake_resource.rc
|
||||
/attorney_online_remake_plugin_import.cpp
|
||||
|
||||
server/__pycache__
|
||||
|
0
server/__init__.py
Normal file
0
server/__init__.py
Normal file
641
server/aoprotocol.py
Normal file
641
server/aoprotocol.py
Normal file
@ -0,0 +1,641 @@
|
||||
# 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
|
||||
|
||||
|
||||
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:
|
||||
self.client.disconnect()
|
||||
return
|
||||
# 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:
|
||||
return
|
||||
|
||||
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.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')
|
||||
|
||||
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
|
||||
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):
|
||||
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args
|
||||
showname = self.client.get_char_name()
|
||||
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):
|
||||
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args
|
||||
if len(showname) > 0 and not self.client.area.showname_changes_allowed == "true":
|
||||
self.client.send_host_message("Showname changes are forbidden in this area!")
|
||||
return
|
||||
else:
|
||||
return
|
||||
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args
|
||||
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 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 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:
|
||||
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'):
|
||||
return
|
||||
msg = text[:256]
|
||||
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()
|
||||
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)
|
||||
self.client.area.set_next_msg_delay(len(msg))
|
||||
logger.log_server('[IC][{}][{}]{}'.format(self.client.area.id, 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
|
||||
if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('<dollar>G'):
|
||||
self.client.send_host_message('That name is reserved!')
|
||||
return
|
||||
if args[1].startswith('/'):
|
||||
spl = args[1][1:].split(' ', 1)
|
||||
cmd = spl[0]
|
||||
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.disemvowel:
|
||||
args[1] = self.client.disemvowel_message(args[1])
|
||||
self.client.area.send_command('CT', self.client.name, args[1])
|
||||
logger.log_server(
|
||||
'[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.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 not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT):
|
||||
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])
|
||||
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.id, 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 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 not self.validate_net_cmd(args, self.ArgType.STR):
|
||||
return
|
||||
if args[0] == 'testimony1':
|
||||
sign = 'WT'
|
||||
elif args[0] == 'testimony2':
|
||||
sign = 'CE'
|
||||
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
|
||||
self.client.area.send_command('RT', args[0])
|
||||
self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign))
|
||||
logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, 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 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.id, 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,
|
||||
self.client.area.id), pred=lambda c: c.is_mod)
|
||||
self.client.set_mod_call_delay()
|
||||
logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name()))
|
||||
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,
|
||||
self.client.area.id, args[0]), pred=lambda c: c.is_mod)
|
||||
self.client.set_mod_call_delay()
|
||||
logger.log_server('[{}][{}]{} called a moderator: {}.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name(), args[0]))
|
||||
|
||||
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
|
||||
}
|
210
server/area_manager.py
Normal file
210
server/area_manager.py
Normal file
@ -0,0 +1,210 @@
|
||||
# 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
|
||||
|
||||
|
||||
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):
|
||||
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.evi_list = EvidenceList()
|
||||
self.is_recording = False
|
||||
self.recorded_messages = []
|
||||
self.evidence_mod = evidence_mod
|
||||
self.locking_allowed = locking_allowed
|
||||
self.owned = False
|
||||
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 = False
|
||||
|
||||
def new_client(self, client):
|
||||
self.clients.add(client)
|
||||
|
||||
def remove_client(self, client):
|
||||
self.clients.remove(client)
|
||||
if client.is_cm:
|
||||
client.is_cm = False
|
||||
self.owned = False
|
||||
if self.is_locked:
|
||||
self.unlock()
|
||||
|
||||
def unlock(self):
|
||||
self.is_locked = False
|
||||
self.invite_list = {}
|
||||
self.send_host_message('This area is open 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_host_message(self, msg):
|
||||
self.send_command('CT', self.server.config['hostname'], msg)
|
||||
|
||||
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 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 can_send_message(self, client):
|
||||
if self.is_locked and not client.is_mod and not client.ipid in self.invite_list:
|
||||
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 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', 'building-open', 'building-full', 'casing-open', 'casing-full', 'recess')
|
||||
if value.lower() not in allowed_values:
|
||||
raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values)))
|
||||
self.status = value.upper()
|
||||
|
||||
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 = 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 __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
|
||||
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']))
|
||||
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.')
|
54
server/ban_manager.py
Normal file
54
server/ban_manager.py
Normal file
@ -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)
|
380
server/client_manager.py
Normal file
380
server/client_manager.py
Normal file
@ -0,0 +1,380 @@
|
||||
# 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.is_cm = False
|
||||
self.evi_list = []
|
||||
self.disemvowel = False
|
||||
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
|
||||
|
||||
#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)
|
||||
|
||||
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 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)
|
||||
logger.log_server('[{}]Changed character from {} to {}.'
|
||||
.format(self.area.id, old_char, self.get_char_name()), self)
|
||||
|
||||
def change_music_cd(self):
|
||||
if self.is_mod or self.is_cm:
|
||||
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.is_cm:
|
||||
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 and not self.is_mod and not self.ipid in area.invite_list:
|
||||
#self.send_host_message('This area is locked - you will be unable to send messages ICly.')
|
||||
raise ClientError("That area is locked!")
|
||||
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.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 ==='
|
||||
lock = {True: '[LOCKED]', False: ''}
|
||||
for i, area in enumerate(self.server.area_manager.areas):
|
||||
owner = 'FREE'
|
||||
if area.owned:
|
||||
for client in [x for x in area.clients if x.is_cm]:
|
||||
owner = 'MASTER: {}'.format(client.get_char_name())
|
||||
break
|
||||
msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(i, 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 = ''
|
||||
try:
|
||||
area = self.server.area_manager.get_area_by_id(area_id)
|
||||
except AreaError:
|
||||
raise
|
||||
info += '= Area {}: {} =='.format(area.id, area.name)
|
||||
sorted_clients = []
|
||||
for client in area.clients:
|
||||
if (not mods) or client.is_mod:
|
||||
sorted_clients.append(client)
|
||||
sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name())
|
||||
for c in sorted_clients:
|
||||
info += '\r\n[{}] {}'.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:
|
||||
cnt += len(self.server.area_manager.areas[i].clients)
|
||||
info += '\r\n{}'.format(self.get_area_info(i, mods))
|
||||
info = 'Current online: {}'.format(cnt) + info
|
||||
else:
|
||||
try:
|
||||
info = 'People in this area: {}\n'.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):
|
||||
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
|
||||
self.send_command('CharsCheck', *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.send_command('DONE')
|
||||
|
||||
def char_select(self):
|
||||
self.char_id = -1
|
||||
self.send_done()
|
||||
|
||||
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'):
|
||||
raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit.')
|
||||
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 disemvowel_message(self, message):
|
||||
message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE)
|
||||
return re.sub(r"\s+", " ", message)
|
||||
|
||||
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):
|
||||
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
|
848
server/commands.py
Normal file
848
server/commands.py
Normal file
@ -0,0 +1,848 @@
|
||||
# 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 hashlib
|
||||
import string
|
||||
from server.constants import TargetType
|
||||
|
||||
from server import logger
|
||||
from server.exceptions import ClientError, ServerError, ArgumentError, AreaError
|
||||
|
||||
|
||||
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.id, 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('A mod has set the background lock to {}.'.format(client.area.bg_lock))
|
||||
logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.id, 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_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.id, client.get_char_name(), roll, val[0]))
|
||||
|
||||
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 /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.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0]))
|
||||
client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0]))
|
||||
SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))
|
||||
logger.log_server(
|
||||
'[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0]))
|
||||
|
||||
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.')
|
||||
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_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.id, client.get_char_name(), flip))
|
||||
|
||||
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.is_cm 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.id, client.get_char_name(), pos, len(targets)))
|
||||
|
||||
def ooc_cmd_help(client, arg):
|
||||
if len(arg) != 0:
|
||||
raise ArgumentError('This command has no arguments.')
|
||||
help_url = 'https://github.com/AttorneyOnline/tsuserver3/blob/master/README.md'
|
||||
help_msg = 'Available commands, source code and issues 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>.')
|
||||
targets = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)
|
||||
if targets:
|
||||
for c in targets:
|
||||
logger.log_server('Kicked {}.'.format(c.ipid), client)
|
||||
client.send_host_message("{} was kicked.".format(c.get_char_name()))
|
||||
c.disconnect()
|
||||
else:
|
||||
client.send_host_message("No targets found.")
|
||||
|
||||
def ooc_cmd_ban(client, arg):
|
||||
if not client.is_mod:
|
||||
raise ClientError('You must be authorized to do that.')
|
||||
try:
|
||||
ipid = int(arg.strip())
|
||||
except:
|
||||
raise ClientError('You must specify 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.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)
|
||||
|
||||
def ooc_cmd_unban(client, arg):
|
||||
if not client.is_mod:
|
||||
raise ClientError('You must be authorized to do that.')
|
||||
try:
|
||||
client.server.ban_manager.remove_ban(int(arg.strip()))
|
||||
except:
|
||||
raise ClientError('You must specify \'hdid\'')
|
||||
logger.log_server('Unbanned {}.'.format(arg), client)
|
||||
client.send_host_message('Unbanned {}'.format(arg))
|
||||
|
||||
|
||||
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.id, 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.')
|
||||
try:
|
||||
c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0]
|
||||
c.is_muted = True
|
||||
client.send_host_message('{} existing client(s).'.format(c.get_char_name()))
|
||||
except:
|
||||
client.send_host_message("No targets found. Use /mute <id> for mute")
|
||||
|
||||
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.')
|
||||
try:
|
||||
c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0]
|
||||
c.is_muted = False
|
||||
client.send_host_message('{} existing client(s).'.format(c.get_char_name()))
|
||||
except:
|
||||
client.send_host_message("No targets found. Use /mute <id> for mute")
|
||||
|
||||
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)
|
||||
|
||||
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.id, 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.id, 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.id, 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))
|
||||
logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.id, 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.id, 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.id, client.get_char_name(), client.area.doc))
|
||||
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.id, client.get_char_name(), arg))
|
||||
|
||||
|
||||
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.id, client.get_char_name(), client.area.doc))
|
||||
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.id, client.get_char_name(), client.area.status))
|
||||
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:
|
||||
c.send_host_message('PM from {} in {} ({}): {}'.format(client.name, 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.')
|
||||
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 client.area.owned == False:
|
||||
client.area.owned = True
|
||||
client.is_cm = True
|
||||
if client.area.evidence_mod == 'HiddenCM':
|
||||
client.area.broadcast_evidence_list()
|
||||
client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name()))
|
||||
|
||||
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.send_host_message('Area is already locked.')
|
||||
if client.is_cm:
|
||||
client.area.is_locked = True
|
||||
client.area.send_host_message('Area is locked.')
|
||||
for i in client.area.clients:
|
||||
client.area.invite_list[i.ipid] = None
|
||||
return
|
||||
else:
|
||||
raise ClientError('Only CM can lock the area.')
|
||||
|
||||
def ooc_cmd_area_unlock(client, arg):
|
||||
if not client.area.is_locked:
|
||||
raise ClientError('Area is already unlocked.')
|
||||
if not client.is_cm:
|
||||
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 not client.area.is_locked:
|
||||
raise ClientError('Area isn\'t locked.')
|
||||
if not client.is_cm 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.ipid] = 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 area {}.'.format(client.area.id))
|
||||
except:
|
||||
raise ClientError('You must specify a target. Use /invite <id>')
|
||||
|
||||
def ooc_cmd_uninvite(client, arg):
|
||||
if not client.is_cm and not client.is_mod:
|
||||
raise ClientError('You must be authorized to do that.')
|
||||
if not client.area.is_locked and not client.is_mod:
|
||||
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.invite_list.pop(c.ipid)
|
||||
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 not client.area.is_locked and not client.is_mod:
|
||||
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.invite_list.pop(c.ipid)
|
||||
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_mute <OOC-name>.')
|
||||
targets = client.server.client_manager.get_targets(client, TargetType.ID, arg, False)
|
||||
if not targets:
|
||||
raise ArgumentError('Target not found. Use /ooc_mute <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)
|
||||
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 /disemvowel <id>.')
|
||||
if targets:
|
||||
for c in targets:
|
||||
logger.log_server('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_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.')
|
||||
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.')
|
||||
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.')
|
||||
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.')
|
||||
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.is_cm 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 = 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.')
|
11
server/constants.py
Normal file
11
server/constants.py
Normal file
@ -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
|
79
server/districtclient.py
Normal file
79
server/districtclient.py
Normal file
@ -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,
|
||||
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())
|
91
server/evidence.py
Normal file
91
server/evidence.py
Normal file
@ -0,0 +1,91 @@
|
||||
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'], 'hlp':['hlp', 'pro'], 'hld':['hld', 'def'], 'jud':['jud'], 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', ''], 'pos':[]}
|
||||
|
||||
def login(self, client):
|
||||
if client.area.evidence_mod == 'FFA':
|
||||
pass
|
||||
if client.area.evidence_mod == 'Mods':
|
||||
if not client.is_cm:
|
||||
return False
|
||||
if client.area.evidence_mod == 'CM':
|
||||
if not client.is_cm and not client.is_mod:
|
||||
return False
|
||||
if client.area.evidence_mod == 'HiddenCM':
|
||||
if not client.is_cm 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])
|
32
server/exceptions.py
Normal file
32
server/exceptions.py
Normal file
@ -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
|
45
server/fantacrypt.py
Normal file
45
server/fantacrypt.py
Normal file
@ -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
|
64
server/logger.py
Normal file
64
server/logger.py
Normal file
@ -0,0 +1,64 @@
|
||||
# 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')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 parse_client_info(client):
|
||||
if client is None:
|
||||
return ''
|
||||
info = client.get_ip()
|
||||
if client.is_mod:
|
||||
return '[{:<15}][{}][MOD]'.format(info, client.id)
|
||||
return '[{:<15}][{}]'.format(info, client.id)
|
89
server/masterserverclient.py
Normal file
89
server/masterserverclient.py
Normal file
@ -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
|
263
server/tsuserver.py
Normal file
263
server/tsuserver.py
Normal file
@ -0,0 +1,263 @@
|
||||
# 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.id, 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 broadcast_need(self, client, msg):
|
||||
char_name = client.get_char_name()
|
||||
area_name = client.area.name
|
||||
area_id = client.area.id
|
||||
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), 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 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)
|
212
server/websocket.py
Normal file
212
server/websocket.py
Normal file
@ -0,0 +1,212 @@
|
||||
# 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
|
||||
|
||||
if payload_length == 126:
|
||||
payload_length = struct.unpack(">H", data[2:4])[0]
|
||||
elif payload_length == 127:
|
||||
payload_length = struct.unpack(">Q", data[2:10])[0]
|
||||
|
||||
masks = data[2:6]
|
||||
decoded = ""
|
||||
for char in data[6:payload_length + 6]:
|
||||
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)
|
Loading…
Reference in New Issue
Block a user