У системного администратора, работающего с Asterisk, часто возникает необходимость составить маски расширений для диалплана по интервалам DEF кодов. Вручную делать это трудоёмко, а чуть ли не единственный работающий скрипт, который можно найти в интернете, на самом деле не такой уж и работающий.

Надеюсь, кому-нибудь пригодится небольшой cli генератор на go, который пришлось недавно написать, чтобы обновить существующий диалплан.

Возможности:

  • фильтр по региону
  • фильтр по оператору
  • простое форматирование вывода
  • комментарии в стиле asterisk с информацией об интервале
  • группировка по операторам связи

Исходники и бинарники для Mac OS и Linux на GitHub

Код
package main

import (
	"encoding/csv"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"

	"golang.org/x/text/encoding/charmap"
)

type params struct {
	URL      string
	Region   string
	Operator string
	Comment  bool
	Prefix   string
	Suffix   string
	Group    bool
}

func main() {
	p := readArgs()

	var values [][]string
	if p.Region != "" {
		values = filterRegion(parse(getCodes(p.URL)), p.Region)
	} else {
		values = parse(getCodes(p.URL))
	}

	if p.Operator != "" {
		values = filterOperator(values, p.Operator)
	}

	if p.Group {
		sort.Slice(values, func(i, j int) bool { return values[i][4] < values[j][4] })
	}
	op := ""
	for _, v := range values {
		_, min, max, dif := convert(v)
		if !validate(min, max, dif) {
			fmt.Printf("wrong interval: from %d to %d != %d\n", min, max, dif)
			continue
		}
		if p.Comment {
			fmt.Printf("; %v, %v, %v, %v, %v, %v\n", v[0], v[1], v[2], v[3], v[4], v[5])
		}
		if p.Group {
			if v[4] != op {
				fmt.Printf("; %s\n", v[4])
				op = v[4]
			}
		}
		if len([]rune(v[0])) != 3 || len([]rune(v[1])) != 7 || len([]rune(v[2])) != 7 {
			fmt.Printf("wrong interval: from %d to %d != %d\n", min, max, dif)
			continue
		}
		compute(p.Prefix+v[0], v[1], v[2], ""+p.Suffix)
	}
}

func getCodes(url string) string {
	var client http.Client
	res, err := client.Get(url)
	if err != nil {
		log.Panic(err)
	}

	defer res.Body.Close()

	if res.StatusCode == http.StatusOK {
		b, err := ioutil.ReadAll(res.Body)
		if err != nil {
			log.Panic(err)
		}
		dec := charmap.Windows1251.NewDecoder()
		body := make([]byte, len(b)*2)
		n, _, err := dec.Transform(body, []byte(b), false)

		if err != nil {
			log.Println(err)
		}

		return string(body[:n])
	}
	log.Panic("bad response")
	return ""
}

// returns slice without row names
// element 0: code
// 1: min value of interval
// 2: max value of interval
// 3: interval length
// 4: cellular operator
// 5: region
func parse(data string) [][]string {
	codes := csv.NewReader(strings.NewReader(fixCodes(data, 6)))

	codes.LazyQuotes = true
	codes.Comma = ';'

	ext, err := codes.ReadAll()
	if err != nil {
		log.Println(err)
	}
	return ext[1:]
}

// I found three records with excess separators
// in https://rossvyaz.gov.ru/docs/articles/DEF-9x.csv
// (start at 955;5550000;5559999;10000 ...)
func fixCodes(data string, f int) string {
	c := 0
	var x []rune
	for _, v := range []rune(data) {
		if v == '\n' {
			c = 0
		}

		// replace excess separators with spaces
		if v == ';' {
			c++
			if c > f-1 {
				v = ' '
			}
		}
		x = append(x, v)
	}
	return string(x)
}

func filterRegion(values [][]string, region string) [][]string {
	var res [][]string
	for _, v := range values {
		if strings.Contains(strings.ToLower(v[5]), strings.ToLower(region)) {
			res = append(res, v)
		}
	}
	return res
}

func filterOperator(values [][]string, operator string) [][]string {
	var res [][]string
	for _, v := range values {
		if strings.Contains(strings.ToLower(v[4]), strings.ToLower(operator)) {
			res = append(res, v)
		}
	}
	return res
}

func validate(min, max, dif int) bool {
	if max-min == dif-1 {
		return true
	}
	return false
}

func convert(v []string) (pre, min, max, dif int) {
	pre, err := strconv.Atoi(v[0])
	if err != nil {
		log.Panic(err)
	}
	min, err = strconv.Atoi(v[1])
	if err != nil {
		log.Panic(err)
	}
	max, err = strconv.Atoi(v[2])
	if err != nil {
		log.Panic(err)
	}
	dif, err = strconv.Atoi(v[3])
	if err != nil {
		log.Panic(err)
	}
	return
}

type runes []rune

