Coalesce server changes into patch file (this is not a monorepo)

This commit is contained in:
oldmud0 2018-11-10 23:15:54 -06:00
parent 56ec03a23a
commit de348c22d5
16 changed files with 2227 additions and 3939 deletions

View File

View File

@ -1,807 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import re
from time import localtime, strftime
from enum import Enum
from . import commands
from . import logger
from .exceptions import ClientError, AreaError, ArgumentError, ServerError
from .fantacrypt import fanta_decrypt
from .evidence import EvidenceList
from .websocket import WebSocket
import unicodedata
class AOProtocol(asyncio.Protocol):
"""
The main class that deals with the AO protocol.
"""
class ArgType(Enum):
STR = 1,
STR_OR_EMPTY = 2,
INT = 3
def __init__(self, server):
super().__init__()
self.server = server
self.client = None
self.buffer = ''
self.ping_timeout = None
self.websocket = None
def data_received(self, data):
""" Handles any data received from the network.
Receives data, parses them into a command and passes it
to the command handler.
:param data: bytes of data
"""
if self.websocket is None:
self.websocket = WebSocket(self.client, self)
if not self.websocket.handshake(data):
self.websocket = False
else:
self.client.websocket = self.websocket
buf = data
if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid):
self.client.transport.close()
else:
self.client.is_checked = True
if self.websocket:
buf = self.websocket.handle(data)
if buf is None:
buf = b''
if not isinstance(buf, str):
# try to decode as utf-8, ignore any erroneous characters
self.buffer += buf.decode('utf-8', 'ignore')
else:
self.buffer = buf
if len(self.buffer) > 8192:
self.client.disconnect()
for msg in self.get_messages():
if len(msg) < 2:
continue
# general netcode structure is not great
if msg[0] in ('#', '3', '4'):
if msg[0] == '#':
msg = msg[1:]
spl = msg.split('#', 1)
msg = '#'.join([fanta_decrypt(spl[0])] + spl[1:])
logger.log_debug('[INC][RAW]{}'.format(msg), self.client)
try:
cmd, *args = msg.split('#')
self.net_cmd_dispatcher[cmd](self, args)
except KeyError:
logger.log_debug('[INC][UNK]{}'.format(msg), self.client)
def connection_made(self, transport):
""" Called upon a new client connecting
:param transport: the transport object
"""
self.client = self.server.new_client(transport)
self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect)
asyncio.get_event_loop().call_later(0.25, self.client.send_command, 'decryptor', 34) # just fantacrypt things)
def connection_lost(self, exc):
""" User disconnected
:param exc: reason
"""
self.server.remove_client(self.client)
self.ping_timeout.cancel()
def get_messages(self):
""" Parses out full messages from the buffer.
:return: yields messages
"""
while '#%' in self.buffer:
spl = self.buffer.split('#%', 1)
self.buffer = spl[1]
yield spl[0]
# exception because bad netcode
askchar2 = '#615810BC07D12A5A#'
if self.buffer == askchar2:
self.buffer = ''
yield askchar2
def validate_net_cmd(self, args, *types, needs_auth=True):
""" Makes sure the net command's arguments match expectations.
:param args: actual arguments to the net command
:param types: what kind of data types are expected
:param needs_auth: whether you need to have chosen a character
:return: returns True if message was validated
"""
if needs_auth and self.client.char_id == -1:
return False
if len(args) != len(types):
return False
for i, arg in enumerate(args):
if len(arg) == 0 and types[i] != self.ArgType.STR_OR_EMPTY:
return False
if types[i] == self.ArgType.INT:
try:
args[i] = int(arg)
except ValueError:
return False
return True
def net_cmd_hi(self, args):
""" Handshake.
HI#<hdid:string>#%
:param args: a list containing all the arguments
"""
if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False):
return
self.client.hdid = args[0]
if self.client.hdid not in self.client.server.hdid_list:
self.client.server.hdid_list[self.client.hdid] = []
if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]:
self.client.server.hdid_list[self.client.hdid].append(self.client.ipid)
self.client.server.dump_hdids()
for ipid in self.client.server.hdid_list[self.client.hdid]:
if self.server.ban_manager.is_banned(ipid):
self.client.send_command('BD')
self.client.disconnect()
return
logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client)
self.client.send_command('ID', self.client.id, self.server.software, self.server.get_version_string())
self.client.send_command('PN', self.server.get_player_count() - 1, self.server.config['playerlimit'])
def net_cmd_id(self, args):
""" Client version and PV
ID#<pv:int>#<software:string>#<version:string>#%
"""
self.client.is_ao2 = False
if len(args) < 2:
return
version_list = args[1].split('.')
if len(version_list) < 3:
return
release = int(version_list[0])
major = int(version_list[1])
minor = int(version_list[2])
if args[0] != 'AO2':
return
if release < 2:
return
elif release == 2:
if major < 2:
return
elif major == 2:
if minor < 5:
return
self.client.is_ao2 = True
self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts')
def net_cmd_ch(self, _):
""" Periodically checks the connection.
CHECK#%
"""
self.client.send_command('CHECK')
self.ping_timeout.cancel()
self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect)
def net_cmd_askchaa(self, _):
""" Ask for the counts of characters/evidence/music
askchaa#%
"""
char_cnt = len(self.server.char_list)
evi_cnt = 0
music_cnt = sum([len(x) for x in self.server.music_pages_ao1])
self.client.send_command('SI', char_cnt, evi_cnt, music_cnt)
def net_cmd_askchar2(self, _):
""" Asks for the character list.
askchar2#%
"""
self.client.send_command('CI', *self.server.char_pages_ao1[0])
def net_cmd_an(self, args):
""" Asks for specific pages of the character list.
AN#<page:int>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False):
return
if len(self.server.char_pages_ao1) > args[0] >= 0:
self.client.send_command('CI', *self.server.char_pages_ao1[args[0]])
else:
self.client.send_command('EM', *self.server.music_pages_ao1[0])
def net_cmd_ae(self, _):
""" Asks for specific pages of the evidence list.
AE#<page:int>#%
"""
pass # todo evidence maybe later
def net_cmd_am(self, args):
""" Asks for specific pages of the music list.
AM#<page:int>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False):
return
if len(self.server.music_pages_ao1) > args[0] >= 0:
self.client.send_command('EM', *self.server.music_pages_ao1[args[0]])
else:
self.client.send_done()
self.client.send_area_list()
self.client.send_motd()
def net_cmd_rc(self, _):
""" Asks for the whole character list(AO2)
AC#%
"""
self.client.send_command('SC', *self.server.char_list)
def net_cmd_rm(self, _):
""" Asks for the whole music list(AO2)
AM#%
"""
self.client.send_command('SM', *self.server.music_list_ao2)
def net_cmd_rd(self, _):
""" Asks for server metadata(charscheck, motd etc.) and a DONE#% signal(also best packet)
RD#%
"""
self.client.send_done()
self.client.send_area_list()
self.client.send_motd()
def net_cmd_cc(self, args):
""" Character selection.
CC#<client_id:int>#<char_id:int>#<hdid:string>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR, needs_auth=False):
return
cid = args[1]
try:
self.client.change_character(cid)
except ClientError:
return
def net_cmd_ms(self, args):
""" IC message.
Refer to the implementation for details.
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.area.can_send_message(self.client):
return
target_area = []
if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT):
# Vanilla validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args
showname = ""
charid_pair = -1
offset_pair = 0
nonint_pre = 0
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY):
# 1.3.0 validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args
charid_pair = -1
offset_pair = 0
nonint_pre = 0
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT):
# 1.3.5 validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args
nonint_pre = 0
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT):
# 1.4.0 validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
else:
return
if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name():
self.client.send_host_message("Iniswap is blocked in this area")
return
if len(self.client.charcurse) > 0 and folder != self.client.get_char_name():
self.client.send_host_message("You may not iniswap while you are charcursed!")
return
if not self.client.area.blankposting_allowed:
if text == ' ':
self.client.send_host_message("Blankposting is forbidden in this area!")
return
if text.isspace():
self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.")
return
if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>':
self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.")
return
if text.startswith('/a '):
part = text.split(' ')
try:
aid = int(part[1])
if self.client in self.server.area_manager.get_area_by_id(aid).owners:
target_area.append(aid)
if not target_area:
self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name))
return
text = ' '.join(part[2:])
except ValueError:
self.client.send_host_message("That does not look like a valid area ID!")
return
elif text.startswith('/s '):
part = text.split(' ')
for a in self.server.area_manager.areas:
if self.client in a.owners:
target_area.append(a.id)
if not target_area:
self.client.send_host_message('You don\'t any areas!')
return
text = ' '.join(part[1:])
if msg_type not in ('chat', '0', '1'):
return
if anim_type not in (0, 1, 2, 5, 6):
return
if cid != self.client.char_id:
return
if sfx_delay < 0:
return
if button not in (0, 1, 2, 3, 4):
return
if evidence < 0:
return
if ding not in (0, 1):
return
if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8):
return
if len(showname) > 15:
self.client.send_host_message("Your IC showname is way too long!")
return
if nonint_pre == 1:
if button in (1, 2, 3, 4, 23):
if anim_type == 1 or anim_type == 2:
anim_type = 0
elif anim_type == 6:
anim_type = 5
if self.client.area.non_int_pres_only:
if anim_type == 1 or anim_type == 2:
anim_type = 0
nonint_pre = 1
elif anim_type == 6:
anim_type = 5
nonint_pre = 1
if not self.client.area.shouts_allowed:
# Old clients communicate the objecting in anim_type.
if anim_type == 2:
anim_type = 1
elif anim_type == 6:
anim_type = 5
# New clients do it in a specific objection message area.
button = 0
# Turn off the ding.
ding = 0
if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners):
color = 0
if color == 6:
text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse
if len(text.strip( ' ' )) == 1:
color = 0
else:
if text.strip( ' ' ) in ('<num>', '<percent>', '<dollar>', '<and>'):
color = 0
if self.client.pos:
pos = self.client.pos
else:
if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'):
return
msg = text[:256]
if self.client.shaken:
msg = self.client.shake_message(msg)
if self.client.disemvowel:
msg = self.client.disemvowel_message(msg)
self.client.pos = pos
if evidence:
if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all':
self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all'
self.client.area.broadcast_evidence_list()
# Here, we check the pair stuff, and save info about it to the client.
# Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset
# and an other offset.
self.client.charid_pair = charid_pair
self.client.offset_pair = offset_pair
if anim_type not in (5, 6):
self.client.last_sprite = anim
self.client.flip = flip
self.client.claimed_folder = folder
other_offset = 0
other_emote = ''
other_flip = 0
other_folder = ''
confirmed = False
if charid_pair > -1:
for target in self.client.area.clients:
if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos:
confirmed = True
other_offset = target.offset_pair
other_emote = target.last_sprite
other_flip = target.flip
other_folder = target.claimed_folder
break
if not confirmed:
charid_pair = -1
offset_pair = 0
self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname,
charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre)
self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname,
charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre)
self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname,
charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre)
self.client.area.set_next_msg_delay(len(msg))
logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client)
if (self.client.area.is_recording):
self.client.area.recorded_messages.append(args)
def net_cmd_ct(self, args):
""" OOC Message
CT#<name:string>#<message:string>#%
"""
if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR):
return
if self.client.name != args[0] and self.client.fake_name != args[0]:
if self.client.is_valid_name(args[0]):
self.client.name = args[0]
self.client.fake_name = args[0]
else:
self.client.fake_name = args[0]
if self.client.name == '':
self.client.send_host_message('You must insert a name with at least one letter')
return
if len(self.client.name) > 30:
self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.')
return
for c in self.client.name:
if unicodedata.category(c) == 'Cf':
self.client.send_host_message('You cannot use format characters in your name!')
return
if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('<dollar>G') or self.client.name.startswith('<dollar>M'):
self.client.send_host_message('That name is reserved!')
return
if args[1].startswith(' /'):
self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.')
return
if args[1].startswith('/'):
spl = args[1][1:].split(' ', 1)
cmd = spl[0].lower()
arg = ''
if len(spl) == 2:
arg = spl[1][:256]
try:
called_function = 'ooc_cmd_{}'.format(cmd)
getattr(commands, called_function)(self.client, arg)
except AttributeError:
print('Attribute error with ' + called_function)
self.client.send_host_message('Invalid command.')
except (ClientError, AreaError, ArgumentError, ServerError) as ex:
self.client.send_host_message(ex)
else:
if self.client.shaken:
args[1] = self.client.shake_message(args[1])
if self.client.disemvowel:
args[1] = self.client.disemvowel_message(args[1])
self.client.area.send_command('CT', self.client.name, args[1])
self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1])
logger.log_server(
'[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(),
args[1]), self.client)
def net_cmd_mc(self, args):
""" Play music.
MC#<song_name:int>#<???:int>#%
"""
try:
area = self.server.area_manager.get_area_by_name(args[0])
self.client.change_area(area)
except AreaError:
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.is_dj:
self.client.send_host_message('You were blockdj\'d by a moderator.')
return
if self.client.area.cannot_ic_interact(self.client):
self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!")
return
if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR):
return
if args[1] != self.client.char_id:
return
if self.client.change_music_cd():
self.client.send_host_message('You changed song too many times. Please try again after {} seconds.'.format(int(self.client.change_music_cd())))
return
try:
name, length = self.server.get_song_data(args[0])
if self.client.area.jukebox:
showname = ''
if len(args) > 2:
showname = args[2]
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
self.client.area.add_jukebox_vote(self.client, name, length, showname)
logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client)
else:
if len(args) > 2:
showname = args[2]
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
self.client.area.play_music_shownamed(name, self.client.char_id, showname, length)
self.client.area.add_music_playing_shownamed(self.client, showname, name)
else:
self.client.area.play_music(name, self.client.char_id, length)
self.client.area.add_music_playing(self.client, name)
logger.log_server('[{}][{}]Changed music to {}.'
.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client)
except ServerError:
return
except ClientError as ex:
self.client.send_host_message(ex)
def net_cmd_rt(self, args):
""" Plays the Testimony/CE animation.
RT#<type:string>#%
"""
if not self.client.area.shouts_allowed:
self.client.send_host_message("You cannot use the testimony buttons here!")
return
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.can_wtce:
self.client.send_host_message('You were blocked from using judge signs by a moderator.')
return
if self.client.area.cannot_ic_interact(self.client):
self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!")
return
if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT):
return
if args[0] == 'testimony1':
sign = 'WT'
elif args[0] == 'testimony2':
sign = 'CE'
elif args[0] == 'judgeruling':
sign = 'JR'
else:
return
if self.client.wtce_mute():
self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute())))
return
if len(args) == 1:
self.client.area.send_command('RT', args[0])
elif len(args) == 2:
self.client.area.send_command('RT', args[0], args[1])
self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign))
logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client)
def net_cmd_hp(self, args):
""" Sets the penalty bar.
HP#<type:int>#<new_value:int>#%
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if self.client.area.cannot_ic_interact(self.client):
self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!")
return
if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT):
return
try:
self.client.area.change_hp(args[0], args[1])
self.client.area.add_to_judgelog(self.client, 'changed the penalties')
logger.log_server('[{}]{} changed HP ({}) to {}'
.format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client)
except AreaError:
return
def net_cmd_pe(self, args):
""" Adds a piece of evidence.
PE#<name: string>#<description: string>#<image: string>#%
"""
if len(args) < 3:
return
#evi = Evidence(args[0], args[1], args[2], self.client.pos)
self.client.area.evi_list.add_evidence(self.client, args[0], args[1], args[2], 'all')
self.client.area.broadcast_evidence_list()
def net_cmd_de(self, args):
""" Deletes a piece of evidence.
DE#<id: int>#%
"""
self.client.area.evi_list.del_evidence(self.client, self.client.evi_list[int(args[0])])
self.client.area.broadcast_evidence_list()
def net_cmd_ee(self, args):
""" Edits a piece of evidence.
EE#<id: int>#<name: string>#<description: string>#<image: string>#%
"""
if len(args) < 4:
return
evi = (args[1], args[2], args[3], 'all')
self.client.area.evi_list.edit_evidence(self.client, self.client.evi_list[int(args[0])], evi)
self.client.area.broadcast_evidence_list()
def net_cmd_zz(self, args):
""" Sent on mod call.
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.can_call_mod():
self.client.send_host_message("You must wait 30 seconds between mod calls.")
return
current_time = strftime("%H:%M", localtime())
if len(args) < 1:
self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)'
.format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod)
self.client.set_mod_call_delay()
logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client)
else:
self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}'
.format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod)
self.client.set_mod_call_delay()
logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client)
def net_cmd_opKICK(self, args):
self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])])
def net_cmd_opBAN(self, args):
self.net_cmd_ct(['opban', '/ban {}'.format(args[0])])
net_cmd_dispatcher = {
'HI': net_cmd_hi, # handshake
'ID': net_cmd_id, # client version
'CH': net_cmd_ch, # keepalive
'askchaa': net_cmd_askchaa, # ask for list lengths
'askchar2': net_cmd_askchar2, # ask for list of characters
'AN': net_cmd_an, # character list
'AE': net_cmd_ae, # evidence list
'AM': net_cmd_am, # music list
'RC': net_cmd_rc, # AO2 character list
'RM': net_cmd_rm, # AO2 music list
'RD': net_cmd_rd, # AO2 done request, charscheck etc.
'CC': net_cmd_cc, # select character
'MS': net_cmd_ms, # IC message
'CT': net_cmd_ct, # OOC message
'MC': net_cmd_mc, # play song
'RT': net_cmd_rt, # WT/CE buttons
'HP': net_cmd_hp, # penalties
'PE': net_cmd_pe, # add evidence
'DE': net_cmd_de, # delete evidence
'EE': net_cmd_ee, # edit evidence
'ZZ': net_cmd_zz, # call mod button
'opKICK': net_cmd_opKICK, # /kick with guard on
'opBAN': net_cmd_opBAN, # /ban with guard on
}

