🥴 Um quem é quem na assembleia que faz Santa Catarina andar pra trás desanda.online

Uses ALECT <-> TSE table to match people

+200 -145
+40
alesc_tse.csv
··· 1 + ant%C3%ADdio-lunelli,240001605916 2 + jair-miotto,240001616203 3 + lucas-neves,240001610819 4 + m%C3%A1rio-motta,240001614375 5 + marquito,240001610066 6 + maur%C3%ADcio-peixer,240001614353 7 + sargento-lima,240001614329 8 + volnei-weber,240001605921 9 + altair-silva,240001644767 10 + padre-pedro-baldissera,240001679648 11 + rodrigo-minotto,240001644398 12 + matheus-cadorin,240001597559 13 + jess%C3%A9-lopes,240001614350 14 + dr.-vicente-caropreso,240001612510 15 + neodi-saretta,240001679646 16 + oscar-gutz,240001614346 17 + pep%C3%AA-colla%C3%A7o,240001644772 18 + tiago-zilli,240001605911 19 + alex-brasil-,240001614349 20 + fabiano-da-luz,240001679627 21 + marcos-da-rosa,240001616186 22 + maur%C3%ADcio-eskudlark,240001614331 23 + paulinha,240001610824 24 + sergio-motta,240001610226 25 + ana-campagnolo,240001614348 26 + marcos-vieira,240001612502 27 + nilso-berlanda,240001614335 28 + julio-garcia,240001614390 29 + marcius-machado,240001614339 30 + mauro-de-nadal,240001605913 31 + napole%C3%A3o-bernardes-,240001614384 32 + luciane-carminatti,240001679643 33 + jos%C3%A9-milton-scheffer,240001644756 34 + camilo-martins,240001610835 35 + emerson-stein,240001605929 36 + fernando-krelling,240001605919 37 + ivan-naatz,240001614356 38 + junior-cardoso,240001645371 39 + s%C3%A9rgio-guimar%C3%A3es,240001616196 40 + carlos-humberto,240001614337
+160 -145
main.go
··· 2 2 3 3 import ( 4 4 "archive/zip" 5 + "crypto/tls" 5 6 _ "embed" 6 7 "encoding/csv" 7 8 "errors" ··· 10 11 "io" 11 12 "log" 12 13 "log/slog" 14 + "net" 13 15 "net/http" 14 16 "os" 15 17 "path/filepath" 16 18 "sort" 17 19 "strings" 20 + "time" 18 21 19 22 "golang.org/x/sync/errgroup" 20 23 "golang.org/x/text/encoding/charmap" ··· 24 27 //go:embed index.html 25 28 var index string 26 29 27 - type bill struct { 30 + type votes struct { 28 31 name string 29 32 inFavor []string 30 33 against []string 31 34 } 32 35 33 36 var ( 34 - quotas = bill{ 35 - "Contra cotas raciais na UDESC", 37 + quotas = votes{ 38 + "Contra cotas raciais", 36 39 []string{ 37 40 "ALEX BRASIL", 38 41 "ALTAIR SILVA", ··· 76 79 "DR. VICENTE", 77 80 }, 78 81 } 79 - cameras = bill{ 82 + cameras = votes{ 80 83 "Câmeras em sala de aula", 81 84 []string{ 82 85 "PADRE PEDRO", ··· 136 139 type Person struct { 137 140 ID string 138 141 Name string 139 - Status string 140 - SocialMedia []SocialMedia 141 - Photo *string 142 + Party string 143 + Photo string 142 144 URL string 143 - Party string 144 145 Votes []Bill 146 + SocialMedia []SocialMedia 145 147 } 146 148 147 - func urlFor(client *http.Client, name string) (string, bool, error) { 148 - n := strings.ToLower(name) 149 - switch { 150 - case strings.Contains(n, "marquito"): 151 - n = "marquito" 152 - case strings.Contains(n, "padre pedro"): 153 - n = "padre-pedro-baldissera" 154 - case strings.Contains(n, "dr. vicente"): 155 - n = "dr.-vicente-caropreso" 156 - default: 157 - n = strings.ReplaceAll(n, " ", "-") 158 - n = strings.ReplaceAll(n, ".", "") 159 - n = strings.ReplaceAll(n, "'", "") 160 - } 161 - url := fmt.Sprintf("https://www.alesc.sc.gov.br/deputados/%s/", n) 162 - resp, err := client.Get(url) 149 + func readData() (map[string]string, error) { 150 + f, err := os.Open("alesc_tse.csv") 163 151 if err != nil { 164 - slog.Warn("HTTP error", "error", err, "url", url) 165 - return "", false, err 152 + return nil, err 166 153 } 167 154 defer func() { 168 - if err := resp.Body.Close(); err != nil { 169 - slog.Warn("could not close response body", "error", err, "url", url) 155 + if err := f.Close(); err != nil { 156 + slog.Warn("could not close CSV file", "error", err) 170 157 } 171 158 }() 172 - if resp.StatusCode != http.StatusOK { 173 - return "", false, errors.New(resp.Status) 159 + r := csv.NewReader(f) 160 + rows, err := r.ReadAll() 161 + if err != nil { 162 + return nil, err 174 163 } 175 - return url, true, nil 164 + m := make(map[string]string) 165 + for _, row := range rows { 166 + m[row[1]] = row[0] 167 + } 168 + return m, nil 176 169 } 177 170 178 171 func copyPhotoFrom(id string, pth string) (string, error) { ··· 187 180 }() 188 181 for _, f := range r.File { 189 182 n := filepath.Base(f.Name) 190 - if strings.HasPrefix(n, fmt.Sprintf("FSC%s_div", id)) { 191 - src, err := f.Open() 192 - if err != nil { 193 - return "", err 183 + if !strings.HasPrefix(n, fmt.Sprintf("FSC%s_div", id)) { 184 + continue 185 + } 186 + src, err := f.Open() 187 + if err != nil { 188 + return "", err 189 + } 190 + defer func() { 191 + if err := src.Close(); err != nil { 192 + slog.Warn("could not close zip file source", "error", err) 194 193 } 195 - defer func() { 196 - if err := src.Close(); err != nil { 197 - slog.Warn("could not close zip file source", "error", err) 198 - } 199 - }() 200 - if err := os.MkdirAll("site/images", 0755); err != nil { 201 - return "", err 202 - } 203 - dst, err := os.Create(fmt.Sprintf("site/images/%s.jpg", id)) 204 - if err != nil { 205 - return "", err 206 - } 207 - defer func() { 208 - if err := dst.Close(); err != nil { 209 - slog.Warn("could not close destination file", "error", err, "id", id) 210 - } 211 - }() 212 - if _, err := io.Copy(dst, src); err != nil { 213 - return "", err 194 + }() 195 + if err := os.MkdirAll("site/images", 0755); err != nil { 196 + return "", err 197 + } 198 + dst, err := os.Create(fmt.Sprintf("site/images/%s.jpg", id)) 199 + if err != nil { 200 + return "", err 201 + } 202 + defer func() { 203 + if err := dst.Close(); err != nil { 204 + slog.Warn("could not close destination file", "error", err, "id", id) 214 205 } 215 - return fmt.Sprintf("images/%s.jpg", id), nil 206 + }() 207 + if _, err := io.Copy(dst, src); err != nil { 208 + return "", err 216 209 } 210 + return fmt.Sprintf("images/%s.jpg", id), nil 217 211 } 218 212 return "", nil 219 213 } 220 214 221 - func getVoteFor(name string, b bill) string { 215 + func getVoteFor(name string, b votes) string { 222 216 for _, nm := range b.against { 223 217 if strings.EqualFold(nm, name) { 224 218 return "no" ··· 233 227 } 234 228 235 229 func getBillsFor(name string) []Bill { 236 - return []Bill{ 237 - {Name: quotas.name, Vote: getVoteFor(name, quotas)}, 238 - {Name: cameras.name, Vote: getVoteFor(name, cameras)}, 230 + var b []Bill 231 + for _, d := range []votes{quotas, cameras} { 232 + v := getVoteFor(name, d) 233 + if v == "" { 234 + continue 235 + } 236 + b = append(b, Bill{d.name, v}) 239 237 } 238 + return b 239 + } 240 + 241 + func csvFromZip(pth string, name string) ([][]string, error) { 242 + r, err := zip.OpenReader(pth) 243 + if err != nil { 244 + return nil, err 245 + } 246 + defer func() { 247 + if err := r.Close(); err != nil { 248 + slog.Warn("could not close file", "path", pth, "error", err) 249 + } 250 + }() 251 + for _, f := range r.File { 252 + if filepath.Base(f.Name) == name { 253 + src, err := f.Open() 254 + if err != nil { 255 + return nil, err 256 + } 257 + defer func() { 258 + if err := src.Close(); err != nil { 259 + slog.Warn("could not close archived file", "path", pth, "name", f.Name, "error", err) 260 + } 261 + }() 262 + r := csv.NewReader(transform.NewReader(src, charmap.Windows1252.NewDecoder())) 263 + r.Comma = ';' 264 + r.LazyQuotes = true 265 + return r.ReadAll() 266 + } 267 + } 268 + return nil, fmt.Errorf("file %s not found in %s", name, pth) 240 269 } 241 270 242 271 func socialIcon(url string) string { ··· 261 290 return "fas fa-globe" 262 291 } 263 292 264 - func checkURL(client *http.Client, url string) bool { 265 - resp, err := client.Head(url) 266 - if err != nil { 267 - return false 268 - } 269 - defer func() { 270 - if err := resp.Body.Close(); err != nil { 271 - log.Printf("Error closing response body for %s: %v", url, err) 272 - } 273 - }() 274 - return resp.StatusCode == http.StatusOK 275 - } 276 - 277 - func readCSVFromZip(pth string, name string) ([][]string, error) { 278 - r, err := zip.OpenReader(pth) 279 - if err != nil { 280 - return nil, err 281 - } 282 - defer func() { 283 - if err := r.Close(); err != nil { 284 - log.Printf("Error closing zip reader for %s: %v", pth, err) 285 - } 286 - }() 287 - for _, f := range r.File { 288 - if filepath.Base(f.Name) == name { 289 - src, err := f.Open() 293 + func socialMediaFor(m *map[string][]string, client *http.Client, id string) ([]SocialMedia, error) { 294 + var urls []SocialMedia 295 + var g errgroup.Group 296 + ch := make(chan string) 297 + for _, url := range (*m)[id] { 298 + g.Go(func() error { 299 + resp, err := client.Head(url) 290 300 if err != nil { 291 - return nil, err 301 + var nf *net.DNSError 302 + if errors.As(err, &nf) && nf.IsNotFound { 303 + return nil 304 + } 305 + return fmt.Errorf("could not check %s: %w", url, err) 292 306 } 293 307 defer func() { 294 - if err := src.Close(); err != nil { 295 - log.Printf("Error closing zip file source: %v", err) 308 + if err := resp.Body.Close(); err != nil { 309 + slog.Warn("could not close http response body", "url", url, "error", err) 296 310 } 297 311 }() 298 - reader := transform.NewReader(src, charmap.Windows1252.NewDecoder()) 299 - r := csv.NewReader(reader) 300 - r.Comma = ';' 301 - r.LazyQuotes = true 302 - return r.ReadAll() 312 + ok := resp.StatusCode >= 200 && resp.StatusCode < 400 313 + if ok { 314 + ch <- url 315 + } 316 + return nil 317 + }) 318 + } 319 + go func() { 320 + defer close(ch) 321 + if err := g.Wait(); err != nil { 322 + slog.Warn("error waiting for social media checks", "error", err) 303 323 } 324 + }() 325 + for url := range ch { 326 + urls = append(urls, SocialMedia{URL: url, Icon: socialIcon(url)}) 304 327 } 305 - return nil, fmt.Errorf("file %s not found in %s", name, pth) 328 + return urls, nil 306 329 } 307 330 308 331 func main() { 309 - urls, err := readCSVFromZip("tse/rede_social_candidato_2022_SC.zip", "rede_social_candidato_2022_SC.csv") 332 + init := time.Now() 333 + defer func() { 334 + slog.Info("site built", "elapsed", time.Since(init)) 335 + }() 336 + slugs, err := readData() 337 + if err != nil { 338 + log.Fatal(err) 339 + } 340 + urls, err := csvFromZip("tse/rede_social_candidato_2022_SC.zip", "rede_social_candidato_2022_SC.csv") 310 341 if err != nil { 311 342 log.Fatal(err) 312 343 } ··· 315 346 if i == 0 { 316 347 continue 317 348 } 318 - id := row[8] 319 - url := row[10] 320 - m[id] = append(m[id], url) 349 + m[row[8]] = append(m[row[8]], row[10]) 321 350 } 322 - all, err := readCSVFromZip("tse/consulta_cand_2022.zip", "consulta_cand_2022_SC.csv") 351 + rows, err := csvFromZip("tse/consulta_cand_2022.zip", "consulta_cand_2022_SC.csv") 323 352 if err != nil { 324 353 log.Fatal(err) 325 354 } 355 + client := &http.Client{ 356 + CheckRedirect: func(req *http.Request, via []*http.Request) error { 357 + return http.ErrUseLastResponse 358 + }, 359 + Transport: &http.Transport{ 360 + MaxConnsPerHost: 8, 361 + TLSClientConfig: &tls.Config{ 362 + InsecureSkipVerify: true, 363 + }, 364 + }, 365 + } 326 366 var g errgroup.Group 327 367 g.SetLimit(16) 328 368 idIdx := 15 329 369 nameIdx := 18 330 370 partyIdx := 26 331 - statusIdx := 49 332 371 ch := make(chan Person) 333 372 done := make(chan struct{}) 334 - client := &http.Client{ 335 - CheckRedirect: func(req *http.Request, via []*http.Request) error { 336 - return http.ErrUseLastResponse 337 - }, 338 - } 339 373 var ps []Person 340 374 go func() { 341 375 var t uint ··· 349 383 }) 350 384 close(done) 351 385 }() 352 - for i, row := range all { 386 + for i, row := range rows { 353 387 if i == 0 { 354 388 continue 355 389 } 356 - s := row[statusIdx] 357 - if s == "4" || s == "-1" { 358 - continue 359 - } 360 390 g.Go(func() error { 361 391 id := row[idIdx] 362 - p := Person{ID: id, Name: row[nameIdx], Party: row[partyIdx]} 363 - p.Name = strings.TrimPrefix(strings.TrimSpace(p.Name), "A ") 364 - p.Votes = getBillsFor(p.Name) 365 - ok := false 366 - for _, b := range p.Votes { 367 - if b.Vote != "" { 368 - ok = true 369 - break 370 - } 371 - } 392 + slug, ok := slugs[id] 372 393 if !ok { 373 394 return nil 374 395 } 375 - u, ok, err := urlFor(client, p.Name) 376 - if err != nil || !ok { 396 + n := strings.TrimPrefix(strings.TrimSpace(row[nameIdx]), "A ") // bad data? 397 + v := getBillsFor(n) 398 + if len(v) == 0 { 377 399 return nil 378 400 } 379 - p.URL = u 380 401 pic, err := copyPhotoFrom(id, "tse/foto_cand2022_SC_div.zip") 381 - if err == nil && pic != "" { 382 - p.Photo = &pic 402 + if err != nil { 403 + return fmt.Errorf("could not get photo for %s (%s): %w", n, id, err) 383 404 } 384 - var us []SocialMedia 385 - var sg errgroup.Group 386 - c := make(chan string, len(m[id])) 387 - for _, u := range m[id] { 388 - u := u 389 - sg.Go(func() error { 390 - if checkURL(client, u) { 391 - c <- u 392 - } 393 - return nil 394 - }) 405 + if pic == "" { 406 + return fmt.Errorf("could not get photo for %s (%s)", n, id) 395 407 } 396 - go func() { 397 - if err := sg.Wait(); err != nil { 398 - slog.Warn("error waiting for social media checks", "error", err) 399 - } 400 - close(c) 401 - }() 402 - for u := range c { 403 - us = append(us, SocialMedia{URL: u, Icon: socialIcon(u)}) 408 + sm, err := socialMediaFor(&m, client, id) 409 + if err != nil { 410 + return fmt.Errorf("could not get social media for %s (%s): %w", n, id, err) 411 + } 412 + p := Person{ 413 + id, 414 + n, 415 + row[partyIdx], 416 + pic, 417 + fmt.Sprintf("https://www.alesc.sc.gov.br/deputados/%s/", slug), 418 + v, 419 + sm, 404 420 } 405 - p.SocialMedia = us 406 421 ch <- p 407 422 return nil 408 423 })