Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 19 additions & 22 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,9 @@ def from_dict(cls, data: Dict[str, Any]) -> "StreamingConfig":
# that rely on the generic ``token or api_key`` check (Telegram, Discord,
# Slack, Matrix, Mattermost, HomeAssistant) do not need an entry here.
_PLATFORM_CONNECTED_CHECKERS: dict[Platform, Callable[[PlatformConfig], bool]] = {
Platform.WEIXIN: lambda cfg: bool(
cfg.extra.get("account_id") and (cfg.token or cfg.extra.get("token"))
),
# weixin migrated to a bundled plugin (plugins/platforms/weixin/); its
# connection check (account_id + token) is registered via is_connected on
# the PlatformEntry and consulted before the generic token branch.
Platform.WHATSAPP: lambda cfg: True, # bridge handles auth
Platform.SIGNAL: lambda cfg: bool(cfg.extra.get("http_url")),
Platform.EMAIL: lambda cfg: bool(cfg.extra.get("address")),
Expand Down Expand Up @@ -521,24 +521,13 @@ def get_connected_platforms(self) -> List[Platform]:

def _is_platform_connected(self, platform: Platform, config: PlatformConfig) -> bool:
"""Check whether a single platform is sufficiently configured."""
# Weixin requires both a token and an account_id (checked first so
# the generic token branch doesn't let it through without account_id).
if platform == Platform.WEIXIN:
return bool(
config.extra.get("account_id")
and (config.token or config.extra.get("token"))
)

# Generic token/api_key auth covers Telegram, Discord, Slack, etc.
if config.token or config.api_key:
return True

# Platform-specific check
checker = _PLATFORM_CONNECTED_CHECKERS.get(platform)
if checker is not None:
return checker(config)

# Plugin-registered platforms
# Plugin-registered platforms: consult the plugin's own is_connected
# FIRST, before the generic token branch. Some plugins (e.g. weixin,
# which migrated to plugins/platforms/weixin/) require more than a bare
# token — weixin needs account_id AND token, and it sets config.token
# during env-enablement, so the generic branch below would wrongly pass
# it on token alone. Letting the plugin's is_connected run first keeps
# that requirement intact without a per-platform special-case here.
try:
from gateway.platform_registry import platform_registry
entry = platform_registry.get(platform.value)
Expand All @@ -547,10 +536,18 @@ def _is_platform_connected(self, platform: Platform, config: PlatformConfig) ->
return entry.is_connected(config)
if entry.validate_config is not None:
return entry.validate_config(config)
return True
except Exception:
pass # Registry not yet initialised during early import

# Generic token/api_key auth covers Telegram, Discord, Slack, etc.
if config.token or config.api_key:
return True

# Platform-specific check
checker = _PLATFORM_CONNECTED_CHECKERS.get(platform)
if checker is not None:
return checker(config)

return False

def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
Expand Down
10 changes: 3 additions & 7 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6945,12 +6945,8 @@ def _create_adapter(
return None
return WeComAdapter(config)

elif platform == Platform.WEIXIN:
from gateway.platforms.weixin import WeixinAdapter, check_weixin_requirements
if not check_weixin_requirements():
logger.warning("Weixin: aiohttp/cryptography not installed")
return None
return WeixinAdapter(config)
# weixin migrated to a bundled plugin (plugins/platforms/weixin/);
# the platform_registry check at the top of this method creates it.

elif platform == Platform.MATRIX:
from gateway.platforms.matrix import MatrixAdapter, check_matrix_requirements
Expand Down Expand Up @@ -14977,7 +14973,7 @@ async def _handle_deny_command(self, event: MessageEvent) -> str:
Platform.TELEGRAM, Platform.SLACK, Platform.WHATSAPP,
Platform.SIGNAL, Platform.MATRIX,
Platform.EMAIL, Platform.SMS, Platform.DINGTALK,
Platform.FEISHU, Platform.WECOM, Platform.WECOM_CALLBACK, Platform.WEIXIN, Platform.BLUEBUBBLES, Platform.QQBOT, Platform.LOCAL,
Platform.FEISHU, Platform.WECOM, Platform.WECOM_CALLBACK, Platform.BLUEBUBBLES, Platform.QQBOT, Platform.LOCAL,
})

