1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
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("📮 <b>" + esc(address.FormattedCEP()) + "</b>\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("<i>" + esc(line) + "</i>\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 "\n<i>Sem coordenada para este CEP na base do IBGE.</i>\n"
}
if geo.IsPrecise() {
return "\n🗺 <a href=\"" + esc(mapURL(geo.Lat, geo.Lon)) + "\">Ver no mapa</a>\n" +
"<i>Precisão de logradouro — o ponto é o trecho de rua (raio aprox. " +
esc(formatRadius(geo.RaioM)) + ").</i>\n"
}
return "\n⚠️ <b>CEP geral de município</b> — 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" +
"<i>" + esc(coordText(geo.Lat, geo.Lon)) + "</i>\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: <code>01001000</code> " +
"ou <code>01001-000</code>."
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 "<b>cep.is</b> — consulta de CEP na base oficial dos Correios (DNE).\n\n" +
"<b>Aqui no privado</b>\n" +
"Mande o CEP e pronto: <code>01001000</code> ou <code>01001-000</code>.\n\n" +
"<b>Em grupos e canais</b>\n" +
"Use <code>/cep 01001000</code>. Se houver outros bots, " +
"<code>/cep" + esc(at) + " 01001000</code> também funciona.\n\n" +
"<b>Em qualquer conversa</b>\n" +
"Digite <code>" + esc(at) + " 01001000</code> 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 <code>/cep 01001000</code>.\n" +
"Em qualquer conversa também dá para usar <code>@" + esc(username) +
" 01001000</code>."
}
// 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 (<code>01001000</code>) " +
"ou use /help."
func esc(text string) string { return html.EscapeString(text) }
|