import json
import math
import typing
from core.buddy_service import BuddyService
from core.chat_blob import ChatBlob
from core.command_param_types import Const, Character, Any, NamedParameters
from core.decorators import instance, command, event
from core.dict_object import DictObject
from core.logger import Logger
from core.setting_service import SettingService
from core.setting_types import TextSettingType
from core.text import Text
from core.translation_service import TranslationService
from core.igncore import IgnCore
from core.private_channel_service import PrivateChannelService
if typing.TYPE_CHECKING:
from core.lookup.character_service import CharacterService
from core.lookup.pork_service import PorkService
from core.event_service import EventService
# noinspection SqlCaseVsIf,SqlCaseVsCoalesce
@instance()
class RIAdminController:
PAGE_SIZE = 20
UNASSIGNED_RAID_INSTANCE_ID = 0
def __init__(self):
self.logger = Logger(__name__)
def inject(self, registry):
self.bot: IgnCore = registry.get_instance("bot")
self.db = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.setting_service: SettingService = registry.get_instance("setting_service")
self.util = registry.get_instance("util")
self.character_service: CharacterService = registry.get_instance("character_service")
self.private_channel_service: PrivateChannelService = registry.get_instance("private_channel_service")
self.ts: TranslationService = registry.get_instance("translation_service")
self.getresp = self.ts.get_response
self.buddy_service: BuddyService = registry.get_instance("buddy_service")
self.pork: PorkService = registry.get_instance("pork_service")
self.event_service: EventService = registry.get_instance("event_service")
def start(self):
self.db.exec(
"CREATE TABLE IF NOT EXISTS raid_instance (id INT PRIMARY KEY AUTO_INCREMENT, "
"name VARCHAR(255) NOT NULL, "
"bot varchar(32)) ENGINE MEMORY")
self.db.exec(
"CREATE TABLE IF NOT EXISTS raid_instance_char ("
"raid_instance_id INT NOT NULL, "
"char_id INT PRIMARY KEY, "
"is_leader TINYINT NOT NULL) ENGINE MEMORY")
self.setting_service.register(self.module_name, "riadmin_network",
"[905848882, 272234, 1923370, 313107, 1217210821, 1134420908]",
TextSettingType(), "Allowed bots (charID's)",
extended_description="This setting is *NOT* synchronized across the network;"
" this needs to be done manually!")
def pre_start(self):
self.event_service.register_event_type("RAID_END")
@event(event_type="buddy_logoff", description="Track raiders")
def raider_logoff(self, _, event_data):
if self.bot.is_ready():
buddy = self.buddy_service.get_buddy(event_data.char_id)
if not buddy:
return
if "raider" in buddy.get('types', []):
user = self.db.query_single("SELECT * from player where char_id=?", [event_data.char_id])
raid = self.get_raid_instance_by_char(event_data.char_id)
if user and raid:
if raid.bot != 0:
self.bot.send_private_channel_message(f'[RI] Raider logged off: '
f'{user.name} - {user.profession} [{raid.name}]')
self.db.exec("DELETE FROM raid_instance_char WHERE char_id = ?", [event_data.char_id])
self.buddy_service.remove_buddy(event_data.char_id, 'raider')
@event(event_type=PrivateChannelService.LEFT_PRIVATE_CHANNEL_EVENT, description="Track Raiders")
def raider_leave(self, _, event_data):
if self.bot.is_ready():
buddy = self.buddy_service.get_buddy(event_data.char_id)
if not buddy:
return
if "raider" in buddy.get('types', []):
user = self.pork.get_character_info(event_data.char_id)
raid = self.get_raid_instance_by_char(event_data.char_id)
part = int(raid.bot) if raid else 0
if part in [self.bot.get_char_id(), 0]:
if part != 0:
self.bot.send_private_channel_message(
f'[RI] Raider left: {user.name} - {user.profession} '
f'[{raid.name if raid else "UNASSIGNED"}]')
self.db.exec("DELETE FROM raid_instance_char WHERE char_id = ?", [event_data.char_id])
self.buddy_service.remove_buddy(event_data.char_id, 'raider')
@event(event_type="connect", description="Init raider tracking on startup")
def connect(self, _, _1):
for user in self.get_raid_instance_chars():
self.buddy_service.add_buddy(user.char_id, 'raider')
@command(command="riadmin", params=[NamedParameters(['page'])], access_level="leader",
description="Show the current RIs")
def riadmin(self, _, named_params):
refresh = self.text.make_tellcmd("Refresh", "riadmin")
send = self.text.make_tellcmd("Apply", "riadmin send")
clear = self.text.make_tellcmd("Clear All", "riadmin clear")
blob = f"[{refresh}] - Update the RI List\n" \
f"[{send}] - Send all RI's to their bots\n" \
f"[{clear}] - Clear all RI's, and delete them"
blob += "\n\n"
page = int(named_params.page or "1")
offset = (page - 1) * self.PAGE_SIZE
data = self.get_raid_instance_chars()
return self.format_pagination(data, offset, page, self.compact_char_display, 'Raid Instances',
'No Raidinstances found.', 'riadmin ', self.PAGE_SIZE, blob)
def format_pagination(self, data, offset, page, formatter, title, nullmsg, cmd, page_size=10, headline=""):
raid_instances = self.get_raid_instances()
blob = ""
if page == 1:
blob = headline
selected = data[offset:offset + page_size]
if len(data) == 0:
return nullmsg
num_assigned = 0
num_unassigned = 0
pages = ""
if page > 1:
pages += "Pages: " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}')
if offset + page_size < len(data):
pages += f" Page {page}/{math.ceil(len(data) / page_size)}"
pages += " " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}')
pages += "\n"
current_raid_instance_id = None
for row in data:
if row.raid_instance_id == self.UNASSIGNED_RAID_INSTANCE_ID:
num_unassigned += 1
else:
if row.name:
num_assigned += 1
blob += "" + pages
for row in selected:
if row.raid_instance_id != current_raid_instance_id:
name = ""
if row.bot:
bot = self.pork.get_character_info(row.bot)
name = f"[{bot.name}]" if bot else ""
blob += f"\n{row.raid_instance_name} {name}\n"
current_raid_instance_id = row.raid_instance_id
if not row.char_id:
continue
blob += formatter(row)
add_leader_link = (row.raid_instance_id != self.UNASSIGNED_RAID_INSTANCE_ID and not row.is_leader)
blob += " " + self.get_assignment_links(raid_instances, row.name, add_leader_link)
blob += "\n"
blob += "\n" + pages
out = ChatBlob(title, blob)
out.page_postfix = f" ({num_assigned} Players)"
if num_unassigned > 0:
out.page_postfix = f" (Assigned: {num_assigned}, " \
f"Unassigned: {num_unassigned})"
return out
@command(command="ritake", params=[Any("raiders")],
access_level="all",
description="take the RI from another bot")
def ritake(self, request, raiders: str):
users = raiders.split(",")
if request.sender.char_id not in json.loads(self.setting_service.get_value('riadmin_network')):
return
for user in users:
char = self.character_service.resolve_char_to_id(user.strip())
if not char:
continue
if self.private_channel_service.in_private_channel(char):
self.buddy_service.add_buddy(char, 'raider')
continue
self.bot.send_mass_message(char, 'You have been assigned to my RI :: accept the invite!')
self.private_channel_service.invite(char)
@command(command="riadmin",
params=[Const("add"), Any("raid_instance"), Character("char")],
access_level="leader",
description="Add a character to a RI", sub_command="leader")
def riadmin_add(self, _, _1, raid_instance_name, char):
if not char.char_id:
return self.getresp("global", "char_not_found", {"char": char.name})
raid_instance = self.get_raid_instance(raid_instance_name)
if not raid_instance:
return f"Raid instance {raid_instance_name} does not exist."
self.refresh_raid_instance_chars()
row = self.db.query_single("SELECT raid_instance_id FROM raid_instance_char WHERE char_id = ?", [char.char_id])
if row:
if raid_instance.id == row.raid_instance_id:
if 'raider' not in (
self.buddy_service.get_buddy(char.char_id) or DictObject({'types': []}).get('types', [])):
self.buddy_service.add_buddy(char.char_id, 'raider')
return f"Character {char.name} is already assigned to " \
f"raid instance {raid_instance.name}."
else:
if 'raider' not in (
self.buddy_service.get_buddy(char.char_id) or DictObject({'types': []}).get('types', [])):
self.buddy_service.add_buddy(char.char_id, 'raider')
self.update_char_raid_instance(char.char_id, raid_instance.id)
return f"Character {char.name} has been assigned to " \
f"raid instance {raid_instance.name}."
else:
return f"Character {char.name} is not in the private channel."
@command(command="riadmin", params=[Const("clear")], access_level="leader",
description="Remove all raids and their players", sub_command="leader")
def riadmin_clear(self, _, _1):
query = self.db.query("SELECT * FROM raid_instance_char")
for user in query:
self.buddy_service.remove_buddy(user.char_id, 'raider')
self.db.exec("DELETE FROM raid_instance_char where 1")
self.db.exec("DELETE FROM raid_instance where 1")
return f"All characters have been removed from raid instances."
@command(command="riadmin", params=[Const("rem"), Character("char")], access_level="leader",
description="remove a character from the RI", sub_command="leader")
def riadmin_rem(self, _, _2, char):
if not char.char_id:
return self.getresp("global", "char_not_found", {"char": char.name})
self.refresh_raid_instance_chars()
row = self.db.query_single(
"SELECT r2.name FROM raid_instance_char r1 "
"JOIN raid_instance r2 ON r1.raid_instance_id = r2.id WHERE r1.char_id = ?",
[char.char_id])
if row:
self.update_char_raid_instance(char.char_id, self.UNASSIGNED_RAID_INSTANCE_ID)
return f"Character {char.name} has been removed from " \
f"raid instance {row.name}."
else:
return f"Character {char.name} is not assigned to any raid instances."
@command(command="riadmin", params=[Const("leader"), Character("char")], access_level="leader",
description="Set the leader for a RI", sub_command="leader")
def riadmin_leader(self, _, _2, char):
if not char.char_id:
return self.getresp("global", "char_not_found", {"char": char.name})
raid_instance = self.get_raid_instance_by_char(char.char_id)
if not raid_instance:
return f"Character {char.name} does not belong to a raid instance."
self.set_leader(char.char_id, raid_instance.id)
return f"Character {char.name} has been set as the leader for " \
f"raid instance {raid_instance.name}."
@command(command="riadmin", params=[Const("send")], access_level="leader",
description="Send the RIs to their preset bots", sub_command="leader")
def riadmin_send(self, _, _2):
self.bot.send_private_channel_message("Exporting raids..")
for raid_instance in self.get_raid_instances():
data = self.db.query(
"SELECT r.char_id, p.name, r.is_leader FROM raid_instance_char r "
"left join player p on r.char_id=p.char_id WHERE raid_instance_id = ?",
[raid_instance.id])
if int(raid_instance.bot) == self.bot.get_char_id() or raid_instance.bot == "0" or not raid_instance.bot:
continue
self.bot.send_private_message(int(raid_instance.bot), "ritake " + ", ".join([k['name'] for k in data]),
add_color=False)
for char in data:
if char.is_leader == 0:
self.private_channel_service.kick(char.char_id)
return "Raid instance configuration has been applied."
@command(command="riadmin", params=[Const("create"), Any("Raid_name"), Character("bot")],
access_level="leader",
description="Create or change a raid instance", sub_command="leader")
def riadmin_create(self, _, _2, raid_instance_name, bot):
if not bot.char_id:
return self.getresp("global", "char_not_found", {"char": bot.name})
if bot.char_id not in json.loads(self.setting_service.get_value('riadmin_network')):
return "Bot not valid: please ask an Administrator to verify it first."
raid_instance = self.get_raid_instance(raid_instance_name)
if raid_instance:
if raid_instance.name == raid_instance_name and raid_instance.bot == bot.char_id:
return f"Raid instance {raid_instance_name} already exists."
else:
self.db.exec("UPDATE raid_instance SET name = ?, bot = ? WHERE id = ?",
[raid_instance_name, bot.char_id, raid_instance.id])
return f"Raid instance {raid_instance_name} has been updated."
else:
self.db.exec("INSERT INTO raid_instance (name, bot) VALUES (?, ?)", [raid_instance_name, bot.char_id])
return f"Raid instance {raid_instance_name} [{bot.name}] " \
f"has been created."
@command(command="riadmin", params=[Const("delete"), Any("raid_instance_name")], access_level="leader",
description="Remove a RI", sub_command="leader")
def raid_instance_delete_cmd(self, _, _1, raid_instance_name):
raid_instance = self.get_raid_instance(raid_instance_name)
if not raid_instance:
return f"Raid instance {raid_instance_name} does not exist."
query = self.db.query("SELECT char_id from raid_instance_char where raid_instance_id=?", [raid_instance.id])
for user in query:
self.buddy_service.remove_buddy(user.char_id, 'raider')
self.db.exec("DELETE FROM raid_instance_char WHERE raid_instance_id = ?", [raid_instance.id])
self.db.exec("DELETE FROM raid_instance WHERE id = ?", [raid_instance.id])
return f"Raid instance {raid_instance_name} has been deleted."
def get_raid_instance_chars(self):
self.refresh_raid_instance_chars()
data = self.db.query("SELECT * FROM ("
"SELECT p.*, r2.id AS raid_instance_id, "
"r1.is_leader, r2.name AS raid_instance_name, r2.bot "
"FROM raid_instance r2 "
"LEFT JOIN raid_instance_char r1 ON r1.raid_instance_id = r2.id "
"LEFT JOIN player p ON r1.char_id = p.char_id "
"UNION "
"SELECT p.*, r3.raid_instance_id, "
"r3.is_leader, 'Unassigned' AS raid_instance_name, '' AS bot "
"FROM raid_instance_char r3 "
"LEFT JOIN player p ON r3.char_id = p.char_id "
"WHERE r3.raid_instance_id = ?) as r2r1pr3p "
"ORDER BY raid_instance_id != ? DESC, "
"raid_instance_name, "
"is_leader desc, "
"profession, level desc, "
"name",
[self.UNASSIGNED_RAID_INSTANCE_ID, self.UNASSIGNED_RAID_INSTANCE_ID])
return data
def refresh_raid_instance_chars(self):
users = self.db.query('SELECT * from online o '
'left join player p on o.char_id=p.char_id '
'where o.channel = ?', [self.bot.name])
for user in users:
self.db.exec(
"INSERT IGNORE INTO raid_instance_char (char_id, raid_instance_id, is_leader) VALUES (?, ?, 0)",
[user.char_id, self.UNASSIGNED_RAID_INSTANCE_ID])
self.buddy_service.add_buddy(user.char_id, 'raider')
def update_char_raid_instance(self, char_id, raid_instance_id):
return self.db.exec("UPDATE raid_instance_char SET raid_instance_id = ?, is_leader = 0 WHERE char_id = ?",
[raid_instance_id, char_id])
def set_leader(self, char_id, raid_instance_id):
self.db.exec("UPDATE raid_instance_char SET is_leader = 0 WHERE raid_instance_id = ?", [raid_instance_id])
self.db.exec("UPDATE raid_instance_char SET is_leader = 1 WHERE raid_instance_id = ? AND char_id = ?",
[raid_instance_id, char_id])
def compact_char_display(self, char_info):
if char_info.level:
msg = f" {self.util.get_prof_icon(char_info.profession)} " \
f"{self.text.zfill(char_info.level, 220)}/{self.text.zfill(char_info.ai_level, 30)} " \
f"{char_info.name: <13}"
elif char_info.name:
msg = " %s" % char_info.name
else:
msg = " Unknown(%d)" % char_info.char_id
if char_info.is_leader:
msg += " [Leader]"
return msg
def get_assignment_links(self, raid_instances, char_name, add_leader_link):
links = list(map(lambda x: self.text.make_tellcmd(x.name, f"riadmin add {x.name} {char_name}"), raid_instances))
links.insert(0, self.text.make_tellcmd("Rem", f"riadmin rem {char_name}"))
if add_leader_link:
links.insert(0, self.text.make_tellcmd("L", f"riadmin leader {char_name}"))
return f'[{"|".join(links)}]'
def get_raid_instances(self):
data = self.db.query("SELECT id, name, bot FROM raid_instance ORDER BY name")
return data
def get_raid_instance(self, raid_instance_name):
return self.db.query_single("SELECT id, name, bot FROM raid_instance WHERE name LIKE ?", [raid_instance_name])
def get_raid_instance_by_char(self, char_id):
return self.db.query_single("SELECT id, name, CASE WHEN bot IS NOT NULL THEN bot else 0 END as bot "
"FROM raid_instance r1 JOIN raid_instance_char r2 ON r1.id = r2.raid_instance_id "
"WHERE r2.char_id = ?", [char_id])
def get_conn_by_id(self, bot):
conn = self.bot.conns.get(bot)
if conn:
return conn
conns = self.bot.get_conns(lambda x: x.char_name.lower() == bot.lower())
if conns:
return conns[0][1]
return None