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) }