historyinfo

package
v1.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package historyinfo implements RFC 7044 History-Info and RFC 5806 Diversion header processing for the SIP call-transfer / retargeting path.

Why we need this:

  • When a call arrives at us on trunk-number T and the business layer (AI tool / DTMF / inbound REFER) decides to retarget to agent A, we run a B2BUA bridge (see docs/sip_gap_analysis.md § "转接架构说明"). That means the agent's UAS sees an INVITE whose Request-URI is the agent, From is OUR trunk's CLI, To is the agent — there is no surface signal that this call was originally targeted at T or who retargeted it.
  • Downstream PBXs / phones use History-Info (modern, RFC 7044) or Diversion (legacy, RFC 5806) to render "original called number was T" and to drive routing rules ("if Diversion is present, do not deflect again"). Without these headers, transferred calls look indistinguishable from primary calls and the agent can't tell the customer "您拨打的 trunk T 已为您转人工".
  • We emit BOTH headers because real-world PBX populations are mixed: Avaya/Cisco modern firmware honor History-Info, but Yealink/Polycom phones and older Asterisk read Diversion.

This package does parsing/formatting only. The wiring into INVITE emission lives in pkg/sip/outbound; the wiring into the transfer chain lives in pkg/sip/conversation.

Index

Constants

View Source
const (
	DiversionUnknown       = "unknown"
	DiversionUserBusy      = "user-busy"
	DiversionNoAnswer      = "no-answer"
	DiversionUnavailable   = "unavailable"
	DiversionUnconditional = "unconditional"
	DiversionTimeOfDay     = "time-of-day"
	DiversionDoNotDisturb  = "do-not-disturb"
	DiversionDeflection    = "deflection"
	DiversionFollowMe      = "follow-me"
	DiversionOutOfService  = "out-of-service"
	DiversionAway          = "away"
)

DiversionReason values per RFC 5806 §4.1.1.

Variables

This section is empty.

Functions

func ExtractURIFromToHeader

func ExtractURIFromToHeader(toHeader string) string

ExtractURIFromToHeader strips the display-name and angle brackets from a To header value, returning just the URI ("sip:foo@bar"). On malformed input returns "" so callers can short-circuit before emitting a malformed History-Info entry.

Lives here because the only consumers are this package and the conversation transfer code that calls AppendTransferEntry.

func FormatChain

func FormatChain(chain []Entry) string

FormatChain renders an entire History-Info chain as a comma- separated header value. Pass to msg.SetHeader("History-Info", ...). Empty input → "".

func FormatDiversionChain

func FormatDiversionChain(chain []Diversion) string

FormatDiversionChain renders the Diversion header value (multiple rows comma-separated, RFC 5806 §4.1).

func MaxCounter

func MaxCounter(chain []Diversion) int

MaxCounter returns the largest counter value in the chain; used when extending so the new entry can be counter=Max+1.

func NextIndex

func NextIndex(chain []Entry) string

NextIndex returns "N+1" where N is the largest top-level integer index in chain. Used when we extend the chain on retarget — RFC 7044 §4.1 says the new entry's index must be at the same depth as the prior entry plus one. For our B2BUA we always extend flat.

Examples (chain index sequence → returned next):

(empty)               → "1"
"1"                   → "2"
"1", "2"              → "3"
"1", "1.1"            → "2"   (siblings of root)
"1", "2", "2.1"       → "3"

Types

type Diversion

type Diversion struct {
	URI         string
	Reason      string // free-form per RFC 5806; commonly one of DiversionXxx
	Counter     int    // hop counter; missing → 0; we increment when extending
	Privacy     string // "full" / "name" / "uri" / "off" / ...
	Limit       int    // hop limit (counter <= limit before drop); 0 = unset
	Screen      string // "yes" / "no" — was the diversion source verified
	ExtraParams []string
}

Diversion is one row of an RFC 5806 Diversion header chain. Unlike History-Info, Diversion does NOT carry indices — chain order is by header appearance order, oldest first.

func AppendDiversionEntry

func AppendDiversionEntry(inboundChain []Diversion, originalToURI, reason string) []Diversion

AppendDiversionEntry returns a new Diversion chain with one additional entry pointing at originalToURI (RFC 5806 semantics: "the call was diverted FROM originalToURI"). The new entry's counter is incremented by 1 over the chain's current max.

We default reason to "unconditional" since our retarget is platform-initiated (AI / pool / REFER), not a phone busy/no-answer. Callers can override (e.g. DiversionDeflection for REFER-driven).

func ParseDiversionChain

func ParseDiversionChain(raw string) []Diversion

ParseDiversionChain parses Diversion header value(s). Lenient — same philosophy as ParseChain.

func (Diversion) Format

func (d Diversion) Format() string

Format renders one Diversion entry. RFC 5806 §4.1: the URI is angle-bracketed; params live outside, semicolon-separated.

type Entry

type Entry struct {
	// URI is the bare target URI (e.g. "sip:+8613800138000@trust.example").
	// Whether/not angle-bracketed, we normalise on parse to no brackets.
	URI string

	// Index is the dotted-decimal index per RFC 7044 §4.1. We always
	// emit flat integers like "1", "2", "3" (no forking).
	Index string

	// ReasonHeader optionally carries a SIP Reason header value
	// embedded in the Request-URI's hi-target-param `Reason=...`. RFC
	// 7044 §4.2: the param is percent-encoded; we decode on parse and
	// re-encode on format. Common values for our case:
	//   "SIP;cause=302;text=\"Moved Temporarily\""  — generic retarget
	//   "SIP;cause=480;text=\"AI Transfer\""         — transfer to agent
	//   "SIP;cause=487;text=\"Cancelled\""           — caller hung up
	// Empty = no reason param (legal, just less informative downstream).
	ReasonHeader string

	// PrivacyHeader: history-info-targeted-toparam Privacy value
	// (RFC 7044 §4.3). Rare; we currently don't emit but parse for
	// round-tripping.
	PrivacyHeader string

	// Extra opaque params we encountered on parse but don't model.
	// Carried through on format so we don't strip carrier annotations.
	Extra []string
}

Entry is one entry in the History-Info chain. RFC 7044 §4: each Request-URI the request has traversed becomes one entry, indexed in dotted decimal so forking proxies can model branches. We do not fork (we're a B2BUA), so our indices are always flat integers.

func AppendTransferEntry

func AppendTransferEntry(inboundChain []Entry, originalToURI, newTargetURI, reasonHeader string) []Entry

AppendTransferEntry returns a new History-Info chain with one additional entry representing the retarget. originalToURI is the To URI from the inbound INVITE; newTargetURI is what we are about to send the outbound INVITE to. reasonHeader is a SIP Reason header value (or "" to omit).

On first call with an empty inbound chain we synthesise a root entry for originalToURI as well, so the resulting chain represents the full path (original → new) instead of just (new).

func ParseChain

func ParseChain(raw string) []Entry

ParseChain parses a (possibly folded multi-instance) History-Info header value into entries. We are LENIENT on input — the goal is to forward whatever upstream produced, not validate carrier behaviour. Unparseable rows are skipped.

func (Entry) Format

func (e Entry) Format() string

Format renders one entry in wire format:

<sip:+8613800138000@trust.example?Reason=SIP%3Bcause%3D302>;index=1

Per RFC 7044 §4: the Reason / Privacy params live inside the URI hi-target as URI-headers (after `?`), separated by `&`, percent encoded; the `;index=` and other hi-target-params live OUTSIDE the angle brackets on the entry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL