# -*- coding: utf-8 -*- """Native API (127.0.0.1:18111) client for the DNF90 GM tool. 本文件依据服务端源码 (go-server/internal/app/dnf90/gm_tool_admin.go、 gm_admin.go、gm_tool_compat.go 以及 internal/services/dnfbridge/*) 精确实现 全部 GM 端点,字段名/类型/校验规则均与源码一致。 两类端点: - 读端点 GET /dnf/gm/... (返回 {"success":true,"data":...}) - 写端点 POST /dnf/gm/... 或 /local/dnf-gm/... (需要 X-Admin-Confirm) 认证头: X-Admin-Token : adminToken(来自 instance.json) X-Admin-Confirm: 写操作设为 "1" """ import json import os import uuid import urllib.request import urllib.error from urllib.parse import urlencode class NativeError(Exception): pass class NativeClient(object): def __init__(self, base_url="http://127.0.0.1:18111", token=""): self.base_url = base_url.rstrip("/") self.token = token # ---- low-level HTTP ------------------------------------------ def _request(self, method, path, body=None, confirm=False): url = self.base_url + path data = None headers = {"Accept": "application/json"} if self.token: headers["X-Admin-Token"] = self.token if confirm: headers["X-Admin-Confirm"] = "1" headers["X-Admin-Idempotency-Key"] = uuid.uuid4().hex if body is not None: data = json.dumps(body, ensure_ascii=False).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=15) as resp: raw = resp.read() if not raw: return {} return json.loads(raw.decode("utf-8")) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", "replace") # 尝试提取服务端返回的 error 字段,给出更友好的提示 try: parsed = json.loads(raw) msg = parsed.get("error") or raw[:200] except Exception: msg = raw[:200] raise NativeError("HTTP %d: %s" % (e.code, msg)) except urllib.error.URLError as e: raise NativeError("连接失败: %s" % e.reason) def _get(self, path): return self._request("GET", path) def _post(self, path, body=None, confirm=True): return self._request("POST", path, body=body, confirm=confirm) def _unwrap(self, raw): """取服务端统一响应里的 data 字段。""" if isinstance(raw, dict) and "data" in raw: return raw["data"] return raw # ---- 状态 / 账号 / 角色 -------------------------------------- def ping(self): return self._get("/dnf/gm/status") def list_accounts(self, query="", limit=50): qs = urlencode({"q": query, "limit": limit}) try: return self._unwrap(self._get("/dnf/gm/accounts?" + qs)) or [] except NativeError: return [] def get_account(self, account_id): return self._unwrap(self._get("/dnf/gm/accounts/%s" % account_id)) def set_account_value(self, account_id, field, value): """POST /dnf/gm/accounts/{account_id}/value {field, value:uint64}""" return self._unwrap(self._post( "/dnf/gm/accounts/%s/value" % account_id, {"field": field, "value": int(value)})) def list_characters(self, account_id=None): """角色按账号枚举:/dnf/gm/accounts/{id}/characters。 无 account_id 时遍历所有账号。""" account_ids = [] if account_id: account_ids = [account_id] else: accs = self.list_accounts() account_ids = [a.get("account_id") for a in accs if isinstance(a, dict) and a.get("account_id")] out = [] for aid in account_ids: try: raw = self._unwrap(self._get("/dnf/gm/accounts/%s/characters" % aid)) except NativeError: continue if not isinstance(raw, list): raw = [raw] if raw else [] for c in raw: if not isinstance(c, dict): continue cid = c.get("character_id") or c.get("id") out.append({ "character_id": str(cid), "account_id": c.get("account_id", aid), "name": c.get("name", ""), "job": c.get("job", "0"), "level": c.get("level", 0), "gold": c.get("gold", 0), "profession_name": c.get("profession_name", ""), "grow_type": c.get("grow_type", 0), "first_grow": c.get("first_grow", 0), "awakening": c.get("awakening", 0), "online": c.get("online", False), "raw": c, }) return out def get_character(self, character_id): raw = self._unwrap(self._get("/dnf/gm/characters/%s" % character_id)) if not isinstance(raw, dict): return raw stats = {k: v for k, v in raw.items() if k not in ("character_id", "account_id", "name", "job", "level", "slot", "profession_name", "grow_type", "first_grow", "awakening", "online", "gold", "cera")} if "gold" in raw: stats["gold"] = raw["gold"] out = { "character_id": str(raw.get("character_id") or character_id), "account_id": raw.get("account_id", ""), "name": raw.get("name", ""), "job": raw.get("job", "0"), "level": raw.get("level", 0), "gold": raw.get("gold", 0), "profession_name": raw.get("profession_name", ""), "grow_type": raw.get("grow_type", 0), "first_grow": raw.get("first_grow", 0), "awakening": raw.get("awakening", 0), "online": raw.get("online", False), "stats": stats, } return out def get_character_detail(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/detail" % character_id)) def get_inventory(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/inventory" % character_id)) def get_equipment(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/equipment-slots" % character_id)) def get_mailbox(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/mailbox" % character_id)) def get_account_cubes(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/account-cubes" % character_id)) # ---- 物品目录 / 搜索 ----------------------------------------- def get_item_detail(self, item_id): return self._unwrap(self._get("/dnf/gm/items/%s/detail" % item_id)) def get_item_mail_options(self, item_id, mode="", query="", rarity=-1, limit=40, offset=0): qs = urlencode({"mode": mode, "q": query, "rarity": rarity, "limit": limit, "offset": offset}) return self._unwrap(self._get("/dnf/gm/items/%s/mail-options?%s" % (item_id, qs))) def get_item_categories(self): return self._unwrap(self._get("/dnf/gm/items/categories")) def search_items(self, query="", kind="", tag="", segment="", group="", job="", min_level=0, max_level=0, rarity=-1, expiration="", offset=0, limit=50): """GET /dnf/gm/items/page —— 完整筛选参数(与源码 GMItemQuery 一致)。 返回归一化结构 {items:[...], total, limit, offset}。""" params = { "q": query, "kind": kind, "tag": tag, "segment": segment, "group": group, "job": job, "min_level": min_level, "max_level": max_level, "rarity": rarity, "expiration": expiration, "offset": offset, "limit": limit, } # 去掉空值参数,避免干扰 params = {k: v for k, v in params.items() if v not in ("", -1, 0) or k in ("offset", "limit", "q", "rarity")} qs = urlencode(params) raw = self._unwrap(self._get("/dnf/gm/items/page?" + qs)) if isinstance(raw, dict) and "items" in raw: return raw return {"items": raw if isinstance(raw, list) else [], "total": 0, "limit": limit, "offset": offset} def search_avatar_sets(self, query="", job="", limit=24, offset=0): qs = urlencode({"q": query, "job": job, "limit": limit, "offset": offset}) return self._unwrap(self._get("/dnf/gm/avatar-sets?" + qs)) # ---- 物品发放 ------------------------------------------------- # 注意:服务端有两个发放端点: # 1) POST /local/dnf-gm/item/grant —— 直接进背包(服务端决定格位) # 请求体 {character_id:string, item_id:uint32, count:uint32} # 2) POST /local/dnf-gm/item/grant-options —— 带强化/增幅/锻造/品质/耐久/附魔/徽章 # 3) POST /local/dnf-gm/item/grant-with-emblems —— 带附魔宝珠 + 徽章(装备/时装) # 4) POST /dnf/gm/characters/{id}/mail —— 邮件发放(带完整选项) def grant_item(self, character_id, item_id, count=1): body = {"character_id": str(character_id), "item_id": int(item_id), "count": int(count)} return self._unwrap(self._post("/local/dnf-gm/item/grant", body)) def grant_item_options(self, character_id, item_id, count=1, reinforce=0, amplify_type=0, forging=0, equipment_quality="", durability=None, expire_days=0, emblem_item_ids=None, enchant_bead_item_id=0): """带强化/增幅/锻造/品质/耐久/附魔的发放。 校验范围(源码 gm_tool_items_extra.go): reinforce 0..31, amplify_type 0..4, forging 0..8, expire_days 0..3650""" body = {"character_id": str(character_id), "item_id": int(item_id), "count": int(count)} if reinforce: body["reinforce"] = int(reinforce) if amplify_type: body["amplify_type"] = int(amplify_type) if forging: body["forging"] = int(forging) if equipment_quality: body["equipment_quality"] = equipment_quality if durability is not None: body["durability"] = int(durability) if expire_days: body["expire_days"] = int(expire_days) if emblem_item_ids: body["emblem_item_ids"] = [int(x) for x in emblem_item_ids] if enchant_bead_item_id: body["enchant_bead_item_id"] = int(enchant_bead_item_id) return self._unwrap(self._post("/local/dnf-gm/item/grant-options", body)) def grant_item_with_emblems(self, character_id, item_id, enchant_bead_item_id=0, enchant_upgrade_count=None, emblems=None, open_sockets=None): """带附魔宝珠 + 徽章发放(仅装备/时装)。 emblems: [{socket_index:int, emblem_item_id:int, emblem_slot:int}, ...]""" body = {"character_id": str(character_id), "item_id": int(item_id)} if enchant_bead_item_id: body["enchant_bead_item_id"] = int(enchant_bead_item_id) if enchant_upgrade_count is not None: body["enchant_upgrade_count"] = int(enchant_upgrade_count) if emblems: body["emblems"] = emblems if open_sockets is not None: body["open_sockets"] = int(open_sockets) return self._unwrap(self._post("/local/dnf-gm/item/grant-with-emblems", body)) def send_item_mail(self, character_id, item_id, count=1, title="", body="", expire_days=0, reinforce=0, amplify_type=0, equipment_quality="", forging=0, emblem_item_ids=None, enchant_bead_item_id=0): """POST /dnf/gm/characters/{id}/mail —— 邮件发放(完整选项)。""" req = {"item_id": int(item_id), "count": int(count)} if title: req["title"] = title if body: req["body"] = body if expire_days: req["expire_days"] = int(expire_days) if reinforce: req["reinforce"] = int(reinforce) if amplify_type: req["amplify_type"] = int(amplify_type) if equipment_quality: req["equipment_quality"] = equipment_quality if forging: req["forging"] = int(forging) if emblem_item_ids: req["emblem_item_ids"] = [int(x) for x in emblem_item_ids] if enchant_bead_item_id: req["enchant_bead_item_id"] = int(enchant_bead_item_id) return self._unwrap(self._post( "/dnf/gm/characters/%s/mail" % character_id, req)) def delete_item(self, character_id, collection="", list_type=None, slot=None, entry_key=""): """POST /local/dnf-gm/item/delete —— 删除背包行或穿戴装备行。 collection: "slots"(背包,默认) 或 "wear"(穿戴装备)""" req = {"character_id": str(character_id)} if collection: req["collection"] = collection if entry_key: req["entry_key"] = entry_key else: req["list_type"] = int(list_type) req["slot"] = int(slot) return self._unwrap(self._post("/local/dnf-gm/item/delete", req)) # ---- 属性修改 ------------------------------------------------- def set_gold(self, character_id, amount): """POST /dnf/gm/characters/{id}/gold {value:int64}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/gold" % str(character_id), {"value": int(amount)})) def set_level(self, character_id, level): """POST /dnf/gm/characters/{id}/level {level:int}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/level" % str(character_id), {"level": int(level)})) def set_skill_bonus(self, character_id, bonus_sp, bonus_tp): """POST /dnf/gm/characters/{id}/skill-points {bonus_sp, bonus_tp:int64}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/skill-points" % str(character_id), {"bonus_sp": int(bonus_sp), "bonus_tp": int(bonus_tp)})) def get_career(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/career" % character_id)) def set_career(self, character_id, job, first, awakening): """POST /dnf/gm/characters/{id}/career {job:int, first:int, awakening:int} 这是修改职业/转职的正确接口(旧 /local/dnf-gm/character/job 语义不同)。""" return self._unwrap(self._post( "/dnf/gm/characters/%s/career" % str(character_id), {"job": int(job), "first": int(first), "awakening": int(awakening)})) def set_cera(self, account_id, amount=None, value=None, currency=""): """POST /local/dnf-gm/account/cera {account_id, currency, amount(增量) 或 value(覆写)} currency: cera / token_cera / happy_token_cera,默认 cera""" req = {"account_id": str(account_id)} if currency: req["currency"] = currency if amount is not None: req["amount"] = int(amount) elif value is not None: req["value"] = int(value) return self._unwrap(self._post("/local/dnf-gm/account/cera", req)) def adjust_revive_coin(self, character_id, amount=None, value=None, source=""): """POST /local/dnf-gm/character/revive-coin amount(增量) 或 value(覆写),二者互斥""" req = {"character_id": str(character_id)} if amount is not None: req["amount"] = int(amount) elif value is not None: req["value"] = int(value) if source: req["source"] = source return self._unwrap(self._post("/local/dnf-gm/character/revive-coin", req)) def query_wallet(self, character_id="", character_name="", currency=""): """POST /local/dnf-gm/wallet/query""" req = {} if character_id: req["character_id"] = str(character_id) if character_name: req["character_name"] = character_name if currency: req["currency"] = currency return self._unwrap(self._post("/local/dnf-gm/wallet/query", req)) def update_wallet(self, character_id="", character_name="", currency="", mode="", amount=0): """POST /local/dnf-gm/wallet/update currency + mode + amount;mode 语义见源码 gm_admin_state.go""" req = {} if character_id: req["character_id"] = str(character_id) if character_name: req["character_name"] = character_name if currency: req["currency"] = currency if mode: req["mode"] = mode if amount: req["amount"] = int(amount) return self._unwrap(self._post("/local/dnf-gm/wallet/update", req)) # ---- 背包 / 装备 --------------------------------------------- def inventory_list(self, character_id): """GET /local/dnf-gm/inventory/list —— 列角色背包全部行 + 穿戴装备行。 返回 {"character_id":..., "rows":[{collection,entry_key,item_id,count,bind,item_name},...]}""" return self._unwrap(self._get( "/local/dnf-gm/inventory/list?character_id=%s" % character_id)) def delete_inventory_item(self, character_id, container, key, count): """POST /dnf/gm/characters/{id}/inventory/delete {container, key, count}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/inventory/delete" % str(character_id), {"container": container, "key": key, "count": int(count)})) def clear_inventory(self, character_id, container="", keep_wallet=True, collection=""): """POST /local/dnf-gm/inventory/clear {character_id, collection, keep_wallet} 工具端版本会保留主背包 0:0~0:2 的固定钱包槽(金币/复活币/点券载体)。""" req = {"character_id": str(character_id)} if collection: req["collection"] = collection req["keep_wallet"] = bool(keep_wallet) return self._unwrap(self._post("/local/dnf-gm/inventory/clear", req)) def unequip_equipment(self, character_id, key): """POST /dnf/gm/characters/{id}/equipment/unequip {key}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/equipment/unequip" % str(character_id), {"key": key})) def delete_equipment(self, character_id, key): """POST /dnf/gm/characters/{id}/equipment/delete {key}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/equipment/delete" % str(character_id), {"key": key})) def unlock_equip_slots(self, character_id, slots): """POST /dnf/gm/characters/{id}/equipment-slots/unlock {slots:[]string}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/equipment-slots/unlock" % str(character_id), {"slots": slots})) def set_account_cube(self, character_id, selection, count): """POST /dnf/gm/characters/{id}/account-cubes {selection:int, count:int64}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/account-cubes" % str(character_id), {"selection": int(selection), "count": int(count)})) # ---- 邮件 ----------------------------------------------------- def send_system_mail(self, character_id="", character_name="", title="", body="", gold=0, attachments=None): """POST /local/dnf-gm/mail {character_id/name, title, body, gold, attachments} attachments: [{item_id, count, bind, reinforce_level, amplify_level, amplify_type}]""" req = {} if character_id: req["character_id"] = str(character_id) if character_name: req["character_name"] = character_name if title: req["title"] = title if body: req["body"] = body if gold: req["gold"] = int(gold) if attachments: req["attachments"] = attachments return self._unwrap(self._post("/local/dnf-gm/mail", req)) def clear_mailbox(self, character_id="", character_name="", confirm=True): """POST /local/dnf-gm/mail/clear {character_id/name, confirm}""" req = {"confirm": bool(confirm)} if character_id: req["character_id"] = str(character_id) if character_name: req["character_name"] = character_name return self._unwrap(self._post("/local/dnf-gm/mail/clear", req)) def delete_mailbox_mails(self, character_id, mail_ids, force=False): """POST /dnf/gm/characters/{id}/mailbox/delete {mail_ids, force}""" return self._unwrap(self._post( "/dnf/gm/characters/%s/mailbox/delete" % str(character_id), {"mail_ids": mail_ids, "force": bool(force)})) # ---- 拍卖行(服务端 GM 接口,线上上架/下架)------------------ def auction_listings(self): """GET /local/dnf-gm/auction/listings —— 列出所有挂单(含过期)。 返回 {"listings":[...], "escrow_gold":int}。""" return self._unwrap(self._get("/local/dnf-gm/auction/listings")) def auction_list(self, seller_id, item_id, count, unit_price, start_price=None, buyout_price=None, duration_hours=24, grant_if_missing=False): """POST /local/dnf-gm/auction/list —— 上架。 服务端 GMAuctionListRequest 字段: seller_id / seller_name / item_id / count / unit_price / start_price(每件起拍,-1=不设) / buyout_price(每件一口价,-1=不设) / duration_hours(24 或 48) / grant_if_missing(背包不足自动发放差额)""" req = { "seller_id": str(seller_id), "item_id": int(item_id), "count": int(count), "unit_price": int(unit_price), } # start/buyout:None 或空 -> -1(服务端约定 -1 表示不设) req["start_price"] = int(start_price) if start_price not in (None, "") else -1 req["buyout_price"] = int(buyout_price) if buyout_price not in (None, "") else -1 req["duration_hours"] = int(duration_hours) if duration_hours else 24 if grant_if_missing: req["grant_if_missing"] = True return self._unwrap(self._post("/local/dnf-gm/auction/list", req)) def auction_cancel(self, listing_id): """POST /local/dnf-gm/auction/cancel —— 下架。 GMAuctionCancelRequest: {listing_id:uint64}。""" return self._unwrap(self._post( "/local/dnf-gm/auction/cancel", {"listing_id": int(listing_id)})) # ---- 任务 ----------------------------------------------------- def get_quest_overview(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/quests" % character_id)) def search_quests(self, character_id, query, limit=40): qs = urlencode({"q": query, "limit": limit}) return self._unwrap(self._get( "/dnf/gm/characters/%s/quests/search?%s" % (character_id, qs))) def get_quest_detail(self, character_id, quest_id): return self._unwrap(self._get( "/dnf/gm/characters/%s/quests/%s/detail" % (character_id, quest_id))) def _quest_mutate(self, character_id, quest_id, action): return self._unwrap(self._post( "/dnf/gm/characters/%s/quests/%s/%s" % (character_id, quest_id, action), {})) def accept_quest(self, character_id, quest_id): return self._quest_mutate(character_id, quest_id, "accept") def mark_quest_ready(self, character_id, quest_id): return self._quest_mutate(character_id, quest_id, "ready") def give_up_quest(self, character_id, quest_id): return self._quest_mutate(character_id, quest_id, "give-up") def complete_quest(self, character_id, quest_id): return self._quest_mutate(character_id, quest_id, "complete") def uncomplete_quest(self, character_id, quest_id): return self._quest_mutate(character_id, quest_id, "uncomplete") def complete_quest_batch(self, character_id, quest_ids): return self._unwrap(self._post( "/dnf/gm/characters/%s/quests/complete-batch" % character_id, {"quest_ids": [int(x) for x in quest_ids]})) # ---- 副本 ----------------------------------------------------- def get_dungeons(self, character_id): return self._unwrap(self._get("/dnf/gm/characters/%s/dungeons" % character_id)) def unlock_dungeon_difficulties(self, character_id): return self._unwrap(self._post( "/dnf/gm/characters/%s/dungeons/unlock-difficulties" % character_id, {})) def unlock_hell_challenges(self, character_id): return self._unwrap(self._post( "/dnf/gm/characters/%s/dungeons/unlock-hell" % character_id, {})) # ---- 黑钻 ----------------------------------------------------- def get_black_diamond(self, character_id="", character_name=""): qs = urlencode({"id": character_id, "name": character_name}) return self._unwrap(self._get("/local/dnf-gm/character/black-diamond?" + qs)) def activate_black_diamond(self, character_id, days=0): req = {"character_id": str(character_id)} if days: req["days"] = int(days) return self._unwrap(self._post("/local/dnf-gm/character/black-diamond", req)) # ---- 运营参数 / 爆率(Native,服务端在线) ---------------------- def get_gameplay_settings(self): """GET /dnf/gm/gameplay -> GMGameplaySettings(深渊史诗掉率等运营参数)""" return self._unwrap(self._get("/dnf/gm/gameplay")) def set_hell_drop_rate(self, percent): """POST /dnf/gm/gameplay/hell-drop-rate {percent:int}(深渊史诗掉落率百分比)""" return self._unwrap(self._post("/dnf/gm/gameplay/hell-drop-rate", {"percent": int(percent)})) def set_epic_potion_multiplier(self, percent): """POST /dnf/gm/gameplay/epic-potion-test-multiplier {percent:int} 城镇增益药剂测试倍率,0 = 关闭按 PVF 表走。""" return self._unwrap(self._post( "/dnf/gm/gameplay/epic-potion-test-multiplier", {"percent": int(percent)})) def set_fatigue_mode(self, mode): """POST /dnf/gm/gameplay/fatigue-mode {mode:str} mode: none(正常)/ free(无限不扣)/ lock(锁死高值)/ refill(一键回满)。""" return self._unwrap(self._post("/dnf/gm/gameplay/fatigue-mode", {"mode": str(mode)})) def set_burning(self, enabled): """POST /dnf/gm/gameplay/burning {enabled:bool} 燃烧疲劳:双倍消耗 + 通关经验翻倍。""" return self._unwrap(self._post("/dnf/gm/gameplay/burning", {"enabled": bool(enabled)})) def refill_fatigue(self, character_id): """POST /dnf/gm/characters/{id}/fatigue/refill 一键回满角色疲劳。""" return self._unwrap(self._post( "/dnf/gm/characters/%s/fatigue/refill" % str(character_id), {})) # ---- 宠物 ----------------------------------------------------- def update_pet(self, character_id, pet_key="", level=None, exp=None, satiety=None, clear_satiety=False, pet_index=None): req = {"character_id": str(character_id)} if pet_key: req["pet_key"] = pet_key if level is not None: req["level"] = int(level) if exp is not None: req["exp"] = int(exp) if satiety is not None: req["satiety"] = int(satiety) if clear_satiety: req["clear_satiety"] = True if pet_index is not None: req["pet_index"] = int(pet_index) return self._unwrap(self._post("/local/dnf-gm/pet/update", req)) # ---- 启动器工具扩展(换装 / 服务端工具 / 支援兵)---------------- def loadout_characters(self): return self._unwrap(self._get("/local/dnf-gm/characters")) or [] def loadout_equipment(self, character_id): qs = urlencode({"character_id": str(character_id)}) return self._unwrap(self._get("/local/dnf-gm/loadout/equipment?" + qs)) def loadout_apply(self, character_id, slots): return self._unwrap(self._post("/local/dnf-gm/loadout/apply", { "character_id": str(character_id), "slots": slots})) # ---- 游戏设置:活动开关 / 频道开关(Native,服务端在线)-------- def get_activity_switches(self): """GET /dnf/gm/activity-switches -> GMActivitySwitches""" return self._unwrap(self._get("/dnf/gm/activity-switches")) def set_activity_switch(self, key, enabled): """POST /dnf/gm/activity-switches {key:str, enabled:bool}""" return self._unwrap(self._post("/dnf/gm/activity-switches", { "key": str(key), "enabled": bool(enabled)})) def get_channel_switches(self): """GET /dnf/gm/channel-switches -> GMChannelSwitches""" return self._unwrap(self._get("/dnf/gm/channel-switches")) def set_channel_switch(self, channel_id, enabled): """POST /dnf/gm/channel-switch {channel_id:int, enabled:bool}""" return self._unwrap(self._post("/dnf/gm/channel-switch", { "channel_id": int(channel_id), "enabled": bool(enabled)}))