Skip to content
Merged
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
295 changes: 274 additions & 21 deletions cmd/lk/phone_number.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@ func createPhoneNumberClient(ctx context.Context, cmd *cli.Command) (*lksdk.Phon
return lksdk.NewPhoneNumberClient(project.URL, project.APIKey, project.APISecret, withDefaultClientOpts(project)...), nil
}

// getPhoneNumberToDispatchRulesMap fetches all dispatch rules and maps phone number IDs to their associated dispatch rule IDs
// Returns a map where key is phone number ID and value is a slice of dispatch rule IDs
func getPhoneNumberToDispatchRulesMap(ctx context.Context, cmd *cli.Command) (map[string][]string, error) {
_, err := requireProject(ctx, cmd)
if err != nil {
return nil, fmt.Errorf("failed to get project: %w", err)
}

sipClient := lksdk.NewSIPClient(project.URL, project.APIKey, project.APISecret, withDefaultClientOpts(project)...)

// List all dispatch rules
resp, err := sipClient.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting, we still haven't resolved the issue with max twirp message size, either here or in SDK. Probably very few customers hitting that but we need to come back to the idea of auto-paginating results when we do things like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is similar to what we've done here -> https://github.com/livekit/livekit-cli/pull/694/files ? I can take that up after landing this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was the idea for a short-term fix. What we probably want to for real tho is something like this if the results are requested in JSON format (complete results are expected) but have real pagination in the CLI for when you expect a table format (just call with nextPageToken on arrow key or something).

if err != nil {
return nil, fmt.Errorf("failed to list dispatch rules: %w", err)
}

// Build map: phone number ID -> []dispatch rule IDs
phoneNumberToRules := make(map[string][]string)
for _, rule := range resp.Items {
for _, trunkID := range rule.TrunkIds {
// Check if trunkID is a phone number ID (starts with PN_PPN_)
if strings.HasPrefix(trunkID, "PN_PPN_") {
phoneNumberToRules[trunkID] = append(phoneNumberToRules[trunkID], rule.SipDispatchRuleId)
}
}
}

return phoneNumberToRules, nil
}

