import re import time from threading import Thread from core.bot_status import BotStatus from core.buddy_service import BuddyService from core.command_alias_service import CommandAliasService from core.command_param_types import Int, Any, Const, Options from core.db import DB from core.decorators import instance, event, command from core.dict_object import DictObject from core.fifo_queue import FifoQueue from core.logger import Logger from core.lookup.character_service import CharacterService from core.private_channel_service import PrivateChannelService from core.public_channel_service import PublicChannelService from core.setting_service import SettingService from core.text import Text from core.igncore import IgnCore from core.util import Util from modules.core.accounting.services.account_service import AccountService from modules.standard.online.online_display import OnlineDisplay class User: def __init__(self, char_id, loggedin=True): self.char_id = char_id self.logged = loggedin @instance() class OnlineController: def __init__(self): self.afk_list = {} self.afk_regex = re.compile("^(afk|brb) ?(.*)$", re.IGNORECASE) self.awaiting_data = FifoQueue() def inject(self, registry): self.logger = Logger(__name__) self.bot: IgnCore = registry.get_instance("bot") self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.util: Util = registry.get_instance("util") self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service") self.buddy_service: BuddyService = registry.get_instance("buddy_service") self.setting_service: SettingService = registry.get_instance("setting_service") self.priv: PrivateChannelService = registry.get_instance("private_channel_service") self.online_display: OnlineDisplay = OnlineDisplay(self.text, self.util, self.db, self.afk_list) self.account_service: AccountService = registry.get_instance("account_service") self.character_service: CharacterService = registry.get_instance("character_service") def pre_start(self): self.db.exec("DROP TABLE IF EXISTS online") self.db.shared.exec( "CREATE TABLE IF NOT EXISTS online (char_id BIGINT NOT NULL primary key, " "channel varchar(32) not null default 'notify', " "bot BIGINT not null, " "index bot(bot)) ENGINE MEMORY") self.db.create_view("online") self.command_alias_service.add_alias("o", "online") @event(event_type="connect", description="Start online watcher") def startup(self, _, _1): self.db.exec("DELETE from online where bot=?", [self.bot.get_char_id()]) self.watchdog = Thread(name="watchdog", target=self.buddy_handler, daemon=True) self.watchdog.start() @event(event_type="private_channel_joined", description="Change online channel", is_hidden=True) def priv_join(self, _, event_data): self.db.exec("insert ignore into online(channel, char_id, bot) VALUES (?, ?, ?)", [self.bot.name, event_data.char_id, self.bot.get_char_id()]) @event(event_type="private_channel_left", description="Change online channel", is_hidden=True) def priv_leave(self, _, event_data): self.db.exec("delete from online where char_id = ? and bot = ? and channel=?", [event_data.char_id, self.bot.get_char_id(), self.bot.name]) for alt in self.account_service.get_alts(event_data.char_id): self.afk_list.pop(alt.char_id, None) @event(event_type="member_logon", description="declare players as online") def logon(self, _, event_data): self.awaiting_data.put([event_data, 'notify', True]) @event(event_type="member_logoff", description="declare players as offline") def logoff(self, _, event_data): if self.bot.is_ready(): self.awaiting_data.put([event_data, 'notify', False]) def buddy_handler(self): while self.bot.status != BotStatus.SHUTDOWN: data, channel, logged = self.awaiting_data.get() buddy = (self.buddy_service.get_buddy(data.packet.char_id) or {}).get("types", []) if ("org_member" in buddy) or ("member" in buddy): if logged: self.db.exec("INSERT IGNORE INTO online VALUES(?, ?, ?)", [data.packet.char_id, channel, self.bot.get_char_id()]) self.db.exec("UPDATE account set last_seen=? where char_id=?", [time.time(), data.packet.char_id]) else: self.db.exec("DELETE FROM online where char_id=? and bot=?", [data.packet.char_id, self.bot.get_char_id()]) self.db.exec("UPDATE account set last_seen=? where char_id=?", [time.time(), data.packet.char_id]) for alt in self.account_service.get_alts(data.packet.char_id): self.afk_list.pop(alt.char_id, None) @command(command="online", params=[Const('all', is_optional=True), Int("min_level", is_optional=True), Any("profession", is_optional=True)], description="shows online players", access_level="member") def online_all_cmd(self, _, const_all, min_level, profession): query = "" params = [self.bot.name, self.bot.get_char_id()] if const_all: query += "and channel_id IN (1, 2, 3) " else: query += "and channel_id IN (1, 2) " if min_level: query += "and p.level >= ? " params.append(min_level) if profession: query += "and p.profession = ? " params.append(self.util.get_profession(profession)) blob = self.online_display.format_by_channel_main(query, params) _.reply(self.online_display.format_blob(blob)) @command(command="count", params=[Options(["org", "prof", "tl"], is_optional=True), Any("filter", is_optional=True)], description="Counts online players", access_level="member") def count(self, _, option, filters): query = "and channel_id IN (1, 2) " params = [self.bot.name, self.bot.get_char_id()] if not option: option = "prof" output = "" if option == "prof": output = self.online_display.count_prof(query, params, filters) elif option == "org": output = self.online_display.count_org(query, params, filters) elif option == "tl": if filters: try: filters = int(filters) except ValueError: return f"Invalid Title level: {filters}" output = self.online_display.count_tl(query, params, filters) return output if output != "" else "Nobody is in my private channel, sorry..." @event(PrivateChannelService.PRIVATE_CHANNEL_MESSAGE_EVENT, "Check for afk messages in private channel") def afk_check_private_channel_event(self, event_type, event_data): self.afk_check(event_data.char_id, event_data.message, lambda msg: self.bot.send_private_channel_message(msg)) @event(PublicChannelService.ORG_CHANNEL_MESSAGE_EVENT, "Check for afk messages in org channel") def afk_check_org_channel_event(self, event_type, event_data): self.afk_check(event_data.char_id, event_data.message, lambda msg: self.bot.send_org_message(msg)) def afk_check(self, char_id, message, channel_reply): matches = self.afk_regex.search(message) if matches: char_name = self.character_service.resolve_char_to_name(char_id) channel_reply(f"{char_name} is now afk.") alts = self.account_service.get_alts(char_id) for alt in alts: self.afk_list[alt.char_id] = DictObject({"message": message, "time": time.time()}) elif char_id in self.afk_list.keys(): # TODO handle multiple rows alts = self.account_service.get_alts(char_id) data = None for alt in alts: out = self.afk_list.pop(alt.char_id) if data: continue data = out char_name = self.character_service.resolve_char_to_name(char_id) time_string = self.util.time_to_readable(int(time.time()) - data.time) channel_reply(f"{char_name} is back after {time_string}.")