Skip to content

ref: python3 conversion#2

Open
patthiel wants to merge 13 commits intokangsterizer:masterfrom
patthiel:ref/python3
Open

ref: python3 conversion#2
patthiel wants to merge 13 commits intokangsterizer:masterfrom
patthiel:ref/python3

Conversation

@patthiel
Copy link
Copy Markdown

@patthiel patthiel commented Oct 2, 2023

  • ran modernize on various python files to fix the compatibility issues
  • going through the toil of fixing various indentation issues
  • will eventually add some unit tests
  • Updated dockerfile to run python3.9, i'll eventually bring this up to 3.11
  • With any luck, github co-pilot will help me spot more issues

Copy link
Copy Markdown
Owner

@kangsterizer kangsterizer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this!

Just a couple of nits - but it would also be helpful if you were able to split the commits in 2:

  1. the tab/space conversions
  2. the rest of the p3 conversion

this allows the diff to be more readable and its easier to catch any possible issue

otherwise, ill take another quick look when possible

Comment thread config.py Outdated
Comment thread config.py Outdated
@patthiel
Copy link
Copy Markdown
Author

patthiel commented Oct 2, 2023

thanks for the review, I totally didn't mean to PR this against upstream yet. I meant to PR it against my own fork as a way to track my progress. Good feedback though, i'll make sure to separate out the tab/space stuff vs. the rest

@patthiel
Copy link
Copy Markdown
Author

patthiel commented Oct 3, 2023

Made some progress..

Now it's a matter of trying to connect and seeing what breaks.

To test i've been running it in th container with:

docker run --rm -v $(pwd):/app -p 5500:5500 -p 6667:5500 phxd:latest

With the latest changes, it'll start the server, but i haven't successfully managed to connect yet. Still working through the issues.

To see the diff without the whitespace changes: 509df1b?diff=unified&w=1

Comment thread shared/HLTypes.py
# this is an avaraline extension for nick coloring
if self.color >= 0:
data += pack( "!L" , self.color )
# if self.color >= 0:
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to fix

@kangsterizer
Copy link
Copy Markdown
Owner

come back! ;)

@patthiel
Copy link
Copy Markdown
Author

patthiel commented May 3, 2026

