refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to Trigger(handlers) → Service(services) → Model(repository/models) + utils): - Split monolithic __init__.py into handlers/services/utils across dailywife, deer_pipe, dice, galgame_card, helldivers_tools, huoziyinshua, learning_chat, makeaquote, mc_server_status, ncm_saying, picfinder_take, picstatus, random_jm_code, regif, steam_info, video_analysis, group_tools - Move static assets under res/: deer_pipe font/img, makeaquote font, helldivers img/templates, huoziyinshua HuoZiYinShua - Add config.py + register_config_items to ncm_saying, random_jm_code, group_tools; learning_chat unified config bridge - Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp - Disable brash_general_supercredits_tools (stub comment only) - bot.py: optional stdout/stderr redirect to log file for Web log viewer, force ANSI colorize on non-TTY sinks - Move runtime data (jm_code.json) out of plugin dir into hexi/data - Docs: plugin-audit-report.md; README reflects removed plugins Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# helldivers_tools —— Service 层。
|
||||
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import random
|
||||
from nonebot import logger
|
||||
from ..utils.equipment_renderer import render_equipment_card
|
||||
from .equipment_store import list_stratagems
|
||||
|
||||
|
||||
basic_path = os.path.dirname(os.path.dirname(__file__))
|
||||
save_path = os.path.join(basic_path, "temp")
|
||||
img_path = os.path.join(basic_path, "res", "img")
|
||||
data_path = os.path.join(basic_path, "data")
|
||||
|
||||
|
||||
async def get_random_equipment(count):
|
||||
data = list_stratagems()
|
||||
indices = select_random_equipment(len(data), count)
|
||||
selected_equipment = [data[i] for i in indices]
|
||||
|
||||
return await render_equipment_card(selected_equipment)
|
||||
|
||||
|
||||
async def get_equipment_by_combination(type_combination):
|
||||
data = list_stratagems()
|
||||
|
||||
equipment_by_type = categorize_equipment_by_type(data)
|
||||
selected_equipment = select_equipment_by_type(equipment_by_type, type_combination)
|
||||
|
||||
logger.debug(f"根据组合选择的装备: {selected_equipment}")
|
||||
return await render_equipment_card(selected_equipment)
|
||||
|
||||
|
||||
def categorize_equipment_by_type(data):
|
||||
equipment_by_type = {}
|
||||
for item in data:
|
||||
equip_type = item['type']
|
||||
if equip_type not in equipment_by_type:
|
||||
equipment_by_type[equip_type] = []
|
||||
equipment_by_type[equip_type].append(item)
|
||||
return equipment_by_type
|
||||
|
||||
|
||||
def select_equipment_by_type(equipment_by_type, type_combination):
|
||||
selected_equipment = []
|
||||
backpack_count = 0
|
||||
|
||||
for equip_type, count in type_combination.items():
|
||||
if equip_type in equipment_by_type:
|
||||
available_items = equipment_by_type[equip_type]
|
||||
random.shuffle(available_items)
|
||||
selected = 0
|
||||
for item in available_items:
|
||||
if selected < count:
|
||||
if item['backpack'] == 1 and backpack_count >= 1:
|
||||
continue
|
||||
selected_equipment.append(item)
|
||||
selected += 1
|
||||
if item['backpack'] == 1:
|
||||
backpack_count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return selected_equipment
|
||||
|
||||
|
||||
def select_random_equipment(max_equipment, count):
|
||||
return random.sample(range(max_equipment), count)
|
||||
@@ -0,0 +1,321 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""战备(SQLite)数据存取层。
|
||||
|
||||
把原来写在 data/equipment.json 的战备数据迁移到 SQLite,供
|
||||
- equipment.py 随机/组合抽取
|
||||
- 战备管理后台(stratagem_admin.py) 的读/写/导入/导出
|
||||
|
||||
三层语义弱化为单表 + is_custom 标记:
|
||||
- 从 data/equipment.json 首次播种的记录 is_custom=0(基础库)
|
||||
- 管理后台手动新增的记录 is_custom=1
|
||||
- 手动编辑都是直接改行,不另建覆盖层(当前不做 API 同步,因此单表即可)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from nonebot import logger
|
||||
|
||||
basic_path = Path(__file__).resolve().parents[1]
|
||||
DATA_DIR = basic_path / "data"
|
||||
DB_PATH = DATA_DIR / "equipment.db"
|
||||
SEED_PATH = DATA_DIR / "archive" / "equipment.json"
|
||||
|
||||
|
||||
def _connect() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _load_seed() -> list[dict[str, Any]]:
|
||||
"""读取随包的 equipment.json 作为初始基础库。"""
|
||||
if not SEED_PATH.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(SEED_PATH.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, list) else []
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"读取 equipment.json 失败,跳过播种: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""建表(WAL 便于网页与 bot 并发读写),空表时从 equipment.json 播种。"""
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with _connect() as conn:
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS stratagems (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
name_en TEXT NOT NULL DEFAULT '',
|
||||
type TEXT NOT NULL DEFAULT 'blue',
|
||||
backpack INTEGER NOT NULL DEFAULT 0,
|
||||
icon TEXT NOT NULL DEFAULT '',
|
||||
alias TEXT NOT NULL DEFAULT '[]',
|
||||
is_custom INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
UNIQUE(name)
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur = conn.execute("SELECT COUNT(*) AS n FROM stratagems")
|
||||
if cur.fetchone()["n"] == 0:
|
||||
now = _now()
|
||||
for item in _load_seed():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = (item.get("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
alias = item.get("alias") or []
|
||||
if isinstance(alias, str):
|
||||
alias = [a.strip() for a in alias.split(",") if a.strip()]
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO stratagems
|
||||
(name, name_en, type, backpack, icon, alias, is_custom, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
item.get("name_en") or "",
|
||||
item.get("type") or "blue",
|
||||
1 if item.get("backpack") else 0,
|
||||
item.get("path") or item.get("icon") or "",
|
||||
json.dumps(alias, ensure_ascii=False),
|
||||
0,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _row_to_item(row: sqlite3.Row) -> dict[str, Any]:
|
||||
try:
|
||||
alias = json.loads(row["alias"] or "[]")
|
||||
if not isinstance(alias, list):
|
||||
alias = []
|
||||
except Exception: # noqa: BLE001
|
||||
alias = []
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"name_en": row["name_en"] or "",
|
||||
"type": row["type"] or "blue",
|
||||
"backpack": bool(row["backpack"]),
|
||||
"path": row["icon"] or "",
|
||||
"icon": row["icon"] or "",
|
||||
"alias": alias,
|
||||
"is_custom": bool(row["is_custom"]),
|
||||
}
|
||||
|
||||
|
||||
def _normalize_alias(value: Any) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
parts = [v.strip() for v in value.replace(",", ",").replace("、", ",").replace("\n", ",").split(",") if v.strip()]
|
||||
elif isinstance(value, list):
|
||||
parts = [v.strip() for v in value if isinstance(v, str) and v.strip()]
|
||||
else:
|
||||
parts = []
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for p in parts:
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def list_stratagems() -> list[dict[str, Any]]:
|
||||
init_db()
|
||||
with _connect() as conn:
|
||||
rows = conn.execute("SELECT * FROM stratagems ORDER BY id").fetchall()
|
||||
return [_row_to_item(r) for r in rows]
|
||||
|
||||
|
||||
def count_stratagems() -> int:
|
||||
init_db()
|
||||
with _connect() as conn:
|
||||
return int(conn.execute("SELECT COUNT(*) AS n FROM stratagems").fetchone()["n"])
|
||||
|
||||
|
||||
def list_stratagems_page(page: int = 1, page_size: int = 20) -> tuple[list[dict[str, Any]], int]:
|
||||
init_db()
|
||||
total = count_stratagems()
|
||||
offset = (page - 1) * page_size
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM stratagems ORDER BY id LIMIT ? OFFSET ?",
|
||||
(page_size, offset),
|
||||
).fetchall()
|
||||
return [_row_to_item(r) for r in rows], total
|
||||
|
||||
|
||||
def get_stratagem(seed: int) -> dict[str, Any] | None:
|
||||
init_db()
|
||||
with _connect() as conn:
|
||||
row = conn.execute("SELECT * FROM stratagems WHERE id=?", (seed,)).fetchone()
|
||||
return _row_to_item(row) if row else None
|
||||
|
||||
|
||||
def create_stratagem(item: dict[str, Any]) -> dict[str, Any]:
|
||||
init_db()
|
||||
name = (item.get("name") or "").strip()
|
||||
if not name:
|
||||
raise ValueError("name 不能为空")
|
||||
now = _now()
|
||||
with _connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO stratagems (name, name_en, type, backpack, icon, alias, is_custom, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
item.get("name_en") or "",
|
||||
item.get("type") or "blue",
|
||||
1 if item.get("backpack") else 0,
|
||||
item.get("icon") or item.get("path") or "",
|
||||
json.dumps(_normalize_alias(item.get("alias")), ensure_ascii=False),
|
||||
1,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
new_id = cursor.lastrowid
|
||||
row = get_stratagem(new_id)
|
||||
assert row is not None
|
||||
return row
|
||||
|
||||
|
||||
def update_stratagem(seed: int, fields: dict[str, Any]) -> dict[str, Any] | None:
|
||||
init_db()
|
||||
if not fields:
|
||||
return get_stratagem(seed)
|
||||
# 只允许安全字段
|
||||
allowed = {"name", "name_en", "type", "backpack", "icon", "path", "alias"}
|
||||
updates: dict[str, Any] = {}
|
||||
for k in allowed:
|
||||
if k in fields:
|
||||
updates[k] = fields[k]
|
||||
if "alias" in updates:
|
||||
updates["alias"] = json.dumps(_normalize_alias(updates["alias"]), ensure_ascii=False)
|
||||
if "path" in updates and "icon" not in updates:
|
||||
updates["icon"] = updates.pop("path")
|
||||
elif "icon" in updates:
|
||||
updates.pop("path", None)
|
||||
if not updates:
|
||||
return get_stratagem(seed)
|
||||
updates["updated_at"] = _now()
|
||||
sets = ", ".join(f"{k}=?" for k in updates)
|
||||
params = list(updates.values()) + [seed]
|
||||
with _connect() as conn:
|
||||
cur = conn.execute(f"UPDATE stratagems SET {sets} WHERE id=?", params)
|
||||
conn.commit()
|
||||
if cur.rowcount == 0:
|
||||
return None
|
||||
return get_stratagem(seed)
|
||||
|
||||
|
||||
def delete_stratagem(seed: int) -> bool:
|
||||
init_db()
|
||||
with _connect() as conn:
|
||||
cur = conn.execute("DELETE FROM stratagems WHERE id=?", (seed,))
|
||||
conn.commit()
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
def import_items(items: list[dict[str, Any]]) -> dict[str, int]:
|
||||
"""按 id(存在则更新) 或 name(存在则更新) 合并导入;新增的记录标记为 custom。
|
||||
|
||||
返回 {added, updated, skipped}
|
||||
"""
|
||||
init_db()
|
||||
added = 0
|
||||
updated = 0
|
||||
skipped = 0
|
||||
now = _now()
|
||||
with _connect() as conn:
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
skipped += 1
|
||||
continue
|
||||
name = (item.get("name") or "").strip()
|
||||
if not name:
|
||||
skipped += 1
|
||||
continue
|
||||
seed = item.get("id")
|
||||
existing: dict[str, Any] | None = None
|
||||
if isinstance(seed, int):
|
||||
row = conn.execute("SELECT * FROM stratagems WHERE id=?", (seed,)).fetchone()
|
||||
existing = _row_to_item(row) if row else None
|
||||
if existing is None:
|
||||
row = conn.execute("SELECT * FROM stratagems WHERE name=?", (name,)).fetchone()
|
||||
existing = _row_to_item(row) if row else None
|
||||
if existing is None:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO stratagems (name, name_en, type, backpack, icon, alias, is_custom, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
item.get("name_en") or "",
|
||||
item.get("type") or "blue",
|
||||
1 if item.get("backpack") else 0,
|
||||
item.get("icon") or item.get("path") or "",
|
||||
json.dumps(_normalize_alias(item.get("alias")), ensure_ascii=False),
|
||||
1,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
added += 1
|
||||
else:
|
||||
fields = {
|
||||
"name": name,
|
||||
"name_en": (item.get("name_en") or existing.get("name_en") or ""),
|
||||
"type": (item.get("type") or existing.get("type") or "blue"),
|
||||
"backpack": bool(item.get("backpack", existing.get("backpack"))),
|
||||
"icon": (item.get("icon") or item.get("path") or existing.get("icon") or ""),
|
||||
"alias": item.get("alias", existing.get("alias")),
|
||||
}
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE stratagems
|
||||
SET name=?, name_en=?, type=?, backpack=?, icon=?, alias=?, updated_at=?
|
||||
WHERE id=?
|
||||
""",
|
||||
(
|
||||
fields["name"],
|
||||
fields["name_en"],
|
||||
fields["type"],
|
||||
1 if fields["backpack"] else 0,
|
||||
fields["icon"],
|
||||
json.dumps(_normalize_alias(fields["alias"]), ensure_ascii=False),
|
||||
now,
|
||||
existing["id"],
|
||||
),
|
||||
)
|
||||
updated += 1
|
||||
conn.commit()
|
||||
return {"added": added, "updated": updated, "skipped": skipped}
|
||||
|
||||
|
||||
def export_items() -> list[dict[str, Any]]:
|
||||
return list_stratagems()
|
||||
@@ -0,0 +1,168 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, Awaitable, Callable
|
||||
|
||||
import httpx
|
||||
from nonebot import logger
|
||||
|
||||
from ..config import config
|
||||
|
||||
# 简单的进程内缓存 {key: (过期时间戳, 数据)},用于聚合战况时只打一次接口
|
||||
_cache: dict[str, tuple[float, Any]] = {}
|
||||
|
||||
|
||||
def _headers(language: str | None = None) -> dict[str, str]:
|
||||
# 两个 X-Super-* 头必须都存在,否则公共实例(ValidateClients)会直接返回 400
|
||||
lang = language or config.hd2_api_language
|
||||
return {
|
||||
"Accept-Language": f"{lang},en-US;q=0.5",
|
||||
"X-Super-Client": config.hd2_super_client,
|
||||
"X-Super-Contact": config.hd2_super_contact or config.hd2_super_client,
|
||||
}
|
||||
|
||||
|
||||
def _base_url() -> str:
|
||||
return config.hd2_api_base_url.rstrip("/")
|
||||
|
||||
|
||||
async def _get_json(url: str, language: str | None = None) -> Any:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
resp = await client.get(url, headers=_headers(language))
|
||||
if resp.status_code != 200:
|
||||
raise httpx.HTTPStatusError(
|
||||
f"{resp.status_code} {resp.reason_phrase} for {url}\n{resp.text[:400]}",
|
||||
request=resp.request,
|
||||
response=resp,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def _cached(key: str, fetcher: Callable[[], Awaitable[Any]]) -> Any:
|
||||
now = time.time()
|
||||
hit = _cache.get(key)
|
||||
if hit and hit[0] > now:
|
||||
return hit[1]
|
||||
data = await fetcher()
|
||||
_cache[key] = (now + config.hd2_cache_ttl, data)
|
||||
return data
|
||||
|
||||
|
||||
# ---- 原始接口 fetchers(不带缓存,供各 get_* 包装) ----
|
||||
|
||||
async def _fetch_war() -> Any:
|
||||
return await _get_json(f"{_base_url()}/api/v1/war")
|
||||
|
||||
|
||||
async def _fetch_major_orders() -> Any:
|
||||
return await _get_json(f"{_base_url()}/api/v1/assignments")
|
||||
|
||||
|
||||
async def _fetch_campaigns(language: str | None) -> Any:
|
||||
return await _get_json(f"{_base_url()}/api/v1/campaigns", language)
|
||||
|
||||
|
||||
async def _fetch_planets(language: str | None) -> Any:
|
||||
return await _get_json(f"{_base_url()}/api/v1/planets", language)
|
||||
|
||||
|
||||
async def _fetch_planet_detail(index: int, language: str | None) -> Any:
|
||||
return await _get_json(f"{_base_url()}/api/v1/planets/{index}", language)
|
||||
|
||||
|
||||
async def _fetch_raw_status() -> Any:
|
||||
# 原始接口一次返回全部:planetActiveEffects / spaceStations / planetStatus 等
|
||||
return await _get_json(f"{_base_url()}/raw/api/WarSeason/801/Status")
|
||||
|
||||
|
||||
# ---- 对外封装(各自带缓存,可单独调用) ----
|
||||
|
||||
async def get_war() -> Any:
|
||||
"""星系战况概要(年代起止/阵营/影响倍率等)。"""
|
||||
return await _cached("war", _fetch_war)
|
||||
|
||||
|
||||
async def get_major_orders() -> Any:
|
||||
"""重要指令(最高指令 / assignments,含任务目标、奖励、阵营)。"""
|
||||
return await _cached("assignments", _fetch_major_orders)
|
||||
|
||||
|
||||
async def get_campaigns(language: str | None = None) -> Any:
|
||||
"""当前参与战役的星球列表(含每颗参战星球的进度/事件/危害等)。"""
|
||||
key = f"campaigns:{language or ''}"
|
||||
return await _cached(key, lambda: _fetch_campaigns(language))
|
||||
|
||||
|
||||
async def get_planets(language: str | None = None) -> Any:
|
||||
"""全星球表(每颗星的 biome/hazards/event/currentOwner/statistics 等)。"""
|
||||
key = f"planets:{language or ''}"
|
||||
return await _cached(key, lambda: _fetch_planets(language))
|
||||
|
||||
|
||||
async def get_planet_detail(index: int, language: str | None = None) -> Any:
|
||||
"""单星球详细情况(biome/hazards/event/statistics/attacking/regions 等)。"""
|
||||
key = f"planet:{index}:{language or ''}"
|
||||
return await _cached(key, lambda: _fetch_planet_detail(index, language))
|
||||
|
||||
|
||||
async def get_raw_status() -> Any:
|
||||
"""原始 WarSeason/801/Status(planetStatus/planetActiveEffects/spaceStations...)。"""
|
||||
return await _cached("raw_status", _fetch_raw_status)
|
||||
|
||||
|
||||
async def get_space_stations() -> list[Any]:
|
||||
"""空间站(民主空间站 DSS 等)。"""
|
||||
raw = await get_raw_status()
|
||||
return list((raw or {}).get("spaceStations") or [])
|
||||
|
||||
|
||||
async def get_planet_effects(index: int | None = None) -> dict[int, list[int]]:
|
||||
"""星球当前生效效果 {星球index: [galacticEffectId...]};传 index 时只返回该星球的集合。"""
|
||||
raw = await get_raw_status()
|
||||
effects: dict[int, list[int]] = {}
|
||||
for item in (raw or {}).get("planetActiveEffects") or []:
|
||||
if (
|
||||
isinstance(item, dict)
|
||||
and item.get("index") is not None
|
||||
and item.get("galacticEffectId") is not None
|
||||
):
|
||||
effects.setdefault(item["index"], []).append(item["galacticEffectId"])
|
||||
if index is not None:
|
||||
return {index: effects.get(index, [])}
|
||||
return effects
|
||||
|
||||
|
||||
# ---- 聚合入口(渲染战报用;也可改为按需调用各 get_* 组装) ----
|
||||
|
||||
async def get_briefing_data() -> dict[str, Any]:
|
||||
"""并发拉取战报所需的分组数据,带缓存,返回聚合结果。"""
|
||||
war, assignments, campaigns, planets, raw_status = await asyncio.gather(
|
||||
get_war(),
|
||||
get_major_orders(),
|
||||
get_campaigns(),
|
||||
get_planets(language="en-US"), # 用英文星名匹配高清地形图;currentOwner 等不受语言影响
|
||||
get_raw_status(),
|
||||
)
|
||||
|
||||
logger.info("已获取 helldivers 战况数据")
|
||||
planet_english = {
|
||||
p["index"]: p.get("name")
|
||||
for p in (planets or [])
|
||||
if p.get("index") is not None and p.get("name")
|
||||
}
|
||||
# 星球当前效果:{星球index: [galacticEffectId...]}
|
||||
planet_effects: dict[int, list[int]] = {}
|
||||
for item in (raw_status or {}).get("planetActiveEffects") or []:
|
||||
if isinstance(item, dict) and item.get("index") is not None and item.get("galacticEffectId") is not None:
|
||||
planet_effects.setdefault(item["index"], []).append(item["galacticEffectId"])
|
||||
# 空间站(民主空间站 DSS 等):独立展示,不混进普通星球效果
|
||||
space_stations = (raw_status or {}).get("spaceStations") or []
|
||||
return {
|
||||
"war": war,
|
||||
"assignments": assignments,
|
||||
"campaigns": campaigns,
|
||||
"planets": planets,
|
||||
"planet_english": planet_english,
|
||||
"planet_effects": planet_effects,
|
||||
"space_stations": space_stations,
|
||||
"fetched_at": int(time.time()),
|
||||
}
|
||||
Reference in New Issue
Block a user