View File

@ -1,412 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import random
import time
import yaml
from server.exceptions import AreaError
from server.evidence import EvidenceList
from enum import Enum
class AreaManager:
class Area:
def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False):
self.iniswap_allowed = iniswap_allowed
self.clients = set()
self.invite_list = {}
self.id = area_id
self.name = name
self.background = background
self.bg_lock = bg_lock
self.server = server
self.music_looper = None
self.next_message_time = 0
self.hp_def = 10
self.hp_pro = 10
self.doc = 'No document.'
self.status = 'IDLE'
self.judgelog = []
self.current_music = ''
self.current_music_player = ''
self.current_music_player_ipid = -1
self.evi_list = EvidenceList()
self.is_recording = False
self.recorded_messages = []
self.evidence_mod = evidence_mod
self.locking_allowed = locking_allowed
self.showname_changes_allowed = showname_changes_allowed
self.shouts_allowed = shouts_allowed
self.abbreviation = abbreviation
self.cards = dict()
"""
#debug
self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
"""
self.is_locked = self.Locked.FREE
self.blankposting_allowed = True
self.non_int_pres_only = non_int_pres_only
self.jukebox = jukebox
self.jukebox_votes = []
self.jukebox_prev_char_id = -1
self.owners = []
class Locked(Enum):
FREE = 1,
SPECTATABLE = 2,
LOCKED = 3
def new_client(self, client):
self.clients.add(client)
self.server.area_manager.send_arup_players()
def remove_client(self, client):
self.clients.remove(client)
if len(self.clients) == 0:
self.change_status('IDLE')
def unlock(self):
self.is_locked = self.Locked.FREE
self.blankposting_allowed = True
self.invite_list = {}
self.server.area_manager.send_arup_lock()
self.send_host_message('This area is open now.')
def spectator(self):
self.is_locked = self.Locked.SPECTATABLE
for i in self.clients:
self.invite_list[i.id] = None
for i in self.owners:
self.invite_list[i.id] = None
self.server.area_manager.send_arup_lock()
self.send_host_message('This area is spectatable now.')
def lock(self):
self.is_locked = self.Locked.LOCKED
for i in self.clients:
self.invite_list[i.id] = None
for i in self.owners:
self.invite_list[i.id] = None
self.server.area_manager.send_arup_lock()
self.send_host_message('This area is locked now.')
def is_char_available(self, char_id):
return char_id not in [x.char_id for x in self.clients]
def get_rand_avail_char_id(self):
avail_set = set(range(len(self.server.char_list))) - set([x.char_id for x in self.clients])
if len(avail_set) == 0:
raise AreaError('No available characters.')
return random.choice(tuple(avail_set))
def send_command(self, cmd, *args):
for c in self.clients:
c.send_command(cmd, *args)
def send_owner_command(self, cmd, *args):
for c in self.owners:
if not c in self.clients:
c.send_command(cmd, *args)
def send_host_message(self, msg):
self.send_command('CT', self.server.config['hostname'], msg, '1')
self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1')
def set_next_msg_delay(self, msg_length):
delay = min(3000, 100 + 60 * msg_length)
self.next_message_time = round(time.time() * 1000.0 + delay)
def is_iniswap(self, client, anim1, anim2, char):
if self.iniswap_allowed:
return False
if '..' in anim1 or '..' in anim2:
return True
for char_link in self.server.allowed_iniswaps:
if client.get_char_name() in char_link and char in char_link:
return False
return True
def add_jukebox_vote(self, client, music_name, length=-1, showname=''):
if not self.jukebox:
return
if length <= 0:
self.remove_jukebox_vote(client, False)
else:
self.remove_jukebox_vote(client, True)
self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname))
client.send_host_message('Your song was added to the jukebox.')
if len(self.jukebox_votes) == 1:
self.start_jukebox()
def remove_jukebox_vote(self, client, silent):
if not self.jukebox:
return
for current_vote in self.jukebox_votes:
if current_vote.client.id == client.id:
self.jukebox_votes.remove(current_vote)
if not silent:
client.send_host_message('You removed your song from the jukebox.')
def get_jukebox_picked(self):
if not self.jukebox:
return
if len(self.jukebox_votes) == 0:
return None
elif len(self.jukebox_votes) == 1:
return self.jukebox_votes[0]
else:
weighted_votes = []
for current_vote in self.jukebox_votes:
i = 0
while i < current_vote.chance:
weighted_votes.append(current_vote)
i += 1
return random.choice(weighted_votes)
def start_jukebox(self):
# There is a probability that the jukebox feature has been turned off since then,
# we should check that.
# We also do a check if we were the last to play a song, just in case.
if not self.jukebox:
if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID':
self.current_music = ''
return
vote_picked = self.get_jukebox_picked()
if vote_picked is None:
self.current_music = ''
return
if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1:
self.jukebox_prev_char_id = vote_picked.client.char_id
if vote_picked.showname == '':
self.send_command('MC', vote_picked.name, vote_picked.client.char_id)
else:
self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname)
else:
self.send_command('MC', vote_picked.name, -1)
self.current_music_player = 'The Jukebox'
self.current_music_player_ipid = 'has no IPID'
self.current_music = vote_picked.name
for current_vote in self.jukebox_votes:
# Choosing the same song will get your votes down to 0, too.
# Don't want the same song twice in a row!
if current_vote.name == vote_picked.name:
current_vote.chance = 0
else:
current_vote.chance += 1
if self.music_looper:
self.music_looper.cancel()
self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox())
def play_music(self, name, cid, length=-1):
self.send_command('MC', name, cid)
if self.music_looper:
self.music_looper.cancel()
if length > 0:
self.music_looper = asyncio.get_event_loop().call_later(length,
lambda: self.play_music(name, -1, length))
def play_music_shownamed(self, name, cid, showname, length=-1):
self.send_command('MC', name, cid, showname)
if self.music_looper:
self.music_looper.cancel()
if length > 0:
self.music_looper = asyncio.get_event_loop().call_later(length,
lambda: self.play_music(name, -1, length))
def can_send_message(self, client):
if self.cannot_ic_interact(client):
client.send_host_message('This is a locked area - ask the CM to speak.')
return False
return (time.time() * 1000.0 - self.next_message_time) > 0
def cannot_ic_interact(self, client):
return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list
def change_hp(self, side, val):
if not 0 <= val <= 10:
raise AreaError('Invalid penalty value.')
if not 1 <= side <= 2:
raise AreaError('Invalid penalty side.')
if side == 1:
self.hp_def = val
elif side == 2:
self.hp_pro = val
self.send_command('HP', side, val)
def change_background(self, bg):
if bg.lower() not in (name.lower() for name in self.server.backgrounds):
raise AreaError('Invalid background name.')
self.background = bg
self.send_command('BN', self.background)
def change_status(self, value):
allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming')
if value.lower() not in allowed_values:
raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values)))
if value.lower() == 'lfp':
value = 'looking-for-players'
self.status = value.upper()
self.server.area_manager.send_arup_status()
def change_doc(self, doc='No document.'):
self.doc = doc
def add_to_judgelog(self, client, msg):
if len(self.judgelog) >= 10:
self.judgelog = self.judgelog[1:]
self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(), client.get_ip(), msg))
def add_music_playing(self, client, name):
self.current_music_player = client.get_char_name()
self.current_music_player_ipid = client.ipid
self.current_music = name
def add_music_playing_shownamed(self, client, showname, name):
self.current_music_player = showname + " (" + client.get_char_name() + ")"
self.current_music_player_ipid = client.ipid
self.current_music = name
def get_evidence_list(self, client):
client.evi_list, evi_list = self.evi_list.create_evi_list(client)
return evi_list
def broadcast_evidence_list(self):
"""
LE#<name>&<desc>&<img>#<name>
"""
for client in self.clients:
client.send_command('LE', *self.get_evidence_list(client))
def get_cms(self):
msg = ''
for i in self.owners:
msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', '
if len(msg) > 2:
msg = msg[:-2]
return msg
class JukeboxVote:
def __init__(self, client, name, length, showname):
self.client = client
self.name = name
self.length = length
self.chance = 1
self.showname = showname
def __init__(self, server):
self.server = server
self.cur_id = 0
self.areas = []
self.load_areas()
def load_areas(self):
with open('config/areas.yaml', 'r') as chars:
areas = yaml.load(chars)
for item in areas:
if 'evidence_mod' not in item:
item['evidence_mod'] = 'FFA'
if 'locking_allowed' not in item:
item['locking_allowed'] = False
if 'iniswap_allowed' not in item:
item['iniswap_allowed'] = True
if 'showname_changes_allowed' not in item:
item['showname_changes_allowed'] = False
if 'shouts_allowed' not in item:
item['shouts_allowed'] = True
if 'jukebox' not in item:
item['jukebox'] = False
if 'noninterrupting_pres' not in item:
item['noninterrupting_pres'] = False
if 'abbreviation' not in item:
item['abbreviation'] = self.get_generated_abbreviation(item['area'])
self.areas.append(
self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres']))
self.cur_id += 1
def default_area(self):
return self.areas[0]
def get_area_by_name(self, name):
for area in self.areas:
if area.name == name:
return area
raise AreaError('Area not found.')
def get_area_by_id(self, num):
for area in self.areas:
if area.id == num:
return area
raise AreaError('Area not found.')
def get_generated_abbreviation(self, name):
if name.lower().startswith("courtroom"):
return "CR" + name.split()[-1]
elif name.lower().startswith("area"):
return "A" + name.split()[-1]
elif len(name.split()) > 1:
return "".join(item[0].upper() for item in name.split())
elif len(name) > 3:
return name[:3].upper()
else:
return name.upper()
def send_remote_command(self, area_ids, cmd, *args):
for a_id in area_ids:
self.get_area_by_id(a_id).send_command(cmd, *args)
self.get_area_by_id(a_id).send_owner_command(cmd, *args)
def send_arup_players(self):
players_list = [0]
for area in self.areas:
players_list.append(len(area.clients))
self.server.send_arup(players_list)
def send_arup_status(self):
status_list = [1]
for area in self.areas:
status_list.append(area.status)
self.server.send_arup(status_list)
def send_arup_cms(self):
cms_list = [2]
for area in self.areas:
cm = 'FREE'
if len(area.owners) > 0:
cm = area.get_cms()
cms_list.append(cm)
self.server.send_arup(cms_list)
def send_arup_lock(self):
lock_list = [3]
for area in self.areas:
lock_list.append(area.is_locked.name)
self.server.send_arup(lock_list)

View File

@ -1,54 +0,0 @@
# 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)

View File

@ -1,457 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from server import fantacrypt
from server import logger
from server.exceptions import ClientError, AreaError
from enum import Enum
from server.constants import TargetType
from heapq import heappop, heappush
import time
import re
class ClientManager:
class Client:
def __init__(self, server, transport, user_id, ipid):
self.is_checked = False
self.transport = transport
self.hdid = ''
self.pm_mute = False
self.id = user_id
self.char_id = -1
self.area = server.area_manager.default_area()
self.server = server
self.name = ''
self.fake_name = ''
self.is_mod = False
self.is_dj = True
self.can_wtce = True
self.pos = ''
self.evi_list = []
self.disemvowel = False
self.shaken = False
self.charcurse = []
self.muted_global = False
self.muted_adverts = False
self.is_muted = False
self.is_ooc_muted = False
self.pm_mute = False
self.mod_call_time = 0
self.in_rp = False
self.ipid = ipid
self.websocket = None
# Pairing stuff
self.charid_pair = -1
self.offset_pair = 0
self.last_sprite = ''
self.flip = 0
self.claimed_folder = ''
# Casing stuff
self.casing_cm = False
self.casing_cases = ""
self.casing_def = False
self.casing_pro = False
self.casing_jud = False
self.casing_jur = False
self.casing_steno = False
self.case_call_time = 0
#flood-guard stuff
self.mus_counter = 0
self.mus_mute_time = 0
self.mus_change_time = [x * self.server.config['music_change_floodguard']['interval_length'] for x in range(self.server.config['music_change_floodguard']['times_per_interval'])]
self.wtce_counter = 0
self.wtce_mute_time = 0
self.wtce_time = [x * self.server.config['wtce_floodguard']['interval_length'] for x in range(self.server.config['wtce_floodguard']['times_per_interval'])]
def send_raw_message(self, msg):
if self.websocket:
self.websocket.send_text(msg.encode('utf-8'))
else:
self.transport.write(msg.encode('utf-8'))
def send_command(self, command, *args):
if args:
if command == 'MS':
for evi_num in range(len(self.evi_list)):
if self.evi_list[evi_num] == args[11]:
lst = list(args)
lst[11] = evi_num
args = tuple(lst)
break
self.send_raw_message('{}#{}#%'.format(command, '#'.join([str(x) for x in args])))
else:
self.send_raw_message('{}#%'.format(command))
def send_host_message(self, msg):
self.send_command('CT', self.server.config['hostname'], msg, '1')
def send_motd(self):
self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd']))
def send_player_count(self):
self.send_host_message('{}/{} players online.'.format(
self.server.get_player_count(),
self.server.config['playerlimit']))
def is_valid_name(self, name):
name_ws = name.replace(' ', '')
if not name_ws or name_ws.isdigit():
return False
for client in self.server.client_manager.clients:
print(client.name == name)
if client.name == name:
return False
return True
def disconnect(self):
self.transport.close()
def change_character(self, char_id, force=False):
if not self.server.is_valid_char_id(char_id):
raise ClientError('Invalid Character ID.')
if len(self.charcurse) > 0:
if not char_id in self.charcurse:
raise ClientError('Character not available.')
force = True
if not self.area.is_char_available(char_id):
if force:
for client in self.area.clients:
if client.char_id == char_id:
client.char_select()
else:
raise ClientError('Character not available.')
old_char = self.get_char_name()
self.char_id = char_id
self.pos = ''
self.send_command('PV', self.id, 'CID', self.char_id)
self.area.send_command('CharsCheck', *self.get_available_char_list())
logger.log_server('[{}]Changed character from {} to {}.'
.format(self.area.abbreviation, old_char, self.get_char_name()), self)
def change_music_cd(self):
if self.is_mod or self in self.area.owners:
return 0
if self.mus_mute_time:
if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']:
return self.server.config['music_change_floodguard']['mute_length'] - (time.time() - self.mus_mute_time)
else:
self.mus_mute_time = 0
times_per_interval = self.server.config['music_change_floodguard']['times_per_interval']
interval_length = self.server.config['music_change_floodguard']['interval_length']
if time.time() - self.mus_change_time[(self.mus_counter - times_per_interval + 1) % times_per_interval] < interval_length:
self.mus_mute_time = time.time()
return self.server.config['music_change_floodguard']['mute_length']
self.mus_counter = (self.mus_counter + 1) % times_per_interval
self.mus_change_time[self.mus_counter] = time.time()
return 0
def wtce_mute(self):
if self.is_mod or self in self.area.owners:
return 0
if self.wtce_mute_time:
if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']:
return self.server.config['wtce_floodguard']['mute_length'] - (time.time() - self.wtce_mute_time)
else:
self.wtce_mute_time = 0
times_per_interval = self.server.config['wtce_floodguard']['times_per_interval']
interval_length = self.server.config['wtce_floodguard']['interval_length']
if time.time() - self.wtce_time[(self.wtce_counter - times_per_interval + 1) % times_per_interval] < interval_length:
self.wtce_mute_time = time.time()
return self.server.config['music_change_floodguard']['mute_length']
self.wtce_counter = (self.wtce_counter + 1) % times_per_interval
self.wtce_time[self.wtce_counter] = time.time()
return 0
def reload_character(self):
try:
self.change_character(self.char_id, True)
except ClientError:
raise
def change_area(self, area):
if self.area == area:
raise ClientError('User already in specified area.')
if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list:
raise ClientError("That area is locked!")
if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list:
self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.')
if self.area.jukebox:
self.area.remove_jukebox_vote(self, True)
old_area = self.area
if not area.is_char_available(self.char_id):
try:
new_char_id = area.get_rand_avail_char_id()
except AreaError:
raise ClientError('No available characters in that area.')
self.change_character(new_char_id)
self.send_host_message('Character taken, switched to {}.'.format(self.get_char_name()))
self.area.remove_client(self)
self.area = area
area.new_client(self)
self.send_host_message('Changed area to {}.[{}]'.format(area.name, self.area.status))
logger.log_server(
'[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id,
self.area.name, self.area.id), self)
self.area.send_command('CharsCheck', *self.get_available_char_list())
self.send_command('HP', 1, self.area.hp_def)
self.send_command('HP', 2, self.area.hp_pro)
self.send_command('BN', self.area.background)
self.send_command('LE', *self.area.get_evidence_list(self))
def send_area_list(self):
msg = '=== Areas ==='
for i, area in enumerate(self.server.area_manager.areas):
owner = 'FREE'
if len(area.owners) > 0:
owner = 'CM: {}'.format(area.get_cms())
lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'}
msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked])
if self.area == area:
msg += ' [*]'
self.send_host_message(msg)
def get_area_info(self, area_id, mods):
info = '\r\n'
try:
area = self.server.area_manager.get_area_by_id(area_id)
except AreaError:
raise
info += '=== {} ==='.format(area.name)
info += '\r\n'
lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'}
info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked])
sorted_clients = []
for client in area.clients:
if (not mods) or client.is_mod:
sorted_clients.append(client)
for owner in area.owners:
if not (mods or owner in area.clients):
sorted_clients.append(owner)
if not sorted_clients:
return ''
sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name())
for c in sorted_clients:
info += '\r\n'
if c in area.owners:
if not c in area.clients:
info += '[RCM]'
else:
info +='[CM]'
info += '[{}] {}'.format(c.id, c.get_char_name())
if self.is_mod:
info += ' ({})'.format(c.ipid)
info += ': {}'.format(c.name)
return info
def send_area_info(self, area_id, mods):
#if area_id is -1 then return all areas. If mods is True then return only mods
info = ''
if area_id == -1:
# all areas info
cnt = 0
info = '\n== Area List =='
for i in range(len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0:
cnt += len(self.server.area_manager.areas[i].clients)
info += '{}'.format(self.get_area_info(i, mods))
info = 'Current online: {}'.format(cnt) + info
else:
try:
info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods)
except AreaError:
raise
self.send_host_message(info)
def send_area_hdid(self, area_id):
try:
info = self.get_area_hdid(area_id)
except AreaError:
raise
self.send_host_message(info)
def send_all_area_hdid(self):
info = '== HDID List =='
for i in range (len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
info += '\r\n{}'.format(self.get_area_hdid(i))
self.send_host_message(info)
def send_all_area_ip(self):
info = '== IP List =='
for i in range (len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
info += '\r\n{}'.format(self.get_area_ip(i))
self.send_host_message(info)
def send_done(self):
self.send_command('CharsCheck', *self.get_available_char_list())
self.send_command('HP', 1, self.area.hp_def)
self.send_command('HP', 2, self.area.hp_pro)
self.send_command('BN', self.area.background)
self.send_command('LE', *self.area.get_evidence_list(self))
self.send_command('MM', 1)
self.server.area_manager.send_arup_players()
self.server.area_manager.send_arup_status()
self.server.area_manager.send_arup_cms()
self.server.area_manager.send_arup_lock()
self.send_command('DONE')
def char_select(self):
self.char_id = -1
self.send_done()
def get_available_char_list(self):
if len(self.charcurse) > 0:
avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse)
else:
avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients])
char_list = [-1] * len(self.server.char_list)
for x in avail_char_ids:
char_list[x] = 0
return char_list
def auth_mod(self, password):
if self.is_mod:
raise ClientError('Already logged in.')
if password == self.server.config['modpass']:
self.is_mod = True
else:
raise ClientError('Invalid password.')
def get_ip(self):
return self.ipid
def get_char_name(self):
if self.char_id == -1:
return 'CHAR_SELECT'
return self.server.char_list[self.char_id]
def change_position(self, pos=''):
if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'):
raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.')
self.pos = pos
def set_mod_call_delay(self):
self.mod_call_time = round(time.time() * 1000.0 + 30000)
def can_call_mod(self):
return (time.time() * 1000.0 - self.mod_call_time) > 0
def set_case_call_delay(self):
self.case_call_time = round(time.time() * 1000.0 + 60000)
def can_call_case(self):
return (time.time() * 1000.0 - self.case_call_time) > 0
def disemvowel_message(self, message):
message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE)
return re.sub(r"\s+", " ", message)
def shake_message(self, message):
import random
parts = message.split()
random.shuffle(parts)
return ' '.join(parts)
def __init__(self, server):
self.clients = set()
self.server = server
self.cur_id = [i for i in range(self.server.config['playerlimit'])]
self.clients_list = []
def new_client(self, transport):
c = self.Client(self.server, transport, heappop(self.cur_id), self.server.get_ipid(transport.get_extra_info('peername')[0]))
self.clients.add(c)
return c
def remove_client(self, client):
if client.area.jukebox:
client.area.remove_jukebox_vote(client, True)
for a in self.server.area_manager.areas:
if client in a.owners:
a.owners.remove(client)
client.server.area_manager.send_arup_cms()
if len(a.owners) == 0:
if a.is_locked != a.Locked.FREE:
a.unlock()
heappush(self.cur_id, client.id)
self.clients.remove(client)
def get_targets(self, client, key, value, local = False):
#possible keys: ip, OOC, id, cname, ipid, hdid
areas = None
if local:
areas = [client.area]
else:
areas = client.server.area_manager.areas
targets = []
if key == TargetType.ALL:
for nkey in range(6):
targets += self.get_targets(client, nkey, value, local)
for area in areas:
for client in area.clients:
if key == TargetType.IP:
if value.lower().startswith(client.get_ip().lower()):
targets.append(client)
elif key == TargetType.OOC_NAME:
if value.lower().startswith(client.name.lower()) and client.name:
targets.append(client)
elif key == TargetType.CHAR_NAME:
if value.lower().startswith(client.get_char_name().lower()):
targets.append(client)
elif key == TargetType.ID:
if client.id == value:
targets.append(client)
elif key == TargetType.IPID:
if client.ipid == value:
targets.append(client)
return targets
def get_muted_clients(self):
clients = []
for client in self.clients:
if client.is_muted:
clients.append(client)
return clients
def get_ooc_muted_clients(self):
clients = []
for client in self.clients:
if client.is_ooc_muted:
clients.append(client)
return clients

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
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