func increment(r runes) runes {
	if len(r) <= 1 {
		return r
	}
	var res runes
	i, err := strconv.Atoi(string(r))
	if err != nil {
		log.Panic(err)
	}
	i++
	res = runes(strconv.Itoa(i))
	if len(res) == len(r) {
		return res
	}

	res = res.reverse()
	l := len(res)

	for i := 0; i < len(r)-l; i++ {
		res = append(res, '0')

	}

	return res.reverse()
}

func (r runes) reverse() (rev []rune) {
	for i := len(r) - 1; i >= 0; i-- {
		rev = append(rev, r[i])
	}
	return
}

func hi(r runes) runes {
	var res runes
	for i := len(r) - 1; i >= 1; i-- {
		res = append(res, '9')
		if r[i] == '0' {
			continue
		}
		break
	}
	l := len(res)
	r = r.reverse()
	for i := l; i < len(r); i++ {
		res = append(res, r[i])
	}

	return res.reverse()
}

func decrement(r runes) runes {
	if len(r) <= 1 {
		return r
	}
	var res runes
	i, err := strconv.Atoi(string(r))
	if err != nil {
		log.Panic(err)
	}
	i--
	res = runes(strconv.Itoa(i))

	if len(res) == len(r) {
		return res
	}

	res = res.reverse()
	l := len(res)

	for i := 0; i < len(r)-l; i++ {
		res = append(res, '0')

	}

	return res.reverse()
}

func low(r runes) runes {
	var res runes
	res = append(res, r[0])
	for i := 2; i <= len(r); i++ {
		res = append(res, '0')
	}
	return res
}

func compute(pre, min, max, suf string) {

	// mask found if min and max are equal
	if min == max {
		fmt.Printf("%v%v%v\n", pre, min, suf)
		return
	}

	var prefix []rune
	mi := runes(min)
	ma := runes(max)

	if len(mi) != len(ma) {
		log.Panic("the length of min and max values is not equal")
	}

	for k, v := range ma {
		if v == mi[k] {
			prefix = append(prefix, v)
			continue
		}
		break
	}

	if l := len(prefix); l != 0 {
		compute(pre+string(prefix), string(mi[l:]), string(ma[l:]), suf)
		return
	}

	var suffix runes
	for k, v := range mi.reverse() {
		if ma.reverse()[k]-v == 9 {
			suffix = append(suffix, 'X')
			continue
		}
		break
	}

	if l := len(suffix); l != 0 {
		compute(pre,
			string(mi)[:len(mi)-l],
			string(ma)[:len(ma)-l],
			string(suffix)+suf)
		return
	}
	if len(mi) == 1 {
		compute(pre+"["+string(mi)+"-"+string(ma)+"]", "", "", suf)
		return
	}

	zc, err := strconv.Atoi(min)
	if err != nil {
		log.Panic(err)
	}

	if zc == 0 {
		compute(pre, string(mi), string(decrement(low(ma))), suf)
		compute(pre, string(low(ma)), max, suf)
		return
	}

	compute(pre, string(mi), string(hi(mi)), suf)

	if increment(hi(mi))[0] == mi[0] {
		compute(pre, string(increment(hi(mi))), max, suf)
	} else {
		if increment(hi(mi))[0] == ma[0] {
			compute(pre, string(increment(hi(mi))), max, suf)
		} else {
			compute(pre, string(increment(hi(mi))), string(decrement(low(ma))), suf)
			compute(pre, string(low(ma)), max, suf)
		}
	}
}

func readArgs() params {
	p := params{
		URL:      "https://rossvyaz.gov.ru/docs/articles/DEF-9x.csv",
		Region:   "",
		Operator: "",
		Comment:  false,
		Prefix:   "",
		Suffix:   "",
		Group:    false,
	}

	wait := false
	key := ""

	for _, v := range os.Args[1:] {
		if wait {
			switch key {
			case "-u":
				p.URL = v
			case "-r":
				p.Region = v
			case "-o":
				p.Operator = v
			case "-p":
				p.Prefix = v
			case "-s":
				p.Suffix = v
			}
			wait = false
		} else {
			if v == "-c" {
				p.Comment = true
				continue
			}
			if v == "-g" {
				p.Group = true
				continue
			}

			switch v {
			case "-u", "-r", "-o", "-p", "-s":
				key = v
				wait = true
			case "-h":
				help()
				os.Exit(0)
			default:
				fmt.Println("unknown option:", v)
				fmt.Println("show help: genmask -h")
				os.Exit(1)
			}
		}
	}
	if wait == true {
		fmt.Printf("missing value of %s argument\n", key)
		os.Exit(1)
	}
	return p
}

