新增 IRC 渠道的完整扩展实现,包括客户端连接管理、消息收发与流式处理、安全策略与配对验证、消息去重与规范化、状态监控与健康探测、配置模型与账户管理、错误处理等模块。
617 lines
25 KiB
Python
617 lines
25 KiB
Python
import logging
|
|
import uuid
|
|
|
|
from yuxi.channel.capabilities import ChannelCapabilities
|
|
from yuxi.channel.context import ChannelContext
|
|
from yuxi.channel.extensions.base import BaseChannelPlugin
|
|
from yuxi.channel.extensions.irc.accounts import (
|
|
has_configured_state,
|
|
list_irc_account_ids,
|
|
resolve_irc_account,
|
|
)
|
|
from yuxi.channel.extensions.irc.config import DmPolicy, GroupPolicy, IrcConfig
|
|
from yuxi.channel.extensions.irc.monitor import (
|
|
irc_inbound_to_unified,
|
|
monitor_irc_provider,
|
|
)
|
|
from yuxi.channel.extensions.irc.normalize import (
|
|
normalize_irc_target,
|
|
)
|
|
from yuxi.channel.extensions.irc.probe import probe_irc
|
|
from yuxi.channel.extensions.irc.security import (
|
|
check_allowlist_match,
|
|
collect_irc_security_warnings,
|
|
collect_mutable_allowlist_warnings,
|
|
generate_irc_pairing_code,
|
|
is_mentioned,
|
|
match_irc_group,
|
|
normalize_irc_allow_entry,
|
|
resolve_dm_policy,
|
|
resolve_group_policy,
|
|
resolve_irc_require_mention,
|
|
verify_irc_pairing_code,
|
|
)
|
|
from yuxi.channel.extensions.irc.send import send_irc_media, send_irc_message
|
|
from yuxi.channel.extensions.irc.status import build_irc_status_summary
|
|
from yuxi.channel.extensions.irc.dedupe import IrcDedupeCache
|
|
from yuxi.channel.extensions.irc.streaming import IrcStreamBuffer
|
|
from yuxi.channel.extensions.irc.types import IrcInboundMessage, ResolvedIrcAccount
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class IrcPlugin(BaseChannelPlugin):
|
|
id = "irc"
|
|
name = "IRC"
|
|
order = 80
|
|
label = "IRC (Server + Nick)"
|
|
aliases = ["internet-relay-chat"]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._stream_buffers: dict[str, IrcStreamBuffer] = {}
|
|
self._dedupe_cache = IrcDedupeCache()
|
|
|
|
@property
|
|
def capabilities(self) -> ChannelCapabilities:
|
|
return ChannelCapabilities(
|
|
chat_types=["direct", "group"],
|
|
message_types=["text"],
|
|
interactions=[],
|
|
reactions=False,
|
|
typing_indicator=False,
|
|
threads=False,
|
|
edit=False,
|
|
unsend=False,
|
|
reply=True,
|
|
media=False,
|
|
native_commands=False,
|
|
polls=False,
|
|
streaming=True,
|
|
streaming_mode="block",
|
|
block_streaming=True,
|
|
)
|
|
|
|
def _get_account(self) -> ResolvedIrcAccount:
|
|
cfg = getattr(self, "_config", None)
|
|
if cfg is None:
|
|
irc_config = IrcConfig()
|
|
else:
|
|
irc_config = IrcConfig(**cfg.get("channels", {}).get("irc", {}))
|
|
return resolve_irc_account(irc_config)
|
|
|
|
# ── ConfigProtocol ──────────────────────────────────
|
|
|
|
def list_account_ids(self, config: dict) -> list[str]:
|
|
irc_cfg = config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
return list_irc_account_ids(irc_config)
|
|
|
|
def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str | int] | None:
|
|
irc_cfg = config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
account = resolve_irc_account(irc_config, account_id or "default")
|
|
if account.config.allow_from:
|
|
return account.config.allow_from
|
|
return None
|
|
|
|
async def resolve_account(self, account_id: str) -> dict:
|
|
cfg = getattr(self, "_config", None) or {}
|
|
irc_cfg = cfg.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
account = resolve_irc_account(irc_config, account_id)
|
|
return {
|
|
"account_id": account.account_id,
|
|
"enabled": account.enabled,
|
|
"name": account.name,
|
|
"configured": account.configured,
|
|
"host": account.host,
|
|
"port": account.port,
|
|
"tls": account.tls,
|
|
"nick": account.nick,
|
|
"username": account.username,
|
|
"realname": account.realname,
|
|
"dm_policy": account.config.dm_policy.value if account.config.dm_policy else "pairing",
|
|
"group_policy": account.config.group_policy.value if account.config.group_policy else "allowlist",
|
|
"channels": account.config.channels,
|
|
"allow_from": account.config.allow_from,
|
|
"group_allow_from": account.config.group_allow_from,
|
|
}
|
|
|
|
def is_configured(self, account: dict) -> bool:
|
|
return bool(account.get("host") and account.get("nick"))
|
|
|
|
def has_configured_state(self, config: dict) -> bool:
|
|
irc_cfg = config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
return has_configured_state(irc_config)
|
|
|
|
def config_schema(self) -> dict:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {"type": "boolean", "default": True},
|
|
"host": {"type": "string", "description": "IRC server hostname"},
|
|
"port": {"type": "integer", "default": 6697, "description": "Server port (6697 TLS, 6667 plain)"},
|
|
"tls": {"type": "boolean", "default": True, "description": "Enable TLS encryption"},
|
|
"nick": {"type": "string", "description": "IRC nickname"},
|
|
"username": {"type": "string", "default": "openclaw", "description": "IRC username (ident)"},
|
|
"realname": {"type": "string", "default": "OpenClaw", "description": "IRC real name (GECOS)"},
|
|
"password": {"type": "string", "description": "Server connection password (PASS command)"},
|
|
"password_file": {"type": "string", "description": "Path to file containing server password"},
|
|
"channels": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Channels to auto-join on connect",
|
|
},
|
|
"dm_policy": {
|
|
"type": "string",
|
|
"enum": ["pairing", "open", "disabled"],
|
|
"default": "pairing",
|
|
"description": "DM access policy",
|
|
},
|
|
"allow_from": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "DM allowlist entries (nick!user@host format recommended)",
|
|
},
|
|
"group_policy": {
|
|
"type": "string",
|
|
"enum": ["open", "allowlist", "disabled"],
|
|
"default": "allowlist",
|
|
"description": "Group/channel access policy",
|
|
},
|
|
"group_allow_from": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Group sender allowlist entries",
|
|
},
|
|
"text_chunk_limit": {
|
|
"type": "integer",
|
|
"default": 350,
|
|
"description": "Max characters per IRC message chunk",
|
|
},
|
|
"dangerously_allow_name_matching": {
|
|
"type": "boolean",
|
|
"default": False,
|
|
"description": "Allow bare nick matching in allowlist (insecure)",
|
|
},
|
|
"nickserv": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {"type": "boolean", "default": False},
|
|
"service": {"type": "string", "default": "NickServ"},
|
|
"password": {"type": "string", "description": "NickServ IDENTIFY password"},
|
|
"password_file": {"type": "string", "description": "Path to NickServ password file"},
|
|
"register_nick": {"type": "boolean", "default": False},
|
|
"register_email": {"type": "string"},
|
|
},
|
|
},
|
|
"groups": {
|
|
"type": "object",
|
|
"description": "Per-channel config (keyed by channel name, e.g. '#mychannel')",
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"require_mention": {"type": "boolean", "default": True},
|
|
"enabled": {"type": "boolean", "default": True},
|
|
"allow_from": {"type": "array", "items": {"type": "string"}},
|
|
"system_prompt": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
"accounts": {
|
|
"type": "object",
|
|
"description": "Named accounts (keyed by account ID)",
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"enabled": {"type": "boolean", "default": True},
|
|
"host": {"type": "string"},
|
|
"port": {"type": "integer", "default": 6697},
|
|
"tls": {"type": "boolean", "default": True},
|
|
"nick": {"type": "string"},
|
|
"username": {"type": "string", "default": "openclaw"},
|
|
"realname": {"type": "string", "default": "OpenClaw"},
|
|
"password": {"type": "string"},
|
|
"password_file": {"type": "string"},
|
|
"dm_policy": {"type": "string", "enum": ["pairing", "open", "disabled"]},
|
|
"allow_from": {"type": "array", "items": {"type": "string"}},
|
|
"group_policy": {"type": "string", "enum": ["open", "allowlist", "disabled"]},
|
|
"group_allow_from": {"type": "array", "items": {"type": "string"}},
|
|
"channels": {"type": "array", "items": {"type": "string"}},
|
|
"text_chunk_limit": {"type": "integer", "default": 350},
|
|
"dangerously_allow_name_matching": {"type": "boolean", "default": False},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
# ── GatewayProtocol ─────────────────────────────────
|
|
|
|
async def start(self, ctx: ChannelContext) -> object:
|
|
irc_cfg = ctx.config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
account = resolve_irc_account(irc_config, ctx.account_id)
|
|
|
|
if not account.configured:
|
|
raise RuntimeError(f"IRC account '{ctx.account_id}' not configured (host + nick required)")
|
|
|
|
self._account = account
|
|
client_ref: list = [None]
|
|
self._client_ref = client_ref
|
|
|
|
async def status_sink(**kwargs):
|
|
pass
|
|
|
|
async def on_inbound(inbound_msg: IrcInboundMessage):
|
|
if not _check_irc_security(inbound_msg, account, ctx.logger):
|
|
return
|
|
|
|
unified = irc_inbound_to_unified(inbound_msg, account)
|
|
if ctx.queue is not None:
|
|
await ctx.queue.put(unified)
|
|
if ctx.logger:
|
|
ctx.logger.debug("IRC inbound: %s from %s", inbound_msg.text[:50], inbound_msg.sender_nick)
|
|
|
|
await monitor_irc_provider(account, ctx.cancel_event, status_sink, on_inbound, client_ref=client_ref, dedupe_cache=self._dedupe_cache)
|
|
self._client = client_ref[0]
|
|
|
|
async def stop(self, ctx: ChannelContext) -> None:
|
|
ctx.cancel_event.set()
|
|
client = getattr(self, "_client", None)
|
|
if client is not None:
|
|
try:
|
|
from yuxi.channel.extensions.irc.client import graceful_disconnect
|
|
|
|
await graceful_disconnect(client)
|
|
except Exception:
|
|
pass
|
|
self._client = None
|
|
|
|
# ── OutboundProtocol ────────────────────────────────
|
|
|
|
async def send_text(
|
|
self,
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
account = getattr(self, "_account", None) or self._get_account()
|
|
client_ref = getattr(self, "_client_ref", [None])
|
|
client = getattr(self, "_client", None) or client_ref[0]
|
|
result = await send_irc_message(target_id, content, account, reply_to_id=reply_to_id, client=client)
|
|
if not result.ok:
|
|
logger.error("IRC send_text failed: %s", result.error)
|
|
|
|
async def send_media(
|
|
self,
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
) -> None:
|
|
account = getattr(self, "_account", None) or self._get_account()
|
|
client_ref = getattr(self, "_client_ref", [None])
|
|
client = getattr(self, "_client", None) or client_ref[0]
|
|
result = await send_irc_media(target_id, media_url, media_type, account, reply_to_id=reply_to_id, client=client)
|
|
if not result.ok:
|
|
logger.error("IRC send_media failed: %s", result.error)
|
|
|
|
async def create_draft_stream_session(self, target_id: str) -> str:
|
|
session_id = f"irc:stream:{target_id}:{uuid.uuid4().hex[:8]}"
|
|
self._stream_buffers[session_id] = IrcStreamBuffer()
|
|
return session_id
|
|
|
|
async def feed_stream_chunk(self, session_id: str, chunk: str) -> None:
|
|
buf = self._stream_buffers.get(session_id)
|
|
if buf:
|
|
buf.feed(chunk)
|
|
|
|
async def finalize_stream(self, session_id: str) -> str:
|
|
buf = self._stream_buffers.pop(session_id, None)
|
|
if buf:
|
|
return buf.flush()
|
|
return ""
|
|
|
|
async def leave_channel(self, channel: str, reason: str = "") -> bool:
|
|
from yuxi.channel.extensions.irc.client import part_channel
|
|
|
|
client = getattr(self, "_client", None)
|
|
if client is None or client.writer is None:
|
|
logger.warning("Cannot PART %s: no active IRC connection", channel)
|
|
return False
|
|
|
|
try:
|
|
await part_channel(client.writer, channel, reason)
|
|
logger.info("Left channel %s", channel)
|
|
return True
|
|
except Exception:
|
|
logger.exception("Failed to PART %s", channel)
|
|
return False
|
|
|
|
# ── StatusProtocol ──────────────────────────────────
|
|
|
|
async def probe(self, account: dict | None = None) -> bool:
|
|
resolved: ResolvedIrcAccount
|
|
if account is None:
|
|
resolved = getattr(self, "_account", None) or self._get_account()
|
|
else:
|
|
cfg = getattr(self, "_config", None) or {}
|
|
irc_cfg = cfg.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
resolved = resolve_irc_account(irc_config, account.get("account_id", "default"))
|
|
result = await probe_irc(resolved.host, resolved.port, resolved.tls, resolved.nick)
|
|
return result.ok
|
|
|
|
def build_summary(self, snapshot: object) -> dict:
|
|
return build_irc_status_summary(snapshot)
|
|
|
|
# ── ErrorHandlingProtocol ───────────────────────────
|
|
|
|
def classify_error(self, error: BaseException) -> object:
|
|
from yuxi.channel.extensions.irc.errors import classify_error as irc_classify
|
|
|
|
return irc_classify(error)
|
|
|
|
def is_retryable(self, error: BaseException) -> bool:
|
|
from yuxi.channel.extensions.irc.errors import classify_error as irc_classify
|
|
|
|
result = irc_classify(error)
|
|
return result.severity in {"retryable", "rate_limited", "network"}
|
|
|
|
# ── DedupeProtocol ──────────────────────────────────
|
|
|
|
def is_duplicate(self, key: str) -> bool:
|
|
return key in self._dedupe_cache
|
|
|
|
def mark_seen(self, key: str) -> None:
|
|
self._dedupe_cache.add(key)
|
|
|
|
def reset(self) -> None:
|
|
self._dedupe_cache = IrcDedupeCache()
|
|
|
|
@property
|
|
def ttl_seconds(self) -> int:
|
|
return int(self._dedupe_cache._ttl)
|
|
|
|
@property
|
|
def max_entries(self) -> int:
|
|
return self._dedupe_cache._maxsize
|
|
|
|
# ── FormatProtocol ──────────────────────────────────
|
|
|
|
def sanitize_text(self, text: str, payload: object | None = None) -> str:
|
|
from yuxi.channel.extensions.irc.protocol import sanitize_irc_text
|
|
|
|
return sanitize_irc_text(text)
|
|
|
|
def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]:
|
|
from yuxi.channel.extensions.irc.protocol import split_irc_text
|
|
|
|
return split_irc_text(text, limit)
|
|
|
|
def markdown_to_native(self, md_text: str) -> dict | str:
|
|
return md_text
|
|
|
|
def native_to_markdown(self, native_content: dict | str) -> str:
|
|
if isinstance(native_content, dict):
|
|
return native_content.get("text", "")
|
|
return str(native_content)
|
|
|
|
def strip_mentions(self, text: str, ctx: object | None = None) -> str:
|
|
return text
|
|
|
|
# ── AgentPromptProtocol ─────────────────────────────
|
|
|
|
def build_system_prompt(self, context) -> str | None:
|
|
return (
|
|
"You are an AI assistant communicating via IRC. "
|
|
"IRC messages are plain text only — no Markdown, no HTML, no rich formatting. "
|
|
"Keep replies concise and readable as plain text. "
|
|
"Use *asterisks* for emphasis, /me for actions, and conventional IRC formatting. "
|
|
"Avoid long Unicode sequences or emoji that may not render correctly on all IRC clients."
|
|
)
|
|
|
|
def build_context_note(self, context) -> str:
|
|
if context is None:
|
|
return ""
|
|
peer_name = getattr(context, "peer_name", None) or getattr(context, "peer_id", "")
|
|
group_name = getattr(context, "group_name", None)
|
|
if group_name:
|
|
return f"[IRC channel #{group_name} | from {peer_name}]"
|
|
return f"[IRC DM from {peer_name}]"
|
|
|
|
@property
|
|
def channel_format_instructions(self) -> str | None:
|
|
return (
|
|
"IRC messages are plain text only. "
|
|
"Max ~350 chars per message; longer replies are split into chunks. "
|
|
"Use /me for actions and *text* for emphasis."
|
|
)
|
|
|
|
# ── SecurityProtocol ────────────────────────────────
|
|
|
|
async def check_allowlist(self, peer_id: str, channel_type: str) -> bool:
|
|
account = getattr(self, "_account", None) or self._get_account()
|
|
allowlist = account.config.allow_from
|
|
if channel_type == "group":
|
|
allowlist = account.config.group_allow_from or allowlist
|
|
if not allowlist:
|
|
return False
|
|
parts = peer_id.split("!")
|
|
nick = parts[0]
|
|
user_host = parts[1] if len(parts) > 1 else ""
|
|
user = None
|
|
host = None
|
|
if "@" in user_host:
|
|
user, host = user_host.split("@", 1)
|
|
return check_allowlist_match(
|
|
nick, user, host, allowlist, account.config.dangerously_allow_name_matching
|
|
)
|
|
|
|
def resolve_dm_policy(self) -> dict:
|
|
account = getattr(self, "_account", None)
|
|
if account is None:
|
|
return {"mode": "pairing", "allow_from": []}
|
|
return {
|
|
"mode": resolve_dm_policy(account).value,
|
|
"allow_from": account.config.allow_from,
|
|
}
|
|
|
|
def collect_warnings(
|
|
self, config: dict, account_id: str | None = None, account: dict | None = None
|
|
) -> list[str]:
|
|
account_obj = getattr(self, "_account", None) or self._get_account()
|
|
return collect_irc_security_warnings(account_obj)
|
|
|
|
def collect_mutable_allowlist_warnings(self, config: dict) -> list[str]:
|
|
irc_cfg = config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
return collect_mutable_allowlist_warnings(irc_config)
|
|
|
|
# ── GroupsProtocol ──────────────────────────────────
|
|
|
|
def resolve_require_mention(self, ctx) -> bool | None:
|
|
account = getattr(self, "_account", None) or self._get_account()
|
|
return resolve_irc_require_mention(
|
|
ctx.group_channel or ctx.group_id or "",
|
|
account.config.groups,
|
|
account.config.channels,
|
|
)
|
|
|
|
# ── PairingProtocol ─────────────────────────────────
|
|
|
|
@property
|
|
def id_label(self) -> str:
|
|
return "ircNick"
|
|
|
|
async def generate_code(self, peer_id: str) -> str:
|
|
return await generate_irc_pairing_code(peer_id)
|
|
|
|
async def verify_code(self, peer_id: str, code: str) -> bool:
|
|
return await verify_irc_pairing_code(peer_id, code)
|
|
|
|
def normalize_allow_entry(self, entry: str) -> str:
|
|
return normalize_irc_allow_entry(entry)
|
|
|
|
# ── MessagingProtocol ───────────────────────────────
|
|
|
|
def normalize_target(self, raw: str) -> str | None:
|
|
return normalize_irc_target(raw)
|
|
|
|
# ── LifecycleProtocol ───────────────────────────────
|
|
|
|
@property
|
|
def config_prefixes(self) -> list[str]:
|
|
return ["channels.irc"]
|
|
|
|
async def on_config_changed(self, prev_cfg: dict, next_cfg: dict, account_id: str) -> None:
|
|
self._config = next_cfg
|
|
|
|
# ── DirectoryProtocol ───────────────────────────────
|
|
|
|
async def list_peers(self, config: dict) -> list:
|
|
from yuxi.channel.protocols import DirectoryPeer
|
|
|
|
irc_cfg = config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
account = resolve_irc_account(irc_config)
|
|
peers = []
|
|
seen = set()
|
|
for entry in account.config.allow_from:
|
|
if entry in seen or entry == "*":
|
|
continue
|
|
seen.add(entry)
|
|
parts = entry.split("!")
|
|
nick = parts[0] if parts else entry
|
|
peers.append(DirectoryPeer(id=nick, display_name=nick, kind="user"))
|
|
return peers
|
|
|
|
async def list_groups(self, config: dict) -> list:
|
|
from yuxi.channel.protocols import DirectoryGroup
|
|
|
|
irc_cfg = config.get("channels", {}).get("irc", {})
|
|
irc_config = IrcConfig(**irc_cfg)
|
|
account = resolve_irc_account(irc_config)
|
|
groups = []
|
|
seen = set()
|
|
for channel in account.config.channels:
|
|
if channel in seen:
|
|
continue
|
|
seen.add(channel)
|
|
groups.append(DirectoryGroup(id=channel, display_name=channel, kind="group"))
|
|
for group_key in account.config.groups:
|
|
if group_key in seen or group_key == "*":
|
|
continue
|
|
seen.add(group_key)
|
|
groups.append(DirectoryGroup(id=group_key, display_name=group_key, kind="group"))
|
|
return groups
|
|
|
|
|
|
def _check_irc_security(
|
|
msg: IrcInboundMessage,
|
|
account: ResolvedIrcAccount,
|
|
log,
|
|
) -> bool:
|
|
if msg.is_group:
|
|
group_policy = resolve_group_policy(account)
|
|
if group_policy == GroupPolicy.DISABLED:
|
|
log and log.debug("IRC group message rejected: group policy is disabled")
|
|
return False
|
|
|
|
require_mention = resolve_irc_require_mention(
|
|
msg.raw_target, account.config.groups, account.config.channels
|
|
)
|
|
if require_mention and not is_mentioned(account.nick, msg.text):
|
|
log and log.debug("IRC group message rejected: @mention required for %s", msg.raw_target)
|
|
return False
|
|
|
|
if group_policy == GroupPolicy.ALLOWLIST:
|
|
group_match = match_irc_group(
|
|
msg.raw_target, account.config.groups, account.config.channels, account.nick
|
|
)
|
|
effective_allowlist = account.config.group_allow_from
|
|
if group_match is not None:
|
|
group_key, group_cfg = group_match
|
|
if group_cfg is not None and hasattr(group_cfg, "allow_from") and group_cfg.allow_from:
|
|
effective_allowlist = group_cfg.allow_from
|
|
if not check_allowlist_match(
|
|
msg.sender_nick,
|
|
msg.sender_user,
|
|
msg.sender_host,
|
|
effective_allowlist,
|
|
account.config.dangerously_allow_name_matching,
|
|
):
|
|
log and log.debug(
|
|
"IRC group message rejected: '%s' not in allowlist for %s",
|
|
msg.sender_nick, msg.raw_target,
|
|
)
|
|
return False
|
|
else:
|
|
dm_policy = resolve_dm_policy(account)
|
|
if dm_policy == DmPolicy.DISABLED:
|
|
log and log.debug("IRC DM rejected: DM policy is disabled")
|
|
return False
|
|
|
|
if dm_policy == DmPolicy.PAIRING:
|
|
if not check_allowlist_match(
|
|
msg.sender_nick,
|
|
msg.sender_user,
|
|
msg.sender_host,
|
|
account.config.allow_from,
|
|
account.config.dangerously_allow_name_matching,
|
|
):
|
|
log and log.info(
|
|
"IRC DM pairing required: '%s' not in allowlist, letting through for pairing",
|
|
msg.sender_nick,
|
|
)
|
|
|
|
return True
|