View File

@ -1,79 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
from server import logger
class DistrictClient:
def __init__(self, server):
self.server = server
self.reader = None
self.writer = None
self.message_queue = []
async def connect(self):
loop = asyncio.get_event_loop()
while True:
try:
self.reader, self.writer = await asyncio.open_connection(self.server.config['district_ip'],
self.server.config['district_port'], loop=loop)
await self.handle_connection()
except (ConnectionRefusedError, TimeoutError):
pass
except (ConnectionResetError, asyncio.IncompleteReadError):
self.writer = None
self.reader = None
finally:
logger.log_debug("Couldn't connect to the district, retrying in 30 seconds.")
await asyncio.sleep(30)
async def handle_connection(self):
logger.log_debug('District connected.')
self.send_raw_message('AUTH#{}'.format(self.server.config['district_password']))
while True:
data = await self.reader.readuntil(b'\r\n')
if not data:
return
raw_msg = data.decode()[:-2]
logger.log_debug('[DISTRICT][INC][RAW]{}'.format(raw_msg))
cmd, *args = raw_msg.split('#')
if cmd == 'GLOBAL':
glob_name = '{}[{}:{}][{}]'.format('<dollar>G', args[1], args[2], args[3])
if args[0] == '1':
glob_name += '[M]'
self.server.send_all_cmd_pred('CT', glob_name, args[4], pred=lambda x: not x.muted_global)
elif cmd == 'NEED':
need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \
.format(args[1], args[0], args[2], args[3], args[4])
self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1',
pred=lambda x: not x.muted_adverts)
async def write_queue(self):
while self.message_queue:
msg = self.message_queue.pop(0)
try:
self.writer.write(msg)
await self.writer.drain()
except ConnectionResetError:
return
def send_raw_message(self, msg):
if not self.writer:
return
self.message_queue.append('{}\r\n'.format(msg).encode())
asyncio.ensure_future(self.write_queue(), loop=asyncio.get_event_loop())

