diff --git a/alumni.go b/alumni.go new file mode 100644 index 0000000..a00a3c5 --- /dev/null +++ b/alumni.go @@ -0,0 +1,129 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// Structs matching the JSON structure + +type Person struct { + DisplayName string `json:"display_name"` + FirstName string `json:"first_name"` + MiddleName *string `json:"middle_name"` + LastName string `json:"last_name"` + LegalLastName string `json:"legal_last_name"` + NameSuffix *string `json:"name_suffix"` + Username string `json:"username"` + Email string `json:"email"` + Address Address `json:"address"` + Phone Phone `json:"phone"` + Majors []Major `json:"majors"` + Affiliations []string `json:"affiliations"` + Appointments []Appointment `json:"appointments"` +} + +type Address struct { + Building *Building `json:"building"` + RoomNumber *string `json:"room_number"` + Street1 string `json:"street1"` + Street2 *string `json:"street2"` + City string `json:"city"` + State string `json:"state"` + Zip string `json:"zip"` +} + +type Building struct { + Name string `json:"name"` + Number string `json:"number"` + URL string `json:"url"` +} + +type Phone struct { + AreaCode string `json:"area_code"` + Exchange string `json:"exchange"` + Subscriber string `json:"subscriber"` + Formatted string `json:"formatted"` +} + +type Major struct { + Major string `json:"major"` + College string `json:"college"` +} + +type Appointment struct { + JobTitle string `json:"job_title"` + WorkingTitle string `json:"working_title"` + Organization string `json:"organization"` + OrgCode string `json:"org_code"` + VpCollegeName string `json:"vp_college_name"` +} + +// Checks whether a user exists according to the OSU people search. `userid` should be in name.number format +func userExists(userid string) (bool, error) { + resp, err := http.Get(fmt.Sprintf("https://directory.osu.edu/fpjson.php?name_n=%s", userid)) + if err != nil { + return true, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return true, err + } + + var people []Person + err = json.Unmarshal(body, &people) + if err != nil { + return true, err + } + + // if no one shows up, the user does not exist + if len(people) == 0 { + return false, nil + } + return true, nil +} + +// Gets a current student from the database and alumnifies them if appropriate +func alumnusCheckNextUser(b *DiscordBot) error { + row := b.Db.QueryRow("SELECT buck_id, discord_id, name_num FROM users WHERE student=1 AND alum=0 AND discord_id IS NOT NULL ORDER BY last_alum_check_timestamp ASC LIMIT(1)") + var buckId string + var discordId string + var nameNum string + err := row.Scan(&buckId, &discordId, &nameNum) + if err == sql.ErrNoRows { + return nil + } + + if err != nil { + return err + } + + exists, err := userExists(nameNum) + if err != nil { + return err + } + + if !exists { + err = b.Alumnify(discordId) + if err != nil { + return err + } + } + + res, err := b.Db.Exec("UPDATE users SET last_alum_check_timestamp=strftime('%s', 'now') WHERE buck_id=?", buckId) + if err != nil { + return err + } + rowsAffected, err := res.RowsAffected() + if err != nil { + return err + } + if rowsAffected != 1 { + return fmt.Errorf("expected 1 row updated in alumnusCheckNextUser, got %d", rowsAffected) + } + + return nil +} diff --git a/discord.go b/discord.go index 3727926..c30f1a9 100644 --- a/discord.go +++ b/discord.go @@ -15,6 +15,7 @@ type DiscordBot struct { GuildId string AdminRoleId string StudentRoleId string + AlumniRoleId string ClientId string ClientSecret string Db *sql.DB @@ -156,6 +157,8 @@ func (b *DiscordBot) Connect() { b.Session = s + s.Identify.Intents = discordgo.IntentGuildMembers + s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Println("Logged in as", r.User.String()) }) @@ -166,6 +169,16 @@ func (b *DiscordBot) Connect() { } }) + s.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberAdd) { + row := b.Db.QueryRow("SELECT buck_id FROM users WHERE discord_id = ?", m.User.ID) + if row != nil { + err = b.GiveStudentRole(m.User.ID) + if err != nil { + log.Printf("Unable to add student role to user %v\n", m.User.Username) + } + } + }) + err = s.Open() if err != nil { log.Fatalln("Failed to open session", err) @@ -204,3 +217,27 @@ func (b *DiscordBot) RemoveStudentRole(discordId string) error { } return b.Session.GuildMemberRoleRemove(b.GuildId, discordId, b.StudentRoleId) } + +// Turns a student into an alum (removes student role and adds alumni role) +func (b *DiscordBot) Alumnify(discordId string) error { + if b.Session == nil { + return fmt.Errorf("discord bot not connected") + } + + err := b.Session.GuildMemberRoleRemove(b.GuildId, discordId, b.StudentRoleId) + if err != nil { + return fmt.Errorf("failed to remove student role from alum %s. %v", b.StudentRoleId, err) + } + + err = b.Session.GuildMemberRoleAdd(b.GuildId, discordId, b.AlumniRoleId) + if err != nil { + return fmt.Errorf("failed to add student role to alum %s. %v", b.StudentRoleId, err) + } + + _, err = b.Db.Exec("UPDATE Users set alum=1, student=0 WHERE discord_id=?", discordId) + if err != nil { + return err + } + + return nil +} diff --git a/main.go b/main.go index 7486ae8..4bed615 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,12 @@ import ( "fmt" "io/fs" "log" + "math/rand" "net/http" "net/url" "os" "slices" + "strconv" "strings" "time" @@ -404,6 +406,53 @@ func (r *Router) EnforceJwtMiddleware(handler http.Handler) http.Handler { //go:embed migrations/* var migrations embed.FS +func apply_migrations(db *sql.DB) { + dirs, err := migrations.ReadDir("migrations") + if err != nil { + log.Fatalln("Failed to read migrations directory:", err) + } + + slices.SortStableFunc(dirs, func(a fs.DirEntry, b fs.DirEntry) int { + return strings.Compare(a.Name(), b.Name()) + }) + + for _, entry := range dirs { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".up.sql") { + continue + } + + data, err := migrations.ReadFile(fmt.Sprintf("migrations/%v", entry.Name())) + if err != nil { + log.Fatalln("Failed to read", entry.Name(), err) + } + + migration_number, err := strconv.Atoi(strings.TrimSuffix(entry.Name(), ".up.sql")) + if err != nil { + log.Fatalln("migration names should be numeric", err) + } + + version_row := db.QueryRow("PRAGMA user_version") + var version int + err = version_row.Scan(&version) + if err != nil { + log.Fatalln("Unable to get user_version from database.") + } + if migration_number <= version { + fmt.Printf("Not applying migration %s user_version: %v\n", entry.Name(), version) + continue + } else { + fmt.Printf("Applying migration %s user_version: %v\n", entry.Name(), version) + } + + sql := string(data) + + _, err = db.Exec(sql) + if err != nil { + log.Fatalln("Failed to run", entry.Name(), err) + } + } +} + func main() { err := godotenv.Load() if err != nil { @@ -450,32 +499,7 @@ func main() { panic(err) } - dirs, err := migrations.ReadDir("migrations") - if err != nil { - log.Fatalln("Failed to read migrations directory:", err) - } - - slices.SortStableFunc(dirs, func(a fs.DirEntry, b fs.DirEntry) int { - return strings.Compare(a.Name(), b.Name()) - }) - - for _, entry := range dirs { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".up.sql") { - continue - } - - data, err := migrations.ReadFile(fmt.Sprintf("migrations/%v", entry.Name())) - if err != nil { - log.Fatalln("Failed to read", entry.Name(), err) - } - - sql := string(data) - - _, err = db.Exec(sql) - if err != nil { - log.Fatalln("Failed to run", entry.Name(), err) - } - } + apply_migrations(db) authEnvironment := os.Getenv("ENV") @@ -501,6 +525,7 @@ func main() { GuildId: os.Getenv("DISCORD_GUILD_ID"), AdminRoleId: os.Getenv("DISCORD_ADMIN_ROLE_ID"), StudentRoleId: os.Getenv("DISCORD_STUDENT_ROLE_ID"), + AlumniRoleId: os.Getenv("DISCORD_ALUMNI_ROLE_ID"), ClientId: os.Getenv("DISCORD_CLIENT_ID"), ClientSecret: os.Getenv("DISCORD_CLIENT_SECRET"), Db: db, @@ -574,6 +599,21 @@ func main() { mailchimp: mailchimp, } + // auto alumnify + go func() { + for { + fmt.Println("Alumnifying") + err = alumnusCheckNextUser(bot) + if err != nil { + fmt.Printf("error: %s", err.Error()) + } + fmt.Println("Done alumnifying") + time.Sleep(time.Duration(60+rand.Intn(60)) * time.Minute) + // Short time for debugging + // time.Sleep(time.Second * 10) + } + }() + mux.Handle("/", router.InjectJwtMiddleware(http.HandlerFunc(router.index))) mux.Handle("POST /mailchimp", router.InjectJwtMiddleware(router.EnforceJwtMiddleware(http.HandlerFunc(router.SetMailchimp)))) mux.Handle("/vote", router.InjectJwtMiddleware(router.EnforceJwtMiddleware(http.HandlerFunc(router.vote)))) diff --git a/migrations/001.up.sql b/migrations/001.up.sql index 031db4a..0926b2a 100644 --- a/migrations/001.up.sql +++ b/migrations/001.up.sql @@ -1,6 +1,6 @@ PRAGMA journal_mode=WAL; - BEGIN; +PRAGMA user_version=1; CREATE TABLE IF NOT EXISTS users ( buck_id TEXT PRIMARY KEY, @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS users ( is_admin INTEGER NOT NULL DEFAULT (FALSE), last_seen_timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), last_attended_timestamp INTEGER, - + added_to_mailinglist INTEGER NOT NULL DEFAULT (FALSE), -- 0 or 1 depending on if the user has the affiliation diff --git a/migrations/002.up.sql b/migrations/002.up.sql new file mode 100644 index 0000000..f73fe02 --- /dev/null +++ b/migrations/002.up.sql @@ -0,0 +1,4 @@ +BEGIN; +PRAGMA user_version=2; +ALTER TABLE users ADD COLUMN last_alum_check_timestamp INTEGER; +COMMIT; \ No newline at end of file diff --git a/migrations/seed2.sql b/migrations/seed2.sql new file mode 100644 index 0000000..6c83a65 --- /dev/null +++ b/migrations/seed2.sql @@ -0,0 +1,12 @@ +BEGIN; +INSERT OR REPLACE INTO users +(buck_id, discord_id, display_name, name_num, last_seen_timestamp, last_attended_timestamp, added_to_mailinglist, student, alum, employee, faculty, is_admin) +VALUES +(500123456, NULL, 'Brutus Buckeye', 'buckeye.1', NULL, NULL, 1,1,0,0,0,1); + +INSERT OR REPLACE INTO users +(buck_id, discord_id, display_name, name_num, last_seen_timestamp, last_attended_timestamp, added_to_mailinglist, student, alum, employee, faculty) +VALUES +(0, 1014305712312172595, 'Mark Bundschuh', 'bundschuh.15', 0, 0, 0, 1, 0, 0, 0), +(0, 393776786812436491, 'Benjamin Bundschuh', 'bundschuh.13', 0, 0, 0, 1, 0, 0, 0); +COMMIT; \ No newline at end of file