import datetime import json import math import time from threading import Thread import requests from core.aochat.BaseModule import BaseModule from core.aochat.server_packets import BuddyAdded from core.buddy_service import BuddyService from core.cache_service import CacheService from core.chat_blob import ChatBlob from core.command_param_types import Character, Options, Const, Any, NamedParameters from core.db import DB from core.decorators import instance, command, event, timerevent from core.dict_object import DictObject from core.event_service import EventService from core.igncore import IgnCore from core.job_scheduler import JobScheduler from core.logger import Logger from core.lookup.org_pork_service import OrgPorkService from core.lookup.pork_service import PorkService from core.message_hub_service import MessageHubService from core.private_channel_service import PrivateChannelService from core.setting_service import SettingService from core.setting_types import TextSettingType, ColorSettingType from core.text import Text from core.util import Util from modules.core.accounting.services.account_service import AccountService from modules.standard.tower.tower_events import TowerEventController # noinspection DuplicatedCode,SqlCaseVsIf @instance() class TrackController(BaseModule): threads = {} single_org_uri = "https://people.anarchy-online.com/org/stats/d/5/name/%d/basicstats.xml?data_type=json" PRIVATE_CHANNEL_PREFIX = "[Priv]" PAGE_SIZE = 20 MESSAGE_SOURCE = "track_log" def inject(self, registry): self.logger = Logger("TRACK") self.bot: IgnCore = registry.get_instance("bot") self.util: Util = registry.get_instance("util") self.pork: PorkService = registry.get_instance("pork_service") self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler") self.text: Text = registry.get_instance("text") self.event_service: EventService = registry.get_instance("event_service") self.setting_service: SettingService = registry.get_instance("setting_service") self.buddy_service: BuddyService = registry.get_instance("buddy_service") self.account_service: AccountService = registry.get_instance("account_service") self.db: DB = registry.get_instance("db") self.priv: PrivateChannelService = registry.get_instance("private_channel_service") self.tower: TowerEventController = registry.get_instance("tower_controller") self.message_hub_service: MessageHubService = registry.get_instance("message_hub_service") self.org_pork: OrgPorkService = registry.get_instance("org_pork_service") self.cache: CacheService = registry.get_instance("cache_service") self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service") def pre_start(self): self.event_service.register_event_type("track_logon") self.event_service.register_event_type("track_logoff") self.setting_service.register(self.module_name, "track_on_color", "#FF0000", ColorSettingType(), "Color for Track logon") self.setting_service.register(self.module_name, "track_off_color", "#00FF00", ColorSettingType(), "Color for Track logoff") self.setting_service.register(self.module_name, "autotrack", 'none', TextSettingType(['omni', 'clan', 'neutral', "none"]), "Autotrack all players initiating tower attacks towards this faction") self.db.exec( "CREATE TABLE IF NOT EXISTS track(" "char_id int not null primary key, " "initiator int not null, " "reason varchar(255))") self.db.exec("CREATE TABLE IF NOT EXISTS track_orgs(org_id int not null primary key, initiator int not null)") self.db.exec("CREATE TABLE IF NOT EXISTS track_org_members(char_id int not null, org_id int not null)") self.message_hub_service.register_message_source(self.MESSAGE_SOURCE) @event(event_type="connect", description="Autoadd tracked players on connect", is_hidden=True) def connect_add(self, _1, _2): for user in self.db.query("SELECT * from track"): self.buddy_service.add_buddy(user.char_id, "track") for user in self.db.query("SELECT char_id FROM track_org_members"): self.buddy_service.add_buddy(user.char_id, "track") @event(event_type="buddy_logon", description="Fire tracker events", is_hidden=True) def track_fire_logon(self, _, event_data): if self.bot.is_ready(): if "track" in (self.buddy_service.get_buddy(event_data.char_id) or {'types': []})["types"]: print("Login: ", event_data.char_id) if type(event_data) == BuddyAdded: self.event_service.fire_event("track_logon", self.db.query_single("SELECT * from player where char_id=?", [event_data.char_id])) self.db.exec("INSERT IGNORE INTO online VALUES(?, ?, ?)", [event_data.char_id, 'track', self.bot.get_char_id()]) else: self.job_scheduler.delayed_job(self.track_fire_logon, 10, DictObject({'char_id': event_data.char_id, 'repeat': True})) @event(event_type="buddy_logoff", description="Fire tracker events", is_hidden=True) def track_fire_logoff(self, _1, event_data): if buddy := self.buddy_service.get_buddy(event_data.char_id): if "track" in buddy["types"]: self.event_service.fire_event("track_logoff", self.db.query_single("SELECT * from player where char_id=?", [event_data.char_id])) self.db.exec("DELETE FROM online where char_id=? and bot=?", [event_data.char_id, self.bot.get_char_id()]) @event(event_type="track_logon", description="Fire tracker logon events") def track_logon(self, _1, user): if self.bot.is_ready(): color = self.setting_service.get("track_on_color").format_text("ON") self.send_t_warn(0, f'{color} :: {self.text.format_char_info(user)}') @event(event_type="track_logoff", description="Fire tracker logoff events") def track_logoff(self, _1, user): if self.bot.is_ready(): color = self.setting_service.get("track_off_color").format_text("OFF") self.send_t_warn(0, f'{color} :: {self.text.format_char_info(user)}') @event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Autottrack players attacking our faction") def tower_attack_event(self, _, event_data): attacker = event_data.attacker if event_data.defender.faction.lower() == self.setting_service.get_value("autotrack"): if not self.buddy_service.get_buddy(attacker.char_id): if self.buddy_service.add_buddy(attacker.char_id, "track"): self.send_t_warn(0, f"Now tracking: " f"<{attacker.faction.lower()}>{attacker.name}") self.db.exec("INSERT IGNORE INTO track VALUES(?, ?, ?)", [attacker.char_id, self.bot.get_char_id(), "attacked us"]) def send_t_warn(self, _, msg): self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, "[T] " + msg, "[T] " + msg) def get_tracked(self, order, online): where = "where o.char_id IS NOT NULL" if online else "" return self.db.query(f"SELECT p.name, p.level, p.ai_level, p.org_name, p.profession, p.faction, " f"p2.name as initiator, " f"CASE WHEN o.char_id IS NOT NULL THEN 1 ELSE 0 END AS online, " f"t.reason from track t " f"left join player p on p.char_id = t.char_id " f"left join player p2 on p2.char_id = t.initiator " f"left join online o on o.char_id = t.char_id {where} group by t.char_id order by {order}") @command(command="track", params=[Const('add'), Character("character"), Any('reason')], access_level="leader", description="Initiate tracking for the character", sub_command="add") def track_add(self, request, _, user, reason): if not user.char_id: return f"Character {user.name} not found." self.pork.load_character_info(user.char_id, user.name) if acc := self.account_service.get_account(request.sender.char_id): if "track" in (self.buddy_service.get_buddy(user.char_id) or {'types': []})["types"]: return f"Character {user.name} is already being tracked." else: self.pork.load_character_info(user.char_id, user.name) self.send_t_warn(0, f'Tracking initiated: {user.name} by {acc.name}') self.buddy_service.add_buddy(user.char_id, 'track') self.db.exec("INSERT IGNORE INTO track VALUES(?, ?, ?)", [user.char_id, acc.main, reason]) @command(command="orgtrack", params=[Const('add'), Any("Organisation")], access_level="moderator", description="Initiate tracking for an Organisation", sub_command="add") def track_addorg(self, request, _, search): orgs = self.org_pork.find_org(search) if len(orgs) == 1: orgs = orgs[0] if self.db.exec("REPLACE INTO track_orgs(org_id, initiator) VALUES(?, ?)", [orgs.org_id, request.sender.char_id]) == 1: request.reply(f"Adding the organisation {orgs.org_name} to the tracking...") org_adder = Thread(name=orgs.org_id, target=self.fetch_single, args=(orgs.org_id, orgs.org_name, request)) self.threads[orgs.org_id] = org_adder org_adder.start() else: return f"The organisation {orgs.org_name} is already being tracked." elif len(orgs) == 0: return f"No org with the name {search} was found on PoRK." else: blob = "Your search had multiple results; please pick an org:
" for org in orgs: blob += f'[{self.text.make_chatcmd("Add", f"/tell orgtrack add {org.org_id}")}]' \ f'[{self.text.make_chatcmd("More", f"/tell org info {org.org_id}")}]' \ f' {org.org_name} ({org.org_id}) ' \ f'<{org.faction.lower()}>{org.faction} [{org.member_count} members]' \ f'
' return ChatBlob("Pick an Org", blob) @command(command="orgtrack", params=[Const("rem"), Any("Organisation")], access_level="moderator", description="Add an organisation to the tracking", sub_command="rem") def orgtrack_rem(self, request, _, search): orgs = self.org_pork.find_org(search) orgs = [x for x in orgs if x.org_name.lower().startswith(search.lower()) or (x.org_id == int(search) if search.isdigit() else False)] if len(orgs) == 1: orgs = orgs[0] if self.db.exec("DELETE FROM track_orgs where org_id = ?", [orgs.org_id]) == 1: org_remover = Thread(name=orgs.org_id, target=self.remove_single, args=(orgs.org_id, orgs.org_name)) self.threads[orgs.org_id] = org_remover org_remover.start() return f"Removed the organisation {orgs.org_name} from the roster." else: return f"The organisation {orgs.org_name} is not on the roster list." elif len(orgs) == 0: return f"The organisation {search} is not on the roster list." else: blob = "Your search had multiple results; please pick an org:
" for org in orgs: blob += f'[{self.text.make_chatcmd("Remove", f"/tell orgtrack rem {org.org_id}")}]' \ f'[{self.text.make_chatcmd("More", f"/tell org info {org.org_id}")}]' \ f' {org.org_name} ({org.org_id}) ' \ f'<{org.faction.lower()}>{org.faction} [{org.member_count} members]' \ f'
' return ChatBlob("Pick an Org", blob) @command(command="track", params=[Const('rem'), Character("character")], access_level="moderator", description="Remove character from the tracking", sub_command="rem") def track_rem(self, request, _, user): if not user.char_id: return f"Character {user.name} not found." if acc := self.account_service.get_account(request.sender.char_id): if self.buddy_service.remove_buddy(user.char_id, "track"): self.db.exec("DELETE FROM track where char_id=?", [user.char_id]) self.send_t_warn(0, f'Tracking stopped: {user.name} by {acc.name}') return f"Character {user.name} is nolonger being tracked." else: return f"Character {user.name} is not being tracked." @command(command="track", params=[Const('list', is_optional=True), Options(["org", "prof", "tl"], is_optional=True), NamedParameters(["page"])], access_level="member", description="Shows tracked players") def track_list(self, _, const, group, named_params): page = int(named_params.page or "1") offset = (page - 1) * self.PAGE_SIZE if group is None: group = 'tl' if group == "org": players = self.get_tracked('p.org_name, p.profession, p.level desc, p.name', False if const else True) return self.format_page(players, "org", offset, page, f"Tracklist by Organisation ({len(players)})", 'No tracked users online', f"track {const or ''} {group or ''}") elif group == "prof": players = self.get_tracked('p.profession, p.level desc, p.name', False if const else True) return self.format_page(players, "prof", offset, page, f"Tracklist by Profession ({len(players)})", 'No tracked users online', f"track {const or ''} {group or ''}") elif group == "tl": players = self.get_tracked('p.level desc, p.profession, p.name', False if const else True) return self.format_page(players, "tl", offset, page, f"Tracklist by Titlelevel ({len(players)})", 'No tracked users online', f"track {const or ''} {group or ''}") @command(command="orgtrack", params=[Const('info')], access_level="member", description="Shows tracked orgs") def orgtrack_info(self, _, const): blob = "" for org in self.db.query("SELECT a.*, o.* from track_orgs o LEFT JOIN all_orgs a on o.org_id = a.org_id"): blob += f"- [{self.text.make_tellcmd('Info', f'orgs info {org.org_id}')}] " \ f"[{self.text.make_tellcmd('Remove', f'orgtrack rem {org.org_id}')}]" \ f" {self.text.get_formatted_faction(org.faction, org.org_name)} ({org.org_id}) with {org.member_count} memebrs" return ChatBlob("Currently Tracked Organisations", blob) @command(command="orgtrack", params=[Const('list', is_optional=True), NamedParameters(["page"])], access_level="member", description="Shows tracked orgmembers") def track_listorg(self, _, const, named_params): page = int(named_params.page or "1") offset = (page - 1) * self.PAGE_SIZE where = "where o.char_id IS NOT NULL" if not const else "" players = self.db.query(f"SELECT p.name, p.level, p.ai_level, p.org_name, p.profession, p.faction, " f"CASE WHEN o.char_id IS NOT NULL THEN 1 ELSE 0 END AS online from track_org_members t " f"left join player p on p.char_id = t.char_id " f"left join online o on o.char_id = t.char_id {where} group by t.char_id order by p.org_name, p.profession, p.level desc, p.name") return self.format_page_org(players, "org", offset, page, f"Tracklist by Organisation ({len(players)})", 'No tracked users online', f"orgtrack {const or ''} ") def format_row(self, user): org = f"[<{user.faction.lower()}>{user.org_name}] " if user.org_name else "" return f"{self.util.get_prof_icon(user.profession)} " \ f"{self.text.zfill(user.level, 220)}:{self.text.zfill(user.ai_level, 220)} " \ f"<{user.faction.lower()}>{user.name}" \ f" {org} {'[ONLINE]' if user.online == 1 else ''}\n" def format_page(self, tracked, order, offset, page, title, nullmsg, cmd): selected = tracked[offset:offset + self.PAGE_SIZE] count = len(selected) pages = "" if page > 1: pages += "Pages: " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}') if offset + self.PAGE_SIZE < len(tracked): pages += f" Page {page}/{math.ceil(len(tracked) / self.PAGE_SIZE)}" pages += " " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}') pages += "\n" if count == 0: return nullmsg else: blob = "\n\n" + pages + "" if order == "prof": prof = "" for player in selected: if player.profession != prof: prof = player.profession blob += f"\n
{player.profession}
\n" blob += self.format_row(player) elif order == "tl": tl = "" for player in selected: titlelevel = self.util.get_title_level(player.level) if titlelevel != tl: tl = titlelevel blob += f"\n
TL{titlelevel}
\n" blob += self.format_row(player) elif order == "org": org_name = "" for player in selected: if player.org_name != org_name: org_name = player.org_name blob += f"\n
{player.org_name}
\n" blob += self.format_row(player) blob += "
\n" + pages return ChatBlob(title, blob) def format_page_org(self, tracked, order, offset, page, title, nullmsg, cmd): selected = tracked[offset:offset + self.PAGE_SIZE] count = len(selected) pages = "" if page > 1: pages += "Pages: " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}') if offset + self.PAGE_SIZE < len(tracked): pages += f" Page {page}/{math.ceil(len(tracked) / self.PAGE_SIZE)}" pages += " " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}') pages += "\n" if count == 0: return nullmsg else: blob = "\n\n" + pages + "" if order == "org": org_name = "" for player in selected: if player.org_name != org_name: org_name = player.org_name blob += f"\n
{player.org_name}
\n" blob += self.format_row(player) blob += "
\n" + pages return ChatBlob(title, blob) def format_row_org(self, user): org = f"[<{user.faction.lower()}>{user.org_name}] " if user.org_name else "" return f"{self.util.get_prof_icon(user.profession)} " \ f"{self.text.zfill(user.level, 220)}:{self.text.zfill(user.ai_level, 220)} " \ f"<{user.faction.lower()}>{user.name}" \ f" {org}init by {user.initiator}: {user.reason} " \ f"{'[ONLINE]' if user.online == 1 else ''}\n" @timerevent(budatime="24h", description="Update Tracked Orgs") def fetch_orgs(self, _, _1): def discover(): start = time.time() self.logger.info("Fetching orgdata..") output = [] data = [] data2 = [] timestamp = 0 ours = self.db.query("SELECT * FROM track_orgs t LEFT JOIN all_orgs a ON t.org_id = a.org_id") for org in ours: result = requests.get(self.single_org_uri % org.org_id).json() if result and len(result[1]) > 0: self.cache.store('org_roster', f"{org.org_id}.5.json", json.dumps(result)) else: result = json.loads(self.cache.retrieve('org_roster', f"{org.org_id}.5.json").data) d = datetime.datetime.strptime(result[2] + " +0000", '%Y/%m/%d %H:%M:%S %z').timestamp() if d > timestamp: timestamp = d for char_info in result[1]: data.append((char_info["CHAR_INSTANCE"], char_info["NAME"], char_info["FIRSTNAME"], char_info["LASTNAME"], char_info["LEVELX"], char_info["BREED"], char_info["SEX"], result[0]["SIDE_NAME"], char_info["PROF"], char_info["PROF_TITLE"], char_info["DEFENDER_RANK_TITLE"], char_info["ALIENLEVEL"], result[0]["ORG_INSTANCE"], result[0]["NAME"], char_info["RANK_TITLE"], char_info["RANK"], char_info["CHAR_DIMENSION"], char_info["HEADID"], 0, char_info["PVPTITLE"] or "", "roster", int(time.time()))) data2.append((char_info['CHAR_INSTANCE'], result[0]['ORG_INSTANCE'])) if not self.buddy_service.get_buddy(char_info["CHAR_INSTANCE"]): self.buddy_service.add_buddy(char_info["CHAR_INSTANCE"], "track") output.append(DictObject({"action": "JOIN", "name": char_info['NAME'], "org_name": result[0]["NAME"], "org_id": result[0]["ORG_INSTANCE"], "level": char_info["LEVELX"], "ai_level": char_info["ALIENLEVEL"], "ranks": 0})) self.logger.info(f"[TRACK] Organisation {org.org_name} has been updated.") if len(data) > 1: with self.db.lock: with self.db.pool.get_connection() as conn: with conn.cursor() as cur: cur.executemany("INSERT 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 " "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " "ON DUPLICATE KEY UPDATE first_name=VALUE(first_name), " "last_name=VALUE(last_name), level=VALUE(level), " "breed=VALUE(breed), gender=VALUE(gender), " "faction=VALUE(faction), profession=VALUE(profession), " "profession_title=VALUE(profession_title),ai_rank=VALUE(ai_rank), " "ai_level=VALUE(ai_level), org_name=VALUE(org_name), " "org_id=VALUE(org_id), org_rank_name=VALUE(org_rank_name), " "org_rank_id=VALUE(org_rank_id), source=VALUE(source), " "last_updated=VALUE(last_updated)", data) cur.executemany("INSERT INTO track_org_members (char_id, org_id) VALUES(?,?)", data2) conn.commit() for org in ours: left = self.db.query("SELECT p.* FROM track_org_members t LEFT JOIN player p on t.char_id = p.char_id where t.org_id=? and (p.last_updated < ? OR t.org_id != p.org_id)", [org.org_id, time.time()-25*60*60, org.org_id]) for player in left: bonus = None player = DictObject(player) if self.buddy_service.remove_buddy(player.char_id, "track"): bonus = "LEAVE" new_data = self.pork.request_char_info(player.name, player.dimension) if new_data and new_data.char_id == player.char_id: self.pork.save_character_info(new_data) else: bonus = "DEL" if bonus: output.append(DictObject({"action": bonus, "name": player.name, "org_name": player.org_name, "org_id": player.org_id, "level": player.level, "ai_level": player.ai_level})) self.db.exec("DELETE FROM track_org_members where char_id not in (SELECT char_id from player where org_id=?)", [org.org_id]) self.log(output, time.time() - start) self.logger.info( f"Successfully fetched {len(data)} players from {len(ours)} orgs " f"in {time.time() - start:.2f} seconds. - ") while (timestamp + 24 * 60 * 60) < datetime.datetime.now().timestamp(): timestamp += 24 * 60 * 60 self.db.exec("UPDATE timer_event SET next_run=? WHERE handler=? AND event_sub_type=?", [ int(timestamp +24*60*60 + 10*60), "modules.standard.track.track_controller.TrackController.fetch_orgs", 86400 ]) del self.threads['roster'] if "roster" not in self.threads.keys(): thread = Thread(name="roster", target=discover, daemon=True) self.threads["roster"] = thread thread.start() def fetch_single(self, org_id, org_name, sender: object): start = time.time() data = [] data2 = [] accounts = [] self.logger.info("Fetching orgdata..") count = 0 result = requests.get(self.single_org_uri % org_id).json() for char_info in result[1]: data.append((char_info["CHAR_INSTANCE"], char_info["NAME"], char_info["FIRSTNAME"], char_info["LASTNAME"], char_info["LEVELX"], char_info["BREED"], char_info["SEX"], result[0]["SIDE_NAME"], char_info["PROF"], char_info["PROF_TITLE"], char_info["DEFENDER_RANK_TITLE"], char_info["ALIENLEVEL"], result[0]["ORG_INSTANCE"], result[0]["NAME"], char_info["RANK_TITLE"], char_info["RANK"], char_info["CHAR_DIMENSION"], char_info["HEADID"], 0, char_info["PVPTITLE"] or "", "roster", int(time.time()))) data2.append((char_info['CHAR_INSTANCE'], result[0]['ORG_INSTANCE'])) accounts.append((char_info["CHAR_INSTANCE"], char_info["CHAR_INSTANCE"], result[0]["ORG_INSTANCE"], start, start)) self.buddy_service.add_buddy(char_info['CHAR_INSTANCE'], "track") count += 1 with self.db.pool.get_connection() as conn: with conn.cursor() as cur: cur.executemany("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 " "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", data) cur.executemany("REPLACE INTO track_org_members(char_id, org_id) VALUES (?, ?)", data2) self.logger.info(f"Organisation {org_name} is now being tracked!") sender.reply(f"{org_name} is now being tracked. " f"Runtime: {time.time() - start:.2f} seconds.") self.logger.info(f"Successfully fetched {count} players in {time.time() - start} seconds.") del self.threads[org_id] def remove_single(self, org_id, org_name): members = self.db.query("SELECT * from track_org_members where org_id=?", [org_id]) for member in members: self.buddy_service.remove_buddy(member.char_id, "member") self.db.exec("DELETE FROM track_org_members where org_id=?", [org_id]) self.db.exec("DELETE FROM online where char_id in (SELECT char_id from player where org_id=?)", [org_id]) self.logger.info(f"Organisation {org_name} removed!") del self.threads[org_id] def log(self, blob, duration): out = [] s = [] current = "" for entry in blob: s.append(f"[{entry.org_name}] [{entry.action}] {entry.name} " f"({entry.level}/{entry.ai_level})\n") s = sorted(s) if len(s) > 0: s.append(f"\nRuntime: {duration:.2f} seconds.") for entry in s: if len(current) > 1500: out.append(current) current = "" current += entry if len(current) > 10: out.append(current) if len(out) > 0: y = 0 for x in out: y += 1 blob = ChatBlob(f"Recent changes - {datetime.date.today()} - ({y}/{len(out)})", x, embed=True) self.relay_hub_service.send_message("member_logger", None, blob, blob)