View File

@ -1,100 +0,0 @@
class EvidenceList:
limit = 35
class Evidence:
def __init__(self, name, desc, image, pos):
self.name = name
self.desc = desc
self.image = image
self.public = False
self.pos = pos
def set_name(self, name):
self.name = name
def set_desc(self, desc):
self.desc = desc
def set_image(self, image):
self.image = image
def to_string(self):
sequence = (self.name, self.desc, self.image)
return '&'.join(sequence)
def __init__(self):
self.evidences = []
self.poses = {'def':['def', 'hld'],
'pro':['pro', 'hlp'],
'wit':['wit', 'sea'],
'sea':['sea', 'wit'],
'hlp':['hlp', 'pro'],
'hld':['hld', 'def'],
'jud':['jud', 'jur'],
'jur':['jur', 'jud'],
'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''],
'pos':[]}
def login(self, client):
if client.area.evidence_mod == 'FFA':
pass
if client.area.evidence_mod == 'Mods':
if not client in client.area.owners:
return False
if client.area.evidence_mod == 'CM':
if not client in client.area.owners and not client.is_mod:
return False
if client.area.evidence_mod == 'HiddenCM':
if not client in client.area.owners and not client.is_mod:
return False
return True
def correct_format(self, client, desc):
if client.area.evidence_mod != 'HiddenCM':
return True
else:
#correct format: <owner = pos>\ndesc
if desc[:9] == '<owner = ' and desc[9:12] in self.poses and desc[12:14] == '>\n':
return True
return False
def add_evidence(self, client, name, description, image, pos = 'all'):
if self.login(client):
if client.area.evidence_mod == 'HiddenCM':
pos = 'pos'
if len(self.evidences) >= self.limit:
client.send_host_message('You can\'t have more than {} evidence items at a time.'.format(self.limit))
else:
self.evidences.append(self.Evidence(name, description, image, pos))
def evidence_swap(self, client, id1, id2):
if self.login(client):
self.evidences[id1], self.evidences[id2] = self.evidences[id2], self.evidences[id1]
def create_evi_list(self, client):
evi_list = []
nums_list = [0]
for i in range(len(self.evidences)):
if client.area.evidence_mod == 'HiddenCM' and self.login(client):
nums_list.append(i + 1)
evi = self.evidences[i]
evi_list.append(self.Evidence(evi.name, '<owner = {}>\n{}'.format(evi.pos, evi.desc), evi.image, evi.pos).to_string())
elif client.pos in self.poses[self.evidences[i].pos]:
nums_list.append(i + 1)
evi_list.append(self.evidences[i].to_string())
return nums_list, evi_list
def del_evidence(self, client, id):
if self.login(client):
self.evidences.pop(id)
def edit_evidence(self, client, id, arg):
if self.login(client):
if client.area.evidence_mod == 'HiddenCM' and self.correct_format(client, arg[1]):
self.evidences[id] = self.Evidence(arg[0], arg[1][14:], arg[2], arg[1][9:12])
return
if client.area.evidence_mod == 'HiddenCM':
client.send_host_message('You entered a wrong pos.')
return
self.evidences[id] = self.Evidence(arg[0], arg[1], arg[2], arg[3])

