IRC work
This commit is contained in:
parent
21bbba74c0
commit
565dc27a30
4 changed files with 141 additions and 43 deletions
|
@ -20,8 +20,10 @@
|
||||||
:<servername> 332 <client> #main :Welcome to the main channel!
|
:<servername> 332 <client> #main :Welcome to the main channel!
|
||||||
:<servername> 353 <client> = #main :<client>, ...other clients here...
|
:<servername> 353 <client> = #main :<client>, ...other clients here...
|
||||||
:<servername> 366 <client> #main :End of /NAMES list
|
:<servername> 366 <client> #main :End of /NAMES list
|
||||||
|
|
||||||
# On Attempting to join/part a channel, or anything else I don't want to deal with
|
# On Attempting to join/part a channel, or anything else I don't want to deal with
|
||||||
:<servername> 421 <client> <command> :Unknown command
|
:<servername> 421 <client> <command> :Unknown command
|
||||||
|
|
||||||
# I should probably return the "NOSUCHCHANNEL" thing for joins
|
# I should probably return the "NOSUCHCHANNEL" thing for joins
|
||||||
|
|
||||||
#PING PONG
|
#PING PONG
|
||||||
|
|
2
logs.py
2
logs.py
|
@ -7,7 +7,7 @@ from typing import Union
|
||||||
def log(
|
def log(
|
||||||
message: str,
|
message: str,
|
||||||
level: str = "LOG",
|
level: str = "LOG",
|
||||||
origin: str = None,
|
origin: str = None, # pyright: ignore[reportArgumentType]
|
||||||
time: Union[dt, str] = "now",
|
time: Union[dt, str] = "now",
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
if level in ["EXIT", "CRASH", "FATAL", "ERROR"]:
|
if level in ["EXIT", "CRASH", "FATAL", "ERROR"]:
|
||||||
|
|
15
poetry.lock
generated
15
poetry.lock
generated
|
@ -1,15 +1,14 @@
|
||||||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "firepup650"
|
name = "firepup650"
|
||||||
version = "1.0.40"
|
version = "1.0.43"
|
||||||
description = "Package containing various shorthand things I use, and a few imports I almost always use"
|
description = "Package containing various shorthand things I use, and a few imports I almost always use"
|
||||||
category = "main"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.8"
|
python-versions = "<4.0,>=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "firepup650-1.0.40-py3-none-any.whl", hash = "sha256:f8c02fd35667fe38a34e1e26f132d00eb3c1ca4fe0585b8bf8e2c3b2feafa659"},
|
{file = "firepup650-1.0.43-py3-none-any.whl", hash = "sha256:685a3e00586bfa483108675de52bd05ef1ddbf50864fc4071c735bf36a726b23"},
|
||||||
{file = "firepup650-1.0.40.tar.gz", hash = "sha256:d1043a8e9deec63a5f057a279528ecddff95b9e53299d2a0b799378bf97294e9"},
|
{file = "firepup650-1.0.43.tar.gz", hash = "sha256:985d679370d61490e4a7efa2022d0c1971c63223b5a455b374bfd1fe1b910612"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -20,7 +19,6 @@ fpsql = ">=1,<1.0.26 || >1.0.26,<2"
|
||||||
name = "fkeycapture"
|
name = "fkeycapture"
|
||||||
version = "1.2.7"
|
version = "1.2.7"
|
||||||
description = "A way to capture keystrokes"
|
description = "A way to capture keystrokes"
|
||||||
category = "main"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8,<4.0"
|
python-versions = ">=3.8,<4.0"
|
||||||
files = [
|
files = [
|
||||||
|
@ -32,7 +30,6 @@ files = [
|
||||||
name = "fpsql"
|
name = "fpsql"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
description = "An easy to use SQLite package"
|
description = "An easy to use SQLite package"
|
||||||
category = "main"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8,<4.0"
|
python-versions = ">=3.8,<4.0"
|
||||||
files = [
|
files = [
|
||||||
|
@ -42,5 +39,5 @@ files = [
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.10"
|
||||||
content-hash = "a5fdf74d67707a0d1acbce90d478d8405e7bdc6826f33c0769eb1eb9650649c0"
|
content-hash = "883fa6484539c99f51b0004d7241922c884911ba7aaacf6517a3bae6b7a6c3e7"
|
||||||
|
|
165
server.py
165
server.py
|
@ -3,6 +3,7 @@ import os, sys, asyncio, re, signal, socket, random
|
||||||
from platform import uname
|
from platform import uname
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from logs import log
|
from logs import log
|
||||||
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
|
||||||
|
|
||||||
class LinkDownError(Exception): ...
|
class LinkDownError(Exception): ...
|
||||||
|
@ -11,7 +12,26 @@ class LinkDownError(Exception): ...
|
||||||
class FailedLinkError(Exception): ...
|
class FailedLinkError(Exception): ...
|
||||||
|
|
||||||
|
|
||||||
class Globals: ...
|
class Globals:
|
||||||
|
def __init__(G):
|
||||||
|
G.uniqueClients = 0
|
||||||
|
G.servers = {}
|
||||||
|
G.clientsConnected = {}
|
||||||
|
G.msgs = []
|
||||||
|
G.remoteID = uname().node
|
||||||
|
G.event = asyncio.Event()
|
||||||
|
G.loop = asyncio.get_event_loop()
|
||||||
|
G.interruptCount = 0
|
||||||
|
G.killList = {}
|
||||||
|
G.outboundLinks = []
|
||||||
|
G.S2SLogs = []
|
||||||
|
G.cwlgd = False
|
||||||
|
G.NUL = "\x1e"
|
||||||
|
dtime = dt.now(tz.utc)
|
||||||
|
dtime.replace(tzinfo=tz.utc)
|
||||||
|
time = dtime.strftime("%d-%m-%Y at %H:%M:%S UTC")
|
||||||
|
G.IRCStats = {"users": {"localMax": 0, "localCurrent": 0, "globalMax": 0, "globalCurrent": 0}, "servers": 0, "bootTime": time}
|
||||||
|
G.queue = []
|
||||||
|
|
||||||
|
|
||||||
TimeoutErrors = (TimeoutError, asyncio.exceptions.TimeoutError)
|
TimeoutErrors = (TimeoutError, asyncio.exceptions.TimeoutError)
|
||||||
|
@ -23,23 +43,8 @@ DisconnectErrors = (
|
||||||
*TimeoutErrors,
|
*TimeoutErrors,
|
||||||
)
|
)
|
||||||
G = Globals()
|
G = Globals()
|
||||||
G.uniqueClients = 0
|
|
||||||
G.servers = {}
|
|
||||||
G.clientsConnected = {}
|
|
||||||
port = 65048
|
|
||||||
G.msgs = []
|
|
||||||
G.remoteID = uname().node
|
|
||||||
G.event = asyncio.Event()
|
|
||||||
G.loop = asyncio.get_event_loop()
|
|
||||||
G.interruptCount = 0
|
|
||||||
G.killList = {}
|
|
||||||
G.outboundLinks = []
|
|
||||||
G.S2SLogs = []
|
|
||||||
G.cwlgd = False
|
|
||||||
G.NUL = "\x1e"
|
|
||||||
G.IRCStats = {"users": {"localMax": 0, "localCurrent": 0, "globalMax": 0, "globalCurrent": 0}, "servers": 0}
|
|
||||||
G.queue = []
|
|
||||||
saveLogs = True
|
saveLogs = True
|
||||||
|
port = 65048
|
||||||
address = "::"
|
address = "::"
|
||||||
# Try to load a message log, if one exists
|
# Try to load a message log, if one exists
|
||||||
try:
|
try:
|
||||||
|
@ -115,9 +120,9 @@ if len(G.remoteID) > 16:
|
||||||
G.remoteID = G.remoteID[:15]
|
G.remoteID = G.remoteID[:15]
|
||||||
|
|
||||||
|
|
||||||
async def getMessage(reader, localQueue: list, size: int = 0, timeout: float = 0, sanitize: bool = False) -> str:
|
async def getMessage(reader, localQueue: list, size: int = 0, timeout: float = 0, sanitize: bool = False) -> tuple[str, list[str]]:
|
||||||
if not localQueue:
|
if not localQueue:
|
||||||
data = ""
|
data = b""
|
||||||
if timeout and timeout > 0:
|
if timeout and timeout > 0:
|
||||||
data = await asyncio.wait_for(reader.read(size), timeout)
|
data = await asyncio.wait_for(reader.read(size), timeout)
|
||||||
else:
|
else:
|
||||||
|
@ -157,7 +162,7 @@ async def handleClient(reader, writer):
|
||||||
writer.write(b"Please identify yourself. Nick limit is 20 chars.\r\n")
|
writer.write(b"Please identify yourself. Nick limit is 20 chars.\r\n")
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
name, localQueue = await getMessage(reader, [], 20, 0, True)
|
name, localQueue = await getMessage(reader, [], 20, 0, True)
|
||||||
localQueue = []
|
#log(f"{localQueue}", "DEBUG")
|
||||||
if len(name) > 20:
|
if len(name) > 20:
|
||||||
name = name[
|
name = name[
|
||||||
:19
|
:19
|
||||||
|
@ -175,8 +180,101 @@ async def handleClient(reader, writer):
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
return
|
return
|
||||||
if name == "CAP LS 302":
|
if name == "CAP LS 302":
|
||||||
pass # TODO: IRC Logic
|
name = "Unknown IRC Client"
|
||||||
elif not name.startswith("S2S-"):
|
writer.write(f":{G.remoteID} CAP * LS\r\n".encode("utf8"))
|
||||||
|
await writer.drain()
|
||||||
|
nameFrag = ""
|
||||||
|
if localQueue:
|
||||||
|
nameFrag = localQueue[0]
|
||||||
|
iname, localQueue = await getMessage(reader, [], 50, 0, True)
|
||||||
|
iname = (nameFrag + iname).split(" ", 1)[1].lower()
|
||||||
|
G.uniqueClients += 1
|
||||||
|
G.clientsConnected[iname] = G.remoteID
|
||||||
|
name = iname
|
||||||
|
G.msgs.extend([log(f"{iname} has connected to the server.")])
|
||||||
|
G.S2SLogs.append(("+", iname, G.remoteID))
|
||||||
|
localQueue = [] # Discard "USER" data
|
||||||
|
clientList = iname + ",".join(G.clientsConnected.keys())
|
||||||
|
writer.write(f""":{G.remoteID} 001 {iname} :Welcome to the python-talk Network, {iname}\r
|
||||||
|
:{G.remoteID} 002 {iname} :Your host is {G.remoteID}, running version 0.1\r
|
||||||
|
:{G.remoteID} 003 {iname} :This server was created on {G.IRCStats["bootTime"]}\r
|
||||||
|
:{G.remoteID} 004 {iname} python-talk 0.1 r t\r
|
||||||
|
:{G.remoteID} 005 {iname} AWAYLEN=200,CASEMAPPING=ascii,CHANTYPES=#,ELIST=U,HOSTLEN=16 :are supported by this server\r
|
||||||
|
:{G.remoteID} 005 {iname} CHANLIMIT=#:1,CHANMODES=b,k,l,t,CHANNELLEN=4,KICKLEN=1,MAXLIST=b:1 :are supported by this server\r
|
||||||
|
:{G.remoteID} 005 {iname} MODES=4,NETWORK=talknet,NICKLEN=20,TOPICLEN=30,USERLEN=1 :are supported by this server\r
|
||||||
|
:{G.remoteID} 251 {iname} :There are {G.IRCStats["users"]["globalCurrent"]} users and 0 invisible on {G.IRCStats["servers"]} servers\r
|
||||||
|
:{G.remoteID} 252 {iname} 0 :operator(s) online\r
|
||||||
|
:{G.remoteID} 253 {iname} 0 :unknown connection(s)\r
|
||||||
|
:{G.remoteID} 254 {iname} 1 :channel formed\r
|
||||||
|
:{G.remoteID} 255 {iname} :I have {G.IRCStats["users"]["globalCurrent"]} clients and {G.IRCStats["servers"]} servers\r
|
||||||
|
:{G.remoteID} 265 {iname} [{G.IRCStats["users"]["localCurrent"]} {G.IRCStats["users"]["localMax"]}] :Current local users {G.IRCStats["users"]["localCurrent"]}, max {G.IRCStats["users"]["localMax"]}\r
|
||||||
|
:{G.remoteID} 266 {iname} [{G.IRCStats["users"]["globalCurrent"]} {G.IRCStats["users"]["globalMax"]}] :Current global users {G.IRCStats["users"]["globalCurrent"]}, max {G.IRCStats["users"]["globalMax"]}\r
|
||||||
|
:{G.remoteID} 422 {iname} :This server does not support MOTDs\r
|
||||||
|
:{iname} JOIN #main\r
|
||||||
|
:{G.remoteID} 324 {iname} #main t\r
|
||||||
|
:{G.remoteID} 332 {iname} #main :Welcome to the main channel!\r
|
||||||
|
:{G.remoteID} 353 {iname} = #main :{clientList}\r
|
||||||
|
:{G.remoteID} 366 {iname} #main :End of /NAMES list\r\n""".encode("utf8"))
|
||||||
|
await writer.drain()
|
||||||
|
msgIndex = len(G.msgs)
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
request, localQueue = await getMessage(reader, localQueue, 967, 0.1, False)
|
||||||
|
request = request.replace(G.NUL, "\\x1e")
|
||||||
|
response = None
|
||||||
|
log(request, "DEBUG")
|
||||||
|
cmd = request.split(" ")[0].lower()
|
||||||
|
args = request.split(" ")[1:]
|
||||||
|
if len(args) == 0:
|
||||||
|
args = ["you-gave-me-an-invalid-command-why-would-you-do-this-to-me"]
|
||||||
|
match cmd:
|
||||||
|
case "ping":
|
||||||
|
writer.write(f":{G.remoteID} PONG {G.remoteID} :{' '.join(args)}\r\n".encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
case "join":
|
||||||
|
if args[0].lower() == "#main":
|
||||||
|
writer.write(f":{G.remoteID} 443 {iname} {iname} #main :You're already in #main, and you cannot leave\r\n".encode("utf-8"))
|
||||||
|
else:
|
||||||
|
writer.write(f":{G.remoteID} 405 {iname} {args[0]} :Only #main exists.\r\n".encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
case "privmsg":
|
||||||
|
if args[0].lower() == "#main":
|
||||||
|
msg = " ".join(args[1:])
|
||||||
|
if msg.startswith(":"):
|
||||||
|
msg = msg[1:]
|
||||||
|
if msg.startswith("\x01") and not msg.startswith("\x01ACTION"):
|
||||||
|
writer.write(f":{G.remoteID} 404 {iname} {args[0]} :Unsupported CTCP\r\n".encode("utf-8"))
|
||||||
|
else:
|
||||||
|
writer.write(f":{G.remoteID} 404 {iname} {args[0]} :Channel does not exist\r\n".encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
case "kill":
|
||||||
|
users = G.clientsConnected.keys()
|
||||||
|
if args[0] in users:
|
||||||
|
writer.write(f":{G.remoteID} 481 {iname} :Permission Denied- There are no IRC operators here.\r\n".encode("utf-8"))
|
||||||
|
elif args[0] == G.remoteID:
|
||||||
|
writer.write(f":{G.remoteID} 483 {iname} :You can't kill me, I'm Skynet!\r\n".encode("utf-8"))
|
||||||
|
else:
|
||||||
|
writer.write(f":{G.remoteID} 401 {iname} :{args[0]}\r\n".encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
case "part":
|
||||||
|
if args[0].lower() == "#main":
|
||||||
|
clientList = iname + ",".join(G.clientsConnected.keys())
|
||||||
|
writer.write(f""":{iname} PART #main\r
|
||||||
|
:{iname} JOIN #main\r
|
||||||
|
:{G.remoteID} 324 {iname} #main t\r
|
||||||
|
:{G.remoteID} 332 {iname} #main :Welcome to the main channel!\r
|
||||||
|
:{G.remoteID} 353 {iname} = #main :{clientList}\r
|
||||||
|
:{G.remoteID} 366 {iname} #main :End of /NAMES list\r\n""".encode("utf8"))
|
||||||
|
else:
|
||||||
|
writer.write(f":{G.remoteID} 404 {iname} {args[0]} :Channel does not exist\r\n".encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
case _:
|
||||||
|
writer.write(f":{G.remoteID} 421 {iname} {cmd} :That isn't supported here.\r\n".encode("utf-8"))
|
||||||
|
await writer.drain()
|
||||||
|
except TimeoutErrors:
|
||||||
|
pass
|
||||||
|
elif not name.startswith("S2S-"): # Normal Client logic
|
||||||
|
localQueue = []
|
||||||
G.clientsConnected[name.lower()] = G.remoteID
|
G.clientsConnected[name.lower()] = G.remoteID
|
||||||
msgIndex = 0
|
msgIndex = 0
|
||||||
G.uniqueClients += 1
|
G.uniqueClients += 1
|
||||||
|
@ -254,6 +352,7 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
del G.clientsConnected[name.lower()]
|
del G.clientsConnected[name.lower()]
|
||||||
G.S2SLogs.append(("-", name, G.remoteID))
|
G.S2SLogs.append(("-", name, G.remoteID))
|
||||||
else: # This is... probably a server?
|
else: # This is... probably a server?
|
||||||
|
localQueue = []
|
||||||
sName = name[4:] # Trim off the S2S label
|
sName = name[4:] # Trim off the S2S label
|
||||||
if (
|
if (
|
||||||
G.servers.get(sName, False) != False
|
G.servers.get(sName, False) != False
|
||||||
|
@ -309,9 +408,9 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
case "S": # Server notice
|
case "S": # Server notice
|
||||||
G.msgs.extend([log(buffer[2:])])
|
G.msgs.extend([log(buffer[2:])])
|
||||||
writer.write(b"I Mmm... Blueberries\r\n")
|
writer.write(b"I Mmm... Blueberries\r\n")
|
||||||
case "I":
|
case "I": # Ignore lines, ususally responses to everything else
|
||||||
pass
|
pass
|
||||||
case "+":
|
case "+": # Add client lines
|
||||||
cName = buffer[2:]
|
cName = buffer[2:]
|
||||||
if cName.lower() not in G.clientsConnected:
|
if cName.lower() not in G.clientsConnected:
|
||||||
G.msgs.append(
|
G.msgs.append(
|
||||||
|
@ -323,7 +422,7 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
writer.write(b"I Mmm... Pineapples\r\n")
|
writer.write(b"I Mmm... Pineapples\r\n")
|
||||||
else:
|
else:
|
||||||
writer.write(f"K {cName}\r\n".encode("utf8"))
|
writer.write(f"K {cName}\r\n".encode("utf8"))
|
||||||
case "-":
|
case "-": # Remove client lines
|
||||||
cName = buffer[2:]
|
cName = buffer[2:]
|
||||||
if G.clientsConnected.get(cName.lower(), None) == sName:
|
if G.clientsConnected.get(cName.lower(), None) == sName:
|
||||||
G.msgs.append(
|
G.msgs.append(
|
||||||
|
@ -338,8 +437,8 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
f"S Your server is LYING about who is connected to it. - {G.remoteID}, a fellow server\r\n".encode(
|
f"S Your server is LYING about who is connected to it. - {G.remoteID}, a fellow server\r\n".encode(
|
||||||
"utf8"
|
"utf8"
|
||||||
)
|
)
|
||||||
)
|
) # Prevents servers from removing users they don't have, propogates in a fun way IIRC
|
||||||
case "M":
|
case "M": # Message lines, standard client stuff
|
||||||
cName = buffer[2:].split(G.NUL, 1)[0]
|
cName = buffer[2:].split(G.NUL, 1)[0]
|
||||||
message = buffer[2:].split(G.NUL, 1)[1]
|
message = buffer[2:].split(G.NUL, 1)[1]
|
||||||
G.msgs.append(log(f" {cName}: {message}"))
|
G.msgs.append(log(f" {cName}: {message}"))
|
||||||
|
@ -347,15 +446,15 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
writer.write(
|
writer.write(
|
||||||
b"I Get these damn heretic ghost clients out of my store so i can buy my cult candles in peace.\r\n"
|
b"I Get these damn heretic ghost clients out of my store so i can buy my cult candles in peace.\r\n"
|
||||||
)
|
)
|
||||||
case "A":
|
case "A": # Action lines, standard client stuff
|
||||||
cName = buffer[2:].split(G.NUL, 1)[0]
|
cName = buffer[2:].split(G.NUL, 1)[0]
|
||||||
message = buffer[2:].split(G.NUL, 1)[1]
|
message = buffer[2:].split(G.NUL, 1)[1]
|
||||||
G.msgs.append(log(f"* {cName} {message}"))
|
G.msgs.append(log(f"* {cName} {message}"))
|
||||||
G.S2SLogs.append(("A", (cName, message), sName))
|
G.S2SLogs.append(("A", (cName, message), sName))
|
||||||
writer.write(b"I Mmm... Strawberries\r\n")
|
writer.write(b"I Mmm... Strawberries\r\n")
|
||||||
case "Q":
|
case "Q": # Quit lines
|
||||||
break
|
break
|
||||||
case "K":
|
case "K": # Kill lines
|
||||||
cName = buffer[2:]
|
cName = buffer[2:]
|
||||||
if not G.clientsConnected.get(cName.lower(), False):
|
if not G.clientsConnected.get(cName.lower(), False):
|
||||||
pass # They don't exist, safe to ignore
|
pass # They don't exist, safe to ignore
|
||||||
|
@ -366,9 +465,9 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
cName.lower()
|
cName.lower()
|
||||||
] = True
|
] = True
|
||||||
writer.write(b"I Mmm... Blood\r\n")
|
writer.write(b"I Mmm... Blood\r\n")
|
||||||
case _:
|
case _: # Everything we *don't* know how to handle
|
||||||
writer.write(
|
writer.write(
|
||||||
f"S Your server is doing drugs over here, sending me bullshit messages man - {G.remoteID}, A fellow server\r\n".encode(
|
f"S Your server is doing drugs over here, sending me bullshit messages - {G.remoteID}, A fellow server\r\n".encode(
|
||||||
"utf8"
|
"utf8"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -414,7 +513,7 @@ Please note that this is not network level statistics.\r\n""".encode(
|
||||||
del G.servers[sName]
|
del G.servers[sName]
|
||||||
G.msgs.append(log(f"{sName} has de-linked from the network"))
|
G.msgs.append(log(f"{sName} has de-linked from the network"))
|
||||||
except DisconnectErrors:
|
except DisconnectErrors:
|
||||||
if not name:
|
if not name: # pyright: ignore [reportPossiblyUnboundVariable]
|
||||||
return
|
return
|
||||||
if not name.startswith("S2S-"):
|
if not name.startswith("S2S-"):
|
||||||
G.uniqueClients -= 1
|
G.uniqueClients -= 1
|
||||||
|
|
Loading…
Reference in a new issue