Latest fixes.
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
Factoids plugin – a clone of the classic infobot / supybot Factoids plugin.
|
||||
Stores and retrieves factoids via Matrix chat.
|
||||
|
||||
Commands:
|
||||
!fact <key> – retrieve a factoid
|
||||
!fact search <query> – search factoids by key or value
|
||||
!fact info <key> – show metadata for a factoid
|
||||
!fact random – show a random factoid
|
||||
!fact stats – show database statistics
|
||||
!fact list [glob] – list factoid keys matching a glob pattern
|
||||
!fact lock <key> – lock a factoid (admin only)
|
||||
!fact unlock <key> – unlock a factoid (admin only)
|
||||
!fact change <key> is <val> – change an existing factoid
|
||||
!learn <key> is <value> – teach the bot a new factoid
|
||||
!forget <key> – delete a factoid
|
||||
!also <key> is <value> – append to an existing factoid
|
||||
!no, <key> is <value> – replace a factoid (same as change)
|
||||
|
||||
In‑line query (no prefix needed):
|
||||
<key>? – ask for a factoid
|
||||
|
||||
Special value tags:
|
||||
<reply> text – replies with "text" (not "key is text")
|
||||
<action> text – replies as an emote (/me)
|
||||
a | b | c – picks one option at random
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import asyncio
|
||||
import simplematrixbotlib as botlib
|
||||
from plugins.common import code_block, collapsible_summary, html_escape
|
||||
|
||||
DB_PATH = "factoids.db"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Database helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def init_db():
|
||||
"""Ensure the factoids table exists."""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS factoids (
|
||||
factoid_key VARCHAR(64) NOT NULL DEFAULT '' PRIMARY KEY,
|
||||
requested_by VARCHAR(80),
|
||||
requested_time INTEGER,
|
||||
requested_count SMALLINT,
|
||||
created_by VARCHAR(80),
|
||||
created_time INTEGER DEFAULT 0,
|
||||
modified_by VARCHAR(80),
|
||||
modified_time INTEGER,
|
||||
locked_by VARCHAR(80),
|
||||
locked_time INTEGER,
|
||||
factoid_value TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def _conn():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _normalise_key(raw: str) -> str:
|
||||
"""Lower-case, strip punctuation, collapse whitespace."""
|
||||
key = raw.strip().lower()
|
||||
key = re.sub(r'[?.,!]+$', '', key)
|
||||
key = ' '.join(key.split())
|
||||
return key
|
||||
|
||||
def get_factoid(key: str) -> dict | None:
|
||||
"""Return a factoid row (or None) and bump its request count."""
|
||||
conn = _conn()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM factoids WHERE factoid_key = ?", (key,)
|
||||
).fetchone()
|
||||
if row:
|
||||
conn.execute(
|
||||
"UPDATE factoids SET requested_count = COALESCE(requested_count,0)+1, "
|
||||
"requested_time = ? WHERE factoid_key = ?",
|
||||
(int(time.time()), key)
|
||||
)
|
||||
conn.commit()
|
||||
result = dict(row)
|
||||
else:
|
||||
result = None
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
def set_factoid(key: str, value: str, created_by: str, locked_by: str = None):
|
||||
"""Insert or replace a factoid."""
|
||||
now = int(time.time())
|
||||
conn = _conn()
|
||||
conn.execute(
|
||||
"""INSERT OR REPLACE INTO factoids
|
||||
(factoid_key, factoid_value, created_by, created_time, modified_by, modified_time, locked_by, locked_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(key, value, created_by, now, created_by, now, locked_by, now if locked_by else None)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def append_factoid(key: str, addition: str, modified_by: str) -> bool:
|
||||
"""Append text to an existing factoid. Returns True if it existed."""
|
||||
conn = _conn()
|
||||
row = conn.execute("SELECT factoid_value FROM factoids WHERE factoid_key = ?", (key,)).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return False
|
||||
new_value = row["factoid_value"] + " or " + addition
|
||||
conn.execute(
|
||||
"UPDATE factoids SET factoid_value = ?, modified_by = ?, modified_time = ? WHERE factoid_key = ?",
|
||||
(new_value, modified_by, int(time.time()), key)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
def delete_factoid(key: str) -> bool:
|
||||
"""Delete a factoid. Returns True if it existed."""
|
||||
conn = _conn()
|
||||
cur = conn.execute("DELETE FROM factoids WHERE factoid_key = ?", (key,))
|
||||
existed = cur.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return existed
|
||||
|
||||
def search_factoids(query: str, limit: int = 20) -> list[dict]:
|
||||
"""Search factoids by key or value."""
|
||||
conn = _conn()
|
||||
like = f"%{query}%"
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM factoids WHERE factoid_key LIKE ? OR factoid_value LIKE ? LIMIT ?",
|
||||
(like, like, limit)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def list_keys(glob_pattern: str = None, limit: int = 50) -> list[str]:
|
||||
"""List factoid keys, optionally matching a glob pattern."""
|
||||
conn = _conn()
|
||||
if glob_pattern:
|
||||
like = glob_pattern.replace("*", "%").replace("?", "_")
|
||||
rows = conn.execute(
|
||||
"SELECT factoid_key FROM factoids WHERE factoid_key LIKE ? ORDER BY factoid_key LIMIT ?",
|
||||
(like, limit)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT factoid_key FROM factoids ORDER BY factoid_key LIMIT ?",
|
||||
(limit,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [r["factoid_key"] for r in rows]
|
||||
|
||||
def random_factoid() -> dict | None:
|
||||
"""Return a random factoid."""
|
||||
conn = _conn()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM factoids ORDER BY RANDOM() LIMIT 1"
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return dict(row) if row else None
|
||||
|
||||
def get_stats() -> dict:
|
||||
"""Return aggregate statistics."""
|
||||
conn = _conn()
|
||||
total = conn.execute("SELECT COUNT(*) AS n FROM factoids").fetchone()["n"]
|
||||
top = conn.execute(
|
||||
"SELECT factoid_key, requested_count FROM factoids ORDER BY COALESCE(requested_count,0) DESC LIMIT 10"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return {"total": total, "top": [dict(r) for r in top]}
|
||||
|
||||
def lock_factoid(key: str, locked_by: str) -> bool:
|
||||
"""Lock a factoid. Returns True if it existed."""
|
||||
conn = _conn()
|
||||
cur = conn.execute(
|
||||
"UPDATE factoids SET locked_by = ?, locked_time = ? WHERE factoid_key = ?",
|
||||
(locked_by, int(time.time()), key)
|
||||
)
|
||||
existed = cur.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return existed
|
||||
|
||||
def unlock_factoid(key: str) -> bool:
|
||||
"""Unlock a factoid. Returns True if it existed."""
|
||||
conn = _conn()
|
||||
cur = conn.execute(
|
||||
"UPDATE factoids SET locked_by = NULL, locked_time = NULL WHERE factoid_key = ?",
|
||||
(key,)
|
||||
)
|
||||
existed = cur.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return existed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Value formatting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _format_response(key: str, raw_value: str) -> str:
|
||||
"""Format a factoid value for display, handling <reply>, <action>, and |."""
|
||||
value = raw_value.strip()
|
||||
|
||||
if value.startswith("<reply>"):
|
||||
return value[len("<reply>"):].strip()
|
||||
|
||||
if value.startswith("<action>"):
|
||||
action = value[len("<action>"):].strip()
|
||||
return f"* {key} {action}"
|
||||
|
||||
if "|" in value:
|
||||
parts = [p.strip() for p in value.split("|")]
|
||||
return f"{key} is {random.choice(parts)}"
|
||||
|
||||
return f"{key} is {value}"
|
||||
|
||||
def _format_info(fact: dict) -> str:
|
||||
"""Format factoid metadata as code-block rows."""
|
||||
rows = [
|
||||
("🔑", "Key", fact["factoid_key"]),
|
||||
("📝", "Value", fact["factoid_value"][:200] + ("…" if len(fact.get("factoid_value",""))>200 else "")),
|
||||
]
|
||||
if fact.get("created_by"):
|
||||
rows.append(("👤", "Created by", fact["created_by"]))
|
||||
if fact.get("created_time"):
|
||||
rows.append(("📅", "Created", time.strftime("%Y-%m-%d", time.localtime(fact["created_time"]))))
|
||||
if fact.get("modified_by") and fact["modified_by"] != fact.get("created_by"):
|
||||
rows.append(("✏️", "Modified by", fact["modified_by"]))
|
||||
if fact.get("requested_count"):
|
||||
rows.append(("🔢", "Requested", f"{fact['requested_count']} times"))
|
||||
if fact.get("locked_by"):
|
||||
rows.append(("🔒", "Locked by", fact["locked_by"]))
|
||||
sections = [{"title": "", "rows": rows}]
|
||||
return code_block(f"ℹ️ Factoid Info: {fact['factoid_key']}", sections)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command handler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def handle_command(room, message, bot, prefix, config):
|
||||
init_db()
|
||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||
room_id = room.room_id
|
||||
sender = str(message.sender)
|
||||
body = (message.body or "").strip()
|
||||
is_admin = (sender == config.admin_user)
|
||||
|
||||
# ---- In-line factoid query: X? (no prefix needed) ----
|
||||
# Only retrieval is allowed without prefix; learning requires !learn etc.
|
||||
if match.is_not_from_this_bot() and not match.prefix():
|
||||
stripped = body.strip()
|
||||
if stripped.endswith("?") and not stripped.startswith("!"):
|
||||
key = _normalise_key(stripped[:-1])
|
||||
if key:
|
||||
fact = get_factoid(key)
|
||||
if fact:
|
||||
resp = _format_response(key, fact["factoid_value"])
|
||||
await bot.api.send_markdown_message(room_id, resp)
|
||||
return
|
||||
# All learning now requires a ! prefix, so we ignore unprefixed messages
|
||||
return
|
||||
|
||||
# ---- Prefixed commands ----
|
||||
if not (match.is_not_from_this_bot() and match.prefix()):
|
||||
return
|
||||
|
||||
cmd = match.command()
|
||||
args = match.args()
|
||||
|
||||
# !fact
|
||||
if cmd == "fact":
|
||||
if not args:
|
||||
await _send_help(room, bot)
|
||||
return
|
||||
|
||||
sub = args[0].lower()
|
||||
|
||||
# !fact search <query>
|
||||
if sub == "search" and len(args) >= 2:
|
||||
query = " ".join(args[1:])
|
||||
results = search_factoids(query)
|
||||
if not results:
|
||||
await bot.api.send_text_message(room_id, f"🔍 No factoids matching '{html_escape(query)}'.")
|
||||
return
|
||||
rows = []
|
||||
for f in results:
|
||||
val = f["factoid_value"][:80] + ("…" if len(f["factoid_value"]) > 80 else "")
|
||||
rows.append(("📌", f["factoid_key"], val))
|
||||
sections = [{"title": f"Search: {html_escape(query)}", "rows": rows}]
|
||||
block = code_block(f"🔍 Factoid Search: {html_escape(query)}", sections)
|
||||
output = collapsible_summary(f"🔍 Factoids matching '{html_escape(query)}'", block)
|
||||
await bot.api.send_markdown_message(room_id, output)
|
||||
return
|
||||
|
||||
# !fact info <key>
|
||||
if sub == "info" and len(args) >= 2:
|
||||
key = _normalise_key(" ".join(args[1:]))
|
||||
fact = get_factoid(key)
|
||||
if not fact:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'.")
|
||||
return
|
||||
await bot.api.send_markdown_message(room_id, _format_info(fact))
|
||||
return
|
||||
|
||||
# !fact random
|
||||
if sub == "random":
|
||||
fact = random_factoid()
|
||||
if not fact:
|
||||
await bot.api.send_text_message(room_id, "📭 No factoids in the database yet.")
|
||||
return
|
||||
resp = _format_response(fact["factoid_key"], fact["factoid_value"])
|
||||
await bot.api.send_markdown_message(room_id, resp)
|
||||
return
|
||||
|
||||
# !fact stats
|
||||
if sub == "stats":
|
||||
stats = get_stats()
|
||||
rows = [("📊", "Total factoids", str(stats["total"]))]
|
||||
for i, t in enumerate(stats["top"], 1):
|
||||
count = t.get("requested_count") or 0
|
||||
rows.append(("🏅", f"#{i} {t['factoid_key']}", f"{count} requests"))
|
||||
sections = [{"title": "Factoid Statistics", "rows": rows}]
|
||||
block = code_block("📊 Factoid Stats", sections)
|
||||
output = collapsible_summary("📊 Factoid Statistics", block)
|
||||
await bot.api.send_markdown_message(room_id, output)
|
||||
return
|
||||
|
||||
# !fact list [glob]
|
||||
if sub == "list":
|
||||
pattern = " ".join(args[1:]) if len(args) > 1 else None
|
||||
keys = list_keys(pattern)
|
||||
if not keys:
|
||||
await bot.api.send_text_message(room_id, "📭 No factoids found.")
|
||||
return
|
||||
rows = [(f"{i}.", k, "") for i, k in enumerate(keys, 1)]
|
||||
title = f"Factoid Keys ({len(keys)} total)"
|
||||
sections = [{"title": title, "rows": rows}]
|
||||
block = code_block(f"📋 {title}", sections)
|
||||
output = collapsible_summary(title, block)
|
||||
await bot.api.send_markdown_message(room_id, output)
|
||||
return
|
||||
|
||||
# !fact lock <key>
|
||||
if sub == "lock" and len(args) >= 2:
|
||||
if not is_admin:
|
||||
await bot.api.send_text_message(room_id, "⛔ Admin only.")
|
||||
return
|
||||
key = _normalise_key(" ".join(args[1:]))
|
||||
if lock_factoid(key, sender):
|
||||
await bot.api.send_text_message(room_id, f"🔒 Locked '{html_escape(key)}'.")
|
||||
else:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
# !fact unlock <key>
|
||||
if sub == "unlock" and len(args) >= 2:
|
||||
if not is_admin:
|
||||
await bot.api.send_text_message(room_id, "⛔ Admin only.")
|
||||
return
|
||||
key = _normalise_key(" ".join(args[1:]))
|
||||
if unlock_factoid(key):
|
||||
await bot.api.send_text_message(room_id, f"🔓 Unlocked '{html_escape(key)}'.")
|
||||
else:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
# !fact change <key> is <value>
|
||||
if sub == "change" and len(args) >= 2:
|
||||
rest = " ".join(args[1:])
|
||||
m = re.match(r'^(.+?)\s+is\s+(.+)$', rest, re.IGNORECASE)
|
||||
if not m:
|
||||
await bot.api.send_text_message(room_id, "Usage: !fact change <key> is <value>")
|
||||
return
|
||||
key = _normalise_key(m.group(1).strip())
|
||||
value = m.group(2).strip()
|
||||
existing = get_factoid(key)
|
||||
if not existing:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'. Use !learn to create one.")
|
||||
return
|
||||
if existing.get("locked_by") and not is_admin:
|
||||
await bot.api.send_text_message(room_id, f"🔒 '{html_escape(key)}' is locked by {existing['locked_by']}.")
|
||||
return
|
||||
set_factoid(key, value, sender)
|
||||
await bot.api.send_text_message(room_id, f"✏️ Changed '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
# !fact <key> (bare retrieval)
|
||||
key = _normalise_key(" ".join(args))
|
||||
fact = get_factoid(key)
|
||||
if not fact:
|
||||
keys = list_keys(f"*{key}*")
|
||||
if keys:
|
||||
suggestions = ", ".join(keys[:10])
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'. Did you mean: {suggestions}?")
|
||||
else:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'.")
|
||||
return
|
||||
resp = _format_response(key, fact["factoid_value"])
|
||||
await bot.api.send_markdown_message(room_id, resp)
|
||||
return
|
||||
|
||||
# !learn <key> is <value>
|
||||
if cmd == "learn":
|
||||
if not args:
|
||||
await bot.api.send_text_message(room_id, "Usage: !learn <key> is <value>")
|
||||
return
|
||||
rest = " ".join(args)
|
||||
m = re.match(r'^(.+?)\s+is\s+(.+)$', rest, re.IGNORECASE)
|
||||
if not m:
|
||||
await bot.api.send_text_message(room_id, "Usage: !learn <key> is <value>")
|
||||
return
|
||||
key = _normalise_key(m.group(1).strip())
|
||||
value = m.group(2).strip()
|
||||
existing = get_factoid(key)
|
||||
if existing and existing.get("locked_by") and not is_admin:
|
||||
await bot.api.send_text_message(room_id, f"🔒 '{html_escape(key)}' is locked by {existing['locked_by']}.")
|
||||
return
|
||||
set_factoid(key, value, sender)
|
||||
await bot.api.send_text_message(room_id, f"💡 Learned '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
# !forget <key>
|
||||
if cmd == "forget":
|
||||
if not args:
|
||||
await bot.api.send_text_message(room_id, "Usage: !forget <key>")
|
||||
return
|
||||
key = _normalise_key(" ".join(args))
|
||||
fact = get_factoid(key)
|
||||
if fact and fact.get("locked_by") and not is_admin:
|
||||
await bot.api.send_text_message(room_id, f"🔒 '{html_escape(key)}' is locked by {fact['locked_by']}.")
|
||||
return
|
||||
if delete_factoid(key):
|
||||
await bot.api.send_text_message(room_id, f"🗑️ Forgot '{html_escape(key)}'.")
|
||||
else:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
# !also <key> is <value>
|
||||
if cmd == "also":
|
||||
if not args:
|
||||
await bot.api.send_text_message(room_id, "Usage: !also <key> is <value>")
|
||||
return
|
||||
rest = " ".join(args)
|
||||
m = re.match(r'^(.+?)\s+is\s+(.+)$', rest, re.IGNORECASE)
|
||||
if not m:
|
||||
await bot.api.send_text_message(room_id, "Usage: !also <key> is <value>")
|
||||
return
|
||||
key = _normalise_key(m.group(1).strip())
|
||||
value = m.group(2).strip()
|
||||
fact = get_factoid(key)
|
||||
if fact and fact.get("locked_by") and not is_admin:
|
||||
await bot.api.send_text_message(room_id, f"🔒 '{html_escape(key)}' is locked by {fact['locked_by']}.")
|
||||
return
|
||||
if append_factoid(key, value, sender):
|
||||
await bot.api.send_text_message(room_id, f"📎 Appended to '{html_escape(key)}'.")
|
||||
else:
|
||||
set_factoid(key, value, sender)
|
||||
await bot.api.send_text_message(room_id, f"💡 Learned '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
# !no, <key> is <value> (same as change)
|
||||
if cmd == "no":
|
||||
if not args:
|
||||
await bot.api.send_text_message(room_id, "Usage: !no, <key> is <value>")
|
||||
return
|
||||
rest = " ".join(args).lstrip(",").strip()
|
||||
m = re.match(r'^(.+?)\s+is\s+(.+)$', rest, re.IGNORECASE)
|
||||
if not m:
|
||||
await bot.api.send_text_message(room_id, "Usage: !no, <key> is <value>")
|
||||
return
|
||||
key = _normalise_key(m.group(1).strip())
|
||||
value = m.group(2).strip()
|
||||
existing = get_factoid(key)
|
||||
if not existing:
|
||||
await bot.api.send_text_message(room_id, f"❌ No factoid for '{html_escape(key)}'. Use !learn to create one.")
|
||||
return
|
||||
if existing.get("locked_by") and not is_admin:
|
||||
await bot.api.send_text_message(room_id, f"🔒 '{html_escape(key)}' is locked by {existing['locked_by']}.")
|
||||
return
|
||||
set_factoid(key, value, sender)
|
||||
await bot.api.send_text_message(room_id, f"✏️ Changed '{html_escape(key)}'.")
|
||||
return
|
||||
|
||||
async def _send_help(room, bot):
|
||||
help_text = """
|
||||
<details>
|
||||
<summary><strong>📚 Factoids Plugin Help</strong></summary>
|
||||
<p>
|
||||
<strong>Commands:</strong><br>
|
||||
<code>!fact <key></code> – retrieve a factoid<br>
|
||||
<code><key>?</code> – ask for a factoid inline<br>
|
||||
<code>!learn <key> is <value></code> – teach the bot<br>
|
||||
<code>!forget <key></code> – delete a factoid<br>
|
||||
<code>!also <key> is <value></code> – append to a factoid<br>
|
||||
<code>!no, <key> is <value></code> – replace a factoid<br>
|
||||
<code>!fact change <key> is <value></code> – change a factoid<br>
|
||||
<code>!fact search <query></code> – search factoids<br>
|
||||
<code>!fact info <key></code> – show metadata<br>
|
||||
<code>!fact random</code> – random factoid<br>
|
||||
<code>!fact stats</code> – statistics<br>
|
||||
<code>!fact list [glob]</code> – list keys<br>
|
||||
<code>!fact lock|unlock <key></code> – admin only<br>
|
||||
<br>
|
||||
<strong>Special values:</strong><br>
|
||||
<code><reply> text</code> – replies with just "text"<br>
|
||||
<code><action> text</code> – replies as /me<br>
|
||||
<code>a | b | c</code> – picks one at random
|
||||
</p>
|
||||
</details>
|
||||
"""
|
||||
await bot.api.send_markdown_message(room.room_id, help_text)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin Metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "Funguy Bot"
|
||||
__description__ = "Factoids – infobot/supybot-style factoid storage and retrieval"
|
||||
__help__ = """
|
||||
<details>
|
||||
<summary><strong>!fact</strong> – Factoids (infobot/supybot clone)</summary>
|
||||
<ul>
|
||||
<li><code>!fact <key></code> – retrieve a factoid</li>
|
||||
<li><code><key>?</code> – ask for a factoid inline</li>
|
||||
<li><code>!learn <key> is <value></code> – teach</li>
|
||||
<li><code>!forget <key></code> – delete</li>
|
||||
<li><code>!also <key> is <value></code> – append</li>
|
||||
<li><code>!no, <key> is <value></code> – replace</li>
|
||||
<li><code>!fact search <query></code> – search</li>
|
||||
<li><code>!fact random</code> / <code>!fact stats</code> / <code>!fact list</code></li>
|
||||
<li>Special tags: <code><reply></code>, <code><action></code>, pipe (<code>|</code>) for random</li>
|
||||
</ul>
|
||||
</details>
|
||||
"""
|
||||
Reference in New Issue
Block a user