func help() {
	fmt.Println("usage: genmask [-u <url>] [-r <region filter>] [-c] [-p <prefix>] [-s <suffix>]")
	fmt.Println("\t-u <value>: url to csv file. Default is https://rossvyaz.gov.ru/docs/articles/DEF-9x.csv")
	fmt.Println("\t-r <value>: find entries in the csv file that contain the value in region field.")
	fmt.Println("\t            It's better to use short masks, because errors and typos are possible in the csv file.")
	fmt.Println("\t-o <value>: find entries in the csv file that contain the value in operator field.")
	fmt.Println("\t            It's better to use short masks, because errors and typos are possible in the csv file.")
	fmt.Println("\t-c         Print a comment: <; code, min, max, length, cellular operator, region> before each interval")
	fmt.Println("\t-p <value>: Print a prefix for each mask")
	fmt.Println("\t-s <value>: Print a suffix for each mask")
	fmt.Println("\t-g <value>: Group output by cellular operator")
	fmt.Println("show this help: genmask -h")
}


Пример вывода
genmask -r амурск -g
; АО "Компания ТрансТелеКом"
958034XXXX
; ОАО "МТТ"
95840430XX
; ООО "Газпром телеком"
9584586XXX
; ООО "Глобал Телеком"
958385[0-1]XXX
; ООО "Скартел"
99916[5-6]XXXX
99646[0-2]XXXX
99638[3-7]XXXX
999681[0-4]XXX
991116[8-9]XXX
9911170XXX
; ООО "Т2 Мобайл"
90101368XX
994200XXXX
; ПАО "Вымпел-Коммуникации"
968246XXXX
9696392XXX
965677XXXX
96567[0-2]XXXX
96384[2-9]XXXX
96381[8-9]XXXX
96381[0-7]XXXX
90989[3-5]XXXX
96380XXXXX
96228[3-5]XXXX
96195XXXXX
90988[3-5]XXXX
90981XXXXX
96813[2-4]XXXX
96229[3-5]XXXX
; ПАО "МегаФон"
999251[5-9]XXX
92949[3-4]XXXX
92949[0-1]XXXX
92947[5-9]XXXX
924748XXXX
92484XXXXX
92474[4-6]XXXX
92467XXXXX
92468[0-4]XXXX
92458[0-4]XXXX
92444XXXXX
92434XXXXX
934476XXXX
92404[0-1]XXXX
924025[4-7]XXX
999254[7-8]XXX
92414XXXXX
; ПАО "Мобильные ТелеСистемы"
91453[0-2]XXXX
91406[0-4]XXXX
91404XXXXX
914538XXXX
914539[0-2]XXX
9145[5-9]XXXXX
9146[0-1]XXXXX
9143[8-9]XXXXX

Комментарии (9)


  1. arheops
    24.11.2018 19:57

    Что-то слишком сложный у вас код.

    Вообще говоря, решение — НЕВЕРНОЕ. Вы таким образом заставляете астериск заниматься перелопачиванием 100к+ кодов, что выполняется долго, он на это не рассчитан.

    Верное решение — создать в любой базе(да хоть в mysql) таблицу с индексом и в астериске через func_odbc сделать lookup. Работать будет на порядок быстрее.


    1. prekrati Автор
      24.11.2018 20:19

      Никто не запрещает реализовывать диалплан любым удобным способом. Можно и REST API пильнуть для CURL(), было бы желание.
      Эта статья — не мануал, а приведённый выше код не генерирует диалплан. Это всего лишь инструмент для генерации масок расширений с возможностью фильтрации по регионам и операторам связи. Используйте, как удобно или не используйте вовсе.


  1. willyd
    25.11.2018 22:00

    Просто интересно.
    А в чем задача стоит, что нужно все это валить в план диалплан? Ничего из стандартного, на первый взгляд, такое не требует.


    1. prekrati Автор
      24.11.2018 22:34
      +1

      У компании два филиала в регионах А и Б. Для каждого региона свой транк. Разделите исходящий трафик телефонии так, чтобы вызовы на номера каждого из регионов шли через соответствующий транк.
      Решений много:
      — получайте нужный регион из бд с интервалами DEF кодов;
      — напишите простой бэкенд, возвращающий регион для заданного номера;
      — добавьте в диалплан пару сотен соответсвующих масок расширений;
      — используйте Lua для создания гибкого и компактного диалплана;
      Если будет интересно, про каждый из этих пунктов можно написать отдельную статью.


      1. willyd
        25.11.2018 01:37

        Ясно, соглашусь со сказанным выше.
        Пихать такое в диалплан ради LCR как-то неуместно. Запрос из базы — пустяковый.
        У вас в файле даны диапазоны, номера не начинаются с нуля, значит можно работать с целочисленными значениями.

        select region from table_name where number between start and end;

        Импорт файла тоже в одну строчку делается.
        И можно сразу возвратить несколько направлений для фейловера, если тарифы прикручены.


  1. x86128
    25.11.2018 07:03

    Если абонент перешел с сохранением номера к другому оператору, то как это у вас учитывается?


    1. prekrati Автор
      25.11.2018 21:53

      Ну, режим ясновидения пока в разработке.


  1. g613
    25.11.2018 22:26

    шаблоны в астериске кстати с _ начинаются

    _99646[0-2]XXXX


    1. prekrati Автор
      26.11.2018 10:10

      Используйте флаг -p, чтобы задать префикс.