import time import requests from mysql.connector.cursor import CursorBase from requests import ReadTimeout from core.aochat import server_packets from core.db import DB from core.decorators import instance, timerevent from core.dict_object import DictObject from core.logger import Logger @instance() class PorkService: def __init__(self): self.logger = Logger(__name__) self.updates = [] def inject(self, registry): self.bot = registry.get_instance("bot") self.db: DB = registry.get_instance("db") self.character_service = registry.get_instance("character_service") def pre_start(self): self.bot.register_packet_handler(server_packets.CharacterLookup.id, self.update) self.bot.register_packet_handler(server_packets.CharacterName.id, self.update) def start(self): self.db.shared.exec( "CREATE TABLE IF NOT EXISTS player (" "char_id BIGINT PRIMARY KEY, " "first_name VARCHAR(30) NOT NULL, " "name VARCHAR(20) NOT NULL, " "last_name VARCHAR(30) NOT NULL, " "level SMALLINT NOT NULL, " "breed VARCHAR(20) NOT NULL, " "gender VARCHAR(20) NOT NULL, " "faction VARCHAR(20) NOT NULL, " "profession VARCHAR(20) NOT NULL, " "profession_title VARCHAR(50) NOT NULL, " "ai_rank VARCHAR(20) NOT NULL, " "ai_level SMALLINT, " "org_id INT DEFAULT NULL, " "org_name VARCHAR(255) NOT NULL, " "org_rank_name VARCHAR(20) NOT NULL, " "org_rank_id SMALLINT NOT NULL, " "dimension SMALLINT NOT NULL, " "head_id INT NOT NULL, " "pvp_rating SMALLINT NOT NULL, " "pvp_title VARCHAR(20) NOT NULL, " "source VARCHAR(50) NOT NULL, " "last_updated INT NOT NULL, " "invalid int DEFAULT 0, " "INDEX `name` (`name`) USING BTREE, " "INDEX `org_id` (`org_id`) USING BTREE, " "INDEX `org_name` (`org_name`) USING BTREE," "INDEX `org_rank_name` (`org_rank_name`) USING BTREE, " "INDEX `org_rank_id` (`org_rank_id`) USING BTREE, " "INDEX `profession` (`profession`) USING BTREE, " "INDEX `level` (`level`) USING BTREE, " "INDEX `ai_level` (`ai_level`) USING BTREE)") self.db.create_view("player") # forces a lookup from remote PoRK server # this should not be called directly unless you are requesting info for a char on a different server # since cache will not be used and the result will also update the cache def request_char_info(self, char_name, server_num): url = self.get_pork_url(server_num, char_name) try: r = requests.get(url, timeout=5) result = r.json() except ReadTimeout: self.logger.warning("Timeout while requesting '%s'" % url) result = None except ValueError as e: # noinspection PyUnboundLocalVariable self.logger.debug("Error marshalling value as json for url '%s': %s" % (url, r.text), e) result = None char_info = None if result: char_info_json = result[0] org_info_json = result[1] if result[1] else {} char_info = DictObject({ "name": char_info_json["NAME"], "char_id": char_info_json["CHAR_INSTANCE"], "first_name": char_info_json["FIRSTNAME"], "last_name": char_info_json["LASTNAME"], "level": char_info_json["LEVELX"], "breed": char_info_json["BREED"], "dimension": char_info_json["CHAR_DIMENSION"], "gender": char_info_json["SEX"], "faction": char_info_json["SIDE"], "profession": char_info_json["PROF"], "profession_title": char_info_json["PROFNAME"], "ai_rank": char_info_json["RANK_name"], "ai_level": char_info_json["ALIENLEVEL"], "pvp_rating": char_info_json["PVPRATING"], "pvp_title": char_info_json["PVPTITLE"] or "", "head_id": char_info_json["HEADID"], "org_id": org_info_json.get("ORG_INSTANCE", 0), "org_name": org_info_json.get("NAME", ""), "org_rank_name": org_info_json.get("RANK_TITLE", ""), "org_rank_id": org_info_json.get("RANK", 0), "source": "people.anarchy-online.com", "cache_age": 0 }) return char_info # standard method to get character pork data when character is on the same server def get_character_info(self, char, max_cache_age=86400): char_id = self.character_service.resolve_char_to_id(char) char_name = self.character_service.resolve_char_to_name(char) t = int(time.time()) # if there is an entry in database and it is within the cache time, use that db_char_info = self.get_from_database(char_id=char_id, char_name=char_name) if db_char_info: db_char_info.cache_age = t - db_char_info.last_updated if db_char_info.cache_age < max_cache_age and db_char_info.source != "chat_server": return db_char_info # if we can't resolve to a char_name, we can't make a call to pork if not char_name: return db_char_info char_info = self.request_char_info(char_name, self.bot.dimension) if char_info and char_info.char_id == char_id: self.save_character_info(char_info) return char_info else: # return cached info from database, even tho it's old, and set cache_age (if it exists) if db_char_info: db_char_info.cache_age = t - db_char_info.last_updated return db_char_info # forces a skeleton object into the player table in the case that PoRK does not return any data # call this method if you don't need the data now but want to ensure there is a record in the database def load_character_info(self, char_id, char_name=None, skeleton_only=False): char_info = self.get_character_info(char_id) if not skeleton_only and (not char_info and char_name): char_info = self.get_character_info(char_name) if not char_info: char_info = DictObject({ "name": "Unknown:" + str(char_id), "char_id": char_id, "first_name": "", "last_name": "", "level": 0, "breed": "", "dimension": self.bot.dimension, "gender": "", "faction": "", "profession": "", "profession_title": "", "ai_rank": "", "ai_level": 0, "pvp_rating": 0, "pvp_title": "", "head_id": 0, "org_id": 0, "org_name": "", "org_rank_name": "", "org_rank_id": 6, "source": "stub" }) self.save_character_info(char_info) def save_character_info(self, char_info): if char_info["dimension"] != self.bot.dimension: return with self.db.pool.get_connection() as conn: with conn.cursor() as cur: # cur.execute("DELETE FROM player WHERE char_id = ?", [char_info["char_id"]]) insert_sql = """ REPLACE INTO player (char_id, name, first_name, last_name, level, breed, gender, faction, profession, profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id, pvp_rating, pvp_title, source, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ cur.execute(insert_sql, [char_info["char_id"], char_info["name"], char_info["first_name"], char_info["last_name"], char_info["level"], char_info["breed"], char_info["gender"], char_info["faction"], char_info["profession"], char_info["profession_title"], char_info["ai_rank"], char_info["ai_level"], char_info["org_id"], char_info["org_name"], char_info["org_rank_name"], char_info["org_rank_id"], char_info["dimension"], char_info["head_id"], char_info["pvp_rating"], char_info["pvp_title"], char_info["source"], int(time.time())]) def get_from_database(self, char_id=None, char_name=None): if char_id: return self.db.query_single( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE char_id = ?", [char_id]) elif char_name: return self.db.query_single( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE name = ?", [char_name]) else: return None def update(self, conn, packet): # don't update if we didn't get a valid response if packet.char_id == 4294967295: return self.updates.append(packet) @timerevent(budatime="1min", description="Save player changes", is_hidden=True) def batch_update(self, event_type, event_data): update = "UPDATE player SET name = ? WHERE char_id = ?" insert = "INSERT IGNORE INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, " \ "profession, profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " \ "dimension, head_id, pvp_rating, pvp_title, source, last_updated) " \ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" updates, inserts = [], [] with self.db.pool.get_connection() as conn: with conn.cursor(dictionary=True) as cur: for packet in self.updates: cur: CursorBase cur.execute( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE char_id = ?", [packet.char_id]) data = cur.fetchone() character = None if data: character = DictObject(data) if character: if character.name != packet.name: updates.append((packet.name, packet.char_id)) else: inserts.append((packet.char_id, packet.name, "", "", 0, "", "", "", "", "", "", 0, 0, "", "", 6, self.bot.dimension, 0, 0, "", "chat_server", int(time.time()))) if inserts: cur.executemany(insert, inserts) if updates: cur.executemany(update, updates) self.updates = [] # noinspection SqlResolve def find_orgs(self, search): return self.db.query("SELECT DISTINCT org_name, org_id FROM all_orgs WHERE org_name ?", [search], extended_like=True) def get_pork_url(self, dimension, char_name): # Dont use SSL, as its rather slow compared to normal requests.... # noinspection HttpUrlsUsage return f"http://people.anarchy-online.com/character/bio/d/{dimension}/name/{char_name}/bio.xml?data_type=json"