@kangsterizer a lot remains to be tested. I have yet to login with the native client through Wine (i've only got apple silicon / linux machines to test with)..

so far, simple actions work:

  • login
  • chat (sending, didn't test receiving)
  • download files
  • fetching and posting in news

Full disclosure: I had claude help me get this moving again. So I'll be reviewing it closely as well.

I did update the instructions for running the container with a bind mount and added port mappings for downloads.

Comment on lines +32 to +74
raise HLException( "You are already logged in." , False)

login = HLEncode( packet.getString( DATA_LOGIN , HLEncode( "guest" ) ) )
password = HLEncode( packet.getString( DATA_PASSWORD , "" ) )
reason = server.checkForBan( user.ip )

if reason != None:
raise HLException , ( "You are banned: %s" % reason , True )

user.account = server.database.loadAccount( login )
if user.account == None:
raise HLException , ( "Login is incorrect." , True )
if user.account.password != md5( password ).hexdigest():
user.nick = packet.getString( DATA_NICK , "unnamed" )
server.logEvent( LOG_TYPE_LOGIN , "Login failure" , user )
raise HLException , ( "Password is incorrect." , True )
if user.account.fileRoot == "":
user.account.fileRoot = FILE_ROOT

self.handleUserChange( server , user , packet )

info = HLPacket( HTLS_HDR_TASK , packet.seq )
info.addString( DATA_SERVERNAME , SERVER_NAME )
server.sendPacket( user.uid , info )
server.logEvent( LOG_TYPE_LOGIN , "Login successful" , user )
server.database.updateAccountStats( login , 0 , 0 , True )
# Diagnostics: dump every object so we can see exactly what the
# client sent (type, size, hex). Helpful when porting against
# third-party clients whose framing might differ subtly.
try:
for obj in packet.objs:
hex_data = obj.data.hex() if isinstance( obj.data , (bytes , bytearray) ) else repr( obj.data )
server.log.debug(
" login pkt obj type=0x%04x len=%d data=%s" ,
obj.type , len( obj.data ) , hex_data ,
)
except Exception:
pass

# Set this after login, so the user does not get their own join packet.
# link user.valid = True
server.handleUserLogin( user ) #link

if user.isIRC:
( c , u ) = server.clients[user.uid]
user.nick = user.nick.replace( " " , "_" )

c.transport.write ( ":%s!~%s@localhost JOIN :#public\r\n" % (user.nick, user.nick) )
userlist = server.getOrderedUserlist()
nicks = ""
for myuser in userlist:
if myuser.uid != user.uid:
nicks += " "+ircCheckUserNick( myuser )
data = ":"+IRC_SERVER_NAME+" 353 "+user.nick+" = #public :"+ircCheckUserNick( user )+nicks+"\r\n"
data += ":"+IRC_SERVER_NAME+" 366 "+user.nick+" #public :End of /NAMES list.\r\n"
data += "NOTICE AUTH:*** You have been successfull logged in !\r\n"
data += "NOTICE *:*** You have been forced to join #public\r\n"
c.transport.write( data )

# show welcome msg, needs script support in exec/login !!!
ret = ""
ret = shell_exec( user , 'login', '')
if ret != None:
chat = HLPacket( HTLS_HDR_CHAT )
chat.addString( DATA_STRING , ret )
server.sendPacket( user.uid , chat )

def handleUserChange( self , server , user , packet ):
oldnick = user.nick
user.nick = packet.getString( DATA_NICK , user.nick )
user.icon = packet.getNumber( DATA_ICON , user.icon )
user.color = packet.getNumber( DATA_COLOR , user.color )

# Limit nickname length.
user.nick = user.nick[:MAX_NICK_LEN]

# Set their admin status according to their kick priv.
#if user.hasPriv( PRIV_KICK_USERS ):
# user.status |= STATUS_ADMIN
#else:
# user.status &= ~STATUS_ADMIN

# Check to see if they can use any name; if not, set their nickname to their account name.
if not user.hasPriv( PRIV_USE_ANY_NAME ):
user.nick = user.account.name

change = HLPacket( HTLS_HDR_USER_CHANGE )
change.addNumber( DATA_UID , user.uid )
change.addString( DATA_NICK , user.nick )
change.addNumber( DATA_ICON , user.icon )
change.addNumber( DATA_STATUS , user.status )
change.addString ( DATA_IRC_OLD_NICK , oldnick )
if user.color >= 0L:
change.addInt32( DATA_COLOR , user.color )

server.broadcastPacket( change )

def handleUserList( self , server , user , packet ):
list = HLPacket( HTLS_HDR_TASK , packet.seq )
for u in server.getOrderedUserlist():
list.addBinary( DATA_USER , u.flatten() )
server.sendPacket( user.uid , list )
# ``DATA_LOGIN`` and ``DATA_PASSWORD`` carry XOR-encoded raw bytes,
# not text — use ``getBinary``. ``HLDecode`` auto-detects the
# client's XOR mask (0xFF for classic clients, 0x7F for the
# Mierau Swift client) and returns plaintext ``bytes``. The login
# goes to the account DB which stores ``str``, so decode it; the
# password stays bytes for md5.
raw_login_wire = packet.getBinary( DATA_LOGIN , HLEncode( "guest" ) )
raw_pass_wire = packet.getBinary( DATA_PASSWORD , b"" )
login_bytes = HLDecode( raw_login_wire )
password = HLDecode( raw_pass_wire )
# Some Hotline-1.x-era clients use the documented ``XOR 0xFF`` for
# logins/passwords; if a particular client doesn't, the decoded
# bytes won't match a real login. Log the hex of both the wire form
# and the post-HLEncode form so any encoding mismatch is obvious.
try:
server.log.debug(
"handleLogin wire bytes login=%s password=%s" ,
raw_login_wire.hex() , raw_pass_wire.hex() ,
)
server.log.debug(
"handleLogin decoded login=%s password=<%d bytes>" ,
login_bytes.hex() , len( password ) ,
)
except Exception:
pass
login = login_bytes.decode( 'mac-roman' , errors = 'replace' ) if isinstance( login_bytes , (bytes , bytearray) ) else login_bytes

def handleUserInfo( self , server , user , packet ):
uid = packet.getNumber( DATA_UID , 0 )
u = server.getUser( uid )

if not user.hasPriv( PRIV_USER_INFO ) and ( uid != user.uid ):
raise HLException , "You cannot view user information."
if u == None:
raise HLException , "Invalid user."

# Format the user's idle time.
secs = long( time.time() - u.lastPacketTime )
days = secs / 86400
secs -= ( days * 86400 )
hours = secs / 3600
secs -= ( hours * 3600 )
mins = secs / 60
secs -= ( mins * 60 )
idle = ""
if days > 0:
idle = "%d:%02d:%02d:%02d" % ( days , hours , mins , secs )
else:
idle = "%02d:%02d:%02d" % ( hours , mins , secs )
if u.isIRC:
proto = "IRC"
else:
proto = "Hotline"
str = "nickname: %s\r uid: %s\r login: %s\rrealname: %s\r proto: %s\r address: %s\r idle: %s\r" % ( u.nick , u.uid , u.account.login , u.account.name , proto , u.ip , idle )
str += "--------------------------------\r"
xfers = server.fileserver.findTransfersForUser( uid )
for xfer in xfers:
type = ( "[DL]" , "[UL]" )[xfer.type]
speed = "%dk/sec" % ( xfer.getTotalBPS() / 1024 )
str += "%s %-27.27s\r %d%% @ %s\r" % ( type , xfer.name , xfer.overallPercent() , speed )
if len( xfers ) == 0:
str += "No file transfers.\r"
str += "--------------------------------\r"

info = HLPacket( HTLS_HDR_TASK , packet.seq )
info.addNumber( DATA_UID , u.uid )
info.addString( DATA_NICK , u.nick )
info.addString( DATA_STRING , str )
server.sendPacket( user.uid , info )

def handleMessage( self , server , user , packet ):
uid = packet.getNumber( DATA_UID , 0 )
str = packet.getString( DATA_STRING , "" )

if not user.hasPriv( PRIV_SEND_MESSAGES ):
raise HLException , "You are not allowed to send messages."
if server.getUser( uid ) == None:
raise HLException , "Invalid user."

msg = HLPacket( HTLS_HDR_MSG )
msg.addNumber( DATA_UID , user.uid )
msg.addString( DATA_NICK , user.nick )
msg.addString( DATA_STRING , str )
server.sendPacket( uid , msg )
server.sendPacket( user.uid , HLPacket( HTLS_HDR_TASK , packet.seq ) )

def handleUserKick( self , server , user , packet ):
uid = packet.getNumber( DATA_UID , 0 )
ban = packet.getNumber( DATA_BAN , 0 )
who = server.getUser( uid )

if not user.hasPriv( PRIV_KICK_USERS ):
raise HLException , "You are not allowed to disconnect users."
if who == None:
raise HLException , "Invalid user."
if who.account.login != user.account.login and who.hasPriv( PRIV_KICK_PROTECT ):
raise HLException , "%s cannot be disconnected." % who.nick

action = "Kicked"
if ban > 0:
action = "Banned"
server.addTempBan( who.ip , "Temporary ban." )

server.disconnectUser( uid )
server.sendPacket( user.uid , HLPacket( HTLS_HDR_TASK , packet.seq ) )
server.logEvent( LOG_TYPE_USER , "%s %s [%s]" % ( action , who.nick , who.account.login ) , user )

def handleBroadcast( self , server , user , packet ):
str = packet.getString( DATA_STRING , "" )
if not user.hasPriv( PRIV_BROADCAST ):
raise HLException , "You cannot broadcast messages."
broadcast = HLPacket( HTLS_HDR_BROADCAST )
broadcast.addString( DATA_STRING , str )
server.broadcastPacket( broadcast )
server.sendPacket( user.uid , HLPacket( HTLS_HDR_TASK , packet.seq ) )

def handlePing( self , server , user , packet ):
server.sendPacket( user.uid , HLPacket( HTLS_HDR_PING , packet.seq ) )
server.log.debug( "handleLogin: connID-side login=%r ip=%s" , login , user.ip )
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most debugging information was added here.

@kangsterizer
Copy link
Copy Markdown
Owner

kangsterizer commented May 4, 2026

this may be useful for testing some cases: https://github.com/kangsterizer/rusty-hx

with AI usage, i find two patterns to be most helpful:

  • each new feature or change -> request a unit test (and review this first)
  • top level, have it run a e2e test (rusty-hx can be used for this, its easier to run than hx/shx/mhx)

From a quick read though the diff look'd OK to me

Comment thread shared/HLProtocol.py
HTLS_HDR_NEWS_POST = 0x00000066
HTLS_HDR_MSG = 0x00000068
HTLS_HDR_CHAT = 0x0000006A
# Server→client push: the connection agreement text. Hotline clients
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not working quite right at the moment, and blocking a bunch of features on login..

@patthiel
Copy link
Copy Markdown
Author

patthiel commented May 8, 2026

@kangsterizer good tips. I had it start writing me a pytest suite with some initial coverage added. I'll be pretty opinionated about this as it's what matches most my professional background. It's looking good so far.

In my testing most features are now workin with the QEMU Mac OS 9 client.. I am definitely interested in adding some e2e style tests using your TUI script , but i'll look to get more unit test coverage added first.

Should be straight foward to test:

docker build -t phxd .

docker run \
  -v phxdvol:/app/textdb \
  -v $(pwd)/files:/app/files \
  -d \
  -p 5500:5500 \
  -p 5501:5501 \
  -p 6667:5500 \
  phxd:latest

( the bind mount for /files is assuming you want to share files)

Latest hurdle was getting agreements to work.

@patthiel
Copy link
Copy Markdown
Author

patthiel commented May 8, 2026

I realize this diff is pretty useless.. At this point, it's not really worth making this a PR but rather making it's own p3hxd repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants