diff options
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/conference/model.go (renamed from pkg/schedule/model.go) | 2 | ||||
| -rw-r--r-- | pkg/conference/parse.go (renamed from pkg/schedule/parse.go) | 2 | ||||
| -rw-r--r-- | pkg/conference/service.go | 218 | ||||
| -rw-r--r-- | pkg/database/migrations/0003_multi_conference.sql | 13 | ||||
| -rw-r--r-- | pkg/database/migrations/0004_user_admins.sql | 2 | ||||
| -rw-r--r-- | pkg/database/query/conferences.sql | 21 | ||||
| -rw-r--r-- | pkg/database/query/favourites.sql | 10 | ||||
| -rw-r--r-- | pkg/database/sqlc/conferences.sql.go | 119 | ||||
| -rw-r--r-- | pkg/database/sqlc/favourites.sql.go | 76 | ||||
| -rw-r--r-- | pkg/database/sqlc/models.go | 18 | ||||
| -rw-r--r-- | pkg/database/sqlc/users.sql.go | 36 | ||||
| -rw-r--r-- | pkg/favourites/service.go | 63 | ||||
| -rw-r--r-- | pkg/ical/service.go | 17 | ||||
| -rw-r--r-- | pkg/schedule/service.go | 116 | ||||
| -rw-r--r-- | pkg/session/memory.go | 3 | ||||
| -rw-r--r-- | pkg/session/service.go | 3 |
16 files changed, 543 insertions, 176 deletions
diff --git a/pkg/schedule/model.go b/pkg/conference/model.go index fcf39a5..343271f 100644 --- a/pkg/schedule/model.go +++ b/pkg/conference/model.go @@ -1,4 +1,4 @@ -package schedule +package conference import "time" diff --git a/pkg/schedule/parse.go b/pkg/conference/parse.go index b0ed8df..b7f9dba 100644 --- a/pkg/schedule/parse.go +++ b/pkg/conference/parse.go @@ -1,4 +1,4 @@ -package schedule +package conference import ( "encoding/xml" diff --git a/pkg/conference/service.go b/pkg/conference/service.go new file mode 100644 index 0000000..59c2c8c --- /dev/null +++ b/pkg/conference/service.go @@ -0,0 +1,218 @@ +package conference + +import ( + "bufio" + "context" + "encoding/xml" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "github.com/LMBishop/confplanner/pkg/database/sqlc" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Service interface { + CreateConference(url string) (*sqlc.Conference, error) + DeleteConference(id int32) error + GetConferences() ([]sqlc.Conference, error) + GetSchedule(id int32) (*Schedule, time.Time, error) + GetEventByID(conferenceID, eventID int32) (*Event, error) +} + +type loadedConference struct { + pentabarfUrl string + schedule *Schedule + eventsById map[int32]Event + lastUpdated time.Time + lock sync.RWMutex +} + +var ( + ErrConferenceNotFound = errors.New("conference not found") + ErrScheduleFetch = errors.New("could not fetch schedule") +) + +type service struct { + conferences map[int32]*loadedConference + lock sync.RWMutex + pool *pgxpool.Pool +} + +// TODO: Create a service implementation that persists to DB +// and isn't in memory +func NewService(pool *pgxpool.Pool) (Service, error) { + service := &service{ + pool: pool, + conferences: make(map[int32]*loadedConference), + } + + queries := sqlc.New(pool) + conferences, err := queries.GetConferences(context.Background()) + if err != nil { + return nil, err + } + + for _, conference := range conferences { + c := &loadedConference{ + pentabarfUrl: conference.Url, + lastUpdated: time.Unix(0, 0), + } + service.conferences[conference.ID] = c + } + + return service, nil +} + +func (s *service) CreateConference(url string) (*sqlc.Conference, error) { + s.lock.Lock() + defer s.lock.Unlock() + + c := &loadedConference{ + pentabarfUrl: url, + lastUpdated: time.Unix(0, 0), + } + err := c.updateSchedule() + if err != nil { + return nil, errors.Join(ErrScheduleFetch, err) + } + + queries := sqlc.New(s.pool) + + conference, err := queries.CreateConference(context.Background(), sqlc.CreateConferenceParams{ + Url: url, + Title: pgtype.Text{String: c.schedule.Conference.Title, Valid: true}, + Venue: pgtype.Text{String: c.schedule.Conference.Venue, Valid: true}, + City: pgtype.Text{String: c.schedule.Conference.City, Valid: true}, + }) + if err != nil { + return nil, fmt.Errorf("could not create conference: %w", err) + } + + s.conferences[conference.ID] = c + + return &conference, nil +} + +func (s *service) DeleteConference(id int32) error { + s.lock.Lock() + defer s.lock.Unlock() + + queries := sqlc.New(s.pool) + err := queries.DeleteConference(context.Background(), id) + if err != nil { + return fmt.Errorf("could not delete conference: %w", err) + } + + delete(s.conferences, id) + return nil +} + +func (s *service) GetConferences() ([]sqlc.Conference, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + queries := sqlc.New(s.pool) + return queries.GetConferences(context.Background()) +} + +func (s *service) GetSchedule(id int32) (*Schedule, time.Time, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + c, ok := s.conferences[id] + if !ok { + return nil, time.Time{}, ErrConferenceNotFound + } + + if err := c.updateSchedule(); err != nil { + return nil, time.Time{}, err + } + + c.lock.RLock() + defer c.lock.RUnlock() + + queries := sqlc.New(s.pool) + if _, err := queries.UpdateConferenceDetails(context.Background(), sqlc.UpdateConferenceDetailsParams{ + ID: id, + Title: pgtype.Text{String: c.schedule.Conference.Title, Valid: true}, + Venue: pgtype.Text{String: c.schedule.Conference.Venue, Valid: true}, + City: pgtype.Text{String: c.schedule.Conference.City, Valid: true}, + }); err != nil { + return nil, time.Time{}, fmt.Errorf("failed to update cached conference details: %w", err) + } + + return c.schedule, c.lastUpdated, nil +} + +func (s *service) GetEventByID(conferenceID, eventID int32) (*Event, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + c, ok := s.conferences[conferenceID] + if !ok { + return nil, ErrConferenceNotFound + } + + c.lock.RLock() + defer c.lock.RUnlock() + + event := c.eventsById[eventID] + + return &event, nil +} + +func (c *loadedConference) hasScheduleExpired() bool { + expire := c.lastUpdated.Add(15 * time.Minute) + return time.Now().After(expire) +} + +func (c *loadedConference) updateSchedule() error { + if !c.hasScheduleExpired() { + return nil + } + + if !c.lock.TryLock() { + // don't block if another goroutine is already fetching + return nil + } + defer c.lock.Unlock() + + res, err := http.Get(c.pentabarfUrl) + if err != nil { + return err + } + + reader := bufio.NewReader(res.Body) + + var schedule schedule + + decoder := xml.NewDecoder(reader) + if err := decoder.Decode(&schedule); err != nil { + return fmt.Errorf("failed to decode XML: %w", err) + } + + var newSchedule Schedule + err = newSchedule.Scan(schedule) + if err != nil { + return fmt.Errorf("failed to scan schedule: %w", err) + } + + c.schedule = &newSchedule + c.lastUpdated = time.Now() + + c.eventsById = make(map[int32]Event) + + for _, day := range newSchedule.Days { + for _, room := range day.Rooms { + for _, event := range room.Events { + c.eventsById[event.ID] = event + } + } + } + + return nil +} diff --git a/pkg/database/migrations/0003_multi_conference.sql b/pkg/database/migrations/0003_multi_conference.sql new file mode 100644 index 0000000..31a1f58 --- /dev/null +++ b/pkg/database/migrations/0003_multi_conference.sql @@ -0,0 +1,13 @@ +-- +goose Up +CREATE TABLE conferences ( + id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + url text NOT NULL, + title text, + venue text, + city text +); + +TRUNCATE TABLE favourites CONTINUE IDENTITY; +ALTER TABLE favourites ADD conference_id int NOT NULL REFERENCES conferences(id) ON DELETE CASCADE; +ALTER TABLE favourites DROP CONSTRAINT favourites_user_id_event_guid_event_id_key; +ALTER TABLE favourites ADD UNIQUE(user_id, event_guid, event_id, conference_id); diff --git a/pkg/database/migrations/0004_user_admins.sql b/pkg/database/migrations/0004_user_admins.sql new file mode 100644 index 0000000..5c4efb3 --- /dev/null +++ b/pkg/database/migrations/0004_user_admins.sql @@ -0,0 +1,2 @@ +-- +goose Up +ALTER TABLE users ADD admin boolean NOT NULL DEFAULT false; diff --git a/pkg/database/query/conferences.sql b/pkg/database/query/conferences.sql new file mode 100644 index 0000000..7acac3f --- /dev/null +++ b/pkg/database/query/conferences.sql @@ -0,0 +1,21 @@ +-- name: CreateConference :one +INSERT INTO conferences ( + url, title, venue, city +) VALUES ( + $1, $2, $3, $4 +) +RETURNING *; + +-- name: UpdateConferenceDetails :one +UPDATE conferences SET ( + title, venue, city +) = ($2, $3, $4) +WHERE id = $1 +RETURNING *; + +-- name: GetConferences :many +SELECT * FROM conferences; + +-- name: DeleteConference :exec +DELETE FROM conferences +WHERE id = $1; diff --git a/pkg/database/query/favourites.sql b/pkg/database/query/favourites.sql index 94e914c..a903ac7 100644 --- a/pkg/database/query/favourites.sql +++ b/pkg/database/query/favourites.sql @@ -1,12 +1,16 @@ +-- name: GetFavouritesForUserConference :many +SELECT * FROM favourites +WHERE user_id = $1 AND conference_id = $2; + -- name: GetFavouritesForUser :many SELECT * FROM favourites WHERE user_id = $1; -- name: CreateFavourite :one INSERT INTO favourites ( - user_id, event_guid, event_id + user_id, event_guid, event_id, conference_id ) VALUES ( - $1, $2, $3 + $1, $2, $3, $4 ) RETURNING *; @@ -16,4 +20,4 @@ WHERE id = $1; -- name: DeleteFavouriteByEventDetails :execrows DELETE FROM favourites -WHERE (event_guid = $1 OR event_id = $2) AND user_id = $3; +WHERE (event_guid = $1 OR event_id = $2) AND user_id = $3 AND conference_id = $4; diff --git a/pkg/database/sqlc/conferences.sql.go b/pkg/database/sqlc/conferences.sql.go new file mode 100644 index 0000000..1345185 --- /dev/null +++ b/pkg/database/sqlc/conferences.sql.go @@ -0,0 +1,119 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: conferences.sql + +package sqlc + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createConference = `-- name: CreateConference :one +INSERT INTO conferences ( + url, title, venue, city +) VALUES ( + $1, $2, $3, $4 +) +RETURNING id, url, title, venue, city +` + +type CreateConferenceParams struct { + Url string `json:"url"` + Title pgtype.Text `json:"title"` + Venue pgtype.Text `json:"venue"` + City pgtype.Text `json:"city"` +} + +func (q *Queries) CreateConference(ctx context.Context, arg CreateConferenceParams) (Conference, error) { + row := q.db.QueryRow(ctx, createConference, + arg.Url, + arg.Title, + arg.Venue, + arg.City, + ) + var i Conference + err := row.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.Venue, + &i.City, + ) + return i, err +} + +const deleteConference = `-- name: DeleteConference :exec +DELETE FROM conferences +WHERE id = $1 +` + +func (q *Queries) DeleteConference(ctx context.Context, id int32) error { + _, err := q.db.Exec(ctx, deleteConference, id) + return err +} + +const getConferences = `-- name: GetConferences :many +SELECT id, url, title, venue, city FROM conferences +` + +func (q *Queries) GetConferences(ctx context.Context) ([]Conference, error) { + rows, err := q.db.Query(ctx, getConferences) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Conference + for rows.Next() { + var i Conference + if err := rows.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.Venue, + &i.City, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateConferenceDetails = `-- name: UpdateConferenceDetails :one +UPDATE conferences SET ( + title, venue, city +) = ($2, $3, $4) +WHERE id = $1 +RETURNING id, url, title, venue, city +` + +type UpdateConferenceDetailsParams struct { + ID int32 `json:"id"` + Title pgtype.Text `json:"title"` + Venue pgtype.Text `json:"venue"` + City pgtype.Text `json:"city"` +} + +func (q *Queries) UpdateConferenceDetails(ctx context.Context, arg UpdateConferenceDetailsParams) (Conference, error) { + row := q.db.QueryRow(ctx, updateConferenceDetails, + arg.ID, + arg.Title, + arg.Venue, + arg.City, + ) + var i Conference + err := row.Scan( + &i.ID, + &i.Url, + &i.Title, + &i.Venue, + &i.City, + ) + return i, err +} diff --git a/pkg/database/sqlc/favourites.sql.go b/pkg/database/sqlc/favourites.sql.go index b13261f..d28470d 100644 --- a/pkg/database/sqlc/favourites.sql.go +++ b/pkg/database/sqlc/favourites.sql.go @@ -13,27 +13,34 @@ import ( const createFavourite = `-- name: CreateFavourite :one INSERT INTO favourites ( - user_id, event_guid, event_id + user_id, event_guid, event_id, conference_id ) VALUES ( - $1, $2, $3 + $1, $2, $3, $4 ) -RETURNING id, user_id, event_guid, event_id +RETURNING id, user_id, event_guid, event_id, conference_id ` type CreateFavouriteParams struct { - UserID int32 `json:"user_id"` - EventGuid pgtype.UUID `json:"event_guid"` - EventID pgtype.Int4 `json:"event_id"` + UserID int32 `json:"user_id"` + EventGuid pgtype.UUID `json:"event_guid"` + EventID pgtype.Int4 `json:"event_id"` + ConferenceID int32 `json:"conference_id"` } func (q *Queries) CreateFavourite(ctx context.Context, arg CreateFavouriteParams) (Favourite, error) { - row := q.db.QueryRow(ctx, createFavourite, arg.UserID, arg.EventGuid, arg.EventID) + row := q.db.QueryRow(ctx, createFavourite, + arg.UserID, + arg.EventGuid, + arg.EventID, + arg.ConferenceID, + ) var i Favourite err := row.Scan( &i.ID, &i.UserID, &i.EventGuid, &i.EventID, + &i.ConferenceID, ) return i, err } @@ -50,17 +57,23 @@ func (q *Queries) DeleteFavourite(ctx context.Context, id int32) error { const deleteFavouriteByEventDetails = `-- name: DeleteFavouriteByEventDetails :execrows DELETE FROM favourites -WHERE (event_guid = $1 OR event_id = $2) AND user_id = $3 +WHERE (event_guid = $1 OR event_id = $2) AND user_id = $3 AND conference_id = $4 ` type DeleteFavouriteByEventDetailsParams struct { - EventGuid pgtype.UUID `json:"event_guid"` - EventID pgtype.Int4 `json:"event_id"` - UserID int32 `json:"user_id"` + EventGuid pgtype.UUID `json:"event_guid"` + EventID pgtype.Int4 `json:"event_id"` + UserID int32 `json:"user_id"` + ConferenceID int32 `json:"conference_id"` } func (q *Queries) DeleteFavouriteByEventDetails(ctx context.Context, arg DeleteFavouriteByEventDetailsParams) (int64, error) { - result, err := q.db.Exec(ctx, deleteFavouriteByEventDetails, arg.EventGuid, arg.EventID, arg.UserID) + result, err := q.db.Exec(ctx, deleteFavouriteByEventDetails, + arg.EventGuid, + arg.EventID, + arg.UserID, + arg.ConferenceID, + ) if err != nil { return 0, err } @@ -68,7 +81,7 @@ func (q *Queries) DeleteFavouriteByEventDetails(ctx context.Context, arg DeleteF } const getFavouritesForUser = `-- name: GetFavouritesForUser :many -SELECT id, user_id, event_guid, event_id FROM favourites +SELECT id, user_id, event_guid, event_id, conference_id FROM favourites WHERE user_id = $1 ` @@ -86,6 +99,43 @@ func (q *Queries) GetFavouritesForUser(ctx context.Context, userID int32) ([]Fav &i.UserID, &i.EventGuid, &i.EventID, + &i.ConferenceID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getFavouritesForUserConference = `-- name: GetFavouritesForUserConference :many +SELECT id, user_id, event_guid, event_id, conference_id FROM favourites +WHERE user_id = $1 AND conference_id = $2 +` + +type GetFavouritesForUserConferenceParams struct { + UserID int32 `json:"user_id"` + ConferenceID int32 `json:"conference_id"` +} + +func (q *Queries) GetFavouritesForUserConference(ctx context.Context, arg GetFavouritesForUserConferenceParams) ([]Favourite, error) { + rows, err := q.db.Query(ctx, getFavouritesForUserConference, arg.UserID, arg.ConferenceID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Favourite + for rows.Next() { + var i Favourite + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.EventGuid, + &i.EventID, + &i.ConferenceID, ); err != nil { return nil, err } diff --git a/pkg/database/sqlc/models.go b/pkg/database/sqlc/models.go index 57fd082..b7dfc19 100644 --- a/pkg/database/sqlc/models.go +++ b/pkg/database/sqlc/models.go @@ -15,15 +15,25 @@ type Calendar struct { Key string `json:"key"` } +type Conference struct { + ID int32 `json:"id"` + Url string `json:"url"` + Title pgtype.Text `json:"title"` + Venue pgtype.Text `json:"venue"` + City pgtype.Text `json:"city"` +} + type Favourite struct { - ID int32 `json:"id"` - UserID int32 `json:"user_id"` - EventGuid pgtype.UUID `json:"event_guid"` - EventID pgtype.Int4 `json:"event_id"` + ID int32 `json:"id"` + UserID int32 `json:"user_id"` + EventGuid pgtype.UUID `json:"event_guid"` + EventID pgtype.Int4 `json:"event_id"` + ConferenceID int32 `json:"conference_id"` } type User struct { ID int32 `json:"id"` Username string `json:"username"` Password pgtype.Text `json:"password"` + Admin bool `json:"admin"` } diff --git a/pkg/database/sqlc/users.sql.go b/pkg/database/sqlc/users.sql.go index cf0aeb9..45ae019 100644 --- a/pkg/database/sqlc/users.sql.go +++ b/pkg/database/sqlc/users.sql.go @@ -17,7 +17,7 @@ INSERT INTO users ( ) VALUES ( $1, $2 ) -RETURNING id, username, password +RETURNING id, username, password, admin ` type CreateUserParams struct { @@ -28,7 +28,12 @@ type CreateUserParams struct { func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { row := q.db.QueryRow(ctx, createUser, arg.Username, arg.Password) var i User - err := row.Scan(&i.ID, &i.Username, &i.Password) + err := row.Scan( + &i.ID, + &i.Username, + &i.Password, + &i.Admin, + ) return i, err } @@ -43,31 +48,41 @@ func (q *Queries) DeleteUser(ctx context.Context, id int32) error { } const getUserByID = `-- name: GetUserByID :one -SELECT id, username, password FROM users +SELECT id, username, password, admin FROM users WHERE id = $1 LIMIT 1 ` func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) { row := q.db.QueryRow(ctx, getUserByID, id) var i User - err := row.Scan(&i.ID, &i.Username, &i.Password) + err := row.Scan( + &i.ID, + &i.Username, + &i.Password, + &i.Admin, + ) return i, err } const getUserByName = `-- name: GetUserByName :one -SELECT id, username, password FROM users +SELECT id, username, password, admin FROM users WHERE username = $1 LIMIT 1 ` func (q *Queries) GetUserByName(ctx context.Context, username string) (User, error) { row := q.db.QueryRow(ctx, getUserByName, username) var i User - err := row.Scan(&i.ID, &i.Username, &i.Password) + err := row.Scan( + &i.ID, + &i.Username, + &i.Password, + &i.Admin, + ) return i, err } const listUsers = `-- name: ListUsers :many -SELECT id, username, password FROM users +SELECT id, username, password, admin FROM users ORDER BY username ` @@ -80,7 +95,12 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { var items []User for rows.Next() { var i User - if err := rows.Scan(&i.ID, &i.Username, &i.Password); err != nil { + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Password, + &i.Admin, + ); err != nil { return nil, err } items = append(items, i) diff --git a/pkg/favourites/service.go b/pkg/favourites/service.go index cba249e..f73f400 100644 --- a/pkg/favourites/service.go +++ b/pkg/favourites/service.go @@ -12,9 +12,10 @@ import ( ) type Service interface { - GetFavouritesForUser(id int32) (*[]sqlc.Favourite, error) - CreateFavouriteForUser(id int32, eventGuid pgtype.UUID, eventId *int32) (*sqlc.Favourite, error) - DeleteFavouriteForUserByEventDetails(id int32, eventGuid pgtype.UUID, eventId *int32) error + GetAllFavouritesForUser(id int32) (*[]sqlc.Favourite, error) + GetFavouritesForUserConference(id int32, conference int32) (*[]sqlc.Favourite, error) + CreateFavouriteForUser(id int32, eventGUID pgtype.UUID, eventID *int32, conferenceID int32) (*sqlc.Favourite, error) + DeleteFavouriteForUserByEventDetails(id int32, eventGUID pgtype.UUID, eventID *int32, conferenceID int32) error } var ( @@ -32,21 +33,22 @@ func NewService(pool *pgxpool.Pool) Service { } } -func (s *service) CreateFavouriteForUser(id int32, eventGuid pgtype.UUID, eventId *int32) (*sqlc.Favourite, error) { +func (s *service) CreateFavouriteForUser(userID int32, eventGUID pgtype.UUID, eventID *int32, conferenceID int32) (*sqlc.Favourite, error) { queries := sqlc.New(s.pool) - var pgEventId pgtype.Int4 - if eventId != nil { - pgEventId = pgtype.Int4{ - Int32: *eventId, + var pgEventID pgtype.Int4 + if eventID != nil { + pgEventID = pgtype.Int4{ + Int32: *eventID, Valid: true, } } favourite, err := queries.CreateFavourite(context.Background(), sqlc.CreateFavouriteParams{ - UserID: id, - EventGuid: eventGuid, - EventID: pgEventId, + UserID: userID, + EventGuid: eventGUID, + EventID: pgEventID, + ConferenceID: conferenceID, }) if err != nil { return nil, fmt.Errorf("could not create favourite: %w", err) @@ -55,10 +57,10 @@ func (s *service) CreateFavouriteForUser(id int32, eventGuid pgtype.UUID, eventI return &favourite, nil } -func (s *service) GetFavouritesForUser(id int32) (*[]sqlc.Favourite, error) { +func (s *service) GetAllFavouritesForUser(userID int32) (*[]sqlc.Favourite, error) { queries := sqlc.New(s.pool) - favourites, err := queries.GetFavouritesForUser(context.Background(), id) + favourites, err := queries.GetFavouritesForUser(context.Background(), userID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { empty := make([]sqlc.Favourite, 0) @@ -70,20 +72,39 @@ func (s *service) GetFavouritesForUser(id int32) (*[]sqlc.Favourite, error) { return &favourites, nil } -func (s *service) DeleteFavouriteForUserByEventDetails(id int32, eventGuid pgtype.UUID, eventId *int32) error { +func (s *service) GetFavouritesForUserConference(userID int32, conferenceID int32) (*[]sqlc.Favourite, error) { queries := sqlc.New(s.pool) - var pgEventId pgtype.Int4 - if eventId != nil { - pgEventId = pgtype.Int4{ - Int32: *eventId, + favourites, err := queries.GetFavouritesForUserConference(context.Background(), sqlc.GetFavouritesForUserConferenceParams{ + UserID: userID, + ConferenceID: conferenceID, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + empty := make([]sqlc.Favourite, 0) + return &empty, nil + } + return nil, fmt.Errorf("could not fetch user: %w", err) + } + + return &favourites, nil +} + +func (s *service) DeleteFavouriteForUserByEventDetails(id int32, eventGUID pgtype.UUID, eventID *int32, conferenceID int32) error { + queries := sqlc.New(s.pool) + + var pgEventID pgtype.Int4 + if eventID != nil { + pgEventID = pgtype.Int4{ + Int32: *eventID, Valid: true, } } rowsAffected, err := queries.DeleteFavouriteByEventDetails(context.Background(), sqlc.DeleteFavouriteByEventDetailsParams{ - EventGuid: eventGuid, - EventID: pgEventId, - UserID: id, + EventGuid: eventGUID, + EventID: pgEventID, + UserID: id, + ConferenceID: conferenceID, }) if err != nil { return fmt.Errorf("could not delete favourite: %w", err) diff --git a/pkg/ical/service.go b/pkg/ical/service.go index d93c846..bc82248 100644 --- a/pkg/ical/service.go +++ b/pkg/ical/service.go @@ -6,9 +6,9 @@ import ( "strings" "time" + "github.com/LMBishop/confplanner/pkg/conference" "github.com/LMBishop/confplanner/pkg/database/sqlc" "github.com/LMBishop/confplanner/pkg/favourites" - "github.com/LMBishop/confplanner/pkg/schedule" "github.com/microcosm-cc/bluemonday" ) @@ -23,28 +23,31 @@ var ( type service struct { favouritesService favourites.Service - scheduleService schedule.Service + conferenceService conference.Service } func NewService( favouritesService favourites.Service, - scheduleService schedule.Service, + conferenceService conference.Service, ) Service { return &service{ favouritesService: favouritesService, - scheduleService: scheduleService, + conferenceService: conferenceService, } } func (s *service) GenerateIcalForCalendar(calendar sqlc.Calendar) (string, error) { - favourites, err := s.favouritesService.GetFavouritesForUser(calendar.UserID) + favourites, err := s.favouritesService.GetAllFavouritesForUser(calendar.UserID) if err != nil { return "", err } - events := make([]schedule.Event, 0) + events := make([]conference.Event, 0) for _, favourite := range *favourites { - event := s.scheduleService.GetEventByID(favourite.EventID.Int32) + event, err := s.conferenceService.GetEventByID(favourite.ConferenceID, favourite.EventID.Int32) + if err != nil { + continue + } events = append(events, *event) } diff --git a/pkg/schedule/service.go b/pkg/schedule/service.go deleted file mode 100644 index a73a469..0000000 --- a/pkg/schedule/service.go +++ /dev/null @@ -1,116 +0,0 @@ -package schedule - -import ( - "bufio" - "encoding/xml" - "fmt" - "net/http" - "sync" - "time" -) - -type Service interface { - GetSchedule() (*Schedule, time.Time, error) - GetEventByID(id int32) *Event -} - -type service struct { - pentabarfUrl string - - schedule *Schedule - eventsById map[int32]Event - lastUpdated time.Time - accessLock sync.RWMutex - updateLock sync.Mutex -} - -// TODO: Create a service implementation that persists to DB -// and isn't in memory -func NewService(pentabarfUrl string) (Service, error) { - service := &service{ - pentabarfUrl: pentabarfUrl, - lastUpdated: time.Unix(0, 0), - } - - err := service.updateSchedule() - if err != nil { - return nil, fmt.Errorf("could not read schedule from '%s' (is it a valid pentabarf XML file?): %w", pentabarfUrl, err) - } - return service, nil -} - -func (s *service) GetSchedule() (*Schedule, time.Time, error) { - err := s.updateSchedule() - if err != nil { - return nil, time.Time{}, err - } - - s.accessLock.RLock() - defer s.accessLock.RUnlock() - - return s.schedule, s.lastUpdated, nil -} - -func (s *service) GetEventByID(id int32) *Event { - s.accessLock.RLock() - defer s.accessLock.RUnlock() - - event := s.eventsById[id] - - return &event -} - -func (s *service) hasScheduleExpired() bool { - expire := s.lastUpdated.Add(15 * time.Minute) - return time.Now().After(expire) -} - -func (s *service) updateSchedule() error { - if !s.hasScheduleExpired() { - return nil - } - - if !s.updateLock.TryLock() { - // don't block if another goroutine is already fetching - return nil - } - defer s.updateLock.Unlock() - - res, err := http.Get(s.pentabarfUrl) - if err != nil { - return err - } - - reader := bufio.NewReader(res.Body) - - var schedule schedule - - decoder := xml.NewDecoder(reader) - if err := decoder.Decode(&schedule); err != nil { - return fmt.Errorf("failed to decode XML: %w", err) - } - - var newSchedule Schedule - err = newSchedule.Scan(schedule) - if err != nil { - return fmt.Errorf("failed to scan schedule: %w", err) - } - - s.accessLock.Lock() - defer s.accessLock.Unlock() - - s.schedule = &newSchedule - s.lastUpdated = time.Now() - - s.eventsById = make(map[int32]Event) - - for _, day := range newSchedule.Days { - for _, room := range day.Rooms { - for _, event := range room.Events { - s.eventsById[event.ID] = event - } - } - } - - return nil -} diff --git a/pkg/session/memory.go b/pkg/session/memory.go index f02b792..8fa6076 100644 --- a/pkg/session/memory.go +++ b/pkg/session/memory.go @@ -40,7 +40,7 @@ func (s *memoryStore) GetBySID(sid uint) *UserSession { return s.sessionsBySID[sid] } -func (s *memoryStore) Create(uid int32, username string, ip string, ua string) (*UserSession, error) { +func (s *memoryStore) Create(uid int32, username string, ip string, ua string, admin bool) (*UserSession, error) { token := generateSessionToken() s.lock.Lock() @@ -65,6 +65,7 @@ func (s *memoryStore) Create(uid int32, username string, ip string, ua string) ( IP: ip, UserAgent: ua, LoginTime: time.Now(), + Admin: admin, } s.sessionsByToken[token] = session s.sessionsBySID[sessionId] = session diff --git a/pkg/session/service.go b/pkg/session/service.go index 83009bc..a693fe8 100644 --- a/pkg/session/service.go +++ b/pkg/session/service.go @@ -5,7 +5,7 @@ import "time" type Service interface { GetByToken(token string) *UserSession GetBySID(sid uint) *UserSession - Create(uid int32, username string, ip string, ua string) (*UserSession, error) + Create(uid int32, username string, ip string, ua string, admin bool) (*UserSession, error) Destroy(sid uint) error } @@ -17,4 +17,5 @@ type UserSession struct { IP string LoginTime time.Time UserAgent string + Admin bool } |