View File

@ -1,32 +0,0 @@
# 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

View File

@ -1,45 +0,0 @@
# 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

View File

@ -1,78 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
def setup_logger(debug):
logging.Formatter.converter = time.gmtime
debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
debug_log = logging.getLogger('debug')
debug_log.setLevel(logging.DEBUG)
debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8')
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(debug_formatter)
debug_log.addHandler(debug_handler)
if not debug:
debug_log.disabled = True
server_log = logging.getLogger('server')
server_log.setLevel(logging.INFO)
server_handler = logging.FileHandler('logs/server.log', encoding='utf-8')
server_handler.setLevel(logging.INFO)
server_handler.setFormatter(srv_formatter)
server_log.addHandler(server_handler)
mod_log = logging.getLogger('mod')
mod_log.setLevel(logging.INFO)
mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8')
mod_handler.setLevel(logging.INFO)
mod_handler.setFormatter(mod_formatter)
mod_log.addHandler(mod_handler)
def log_debug(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('debug').debug(msg)
def log_server(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('server').info(msg)
def log_mod(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('mod').info(msg)
def parse_client_info(client):
if client is None:
return ''
info = client.get_ip()
if client.is_mod:
return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name)
return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name)

View File

@ -1,89 +0,0 @@
# 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

View File

@ -1,305 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import yaml
import json
from server import logger
from server.aoprotocol import AOProtocol
from server.area_manager import AreaManager
from server.ban_manager import BanManager
from server.client_manager import ClientManager
from server.districtclient import DistrictClient
from server.exceptions import ServerError
from server.masterserverclient import MasterServerClient
class TsuServer3:
def __init__(self):
self.config = None
self.allowed_iniswaps = None
self.load_config()
self.load_iniswaps()
self.client_manager = ClientManager(self)
self.area_manager = AreaManager(self)
self.ban_manager = BanManager()
self.software = 'tsuserver3'
self.version = 'tsuserver3dev'
self.release = 3
self.major_version = 1
self.minor_version = 1
self.ipid_list = {}
self.hdid_list = {}
self.char_list = None
self.char_pages_ao1 = None
self.music_list = None
self.music_list_ao2 = None
self.music_pages_ao1 = None
self.backgrounds = None
self.load_characters()
self.load_music()
self.load_backgrounds()
self.load_ids()
self.district_client = None
self.ms_client = None
self.rp_mode = False
logger.setup_logger(debug=self.config['debug'])
def start(self):
loop = asyncio.get_event_loop()
bound_ip = '0.0.0.0'
if self.config['local']:
bound_ip = '127.0.0.1'
ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port'])
ao_server = loop.run_until_complete(ao_server_crt)
if self.config['use_district']:
self.district_client = DistrictClient(self)
asyncio.ensure_future(self.district_client.connect(), loop=loop)
if self.config['use_masterserver']:
self.ms_client = MasterServerClient(self)
asyncio.ensure_future(self.ms_client.connect(), loop=loop)
logger.log_debug('Server started.')
try:
loop.run_forever()
except KeyboardInterrupt:
pass
logger.log_debug('Server shutting down.')
ao_server.close()
loop.run_until_complete(ao_server.wait_closed())
loop.close()
def get_version_string(self):
return str(self.release) + '.' + str(self.major_version) + '.' + str(self.minor_version)
def new_client(self, transport):
c = self.client_manager.new_client(transport)
if self.rp_mode:
c.in_rp = True
c.server = self
c.area = self.area_manager.default_area()
c.area.new_client(c)
return c
def remove_client(self, client):
client.area.remove_client(client)
self.client_manager.remove_client(client)
def get_player_count(self):
return len(self.client_manager.clients)
def load_config(self):
with open('config/config.yaml', 'r', encoding = 'utf-8') as cfg:
self.config = yaml.load(cfg)
self.config['motd'] = self.config['motd'].replace('\\n', ' \n')
if 'music_change_floodguard' not in self.config:
self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}
if 'wtce_floodguard' not in self.config:
self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}
def load_characters(self):
with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars:
self.char_list = yaml.load(chars)
self.build_char_pages_ao1()
def load_music(self):
with open('config/music.yaml', 'r', encoding = 'utf-8') as music:
self.music_list = yaml.load(music)
self.build_music_pages_ao1()
self.build_music_list_ao2()
def load_ids(self):
self.ipid_list = {}
self.hdid_list = {}
#load ipids
try:
with open('storage/ip_ids.json', 'r', encoding = 'utf-8') as whole_list:
self.ipid_list = json.loads(whole_list.read())
except:
logger.log_debug('Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.')
#load hdids
try:
with open('storage/hd_ids.json', 'r', encoding = 'utf-8') as whole_list:
self.hdid_list = json.loads(whole_list.read())
except:
logger.log_debug('Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.')
def dump_ipids(self):
with open('storage/ip_ids.json', 'w') as whole_list:
json.dump(self.ipid_list, whole_list)
def dump_hdids(self):
with open('storage/hd_ids.json', 'w') as whole_list:
json.dump(self.hdid_list, whole_list)
def get_ipid(self, ip):
if not (ip in self.ipid_list):
self.ipid_list[ip] = len(self.ipid_list)
self.dump_ipids()
return self.ipid_list[ip]
def load_backgrounds(self):
with open('config/backgrounds.yaml', 'r', encoding = 'utf-8') as bgs:
self.backgrounds = yaml.load(bgs)
def load_iniswaps(self):
try:
with open('config/iniswaps.yaml', 'r', encoding = 'utf-8') as iniswaps:
self.allowed_iniswaps = yaml.load(iniswaps)
except:
logger.log_debug('cannot find iniswaps.yaml')
def build_char_pages_ao1(self):
self.char_pages_ao1 = [self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10)]
for i in range(len(self.char_list)):
self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format(i, self.char_list[i])
def build_music_pages_ao1(self):
self.music_pages_ao1 = []
index = 0
# add areas first
for area in self.area_manager.areas:
self.music_pages_ao1.append('{}#{}'.format(index, area.name))
index += 1
# then add music
for item in self.music_list:
self.music_pages_ao1.append('{}#{}'.format(index, item['category']))
index += 1
for song in item['songs']:
self.music_pages_ao1.append('{}#{}'.format(index, song['name']))
index += 1
self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)]
def build_music_list_ao2(self):
self.music_list_ao2 = []
# add areas first
for area in self.area_manager.areas:
self.music_list_ao2.append(area.name)
# then add music
for item in self.music_list:
self.music_list_ao2.append(item['category'])
for song in item['songs']:
self.music_list_ao2.append(song['name'])
def is_valid_char_id(self, char_id):
return len(self.char_list) > char_id >= 0
def get_char_id_by_name(self, name):
for i, ch in enumerate(self.char_list):
if ch.lower() == name.lower():
return i
raise ServerError('Character not found.')
def get_song_data(self, music):
for item in self.music_list:
if item['category'] == music:
return item['category'], -1
for song in item['songs']:
if song['name'] == music:
try:
return song['name'], song['length']
except KeyError:
return song['name'], -1
raise ServerError('Music not found.')
def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True):
for client in self.client_manager.clients:
if pred(client):
client.send_command(cmd, *args)
def broadcast_global(self, client, msg, as_mod=False):
char_name = client.get_char_name()
ooc_name = '{}[{}][{}]'.format('<dollar>G', client.area.abbreviation, char_name)
if as_mod:
ooc_name += '[M]'
self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global)
if self.config['use_district']:
self.district_client.send_raw_message(
'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg))
def send_modchat(self, client, msg):
name = client.name
ooc_name = '{}[{}][{}]'.format('<dollar>M', client.area.abbreviation, name)
self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod)
if self.config['use_district']:
self.district_client.send_raw_message(
'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg))
def broadcast_need(self, client, msg):
char_name = client.get_char_name()
area_name = client.area.name
area_id = client.area.abbreviation
self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']),
['=== Advert ===\r\n{} in {} [{}] needs {}\r\n==============='
.format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts)
if self.config['use_district']:
self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg))
def send_arup(self, args):
""" Updates the area properties on the Case Café Custom Client.
Playercount:
ARUP#0#<area1_p: int>#<area2_p: int>#...
Status:
ARUP#1##<area1_s: string>##<area2_s: string>#...
CM:
ARUP#2##<area1_cm: string>##<area2_cm: string>#...
Lockedness:
ARUP#3##<area1_l: string>##<area2_l: string>#...
"""
if len(args) < 2:
# An argument count smaller than 2 means we only got the identifier of ARUP.
return
if args[0] not in (0,1,2,3):
return
if args[0] == 0:
for part_arg in args[1:]:
try:
sanitised = int(part_arg)
except:
return
elif args[0] in (1, 2, 3):
for part_arg in args[1:]:
try:
sanitised = str(part_arg)
except:
return
self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True)
def refresh(self):
with open('config/config.yaml', 'r') as cfg:
self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n')
with open('config/characters.yaml', 'r') as chars:
self.char_list = yaml.load(chars)
with open('config/music.yaml', 'r') as music:
self.music_list = yaml.load(music)
self.build_music_pages_ao1()
self.build_music_list_ao2()
with open('config/backgrounds.yaml', 'r') as bgs:
self.backgrounds = yaml.load(bgs)

