package bot import ( "errors" "fmt" "html" "strconv" "strings" "time" cepis "cep.is/go" ) // ParseMode is the formatting Telegram applies to every message this bot sends. // HTML is used rather than Markdown because address data is arbitrary text from // the Correios database and escaping three characters is a complete defence, // while Markdown has a dozen context-dependent ones. const ParseMode = "HTML" // FormatAddress renders an address as the HTML body of a Telegram message. // // The coordinate section is where this function earns its keep. A CEP whose // precisao is "logradouro" gets a map link: the point is a stretch of street // and following it lands where the user expects. A CEP whose precisao is // "localidade" gets no link at all, because the CEP is the entire municipality // and its point is the centroid — dropping a pin there looks exactly like an // address and is wrong by kilometres. That case is labelled, given its radius // in kilometres, and told plainly not to be used as a delivery point. func FormatAddress(address *cepis.Address) string { var b strings.Builder b.WriteString("📮 " + esc(address.FormattedCEP()) + "\n") if line := streetLine(address); line != "" { b.WriteString(esc(line) + "\n") } if address.Unidade != "" { b.WriteString(esc(address.Unidade) + "\n") } b.WriteString(esc(placeLine(address)) + "\n") if line := metaLine(address); line != "" { b.WriteString("" + esc(line) + "\n") } b.WriteString(geoSection(address)) return strings.TrimRight(b.String(), "\n") } // InlineTitle is the one-line label shown in the inline results list. func InlineTitle(address *cepis.Address) string { if address.Logradouro != "" { return address.FormattedCEP() + " — " + address.Logradouro } return address.FormattedCEP() + " — " + address.Localidade } // InlineDescription is the second line of an inline result. It repeats the // municipality warning, so someone picking a result from the list sees it // before sending the message rather than after. func InlineDescription(address *cepis.Address) string { parts := []string{placeLine(address)} if address.Geo != nil && !address.Geo.IsPrecise() { parts = append(parts, "CEP geral do município") } return strings.Join(parts, " · ") } func streetLine(address *cepis.Address) string { parts := []string{} if address.Logradouro != "" { parts = append(parts, address.Logradouro) } if address.Complemento != "" { parts = append(parts, address.Complemento) } return strings.Join(parts, ", ") } func placeLine(address *cepis.Address) string { place := address.Localidade if address.UF != "" { place += "/" + address.UF } if address.Bairro != "" { return address.Bairro + " — " + place } return place } func metaLine(address *cepis.Address) string { parts := []string{} if address.Regiao != "" { parts = append(parts, address.Regiao) } if address.DDD != "" { parts = append(parts, "DDD "+address.DDD) } if address.IBGE != "" { parts = append(parts, "IBGE "+address.IBGE) } return strings.Join(parts, " · ") } func geoSection(address *cepis.Address) string { geo := address.Geo if geo == nil { return "\nSem coordenada para este CEP na base do IBGE.\n" } if geo.IsPrecise() { return "\n🗺 Ver no mapa\n" + "Precisão de logradouro — o ponto é o trecho de rua (raio aprox. " + esc(formatRadius(geo.RaioM)) + ").\n" } return "\n⚠️ CEP geral de município — não corresponde a um endereço.\n" + "A única coordenada disponível é o centroide de " + esc(address.Localidade) + ", com raio aproximado de " + esc(formatRadius(geo.RaioM)) + ". Não serve como ponto de entrega nem para calcular frete, " + "por isso não mando link de mapa.\n" + "" + esc(coordText(geo.Lat, geo.Lon)) + "\n" } func mapURL(lat, lon float64) string { latText := strconv.FormatFloat(lat, 'f', 6, 64) lonText := strconv.FormatFloat(lon, 'f', 6, 64) return "https://www.openstreetmap.org/?mlat=" + latText + "&mlon=" + lonText + "#map=17/" + latText + "/" + lonText } func coordText(lat, lon float64) string { return "Centroide: " + strconv.FormatFloat(lat, 'f', 6, 64) + ", " + strconv.FormatFloat(lon, 'f', 6, 64) } func formatRadius(metres int) string { if metres <= 0 { return "desconhecido" } if metres < 1000 { return strconv.Itoa(metres) + " m" } return strings.Replace(fmt.Sprintf("%.1f km", float64(metres)/1000), ".", ",", 1) } // ErrorMessage translates a failure from the cep.is client into a sentence in // Portuguese that says what happened and what the user can do about it. // // Every branch is spelled out. A user who gets no answer assumes the bot is // broken and stops using it, and "não encontrado" and "a API está fora" call // for completely different reactions. func ErrorMessage(err error) string { switch { case errors.Is(err, cepis.ErrCEPNotFound): return "🔍 CEP não encontrado na base dos Correios (DNE).\n" + "Confira os oito dígitos — a base é atualizada a cada quinze dias." case errors.Is(err, cepis.ErrInvalidCEP): return "✋ CEP inválido. Preciso de oito dígitos: 01001000 " + "ou 01001-000." case errors.Is(err, cepis.ErrRateLimited): return "🚦 A API está limitando as consultas neste momento. " + "Tente de novo em alguns instantes." case errors.Is(err, cepis.ErrServer): return "💥 A API da cep.is respondeu com erro. " + "Não é problema do seu CEP — tente de novo em alguns minutos." case errors.Is(err, cepis.ErrNetwork): return "🔌 Não consegui falar com a cep.is. " + "Pode ser instabilidade de rede — tente de novo em alguns minutos." case errors.Is(err, cepis.ErrInvalidResponse): return "🤔 A cep.is respondeu algo que não consegui interpretar. " + "Tente de novo em alguns minutos." } return "⚠️ Não consegui consultar este CEP agora. Tente de novo em alguns minutos." } // RateLimitMessage is what a user held by the local token bucket is told, once // per warning window. func RateLimitMessage(retryAfter time.Duration) string { seconds := int(retryAfter.Seconds()) if seconds < 1 { seconds = 1 } return "🐢 Calma — muitas consultas seguidas. Tente de novo em " + strconv.Itoa(seconds) + "s." } // HelpMessage explains the three ways to use the bot. The username comes from // getMe, so the examples are copy-pasteable for whichever bot the operator // created with @BotFather. func HelpMessage(username string) string { at := "@" + username return "cep.is — consulta de CEP na base oficial dos Correios (DNE).\n\n" + "Aqui no privado\n" + "Mande o CEP e pronto: 01001000 ou 01001-000.\n\n" + "Em grupos e canais\n" + "Use /cep 01001000. Se houver outros bots, " + "/cep" + esc(at) + " 01001000 também funciona.\n\n" + "Em qualquer conversa\n" + "Digite " + esc(at) + " 01001000 e escolha o resultado. " + "Não precisa me adicionar ao grupo.\n\n" + "Cerca de 57% dos CEPs têm coordenada. Quando o CEP é geral do " + "município, eu aviso em vez de mandar um pino no mapa.\n\n" + "API pública e documentação: https://cep.is" } // StartMessage is the answer to /start: a greeting plus the whole of // [HelpMessage], because a new user pressing Start has no other instructions. func StartMessage(username string) string { return "👋 Olá! Eu consulto CEP.\n\n" + HelpMessage(username) } // UsageMessage is the answer to a bare /cep with no argument. func UsageMessage(username string) string { return "Use /cep 01001000.\n" + "Em qualquer conversa também dá para usar @" + esc(username) + " 01001000." } // UnknownMessage is the answer to text in a private chat that is not a CEP and // not a command. It is only ever sent in private: answering unrecognised text // in a group would make the bot a nuisance. const UnknownMessage = "Não entendi. Mande um CEP (01001000) " + "ou use /help." func esc(text string) string { return html.EscapeString(text) }