c04f76c0db
Fixed command & event threading Events are now threaded by event_type (i.e. all buddy_logon events get ran in the same one) Added default preferences Fixed recipe loading for multiple installs (i.e. on different machines)
292 lines
12 KiB
Python
292 lines
12 KiB
Python
import json
|
|
import os
|
|
import re
|
|
|
|
from core.chat_blob import ChatBlob
|
|
from core.command_param_types import Any, Int, NamedParameters
|
|
from core.decorators import instance, command
|
|
from core.logger import Logger
|
|
|
|
|
|
@instance()
|
|
class RecipeController:
|
|
def __init__(self):
|
|
self.logger = Logger(__name__)
|
|
|
|
self.recipe_name_regex = re.compile(r"(\d+)\.(txt|json)")
|
|
self.recipe_item_regex = re.compile(r"#L \"([^\"]+)\" \"([\d+]+)\"")
|
|
self.recipe_link_regex = re.compile(r"#L \"([^\"]+)\" \"([^\"]+)\"")
|
|
|
|
def inject(self, registry):
|
|
self.db = registry.get_instance("db")
|
|
self.text = registry.get_instance("text")
|
|
self.items_controller = registry.get_instance("items_controller")
|
|
self.command_alias_service = registry.get_instance("command_alias_service")
|
|
|
|
def start(self):
|
|
self.command_alias_service.add_alias("r", "recipe")
|
|
self.command_alias_service.add_alias("tradeskill", "recipe")
|
|
|
|
self.db.exec("CREATE TABLE IF NOT EXISTS recipe ("
|
|
"id INT NOT NULL PRIMARY KEY, "
|
|
"name VARCHAR(50) NOT NULL, "
|
|
"author VARCHAR(50) NOT NULL, "
|
|
"recipe TEXT NOT NULL, "
|
|
"dt INT NOT NULL DEFAULT 0)")
|
|
self.db.create_view("recipe")
|
|
recipe_dir = os.path.dirname(os.path.realpath(__file__)) + "/recipes/"
|
|
recipes = self.db.query("SELECT id, dt FROM recipe")
|
|
|
|
for file in os.listdir(recipe_dir):
|
|
if file.startswith("_"):
|
|
continue
|
|
|
|
m = self.recipe_name_regex.match(file)
|
|
if m:
|
|
recipe_id = m.group(1)
|
|
file_type = m.group(2)
|
|
dt = int(os.path.getmtime(recipe_dir + file))
|
|
|
|
recipe = self.find_recipe(recipe_id, recipes)
|
|
if recipe:
|
|
recipes.remove(recipe)
|
|
if recipe.dt >= dt:
|
|
continue
|
|
|
|
self.update_recipe(recipe_dir, recipe_id, file_type, dt)
|
|
else:
|
|
raise Exception(f"Unknown recipe format for '{file}'")
|
|
|
|
@command(command="recipe", params=[Int("recipe_id")], access_level="member", description="Show a recipe")
|
|
def recipe_show_cmd(self, _, recipe_id):
|
|
recipe = self.get_recipe(recipe_id)
|
|
if not recipe:
|
|
return f"Could not find recipe with ID <highlight>{recipe_id:d}</highlight>."
|
|
|
|
return self.format_recipe(recipe)
|
|
|
|
@command(command="recipe",
|
|
params=[Any("search"), NamedParameters(["page"])],
|
|
access_level="member",
|
|
description="Search for a recipe")
|
|
def recipe_search_cmd(self, _, search, named_params):
|
|
page = int(named_params.page or "1")
|
|
page_size = 30
|
|
offset = (page - 1) * page_size
|
|
search = self.text.strip_html_tags(search)
|
|
data = self.db.query("SELECT * FROM recipe "
|
|
"WHERE name LIKE ? "
|
|
"or recipe LIKE ? "
|
|
"ORDER BY name",
|
|
[f"%{search}%", f"%{search}%"])
|
|
count = len(data)
|
|
paged_data = data[offset:offset + page_size]
|
|
if count == 1:
|
|
return self.format_recipe(data[0])
|
|
blob = ""
|
|
|
|
if count > page_size:
|
|
if page > 1 and len(paged_data) > 0:
|
|
blob += " " + self.text.make_chatcmd(f"«« Page {page - 1:d}", self.get_chat_command(search, page - 1))
|
|
if offset + page_size < len(data):
|
|
blob += " Page " + str(page)
|
|
blob += " " + self.text.make_chatcmd(f"Page {page + 1:d} »»", self.get_chat_command(search, page + 1))
|
|
blob += "\n\n"
|
|
|
|
for row in paged_data:
|
|
blob += self.text.make_tellcmd(row.name, f"recipe {row.id:d}") + "\n"
|
|
|
|
return ChatBlob(
|
|
f"Recipes Matching '{search}' ({offset + 1:d} - {min(offset + page_size, count):d} of {count:d})", blob)
|
|
|
|
def get_recipe(self, recipe_id):
|
|
return self.db.query_single("SELECT * FROM recipe WHERE id = ?", [recipe_id])
|
|
|
|
def format_recipe(self, recipe):
|
|
blob = f"Recipe ID: <highlight>{recipe.id:d}</highlight>\n"
|
|
blob += f"Author: <highlight>{recipe.author or 'Unknown'}</highlight>\n\n"
|
|
blob += self.format_recipe_text(recipe.recipe)
|
|
|
|
return ChatBlob(f"Recipe for '{recipe.name}'", blob)
|
|
|
|
def format_recipe_text(self, recipe_text):
|
|
recipe_text = recipe_text.replace("\\n", "\n")
|
|
recipe_text = self.recipe_item_regex.sub(self.lookup_item, recipe_text)
|
|
recipe_text = self.recipe_link_regex.sub("<a href='chatcmd://\\2'>\\1</a>", recipe_text)
|
|
return recipe_text
|
|
|
|
def lookup_item(self, m):
|
|
name = m.group(1)
|
|
item_id = m.group(2)
|
|
|
|
item = self.items_controller.get_by_item_id(item_id)
|
|
if item:
|
|
return self.text.make_item(item.lowid, item.highid, item.highql, item.name)
|
|
else:
|
|
return name
|
|
|
|
def get_chat_command(self, search, page):
|
|
return f"/tell <myname> recipe {search} --page={page:d}"
|
|
|
|
def find_recipe(self, recipe_id, recipes):
|
|
for row in recipes:
|
|
if str(row.id) == recipe_id:
|
|
return row
|
|
return None
|
|
|
|
def update_recipe(self, recipe_dir, recipe_id, _, dt):
|
|
with open(recipe_dir + recipe_id + ".json", mode="r", encoding="UTF-8") as f:
|
|
recipe = json.load(f)
|
|
|
|
name = recipe["name"]
|
|
author = recipe["author"]
|
|
|
|
if "raw" in recipe:
|
|
content = recipe["raw"]
|
|
else:
|
|
content = self.format_json_recipe(recipe_id, recipe)
|
|
self.db.exec("REPLACE INTO recipe (id, name, author, recipe, dt) "
|
|
"VALUES (?, ?, ?, ?, ?)",
|
|
[recipe_id, name, author, content, dt])
|
|
|
|
def format_json_recipe(self, recipe_id, recipe):
|
|
items = {}
|
|
for i in recipe["items"]:
|
|
item = self.items_controller.get_by_item_id(i["item_id"], i.get("ql"))
|
|
if not item:
|
|
raise Exception(f"Could not find recipe item '{i['item_id']:d}' for recipe id {recipe_id}")
|
|
|
|
item.ql = i.get("ql") or (item.highql if i["item_id"] == item.highid else item.lowql)
|
|
items[i["alias"]] = item
|
|
|
|
content = ""
|
|
|
|
ingredients = items.copy()
|
|
for step in recipe["steps"]:
|
|
del ingredients[step["result"]]
|
|
|
|
content += self.format_ingredients(ingredients.items())
|
|
content += "\n"
|
|
content += self.format_steps(items, recipe["steps"])
|
|
|
|
if "details" in recipe:
|
|
content += self.format_details(recipe["details"])
|
|
|
|
return content
|
|
|
|
def format_ingredients(self, ingredients):
|
|
content = "<font color=#FFFF00>------------------------------</font>\n"
|
|
content += "<font color=#FF0000>Ingredients</font>\n"
|
|
content += "<font color=#FFFF00>------------------------------</font>\n\n"
|
|
|
|
for _, ingredient in ingredients:
|
|
content += self.text.make_image(ingredient["icon"]) + "<tab>"
|
|
content += self.text.make_item(ingredient["lowid"], ingredient["highid"], ingredient["ql"],
|
|
ingredient["name"]) + "\n"
|
|
|
|
return content
|
|
|
|
def format_steps(self, items, steps):
|
|
content = ""
|
|
content += "<font color=#FFFF00>------------------------------</font>\n"
|
|
content += "<font color=#FF0000>Recipe</font>\n"
|
|
content += "<font color=#FFFF00>------------------------------</font>\n\n"
|
|
|
|
for step in steps:
|
|
source = items[step["source"]]
|
|
target = items[step["target"]]
|
|
result = items[step["result"]]
|
|
content += f"<a href='itemref://{source['lowid']:d}/{source['highid']:d}/{source['ql']:d}'>" \
|
|
f"{self.text.make_image(source['icon'])}</a>" + ""
|
|
content += "<font color=#FFFFFF><tab>+<tab></font> "
|
|
content += f"<a href='itemref://{target['lowid']:d}/{target['highid']:d}/{target['ql']:d}'>" \
|
|
f"{self.text.make_image(target['icon'])}</a>" + ""
|
|
content += "<font color=#FFFFFF><tab>=<tab></font> "
|
|
content += f"<a href='itemref://{result['lowid']:d}/{result['highid']:d}/{result['ql']:d}'>" \
|
|
f"{self.text.make_image(result['icon'])}</a>"
|
|
content += "\n<tab><tab>" + self.text.make_item(source["lowid"],
|
|
source["highid"], source["ql"], source["name"])
|
|
content += "\n + <tab>" + self.text.make_item(target["lowid"],
|
|
target["highid"], target["ql"], target["name"])
|
|
content += "\n = <tab>" + self.text.make_item(result["lowid"],
|
|
result["highid"], result["ql"], result["name"]) + "\n"
|
|
|
|
if "skills" in step:
|
|
content += f"<font color=#FFFF00>Skills: | {step['skills']} |</font>\n"
|
|
content += "\n\n"
|
|
|
|
return content
|
|
|
|
def format_details(self, details):
|
|
content = ""
|
|
content += "<font color=#FFFF00>------------------------------</font>\n"
|
|
content += "<font color=#FF0000>Details</font>\n"
|
|
content += "<font color=#FFFF00>------------------------------</font>\n\n"
|
|
|
|
last_type = ""
|
|
for detail in details:
|
|
if "item" in detail:
|
|
last_type = "item"
|
|
i = detail["item"]
|
|
|
|
if "ql" in i:
|
|
item = self.items_controller.get_by_item_id(i["id"], i["ql"])
|
|
else:
|
|
item = self.items_controller.get_by_item_id(i["id"])
|
|
item["ql"] = item["highql"]
|
|
|
|
content += "<font color=#009B00>%s</font>" % \
|
|
self.text.make_item(item['lowid'],
|
|
item['highid'],
|
|
item['ql'],
|
|
item['name'])
|
|
|
|
if "comment" in i:
|
|
content += " - " + i["comment"]
|
|
|
|
content += "\n"
|
|
|
|
elif "text" in detail:
|
|
if last_type == "item":
|
|
content += "\n"
|
|
|
|
last_type = "text"
|
|
content += f"<font color=#FFFFFF>{detail['text']}</font>\n"
|
|
|
|
return content
|
|
|
|
def convert_to_json(self, recipe_dir, recipe_id, file):
|
|
with open(recipe_dir + file, mode="r", encoding="UTF-8") as f:
|
|
lines = f.readlines()
|
|
|
|
recipe = {
|
|
"name": lines.pop(0).strip()[6:],
|
|
"author": lines.pop(0).strip()[8:],
|
|
"items": list(),
|
|
"steps": list(),
|
|
"details": list(),
|
|
"raw": None
|
|
}
|
|
|
|
content = "".join(lines)
|
|
items = {}
|
|
|
|
matches = self.recipe_item_regex.findall(content)
|
|
for item_name, item_id in matches:
|
|
item = self.items_controller.get_by_item_id(item_id)
|
|
if not item:
|
|
self.logger.warning(f"Could not find recipe item '{item_id} - {item_name}' for recipe id {recipe_id}")
|
|
else:
|
|
items[item.highid] = {"alias": item.name, "item_id": item.highid}
|
|
|
|
recipe["items"].extend(items.values())
|
|
recipe["raw"] = content
|
|
|
|
with open(recipe_dir + recipe_id + ".json", mode="w", encoding="UTF-8") as f:
|
|
f.write(json.dumps(recipe, indent=4))
|
|
|
|
# delete file
|
|
os.remove(recipe_dir + file)
|
|
|
|
return True
|