View File

@ -1,215 +0,0 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2017 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Partly authored by Johan Hanssen Seferidis (MIT license):
# https://github.com/Pithikos/python-websocket-server
import asyncio
import re
import struct
from base64 import b64encode
from hashlib import sha1
from server import logger
class Bitmasks:
FIN = 0x80
OPCODE = 0x0f
MASKED = 0x80
PAYLOAD_LEN = 0x7f
PAYLOAD_LEN_EXT16 = 0x7e
PAYLOAD_LEN_EXT64 = 0x7f
class Opcode:
CONTINUATION = 0x0
TEXT = 0x1
BINARY = 0x2
CLOSE_CONN = 0x8
PING = 0x9
PONG = 0xA
class WebSocket:
"""
State data for clients that are connected via a WebSocket that wraps
over a conventional TCP connection.
"""
def __init__(self, client, protocol):
self.client = client
self.transport = client.transport
self.protocol = protocol
self.keep_alive = True
self.handshake_done = False
self.valid = False
def handle(self, data):
if not self.handshake_done:
return self.handshake(data)
return self.parse(data)
def parse(self, data):
b1, b2 = 0, 0
if len(data) >= 2:
b1, b2 = data[0], data[1]
fin = b1 & Bitmasks.FIN
opcode = b1 & Bitmasks.OPCODE
masked = b2 & Bitmasks.MASKED
payload_length = b2 & Bitmasks.PAYLOAD_LEN
if not b1:
# Connection closed
self.keep_alive = 0
return
if opcode == Opcode.CLOSE_CONN:
# Connection close requested
self.keep_alive = 0
return
if not masked:
# Client was not masked (spec violation)
logger.log_debug("ws: client was not masked.", self.client)
self.keep_alive = 0
print(data)
return
if opcode == Opcode.CONTINUATION:
# No continuation frames supported
logger.log_debug("ws: client tried to send continuation frame.", self.client)
return
elif opcode == Opcode.BINARY:
# No binary frames supported
logger.log_debug("ws: client tried to send binary frame.", self.client)
return
elif opcode == Opcode.TEXT:
def opcode_handler(s, msg):
return msg
elif opcode == Opcode.PING:
opcode_handler = self.send_pong
elif opcode == Opcode.PONG:
opcode_handler = lambda s, msg: None
else:
# Unknown opcode
logger.log_debug("ws: unknown opcode!", self.client)
self.keep_alive = 0
return
mask_offset = 2
if payload_length == 126:
payload_length = struct.unpack(">H", data[2:4])[0]
mask_offset = 4
elif payload_length == 127:
payload_length = struct.unpack(">Q", data[2:10])[0]
mask_offset = 10
masks = data[mask_offset:mask_offset + 4]
decoded = ""
for char in data[mask_offset + 4:payload_length + mask_offset + 4]:
char ^= masks[len(decoded) % 4]
decoded += chr(char)
return opcode_handler(self, decoded)
def send_message(self, message):
self.send_text(message)
def send_pong(self, message):
self.send_text(message, Opcode.PONG)
def send_text(self, message, opcode=Opcode.TEXT):
"""
Important: Fragmented (continuation) messages are not supported since
their usage cases are limited - when we don't know the payload length.
"""
# Validate message
if isinstance(message, bytes):
message = message.decode("utf-8")
elif isinstance(message, str):
pass
else:
raise TypeError("Message must be either str or bytes")
header = bytearray()
payload = message.encode("utf-8")
payload_length = len(payload)
# Normal payload
if payload_length <= 125:
header.append(Bitmasks.FIN | opcode)
header.append(payload_length)
# Extended payload
elif payload_length >= 126 and payload_length <= 65535:
header.append(Bitmasks.FIN | opcode)
header.append(Bitmasks.PAYLOAD_LEN_EXT16)
header.extend(struct.pack(">H", payload_length))
# Huge extended payload
elif payload_length < (1 << 64):
header.append(Bitmasks.FIN | opcode)
header.append(Bitmasks.PAYLOAD_LEN_EXT64)
header.extend(struct.pack(">Q", payload_length))
else:
raise Exception("Message is too big")
self.transport.write(header + payload)
def handshake(self, data):
try:
message = data[0:1024].decode().strip()
except UnicodeDecodeError:
return False
upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower())
if not upgrade:
self.keep_alive = False
return False
key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message)
if key:
key = key.group(1)
else:
logger.log_debug("Client tried to connect but was missing a key", self.client)
self.keep_alive = False
return False
response = self.make_handshake_response(key)
print(response.encode())
self.transport.write(response.encode())
self.handshake_done = True
self.valid = True
return True
def make_handshake_response(self, key):
return \
'HTTP/1.1 101 Switching Protocols\r\n'\
'Upgrade: websocket\r\n' \
'Connection: Upgrade\r\n' \
'Sec-WebSocket-Accept: %s\r\n' \
'\r\n' % self.calculate_response_key(key)
def calculate_response_key(self, key):
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
hash = sha1(key.encode() + GUID.encode())
response_key = b64encode(hash.digest()).strip()
return response_key.decode('ASCII')
def finish(self):
self.protocol.connection_lost(self)

2227
tsuserver3.patch Normal file

File diff suppressed because it is too large Load Diff