func searchPhoneNumbers(ctx context.Context, cmd *cli.Command) error {
client, err := createPhoneNumberClient(ctx, cmd)
if err != nil {
Expand Down Expand Up @@ -226,16 +256,74 @@ func purchasePhoneNumbers(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("at least one phone number must be provided")
}

dispatchRuleID := cmd.String("sip-dispatch-rule-id")

req := &livekit.PurchasePhoneNumberRequest{
PhoneNumbers: phoneNumbers,
}
if val := cmd.String("sip-dispatch-rule-id"); val != "" {
req.SipDispatchRuleId = &val
if dispatchRuleID != "" {
req.SipDispatchRuleId = &dispatchRuleID
}

resp, err := client.PurchasePhoneNumber(ctx, req)
if err != nil {
return err
// Call purchase and get dispatch rules in parallel
type purchaseResult struct {
resp *livekit.PurchasePhoneNumberResponse
err error
}
type dispatchRulesResult struct {
rules map[string][]string
err error
}

purchaseChan := make(chan purchaseResult, 1)
dispatchRulesChan := make(chan dispatchRulesResult, 1)

// Purchase phone numbers
go func() {
resp, err := client.PurchasePhoneNumber(ctx, req)
purchaseChan <- purchaseResult{resp: resp, err: err}
}()

// Get dispatch rules mapping in parallel
go func() {
rules, err := getPhoneNumberToDispatchRulesMap(ctx, cmd)
dispatchRulesChan <- dispatchRulesResult{rules: rules, err: err}
}()

// Wait for purchase to complete
purchaseRes := <-purchaseChan
if purchaseRes.err != nil {
return purchaseRes.err
}
resp := purchaseRes.resp

// Wait for dispatch rules (ignore errors, we'll just not show them)
dispatchRulesRes := <-dispatchRulesChan
phoneNumberToRules := dispatchRulesRes.rules
if dispatchRulesRes.err != nil {
// Log but don't fail
if cmd.Bool("verbose") {
fmt.Fprintf(cmd.ErrWriter, "Warning: failed to get dispatch rules: %v\n", dispatchRulesRes.err)
}
phoneNumberToRules = make(map[string][]string)
}

// If dispatch rule ID was provided, add it to the mapping for display
// (The actual update is now handled by cloud-io)
if dispatchRuleID != "" {
for _, phoneNumber := range resp.PhoneNumbers {
// Check if dispatchRuleID is already in the list
found := false
for _, ruleID := range phoneNumberToRules[phoneNumber.Id] {
if ruleID == dispatchRuleID {
found = true
break
}
}
if !found {
phoneNumberToRules[phoneNumber.Id] = append(phoneNumberToRules[phoneNumber.Id], dispatchRuleID)
}
}
}

if cmd.Bool("json") {
Expand All @@ -245,7 +333,12 @@ func purchasePhoneNumbers(ctx context.Context, cmd *cli.Command) error {

fmt.Printf("Successfully purchased %d phone numbers:\n", len(resp.PhoneNumbers))
for _, phoneNumber := range resp.PhoneNumbers {
fmt.Printf(" %s (%s) - %s\n", phoneNumber.E164Format, phoneNumber.Id, strings.TrimPrefix(phoneNumber.Status.String(), "PHONE_NUMBER_STATUS_"))
ruleInfo := ""
rules := phoneNumberToRules[phoneNumber.Id]
if len(rules) > 0 {
ruleInfo = fmt.Sprintf(" (SIP Dispatch Rules: %s)", strings.Join(rules, ", "))
}
fmt.Printf(" %s (%s) - %s%s\n", phoneNumber.E164Format, phoneNumber.Id, strings.TrimPrefix(phoneNumber.Status.String(), "PHONE_NUMBER_STATUS_"), ruleInfo)
}

return nil
Expand Down Expand Up @@ -277,9 +370,47 @@ func listPhoneNumbers(ctx context.Context, cmd *cli.Command) error {
req.SipDispatchRuleId = &val
}

resp, err := client.ListPhoneNumbers(ctx, req)
if err != nil {
return err
// Call list and get dispatch rules in parallel
type listResult struct {
resp *livekit.ListPhoneNumbersResponse
err error
}
type dispatchRulesResult struct {
rules map[string][]string
err error
}

listChan := make(chan listResult, 1)
dispatchRulesChan := make(chan dispatchRulesResult, 1)

// List phone numbers
go func() {
resp, err := client.ListPhoneNumbers(ctx, req)
listChan <- listResult{resp: resp, err: err}
}()

// Get dispatch rules mapping in parallel
go func() {
rules, err := getPhoneNumberToDispatchRulesMap(ctx, cmd)
dispatchRulesChan <- dispatchRulesResult{rules: rules, err: err}
}()

// Wait for list to complete
listRes := <-listChan
if listRes.err != nil {
return listRes.err
}
resp := listRes.resp

// Wait for dispatch rules (ignore errors, we'll just not show them)
dispatchRulesRes := <-dispatchRulesChan
phoneNumberToRules := dispatchRulesRes.rules
if dispatchRulesRes.err != nil {
// Log but don't fail
if cmd.Bool("verbose") {
fmt.Fprintf(cmd.ErrWriter, "Warning: failed to get dispatch rules: %v\n", dispatchRulesRes.err)
}
phoneNumberToRules = make(map[string][]string)
}

if cmd.Bool("json") {
Expand All @@ -291,8 +422,17 @@ func listPhoneNumbers(ctx context.Context, cmd *cli.Command) error {
return listAndPrint(ctx, cmd, func(ctx context.Context, req *livekit.ListPhoneNumbersRequest) (*livekit.ListPhoneNumbersResponse, error) {
return client.ListPhoneNumbers(ctx, req)
}, req, []string{
"ID", "E164", "Country", "Area Code", "Type", "Locality", "Region", "Capabilities", "Status", "SIP Dispatch Rule",
"ID", "E164", "Country", "Area Code", "Type", "Locality", "Region", "Capabilities", "Status", "SIP Dispatch Rules",
}, func(item *livekit.PhoneNumber) []string {
rules := phoneNumberToRules[item.Id]
dispatchRulesStr := ""
if len(rules) > 0 {
dispatchRulesStr = strings.Join(rules, ", ")
} else if item.SipDispatchRuleId != "" {
dispatchRulesStr = item.SipDispatchRuleId
} else {
dispatchRulesStr = "-"
}
return []string{
item.Id,
item.E164Format,
Expand All @@ -303,7 +443,7 @@ func listPhoneNumbers(ctx context.Context, cmd *cli.Command) error {
item.Region,
strings.Join(item.Capabilities, ","),
strings.TrimPrefix(item.Status.String(), "PHONE_NUMBER_STATUS_"),
item.SipDispatchRuleId,
dispatchRulesStr,
}
})
}
Expand Down Expand Up @@ -331,9 +471,47 @@ func getPhoneNumber(ctx context.Context, cmd *cli.Command) error {
req.PhoneNumber = &phoneNumber
}

resp, err := client.GetPhoneNumber(ctx, req)
if err != nil {
return err
// Call get and get dispatch rules in parallel
type getResult struct {
resp *livekit.GetPhoneNumberResponse
err error
}
type dispatchRulesResult struct {
rules map[string][]string
err error
}

getChan := make(chan getResult, 1)
dispatchRulesChan := make(chan dispatchRulesResult, 1)

// Get phone number
go func() {
resp, err := client.GetPhoneNumber(ctx, req)
getChan <- getResult{resp: resp, err: err}
}()

// Get dispatch rules mapping in parallel
go func() {
rules, err := getPhoneNumberToDispatchRulesMap(ctx, cmd)
dispatchRulesChan <- dispatchRulesResult{rules: rules, err: err}
}()

// Wait for get to complete
getRes := <-getChan
if getRes.err != nil {
return getRes.err
}
resp := getRes.resp

// Wait for dispatch rules (ignore errors, we'll just not show them)
dispatchRulesRes := <-dispatchRulesChan
phoneNumberToRules := dispatchRulesRes.rules
if dispatchRulesRes.err != nil {
// Log but don't fail
if cmd.Bool("verbose") {
fmt.Fprintf(cmd.ErrWriter, "Warning: failed to get dispatch rules: %v\n", dispatchRulesRes.err)
}
phoneNumberToRules = make(map[string][]string)
}

if cmd.Bool("json") {
Expand All @@ -342,6 +520,16 @@ func getPhoneNumber(ctx context.Context, cmd *cli.Command) error {
}

item := resp.PhoneNumber
rules := phoneNumberToRules[item.Id]
dispatchRulesStr := ""
if len(rules) > 0 {
dispatchRulesStr = strings.Join(rules, ", ")
} else if item.SipDispatchRuleId != "" {
dispatchRulesStr = item.SipDispatchRuleId
} else {
dispatchRulesStr = "-"
}

fmt.Printf("Phone Number Details:\n")
fmt.Printf(" ID: %s\n", item.Id)
fmt.Printf(" E164 Format: %s\n", item.E164Format)
Expand All @@ -352,7 +540,7 @@ func getPhoneNumber(ctx context.Context, cmd *cli.Command) error {
fmt.Printf(" Region: %s\n", item.Region)
fmt.Printf(" Capabilities: %s\n", strings.Join(item.Capabilities, ","))
fmt.Printf(" Status: %s\n", strings.TrimPrefix(item.Status.String(), "PHONE_NUMBER_STATUS_"))
fmt.Printf(" SIP Dispatch Rule: %s\n", item.SipDispatchRuleId)
fmt.Printf(" SIP Dispatch Rules: %s\n", dispatchRulesStr)
if item.ReleasedAt != nil {
fmt.Printf(" Released At: %s\n", item.ReleasedAt.AsTime().Format("2006-01-02 15:04:05"))
}
Expand All @@ -376,19 +564,76 @@ func updatePhoneNumber(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("only one of --id or --number can be provided")
}

dispatchRuleID := cmd.String("sip-dispatch-rule-id")

req := &livekit.UpdatePhoneNumberRequest{}
if id != "" {
req.Id = &id
} else {
req.PhoneNumber = &phoneNumber
}
if val := cmd.String("sip-dispatch-rule-id"); val != "" {
req.SipDispatchRuleId = &val
if dispatchRuleID != "" {
req.SipDispatchRuleId = &dispatchRuleID
}

resp, err := client.UpdatePhoneNumber(ctx, req)
if err != nil {
return err
// Call update and get dispatch rules in parallel
type updateResult struct {
resp *livekit.UpdatePhoneNumberResponse
err error
}
type dispatchRulesResult struct {
rules map[string][]string
err error
}

updateChan := make(chan updateResult, 1)
dispatchRulesChan := make(chan dispatchRulesResult, 1)

// Update phone number
go func() {
resp, err := client.UpdatePhoneNumber(ctx, req)
updateChan <- updateResult{resp: resp, err: err}
}()

// Get dispatch rules mapping in parallel
go func() {
rules, err := getPhoneNumberToDispatchRulesMap(ctx, cmd)
dispatchRulesChan <- dispatchRulesResult{rules: rules, err: err}
}()

// Wait for update to complete
updateRes := <-updateChan
if updateRes.err != nil {
return updateRes.err
}
resp := updateRes.resp

// Wait for dispatch rules (ignore errors, we'll just not show them)
dispatchRulesRes := <-dispatchRulesChan
phoneNumberToRules := dispatchRulesRes.rules
if dispatchRulesRes.err != nil {
// Log but don't fail
if cmd.Bool("verbose") {
fmt.Fprintf(cmd.ErrWriter, "Warning: failed to get dispatch rules: %v\n", dispatchRulesRes.err)
}
phoneNumberToRules = make(map[string][]string)
}

// If dispatch rule ID was provided, add it to the mapping for display
// (The actual update is now handled by cloud-io)
if dispatchRuleID != "" {
phoneNumberID := resp.PhoneNumber.Id
// Check if dispatchRuleID is already in the list
found := false
for _, ruleID := range phoneNumberToRules[phoneNumberID] {
if ruleID == dispatchRuleID {
found = true
break
}
}
if !found {
phoneNumberToRules[phoneNumberID] = append(phoneNumberToRules[phoneNumberID], dispatchRuleID)
}
}

if cmd.Bool("json") {
Expand All @@ -397,11 +642,19 @@ func updatePhoneNumber(ctx context.Context, cmd *cli.Command) error {
}

item := resp.PhoneNumber
rules := phoneNumberToRules[item.Id]
dispatchRulesStr := ""
if len(rules) > 0 {
dispatchRulesStr = strings.Join(rules, ", ")
} else {
dispatchRulesStr = "-"
}

fmt.Printf("Successfully updated phone number:\n")
fmt.Printf(" ID: %s\n", item.Id)
fmt.Printf(" E164 Format: %s\n", item.E164Format)
fmt.Printf(" Status: %s\n", strings.TrimPrefix(item.Status.String(), "PHONE_NUMBER_STATUS_"))
fmt.Printf(" SIP Dispatch Rule: %s\n", item.SipDispatchRuleId)
fmt.Printf(" SIP Dispatch Rules: %s\n", dispatchRulesStr)

return nil
}
Expand Down
Loading