Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions alumni.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type DiscordBot struct {
GuildId string
AdminRoleId string
StudentRoleId string
AlumniRoleId string
ClientId string
ClientSecret string
Db *sql.DB
Expand Down Expand Up @@ -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())
})
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
92 changes: 66 additions & 26 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
"fmt"
"io/fs"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"slices"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")

Expand All @@ -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,
Expand Down Expand Up @@ -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))))
Expand Down
4 changes: 2 additions & 2 deletions migrations/001.up.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PRAGMA journal_mode=WAL;

BEGIN;
PRAGMA user_version=1;

CREATE TABLE IF NOT EXISTS users (
buck_id TEXT PRIMARY KEY,
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions migrations/002.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BEGIN;
PRAGMA user_version=2;
ALTER TABLE users ADD COLUMN last_alum_check_timestamp INTEGER;
COMMIT;
12 changes: 12 additions & 0 deletions migrations/seed2.sql
Original file line number Diff line number Diff line change
@@ -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;