diff --git a/commands/cch.py b/commands/cch.py index 8ef305e..00ffdc6 100644 --- a/commands/cch.py +++ b/commands/cch.py @@ -1,6 +1,9 @@ +from commands.cpu import handle_cpu from response import ExtensionResponse from typing import TYPE_CHECKING +from utils.filter import ProfanityModel + if TYPE_CHECKING: from server import CafeServer from asyncio import StreamWriter @@ -11,11 +14,14 @@ async def handle_cch(server: 'CafeServer', client: 'StreamWriter', *params: str) player = server.clients[address] message = params[1] + original_message = message - for other_player in player.room.clients: - if other_player != player.client: - response = ExtensionResponse('cch', '-1', '0', str(player.id), message) - await server.send_response(other_player, response) + pf = ProfanityModel() - response = ExtensionResponse('cch', '-1', '0', str(player.id), message) - await server.send_response(client, response) + if pf.has_profanity(message): + message = pf.censor(message) + await handle_cpu(server, client, 3, pf.detect_words(original_message)) + + for other_player in player.room.clients: + response = ExtensionResponse('cch', '-1', '0', str(player.id), message) + await server.send_response(other_player, response) diff --git a/commands/cha.py b/commands/cha.py index 3957574..c2fbbe3 100644 --- a/commands/cha.py +++ b/commands/cha.py @@ -15,11 +15,12 @@ async def handle_cha(server: 'CafeServer', client: 'StreamWriter', *params: str) if player.avatar.gender == '0': player.avatar.gender = old_gender - for user in player.room.clients: - if user != client: - response = ExtensionResponse('cha', '-1', '0', params[1], + if player.room: + for user in player.room.clients: + if user != client: + response = ExtensionResponse('cha', '-1', '0', params[1], str(server.clients[client.get_extra_info('peername')].id)) - await server.send_response(user, response) + await server.send_response(user, response) server.db.update_player(player.id, gender=player.avatar.gender, top_color=player.avatar.top_color, skin_color=player.avatar.skin_color, hair_color=player.avatar.hair_color, diff --git a/commands/ctf.py b/commands/ctf.py new file mode 100644 index 0000000..e31dfc3 --- /dev/null +++ b/commands/ctf.py @@ -0,0 +1,20 @@ +from typing import TYPE_CHECKING + +from objects.customer import spawn_cycle +from asyncio import create_task + +if TYPE_CHECKING: + from server import CafeServer + from asyncio import StreamWriter + + +async def handle_ctf(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: + address = client.get_extra_info('peername') + player = server.clients[address] + + player.xp = 10 + + server.db.update_player(player.id, xp=player.xp) + + print(f'created customer cycle\nused this player\'s cafe: f{player.avatar.username}') + player.customer_cycle = create_task(spawn_cycle(server, player.cafe)) diff --git a/commands/jca.py b/commands/jca.py index 95e7c9b..51dfefe 100644 --- a/commands/jca.py +++ b/commands/jca.py @@ -35,10 +35,11 @@ async def handle_jca(server: 'CafeServer', client: 'StreamWriter', *params: str) # TODO: Broken for other player's cafe # Tries to use player's cafe objects instead of the other player's - if not wanted_player.customer_cycle: - print(f'created waiter cycle\nused this player\'s cafe: f{wanted_player.avatar.username}') - # TODO: add random customers - wanted_player.customer_cycle = create_task(spawn_cycle(server, wanted_player.cafe)) + if not wanted_player.cafe.customer_cycle: + if not player.first_login: + print(f'created waiter cycle\nused this player\'s cafe: f{wanted_player.avatar.username}') + # TODO: add random customers + wanted_player.customer_cycle = create_task(spawn_cycle(server, wanted_player.cafe)) else: customers = wanted_player.cafe.customers @@ -78,4 +79,3 @@ async def handle_jca(server: 'CafeServer', client: 'StreamWriter', *params: str) await server.send_response(client, response_spawn) await server.send_response(client, response) - diff --git a/commands/lca.py b/commands/lca.py index a67a03a..25e2a57 100644 --- a/commands/lca.py +++ b/commands/lca.py @@ -3,6 +3,7 @@ from objects.player import Player from objects.avatar import Avatar from objects.cafe import Cafe +from random import randint if TYPE_CHECKING: from server import CafeServer @@ -10,17 +11,16 @@ async def handle_lca(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: - response = ExtensionResponse('lca', '-1', '0', 'user1', '1') - await server.send_response(client, response) + base_name = "Guest" + guest_name = f'{base_name}_{randint(10000000, 99999999)}' avatar = Avatar.from_string(params[1]) new_player = Player.new_user(avatar.username) new_player.avatar = avatar - new_player.cafe = Cafe.get_starting_cafe() - new_player.cafe.owner_name = avatar.username new_player.client = client - new_player.online = True address = client.get_extra_info('peername') server.clients[address] = new_player - server.players.append(new_player) + + response = ExtensionResponse('lca', '-1', '0', guest_name, '1') + await server.send_response(client, response) diff --git a/commands/lre.py b/commands/lre.py index 9974d17..a8a98cf 100644 --- a/commands/lre.py +++ b/commands/lre.py @@ -1,24 +1,167 @@ +import re +from random import randint from typing import TYPE_CHECKING + +from commands.ifr import handle_ifr +from commands.lmi import handle_lmi +from commands.rlu import handle_rlu +from commands.sbc import handle_sbc from response import ExtensionResponse -from commands.sgc import handle_sgc from commands.gui import handle_gui from objects.cafe import Cafe -from objects.player import Player +from utils.filter import ProfanityModel if TYPE_CHECKING: from server import CafeServer from asyncio import StreamWriter -async def handle_lre(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: - response = ExtensionResponse('lre', '-1', '0') - await server.send_response(client, response) +class ErrorCodes: + USERNAME_WRONG = 1 + USERNAME_SHORT = 2 + USERNAME_LONG = 3 + EMAIL_WRONG = 4 + PASSWORD_WRONG = 5 + ACCEPT_TERMS = 98 + PASSWORD_SHORT = 96 + PASSWORD_INVALID = 10 + EMAIL_LONG = 94 + EMAIL_INVALID = 14 + ACCOUNT_EXIST = 13 + BAD_WORD = 93 + + +invalidChars = ["+", "%", "&", "*", "/", "(", ")", "[", "]", "{", "}", "\"", "'", + "\\", "´", "`", "^", "°", "§", "€", "²", "³", ",", ";", "µ", "$"] + + +def contains_invalid_chars(text): + return any(char in invalidChars for char in text) + + +def is_valid_email(email): + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' + return bool(re.fullmatch(regex, email)) + +def generate_usernames(username): + available_names = [] + max_suffix_length = 24 - len(username) - 1 + + for _ in range(5): + suffix_length = min(max_suffix_length, 4) + suffix = randint(0, 10 ** suffix_length - 1) + new_name = f'{username}_{suffix}' + if len(new_name) <= 24: + available_names.append(new_name) + + return '#'.join(available_names) + + +async def handle_lre(server: 'CafeServer', client: 'StreamWriter', *params: str) -> None: address = client.get_extra_info('peername') player = server.clients[address] - player.avatar.username = params[1] + + username = params[1] email = params[2] password = params[3] - player.cafe.owner_name = player.avatar.username + accepted_terms = params[4] + + if not username or username == '.' or contains_invalid_chars(username): + response = ExtensionResponse('lre', '-1', str(ErrorCodes.USERNAME_WRONG)) + await server.send_response(client, response) + return + + if len(username) < 4: + response = ExtensionResponse('lre', '-1', str(ErrorCodes.USERNAME_SHORT)) + await server.send_response(client, response) + return + + if len(username) > 24: + response = ExtensionResponse('lre', '-1', str(ErrorCodes.USERNAME_LONG)) + await server.send_response(client, response) + return + + if not email or email == '.' or contains_invalid_chars(email): + response = ExtensionResponse('lre', '-1', str(ErrorCodes.EMAIL_WRONG)) + await server.send_response(client, response) + return + + if len(email) > 320: + response = ExtensionResponse('lre', '-1', str(ErrorCodes.EMAIL_LONG)) + await server.send_response(client, response) + return + + if not is_valid_email(email): + response = ExtensionResponse('lre', '-1', str(ErrorCodes.EMAIL_INVALID)) + await server.send_response(client, response) + return + + if not password or password == '.': + response = ExtensionResponse('lre', '-1', str(ErrorCodes.PASSWORD_WRONG)) + await server.send_response(client, response) + return + + if len(password) < 4: + response = ExtensionResponse('lre', '-1', str(ErrorCodes.PASSWORD_SHORT)) + await server.send_response(client, response) + return + + if contains_invalid_chars(password): + response = ExtensionResponse('lre', '-1', str(ErrorCodes.PASSWORD_INVALID)) + await server.send_response(client, response) + return + + if accepted_terms == '0': + response = ExtensionResponse('lre', '-1', str(ErrorCodes.ACCEPT_TERMS)) + await server.send_response(client, response) + return + + if server.db.username_exist(username): + available_usernames = generate_usernames(username) + + response = ExtensionResponse('lre', '-1', '12', available_usernames) + await server.send_response(client, response) + return + + if server.db.email_exist(email): + response = ExtensionResponse('lre', '-1', str(ErrorCodes.ACCOUNT_EXIST)) + await server.send_response(client, response) + return + + pf = ProfanityModel() + if pf.has_profanity(username): + response = ExtensionResponse('lre', '-1', str(ErrorCodes.BAD_WORD), pf.detect_words(username), + generate_usernames('Guest')) + await server.send_response(client, response) + return + + server.db.new_player(username, password, email) + player.id = server.db.get_player_id(username) + + player.online = True + player.first_login = True + + player.cafe = Cafe.get_starting_cafe() + player.cafe.player_id = player.id + player.cafe.user_id = player.id + player.cafe.owner_name = username + + player.avatar.username = username + + server.players.append(player) + + server.db.new_cafe(player.id, username, player.cafe.get_waiters_as_json()) + + response = ExtensionResponse('lre', '-1', '0') + await server.send_response(client, response) + + await handle_rlu(server, client, 'lgn', *params) await handle_gui(server, client, *params) + + await handle_sbc(server, client, *params) + + await handle_lmi(server, client, *params) + + await handle_ifr(server, client, *params) diff --git a/database.py b/database.py index c18e6e3..720df0a 100644 --- a/database.py +++ b/database.py @@ -90,8 +90,9 @@ def get_player_by_id(self, player_id: int) -> Player: def new_player(self, username: str, password: str, email: str) -> None: self.cursor.execute("INSERT INTO player (username, password, email) VALUES (?, ?, ?)", (username, password, email)) - def new_cafe(self, player_id: int) -> None: - pass + def new_cafe(self, player_id: int, owner_name: str, waiters) -> None: + self.cursor.execute("INSERT INTO cafe (player_id, owner_name, waiters) VALUES (?, ?, ?)", + (player_id, owner_name, waiters)) def update_player(self, player_id: int, **kwargs): columns = ", ".join([f"{key} = ?" for key in kwargs.keys()]) @@ -108,3 +109,17 @@ def update_cafe(self, player_id: int, **kwargs): values = [*kwargs.values(), player_id] self.cursor.execute(query, values) + + def username_exist(self, username: str): + self.cursor.execute("SELECT 1 FROM player WHERE username=?", (username,)) + return self.cursor.fetchone() is not None + + def email_exist(self, email: str): + self.cursor.execute("SELECT 1 FROM player WHERE email=?", (email,)) + return self.cursor.fetchone() is not None + + def get_player_id(self, username: str): + self.cursor.execute("SELECT id FROM player WHERE username=?", (username,)) + player_id = self.cursor.fetchone() + + return player_id[0] diff --git a/main.py b/main.py index 4be6f8d..7a1019a 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,13 @@ from server import CafeServer from asyncio import run +from utils.filter import ProfanityModel + if __name__ == '__main__': server = CafeServer() + profanity_model = ProfanityModel() + try: run(server.start()) except KeyboardInterrupt: diff --git a/objects/player.py b/objects/player.py index 277f733..d15e787 100644 --- a/objects/player.py +++ b/objects/player.py @@ -31,6 +31,8 @@ def __init__(self): self.mastery: dict[int, int] = {num: 0 for num in range(1201, 1256)} + self.first_login = False + self.customer_cycle: Optional['Task'] = None def build_mastery(self) -> str: diff --git a/utils/filter.py b/utils/filter.py new file mode 100644 index 0000000..4fdc350 --- /dev/null +++ b/utils/filter.py @@ -0,0 +1,60 @@ +import os +from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline + + +class ProfanityModel: + _instance = None + model_name = "parsawar/profanity_model_3.1" + save_path = "./data/models" + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(ProfanityModel, cls).__new__(cls) + cls._instance.__init__(*args, **kwargs) + return cls._instance + + def __init__(self, model_name=model_name, save_path=save_path): + if not hasattr(self, 'model'): + self.model_name = model_name + self.save_path = save_path + self.model = None + self.tokenizer = None + self.pipe = None + self.censor_char = '*' + + self.load_model() + + def load_model(self): + print(f"[INFO] Loading model: {self.model_name}") + model_dir = os.path.join(self.save_path, f"models--{self.model_name.replace('/', '--')}") + + if not os.path.exists(model_dir): + print(f"[INFO] Model not found, it will be downloaded.") + + try: + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name, cache_dir=self.save_path) + self.model = AutoModelForSequenceClassification.from_pretrained(self.model_name, cache_dir=self.save_path) + self.pipe = pipeline("text-classification", model=self.model, tokenizer=self.tokenizer) + print(f"[INFO] Finished loading model: {self.model_name}") + except Exception as e: + raise Exception(f"Error loading model: {e}") + + def censor(self, word): + return self.censor_char * len(word) + + def detect_words(self, text): + words = text.split() + detected_words = [] + + for word in words: + if self.has_profanity(word): + detected_words.append(word) + + return ','.join(detected_words) + + def has_profanity(self, text): + result = self.pipe(text) + return any(entry['label'] == '1' for entry in result) + + def __str__(self): + return f"ProfanityModel(model_name={self.model_name}, loaded={self.pipe is not None})" diff --git a/utils/items.py b/utils/items.py index 9776d33..fe2c93a 100644 --- a/utils/items.py +++ b/utils/items.py @@ -38,7 +38,9 @@ def get_dish_info(dish_id: int) -> dict: 'ingredients': ingredients, 'duration': int(wod.attrib.get('duration', 0)), 'amount': int(wod.attrib.get('servings', 0)), - 'xp': int(wod.attrib.get('xp', 0)), + 'level': int(wod.attrib.get('level', 0)), + 'events': int(wod.attrib.get('events', 0)), + 'income': int(wod.attrib.get('incomePerServing', 0)) } return {} @@ -56,3 +58,34 @@ def get_expansion(exp_id: int) -> dict: } return {} + + +def get_ingredient(ing_id: int) -> dict: + root = tree.getroot() + + for wod in root.findall('wod'): + if int(wod.attrib.get('id')) == ing_id: + return { + 'level': int(wod.attrib.get('level', 0)), + 'gold': int(wod.attrib.get('gold', 0)), + 'events': int(wod.attrib.get('events', 0)), + } + + return {} + + +def get_fast_food_info(drink_id: int) -> dict: + root = tree.getroot() + + for wod in root.findall("wod"): + if int(wod.attrib.get("id")) == drink_id: + return { + "cash": int(wod.attrib.get("cash", 0)), + "gold": int(wod.attrib.get("gold", 0)), + "xp": int(wod.attrib.get("xp", 0)), + "income": int(wod.attrib.get("incomePerServing", 0)), + "amount": int(wod.attrib.get("servings", 0)), + "rating": int(wod.attrib.get("ratingBonus", 0)), + } + + return {}