async def _handle_debug_command(self, event: MessageEvent) -> str:
Expand Down
181 changes: 9 additions & 172 deletions hermes_cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -4223,12 +4223,9 @@ def _atexit_hook() -> None:
},
],
},
{
"key": "weixin",
"label": "Weixin / WeChat",
"emoji": "💬",
"token_var": "WEIXIN_ACCOUNT_ID",
},
# weixin migrated to a bundled plugin (plugins/platforms/weixin/); its
# setup wizard is registered via setup_fn on the PlatformEntry and
# surfaced through _all_platforms() + _configure_platform().
{
"key": "bluebubbles",
"label": "BlueBubbles (iMessage)",
Expand Down Expand Up @@ -4941,171 +4938,9 @@ def _is_service_running() -> bool:
return len(find_gateway_pids()) > 0


def _setup_weixin():
"""Interactive setup for Weixin / WeChat personal accounts."""
print()
print(color(" ─── 💬 Weixin / WeChat Setup ───", Colors.CYAN))
print()
print_info(" 1. Hermes will open Tencent iLink QR login in this terminal.")
print_info(" 2. Use WeChat to scan and confirm the QR code.")
print_info(
" 3. Hermes will store the returned account_id/token in ~/.hermes/.env."
)
print_info(
" 4. This adapter supports native text, image, video, and document delivery."
)

existing_account = get_env_value("WEIXIN_ACCOUNT_ID")
existing_token = get_env_value("WEIXIN_TOKEN")
if existing_account and existing_token:
print()
print_success("Weixin is already configured.")
if not prompt_yes_no(" Reconfigure Weixin?", False):
return

try:
from gateway.platforms.weixin import check_weixin_requirements, qr_login
except Exception as exc:
print_error(f" Weixin adapter import failed: {exc}")
print_info(" Install gateway dependencies first, then retry.")
return

if not check_weixin_requirements():
print_error(" Missing dependencies: Weixin needs aiohttp and cryptography.")
print_info(" Install them, then rerun `hermes gateway setup`.")
return

print()
if not prompt_yes_no(" Start QR login now?", True):
print_info(" Cancelled.")
return

import asyncio

try:
credentials = asyncio.run(qr_login(str(get_hermes_home())))
except KeyboardInterrupt:
print()
print_warning(" Weixin setup cancelled.")
return
except Exception as exc:
print_error(f" QR login failed: {exc}")
return

if not credentials:
print_warning(" QR login did not complete.")
return

account_id = credentials.get("account_id", "")
token = credentials.get("token", "")
base_url = credentials.get("base_url", "")
user_id = credentials.get("user_id", "")

save_env_value("WEIXIN_ACCOUNT_ID", account_id)
save_env_value("WEIXIN_TOKEN", token)
if base_url:
save_env_value("WEIXIN_BASE_URL", base_url)
save_env_value(
"WEIXIN_CDN_BASE_URL",
get_env_value("WEIXIN_CDN_BASE_URL") or "https://novac2c.cdn.weixin.qq.com/c2c",
)

print()
access_choices = [
"Use DM pairing approval (recommended)",
"Allow all direct messages",
"Only allow listed user IDs",
"Disable direct messages",
]
access_idx = prompt_choice(
" How should direct messages be authorized?", access_choices, 0
)
if access_idx == 0:
save_env_value("WEIXIN_DM_POLICY", "pairing")
save_env_value("WEIXIN_ALLOW_ALL_USERS", "false")
save_env_value("WEIXIN_ALLOWED_USERS", "")
print_success(" DM pairing enabled.")
print_info(
" Unknown DM users can request access and you approve them with `hermes pairing approve`."
)
elif access_idx == 1:
save_env_value("WEIXIN_DM_POLICY", "open")
save_env_value("WEIXIN_ALLOW_ALL_USERS", "true")
save_env_value("WEIXIN_ALLOWED_USERS", "")
print_warning(" Open DM access enabled for Weixin.")
elif access_idx == 2:
default_allow = user_id or ""
allowlist = prompt(
" Allowed Weixin user IDs (comma-separated)", default_allow, password=False
).replace(" ", "")
save_env_value("WEIXIN_DM_POLICY", "allowlist")
save_env_value("WEIXIN_ALLOW_ALL_USERS", "false")
save_env_value("WEIXIN_ALLOWED_USERS", allowlist)
print_success(" Weixin allowlist saved.")
else:
save_env_value("WEIXIN_DM_POLICY", "disabled")
save_env_value("WEIXIN_ALLOW_ALL_USERS", "false")
save_env_value("WEIXIN_ALLOWED_USERS", "")
print_warning(" Direct messages disabled.")

print()
print_info(
" Note: QR login connects an iLink bot identity (e.g. ...@im.bot), not a"
)
print_info(
" scriptable personal WeChat account. Ordinary WeChat groups typically cannot"
)
print_info(
" invite an @im.bot identity, and iLink does not deliver ordinary-group events"
)
print_info(
" to most bot accounts. The settings below only apply when iLink actually"
)
print_info(
" delivers group events for your account type — otherwise DM remains the only"
)
print_info(" working channel regardless of this choice.")
group_choices = [
"Disable group chats (recommended)",
"Allow all group chats",
"Only allow listed group chat IDs",
]
group_idx = prompt_choice(" How should group chats be handled?", group_choices, 0)
if group_idx == 0:
save_env_value("WEIXIN_GROUP_POLICY", "disabled")
save_env_value("WEIXIN_GROUP_ALLOWED_USERS", "")
print_info(" Group chats disabled.")
elif group_idx == 1:
save_env_value("WEIXIN_GROUP_POLICY", "open")
save_env_value("WEIXIN_GROUP_ALLOWED_USERS", "")
print_warning(
" All group chats enabled (only takes effect if iLink delivers group events)."
)
else:
allow_groups = prompt(
" Allowed group chat IDs (comma-separated, not member user IDs)",
"",
password=False,
).replace(" ", "")
save_env_value("WEIXIN_GROUP_POLICY", "allowlist")
save_env_value("WEIXIN_GROUP_ALLOWED_USERS", allow_groups)
print_success(
" Group allowlist saved (only takes effect if iLink delivers group events)."
)

if user_id:
print()
if prompt_yes_no(
f" Use your Weixin user ID ({user_id}) as the home channel?", True
):
save_env_value("WEIXIN_HOME_CHANNEL", user_id)
print_success(f" Home channel set to {user_id}")

print()
print_success("Weixin configured!")
print_info(f" Account ID: {account_id}")
if user_id:
print_info(f" User ID: {user_id}")
# weixin migrated to a bundled plugin (plugins/platforms/weixin/); its
# interactive_setup (iLink QR login + DM/group policy) lives there,
# dispatched via the registry setup_fn path in _configure_platform().


def _setup_feishu():
Expand Down Expand Up @@ -5580,7 +5415,9 @@ def _builtin_setup_fn(key: str):
"webhooks": _s._setup_webhooks,
"signal": _setup_signal,
"whatsapp": _setup_whatsapp,
"weixin": _setup_weixin,
# weixin moved into the plugin: setup_fn is registered by
# plugins/platforms/weixin/adapter.py::register() and dispatched
# via the plugin path in _configure_platform().
"dingtalk": _setup_dingtalk,
"feishu": _setup_feishu,
"wecom": _setup_wecom,
Expand Down
3 changes: 3 additions & 0 deletions plugins/platforms/weixin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .adapter import register

__all__ = ["register"]
Loading
Loading