def create_ability_queries(add_query: typing.Callable): abilities = get_name_map(data.Ability) generic_ability_group_map = collections.defaultdict(list) for name, ab in abilities.items(): # this will need to be addressed if different abilities have the same name add_query(name, ab, True) generic_ability_group_map[ab.generic_name].append(ab) generic_ability_sources_map = collections.defaultdict(dict) entities = [ item for sublist in map(lambda c: c.get_all(), (data.Adventurer, data.Dragon, data.Wyrmprint, data.Weapon)) for item in sublist ] for ent in entities: # assumes that abilities unique to an entity won't be present on that entity in multiple slots ent_ability_groups = ent.get_abilities() for ab_group in ent_ability_groups: for ab in ab_group: generic_ability_sources_map[ab.generic_name][ ent.get_key()] = ab generic_descriptions = config.get_global("ability_disambiguation") for gen_name, source_map in generic_ability_sources_map.items(): if len(source_map) == 1: ab_highest = list(source_map.values())[0] add_query(gen_name, ab_highest, True) if gen_name in generic_descriptions: logger.warning( f"Disambiguation specified for unique generic ability {gen_name}" ) else: ab_list = generic_ability_group_map[gen_name] if len(ab_list) == 1: add_query(gen_name, ab_list[0], True) else: if gen_name in generic_descriptions: desc = f"*{generic_descriptions[gen_name]}*" else: desc = "" logger.warning( f"No description for common generic ability {gen_name}" ) names = natsort.natsorted(set(ab.name for ab in ab_list), reverse=True) if len(names) > 15: names = names[:15] + ["..."] name_list = "\n".join(names) embed = discord.Embed( title=f"{gen_name} (Disambiguation)", description=f"{desc}\n\n{name_list}".strip(), color=0xFF7000) add_query(gen_name, embed)
async def on_init(discord_client): global query_config query_config = config.get_global("custom_query") build_matcher() hook.Hook.get("on_message").attach(scan_for_query) hook.Hook.get("data_downloaded").attach(build_matcher)
def get_emote(name) -> str: """ Gets the emote string for the given emote name. Emote names are case-insensitive. An object may be passed in as an emote, where str(name) will be used as the name of the emote. :param name: name of the emote :return: emote string for the given name """ name = str(name).lower() emote_map = config.get_global("emotes") return emote_map[name] if name in emote_map else ""
def check_command_permissions(message, level) -> bool: """ Determines whether a command of the given level can be used, given the context of a sent message. :param message: context message used to determine whether the command can be used :param level: level of command to use, may be one of "public", "admin", or "owner" :return: True if the command can be used, False otherwise """ if level == "public": return True elif level == "admin": return isinstance(message.channel, discord.abc.GuildChannel) and message.author.guild_permissions.manage_guild elif level == "owner": return message.author.id == config.get_global("general")["owner_id"]
def configure_discord(client: discord.Client): channel = client.get_channel( config.get_global("general")["logging_channel"]) if channel is None: raise ValueError("Logging channel not found") discord_handler = DiscordHandler(channel) discord_handler.setLevel(logging.ERROR) discord_handler.setFormatter( logging.Formatter( fmt="Date: %(asctime)s\nLevel: %(levelname)s\n%(message)s", datefmt="%Y-%m-%d %H:%M:%S")) logging.getLogger().addHandler(discord_handler)
def generate_queries(): queries = {} hdt_data = config.get_global("hdt_data") alias_lists = config.get_global("hdt_alias") for hdt, dragon_info in hdt_data.items(): dragon_name = dragon_info["dragon_name"] dragon_element = data.Element(dragon_info["dragon_element"]) resist_wyrmprint_name = dragon_info["wyrmprint"] aliases = alias_lists[hdt] + [hdt] for difficulty, difficulty_info in dragon_info["fight_info"].items(): embed = discord.Embed( title=f"High {dragon_name} {difficulty.title()} HP Requirement", description=generate_description(resist_wyrmprint_name, difficulty_info), color=dragon_element.get_colour()) for alias in aliases: queries[f"{difficulty} {alias}"] = embed queries[f"{difficulty[0]}{alias}"] = embed queries[f"{alias} {difficulty}"] = embed return queries
def update_data(cls): default_showcase = data.Showcase() default_showcase.name = "none" cls.default_showcase = NormalSS(default_showcase) new_cache = {} showcase_blacklist = config.get_global( "general")["summonable_showcase_blacklist"] for sc in data.Showcase.get_all(): if sc.name not in showcase_blacklist: if sc.type == "Regular" and not sc.name.startswith( "Dragon Special"): new_cache[ sc.get_key()] = SimShowcaseFactory.create_showcase(sc) matcher_additions = new_cache.copy() aliases = config.get_global(f"query_alias/showcase") for alias, expanded in aliases.items(): try: matcher_additions[alias] = new_cache[expanded] except KeyError: continue # add fuzzy matching names name_replacements = { "part one": "part 1", "part two": "part 2", } matcher = fuzzy_match.Matcher(lambda s: 1 + 0.5 * len(s)) for sc_name, sim_sc in matcher_additions.items(): matcher.add(sc_name, sim_sc) for old, new in name_replacements.items(): if old in sc_name: matcher.add(sc_name.replace(old, new), sim_sc) cls.showcases = new_cache cls.showcase_matcher = matcher
async def on_message(message: discord.Message): if message.author.bot or message.author.id in config.get_global( "user_blacklist"): return if (isinstance(message.channel, discord.DMChannel) or isinstance(message.channel, discord.GroupChannel) or message.channel.permissions_for(message.guild.me).send_messages): prefix = config.get_prefix(message.guild) if message.content.startswith(prefix): if not initialised: await message.channel.send( "I've only just woken up, give me a second please!") return command = message.content[len(prefix):].split( " ")[0].lower() # just command text args = message.content[len(prefix) + len(command) + 1:] if Hook.exists("public!" + command) and util.check_command_permissions( message, "public"): await Hook.get("public!" + command)(message, args) elif Hook.exists("admin!" + command) and util.check_command_permissions( message, "admin"): await Hook.get("admin!" + command)(message, args) elif Hook.exists("owner!" + command) and util.check_command_permissions( message, "owner"): await Hook.get("owner!" + command)(message, args) else: await message.channel.send( "I don't know that command, sorry! Use the `help` command for a list of commands." ) else: if not initialised: return await Hook.get("on_message")(message) if discord.utils.find(lambda m: m.id == client.user.id, message.mentions) is not None: await Hook.get("on_mention")(message) if isinstance(message.channel, discord.abc.PrivateChannel): await Hook.get("on_message_private")(message)
def get_name_map(entity_type: typing.Type[data.abc.Entity]): name_map = {str(e).lower(): e for e in entity_type.get_all()} entity_type_name = entity_type.__name__.lower() try: aliases = config.get_global(f"query_alias/{entity_type_name}") except FileNotFoundError: return name_map # add all aliases for this entity type for alias, expanded in aliases.items(): try: resolved_entity = name_map[expanded] except KeyError: logger.warning( f"Alias '{alias}' = '{expanded}' doesn't resolve to any {entity_type_name}" ) continue if alias in name_map: logger.warning( f"Alias '{alias}' already exists as {entity_type_name} name") name_map[alias] = resolved_entity return name_map
async def resist_search(message, args): """ Searches for combinations of elemental affinity and affliction resistance. **Affliction keywords:** *poison, burning/burn, freezing/freeze, paralysis, blind, stun, curses/curse, bog, sleep* **Element keywords:** *flame/fire, water, wind, light, shadow/dark* If no keywords of a category are used, all keywords of that category are included. (E.g. if no element is specified, adventurers of all elements will be included) If you use more than one keyword from a single category, or omit a category, all other categories must have *exactly* one keyword specified. You may also add a number as the minimum percentage resist value to display. If multiple are provided, only the first will be used. Shortcut keywords are available for Advanced Dragon Trials: **hms** (High Midgardsormr) = *flame*, *stun*, *100* **hbh** (High Brunhilda) = *water*, *burning*, *100* **hmc** (High Mercury) = *wind*, *bog*, *100* **hjp** (High Jupiter) = *dark*, *paralysis*, *100* **hzd** (High Zodiark) = *light*, *curse*, *100* """ args = args.lower() alias_lists = config.get_global("hdt_alias") for hdt, aliases in alias_lists.items(): for alias in aliases: args = args.replace(alias, hdt) arg_list = list(map(str.strip, args.split(" "))) shortcuts = { "hbh": (data.Element.WATER, data.Resistance.BURN, 100), "hmc": (data.Element.WIND, data.Resistance.BOG, 100), "hms": (data.Element.FIRE, data.Resistance.STUN, 100), "hjp": (data.Element.DARK, data.Resistance.PARALYSIS, 100), "hzd": (data.Element.LIGHT, data.Resistance.CURSE, 100), } specified_elements = set() specified_resists = set() specified_threshold = -1 # collect criteria for arg in arg_list: if arg == "": continue if arg in shortcuts: specified_elements.add(shortcuts[arg][0]) specified_resists.add(shortcuts[arg][1]) if specified_threshold == -1: specified_threshold = shortcuts[arg][2] continue try: specified_elements.add(data.Element(arg.capitalize())) continue except ValueError: pass try: specified_resists.add(data.Resistance(arg.capitalize())) continue except ValueError: pass # find threshold if specified_threshold == -1: threshold_match = re.findall(r"^(\d+)%?$", arg) if len(threshold_match) > 0 and 0 <= int( threshold_match[0]) <= 100: specified_threshold = int(threshold_match[0]) # if one is empty, use all if len(specified_elements) == 0: element_list = list(data.Element) else: element_list = specified_elements if len(specified_resists) == 0: resist_list = list(data.Resistance) else: resist_list = specified_resists # too broad or no keywords if len(specified_elements) == 0 and len(specified_resists) == 0: await message.channel.send( "I need something to work with, give me an element or resistance!") return elif len(resist_list) > 1 and len(element_list) > 1: await message.channel.send("Too much! Try narrowing down your search.") return result_lines = [] emote = util.get_emote for res in resist_list: match_list = [] for el in element_list: match_list.extend( list( filter(None, map(data.Adventurer.find, resist_data[res][el])))) if len( match_list ) > 0 or res in specified_resists: # include resists specifically searched for result_lines.append(f"{emote(res)} **{res} Resistance**") else: continue if len(match_list) == 0: result_lines.append(f"{emote('blank')*2} *No results.* ") continue # sorting match_list.sort(key=lambda a: a.full_name) # by name match_list.sort(key=lambda a: a.element.value) # by element match_list.sort(key=lambda a: a.rarity, reverse=True) # by rarity match_list.sort(key=lambda a: resist_data[res][a.element][a.get_key()], reverse=True) # by percent # formatting current_resist = 101 for adv in match_list: adv_res_percent = resist_data[res][adv.element][adv.get_key()] if adv_res_percent < specified_threshold: break if adv_res_percent < current_resist: current_resist = adv_res_percent result_lines.append( f"{emote('blank')*2} **{adv_res_percent}%**") result_lines.append( f"{emote('rarity' + str(adv.rarity))}{emote(adv.element)} {adv.full_name}" ) if len(result_lines) == 0: await message.channel.send( "I didn't find anything! Maybe there's nobody that matches your search?" ) await util.send_long_message_in_sections(message.channel, result_lines)
"VII": 7, "VIII": 8, "IX": 9, "X": 10, } asyncio.get_event_loop().run_until_complete(data.update_repositories()) abilities = {str(e).lower(): e for e in data.Ability.get_all()} generic_ability_map = collections.defaultdict(list) for name, ab in abilities.items(): generic_ability_map[ab.generic_name].append(ab) numeric_ex = re.compile(r"(.+) \+(\d+)%?") roman_ex = re.compile(r"(.+) ([IVX]+)") generic_descriptions = config.get_global("ability_disambiguation") non_matching = [] for gen_name, ab_list in generic_ability_map.items(): if len(ab_list) > 1: if gen_name not in generic_descriptions: if any(numeric_ex.match(ab.name) for ab in ab_list): ab_max = None ab_max_value = 0 for ab in ab_list: m = numeric_ex.match(ab.name) if m and int(m.group(2)) > ab_max_value: ab_max = ab ab_max_value = int(m.group(2)) print(description_line(gen_name, ab_max.description)) elif any(roman_ex.match(ab.name) for ab in ab_list): ab_max = None
async def on_init(discord_client): global client, statuses client = discord_client statuses = list(zip(*(config.get_global("status")))) change_status("my favourites!")
async def check_news(reschedule): if reschedule: now = datetime.datetime.utcnow() next_check = now.replace(minute=5 * math.floor(now.minute / 5), second=1, microsecond=0) + datetime.timedelta(minutes=5) time_delta = (next_check - now).total_seconds() asyncio.get_event_loop().call_later( time_delta, lambda: asyncio.ensure_future(check_news(True))) if time_delta < 240: return async with aiohttp.ClientSession() as session: list_base_url = "https://dragalialost.com/api/index.php?" \ "format=json&type=information&action=information_list&lang=en_us&priority_lower_than=" response_json = await get_api_json_response(session, list_base_url) if not response_json: logger.error("Could not retrieve article list") return query_result = response_json["data"] wc = config.get_writeable() stored_ids = wc.news_ids stored_time = wc.news_update_time if not stored_ids and not stored_time: wc.news_ids = query_result["new_article_list"] wc.news_update_time = math.ceil(time.time()) logger.info( f"Regenerated article history, time = {wc.news_update_time}, IDs = {wc.news_ids}" ) await config.set_writeable(wc) return # new posts new_article_ids = list(reversed(query_result["new_article_list"])) new_article_ids = [i for i in new_article_ids if i not in stored_ids] new_stored_ids = query_result["new_article_list"].copy() or stored_ids # updated posts updated_articles = sorted(query_result["update_article_list"], key=lambda d: d["update_time"]) updated_article_ids = [ d["id"] for d in updated_articles if d["update_time"] > stored_time ] updated_article_ids = [ i for i in updated_article_ids if i not in new_article_ids ] new_stored_time = updated_articles[-1][ "update_time"] if updated_articles else stored_time # filter blacklisted articles article_blacklist = config.get_global( "general")["news_article_blacklist"] news_articles = [ i for i in new_article_ids + updated_article_ids if i not in article_blacklist ] if len(news_articles) >= 10: # too many news items, post a generic notification embeds = [ discord.Embed( title="New news posts are available", url="https://dragalialost.com/en/news/", description= f"{len(news_articles)} new news posts are available! Click the link above to read them.", color=get_news_colour()).set_author( name="Dragalia Lost News", icon_url=news_icon) ] else: # generate embeds from articles embeds = [] for article_id in news_articles: article_embed = await get_article_embed( session, article_id, article_id in updated_article_ids) if article_embed: embeds.append(article_embed) # update config wc.news_ids = new_stored_ids wc.news_update_time = new_stored_time if wc.news_ids != stored_ids or wc.news_update_time != stored_time: await config.set_writeable(wc) # post articles if embeds: for guild in client.guilds: active_channel = config.get_guild(guild).active_channel channel = guild.get_channel(active_channel) if channel is not None and channel.permissions_for( guild.me).send_messages: asyncio.ensure_future( exec_in_order([channel.send(embed=e) for e in embeds]))