Rename Section to Crew, stage the load pipeline, dedupe lookups and token minting, drop burned tools
This commit is contained in:
@@ -2,14 +2,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"heliosian/internal/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -19,9 +18,5 @@ func main() {
|
||||
if key == "" || *email == "" {
|
||||
log.Fatal("[ERROR] SESSION_KEY and -email are required")
|
||||
}
|
||||
payload := fmt.Sprintf("%s|%d", *email, time.Now().Add(24*time.Hour).Unix())
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(payload))
|
||||
fmt.Println(base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." +
|
||||
base64.RawURLEncoding.EncodeToString(mac.Sum(nil)))
|
||||
fmt.Println(auth.Token([]byte(key), *email, time.Now().Add(24*time.Hour)))
|
||||
}
|
||||
|
||||
+13
-13
@@ -5,12 +5,21 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"heliosian/internal/data"
|
||||
"heliosian/internal/directory"
|
||||
)
|
||||
|
||||
type staticFiles struct{}
|
||||
|
||||
func (staticFiles) Has(key string) bool {
|
||||
_, err := os.Stat(filepath.Join("web/static", filepath.FromSlash(key)))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
sheet := flag.String("sheet", "", "spreadsheet id")
|
||||
flag.Parse()
|
||||
@@ -21,7 +30,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] sheet source: %v", err)
|
||||
}
|
||||
model, err := directory.LoadModel(source, nil)
|
||||
model, err := directory.LoadModel(source, nil, staticFiles{})
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] load model: %v", err)
|
||||
}
|
||||
@@ -56,11 +65,11 @@ func main() {
|
||||
}
|
||||
fmt.Println("classrooms:")
|
||||
for _, c := range model.Classrooms {
|
||||
fmt.Printf(" %s (image %v, crews %v)\n", c.Name, c.ImageURL != "", c.HasSections)
|
||||
fmt.Printf(" %s (image %v, crews %v)\n", c.Name, c.ImageURL != "", c.HasCrews)
|
||||
}
|
||||
fmt.Println("crews:")
|
||||
for _, s := range model.Sections {
|
||||
fmt.Printf(" %s | %s | %s | teachers %v\n", s.Classroom, s.Name, s.GradeBand, s.Teachers)
|
||||
for _, c := range model.Crews {
|
||||
fmt.Printf(" %s | %s | %s | teachers %v\n", c.Classroom, c.Name, c.GradeBand, c.Teachers)
|
||||
}
|
||||
bands := []string{}
|
||||
for band := range model.RoomParents {
|
||||
@@ -76,13 +85,4 @@ func main() {
|
||||
for _, g := range model.Grades {
|
||||
fmt.Printf(" %s -> %s (%s -> %s)\n", g.Name, g.NextName, g.Band, g.NextBand)
|
||||
}
|
||||
for _, email := range []string{"lexi.augenbergs@heliosschool.org", "daren.liang@heliosschool.org", "yeada.li@heliosschool.org", "dog@heliosschool.org", "mike.orlando@heliosschool.org", "evie.weiss@heliosschool.org"} {
|
||||
for _, p := range model.People {
|
||||
if p.Email == email {
|
||||
fmt.Printf("spot %s: full=%q legal=%q pref=%q roles=S%v/P%v/T%v grade=%q class=%q crew=%q band=%q dept=%q title=%q family=%s\n",
|
||||
email, p.FullName, p.LegalName, p.PreferredName, p.IsStudent, p.IsParent, p.IsStaff,
|
||||
p.Grade, p.Classroom, p.Section, p.GradeBand, p.Department, p.JobTitle, p.FamilyKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Command oneoff appends the Opted Out column to the Overrides and Change Log tabs.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"heliosian/internal/data"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/sheets/v4"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sheet := flag.String("sheet", "", "spreadsheet id")
|
||||
flag.Parse()
|
||||
if *sheet == "" {
|
||||
log.Fatal("[ERROR] -sheet <spreadsheet id> is required")
|
||||
}
|
||||
svc, err := sheets.NewService(context.Background(),
|
||||
option.WithCredentialsFile(data.KeyFile),
|
||||
option.WithScopes(sheets.SpreadsheetsScope))
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] create sheets client: %v", err)
|
||||
}
|
||||
meta, err := svc.Spreadsheets.Get(*sheet).Fields("sheets(properties(sheetId,title,gridProperties(columnCount)))").Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] get spreadsheet: %v", err)
|
||||
}
|
||||
for _, tab := range []string{"Overrides", "Change Log"} {
|
||||
var props *sheets.SheetProperties
|
||||
for _, s := range meta.Sheets {
|
||||
if s.Properties.Title == tab {
|
||||
props = s.Properties
|
||||
}
|
||||
}
|
||||
if props == nil {
|
||||
log.Fatalf("[ERROR] no %s tab", tab)
|
||||
}
|
||||
resp, err := svc.Spreadsheets.Values.Get(*sheet, fmt.Sprintf("'%s'!1:1", tab)).Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] read %s header: %v", tab, err)
|
||||
}
|
||||
width := len(resp.Values[0])
|
||||
for _, cell := range resp.Values[0] {
|
||||
if fmt.Sprint(cell) == "Opted Out" {
|
||||
log.Fatalf("[ERROR] %s already has an Opted Out column", tab)
|
||||
}
|
||||
}
|
||||
if int64(width) >= props.GridProperties.ColumnCount {
|
||||
_, err = svc.Spreadsheets.BatchUpdate(*sheet, &sheets.BatchUpdateSpreadsheetRequest{
|
||||
Requests: []*sheets.Request{{AppendDimension: &sheets.AppendDimensionRequest{
|
||||
SheetId: props.SheetId,
|
||||
Dimension: "COLUMNS",
|
||||
Length: 1,
|
||||
}}},
|
||||
}).Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] widen %s: %v", tab, err)
|
||||
}
|
||||
}
|
||||
cell := fmt.Sprintf("'%s'!%s1", tab, columnName(width))
|
||||
_, err = svc.Spreadsheets.Values.Update(*sheet, cell, &sheets.ValueRange{
|
||||
Values: [][]interface{}{{"Opted Out"}},
|
||||
}).ValueInputOption("RAW").Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] write %s header: %v", tab, err)
|
||||
}
|
||||
log.Printf("added Opted Out to %s at %s", tab, cell)
|
||||
}
|
||||
}
|
||||
|
||||
func columnName(idx int) string {
|
||||
name := ""
|
||||
for idx >= 0 {
|
||||
name = string(rune('A'+idx%26)) + name
|
||||
idx = idx/26 - 1
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Command renamefamilyblobs renames family media in the drive from parent-email names to family key hashes.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"heliosian/internal/data"
|
||||
"heliosian/internal/directory"
|
||||
"google.golang.org/api/drive/v3"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
const folderMime = "application/vnd.google-apps.folder"
|
||||
|
||||
func main() {
|
||||
sheet := flag.String("sheet", "", "spreadsheet id")
|
||||
flag.Parse()
|
||||
if *sheet == "" {
|
||||
log.Fatal("[ERROR] -sheet <spreadsheet id> is required")
|
||||
}
|
||||
source, err := data.NewSheet(map[string]string{"directory": *sheet})
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] sheet source: %v", err)
|
||||
}
|
||||
model, err := directory.LoadModel(source, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] load model: %v", err)
|
||||
}
|
||||
localToHash := map[string]string{}
|
||||
for _, family := range model.Families {
|
||||
for _, adult := range family.AdultEmails {
|
||||
local, _, _ := strings.Cut(adult, "@")
|
||||
if existing, ok := localToHash[local]; ok && existing != family.Key {
|
||||
log.Fatalf("[ERROR] adult local part %s maps to two families", local)
|
||||
}
|
||||
localToHash[local] = family.Key
|
||||
}
|
||||
}
|
||||
|
||||
svc, err := drive.NewService(context.Background(),
|
||||
option.WithCredentialsFile(data.KeyFile),
|
||||
option.WithScopes(drive.DriveScope))
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] drive client: %v", err)
|
||||
}
|
||||
drives, err := svc.Drives.List().Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] list shared drives: %v", err)
|
||||
}
|
||||
if len(drives.Drives) != 1 {
|
||||
log.Fatalf("[ERROR] expected one shared drive, found %d", len(drives.Drives))
|
||||
}
|
||||
folderList, err := svc.Files.List().
|
||||
Q(fmt.Sprintf("name = 'families' and '%s' in parents and mimeType = '%s' and trashed = false", drives.Drives[0].Id, folderMime)).
|
||||
SupportsAllDrives(true).IncludeItemsFromAllDrives(true).Corpora("allDrives").
|
||||
Fields("files(id)").Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] find families folder: %v", err)
|
||||
}
|
||||
if len(folderList.Files) != 1 {
|
||||
log.Fatalf("[ERROR] expected one families folder, found %d", len(folderList.Files))
|
||||
}
|
||||
folderID := folderList.Files[0].Id
|
||||
|
||||
renamed, kept, unknown := 0, 0, 0
|
||||
token := ""
|
||||
for {
|
||||
call := svc.Files.List().
|
||||
Q(fmt.Sprintf("'%s' in parents and trashed = false", folderID)).
|
||||
SupportsAllDrives(true).IncludeItemsFromAllDrives(true).Corpora("allDrives").
|
||||
Fields("nextPageToken, files(id, name, mimeType)").PageSize(1000)
|
||||
if token != "" {
|
||||
call = call.PageToken(token)
|
||||
}
|
||||
list, err := call.Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] list families folder: %v", err)
|
||||
}
|
||||
for _, f := range list.Files {
|
||||
if f.MimeType == folderMime {
|
||||
continue
|
||||
}
|
||||
ext := path.Ext(f.Name)
|
||||
base := strings.TrimSuffix(f.Name, ext)
|
||||
kind := ""
|
||||
for _, k := range []string{"-photo", "-pronunciation"} {
|
||||
if strings.HasSuffix(base, k) {
|
||||
kind = k
|
||||
}
|
||||
}
|
||||
if kind == "" {
|
||||
log.Printf("[ERROR] unrecognized file name %q, leaving it", f.Name)
|
||||
unknown++
|
||||
continue
|
||||
}
|
||||
local := strings.TrimSuffix(base, kind)
|
||||
hash, ok := localToHash[local]
|
||||
if !ok {
|
||||
if _, isCurrent := model.Families[local]; isCurrent {
|
||||
kept++
|
||||
continue
|
||||
}
|
||||
log.Printf("[ERROR] file %q matches no family adult, leaving it", f.Name)
|
||||
unknown++
|
||||
continue
|
||||
}
|
||||
newName := hash + kind + ext
|
||||
_, err := svc.Files.Update(f.Id, &drive.File{Name: newName}).SupportsAllDrives(true).Do()
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] rename %q to %q: %v", f.Name, newName, err)
|
||||
}
|
||||
log.Printf("renamed %q -> %q", f.Name, newName)
|
||||
renamed++
|
||||
}
|
||||
if list.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
token = list.NextPageToken
|
||||
}
|
||||
log.Printf("done: %d renamed, %d already keyed by hash, %d left untouched", renamed, kept, unknown)
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -12,6 +9,8 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"heliosian/internal/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -56,11 +55,7 @@ func main() {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
payload := fmt.Sprintf("%s|%d", *email, time.Now().Add(24*time.Hour).Unix())
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(payload))
|
||||
cookie := base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." +
|
||||
base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
cookie := auth.Token([]byte(key), *email, time.Now().Add(24*time.Hour))
|
||||
|
||||
content, _ := os.ReadFile("/tmp/heliosian-server.log")
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(content)), "\n") {
|
||||
|
||||
Reference in New Issue
Block a user