ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
89 lines
2.4 KiB
Go
89 lines
2.4 KiB
Go
package playback
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"pipu/api/internal/api/gen"
|
|
"pipu/api/internal/auth"
|
|
)
|
|
|
|
// EventService owns event acceptance and its transaction; HTTP only authenticates and maps DTOs.
|
|
type EventService struct {
|
|
store *Store
|
|
now func() time.Time
|
|
}
|
|
|
|
func (s EventService) Accept(ctx context.Context, p auth.Principal, events []gen.PlayEvent) (gen.PostEvents200JSONResponse, error) {
|
|
now := s.now()
|
|
ids := make([]uuid.UUID, 0, len(events))
|
|
for _, e := range events {
|
|
ids = append(ids, e.TrackId)
|
|
}
|
|
durations, err := s.store.TrackDurations(ctx, ids)
|
|
if err != nil {
|
|
return gen.PostEvents200JSONResponse{}, err
|
|
}
|
|
clientType := s.store.SessionClientType(ctx, p.SessionID)
|
|
|
|
out := gen.PostEvents200JSONResponse{}
|
|
tx, err := s.store.Begin(ctx)
|
|
if err != nil {
|
|
return gen.PostEvents200JSONResponse{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
for _, e := range events {
|
|
dur, known := durations[e.TrackId]
|
|
at := e.OccurredAt.UTC()
|
|
if !known || at.Before(now.Add(-maxEventAge)) {
|
|
out.Rejected++
|
|
continue
|
|
}
|
|
if at.After(now.Add(maxEventClockSkew)) {
|
|
at = now // часы клиента спешат
|
|
}
|
|
ev := Event{
|
|
ID: e.Id, Type: string(e.Type), TrackID: e.TrackId, OccurredAt: at, PositionMs: e.PositionMs,
|
|
PlayedMs: e.PlayedMs, ContextID: e.ContextId, WaveSessionID: e.WaveSessionId,
|
|
Offline: e.Offline != nil && *e.Offline,
|
|
}
|
|
if e.ContextType != nil {
|
|
ct := string(*e.ContextType)
|
|
ev.ContextType = &ct
|
|
}
|
|
inserted, err := s.store.InsertEvent(ctx, tx, p.UserID, clientType, ev)
|
|
if err != nil {
|
|
return gen.PostEvents200JSONResponse{}, err
|
|
}
|
|
if !inserted {
|
|
out.Duplicates++
|
|
continue
|
|
}
|
|
out.Accepted++
|
|
played := 0
|
|
if e.PlayedMs != nil {
|
|
played = *e.PlayedMs
|
|
}
|
|
o := Judge(ev.Type, played, dur)
|
|
if o != (Outcome{}) {
|
|
if err := s.store.ApplyOutcome(ctx, tx, p.UserID, ev, o); err != nil {
|
|
return gen.PostEvents200JSONResponse{}, err
|
|
}
|
|
}
|
|
if outcome := impressionOutcome(ev.Type, o); outcome != "" && ev.ContextType != nil && *ev.ContextType == "wave" &&
|
|
ev.ContextID != nil {
|
|
if sid, err := uuid.Parse(*ev.ContextID); err == nil {
|
|
if err := s.store.MarkImpression(ctx, tx, sid, ev.TrackID, outcome); err != nil {
|
|
return gen.PostEvents200JSONResponse{}, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return gen.PostEvents200JSONResponse{}, err
|
|
}
|
|
return out, nil
|
|
}
|