2025-04-08 16:06:44 +10:00

215 lines
7.0 KiB
Go

package collector
import (
"encoding/json"
"errors"
"fmt"
"igdb-database/config"
"igdb-database/db"
"io"
"log"
"net"
"net/http"
"net/url"
"slices"
pb "github.com/bestnite/go-igdb/proto"
"github.com/bestnite/go-igdb"
"github.com/bestnite/go-igdb/endpoint"
"go.mongodb.org/mongo-driver/v2/mongo"
)
func StartWebhookServer(client *igdb.Client) {
baseUrl, err := url.Parse(config.C().ExternalUrl)
if err != nil {
log.Fatalf("failed to parse url: %v", err)
}
http.HandleFunc(webhook(client.AgeRatingCategories))
http.HandleFunc(webhook(client.AgeRatingContentDescriptions))
http.HandleFunc(webhook(client.AgeRatingContentDescriptionsV2))
http.HandleFunc(webhook(client.AgeRatingOrganizations))
http.HandleFunc(webhook(client.AgeRatings))
http.HandleFunc(webhook(client.AlternativeNames))
http.HandleFunc(webhook(client.Artworks))
http.HandleFunc(webhook(client.CharacterGenders))
http.HandleFunc(webhook(client.CharacterMugShots))
http.HandleFunc(webhook(client.Characters))
http.HandleFunc(webhook(client.CharacterSpecies))
http.HandleFunc(webhook(client.CollectionMemberships))
http.HandleFunc(webhook(client.CollectionMembershipTypes))
http.HandleFunc(webhook(client.CollectionRelations))
http.HandleFunc(webhook(client.CollectionRelationTypes))
http.HandleFunc(webhook(client.Collections))
http.HandleFunc(webhook(client.CollectionTypes))
http.HandleFunc(webhook(client.Companies))
http.HandleFunc(webhook(client.CompanyLogos))
http.HandleFunc(webhook(client.CompanyStatuses))
http.HandleFunc(webhook(client.CompanyWebsites))
http.HandleFunc(webhook(client.Covers))
http.HandleFunc(webhook(client.DateFormats))
http.HandleFunc(webhook(client.EventLogos))
http.HandleFunc(webhook(client.EventNetworks))
http.HandleFunc(webhook(client.Events))
http.HandleFunc(webhook(client.ExternalGames))
http.HandleFunc(webhook(client.ExternalGameSources))
http.HandleFunc(webhook(client.Franchises))
http.HandleFunc(webhook(client.GameEngineLogos))
http.HandleFunc(webhook(client.GameEngines))
http.HandleFunc(webhook(client.GameLocalizations))
http.HandleFunc(webhook(client.GameModes))
http.HandleFunc(webhook(client.GameReleaseFormats))
http.HandleFunc(webhook(client.Games))
http.HandleFunc(webhook(client.GameStatuses))
http.HandleFunc(webhook(client.GameTimeToBeats))
http.HandleFunc(webhook(client.GameTypes))
http.HandleFunc(webhook(client.GameVersionFeatures))
http.HandleFunc(webhook(client.GameVersionFeatureValues))
http.HandleFunc(webhook(client.GameVersions))
http.HandleFunc(webhook(client.GameVideos))
http.HandleFunc(webhook(client.Genres))
http.HandleFunc(webhook(client.InvolvedCompanies))
http.HandleFunc(webhook(client.Keywords))
http.HandleFunc(webhook(client.Languages))
http.HandleFunc(webhook(client.LanguageSupports))
http.HandleFunc(webhook(client.LanguageSupportTypes))
http.HandleFunc(webhook(client.MultiplayerModes))
http.HandleFunc(webhook(client.NetworkTypes))
http.HandleFunc(webhook(client.PlatformFamilies))
http.HandleFunc(webhook(client.PlatformLogos))
http.HandleFunc(webhook(client.Platforms))
http.HandleFunc(webhook(client.PlatformTypes))
http.HandleFunc(webhook(client.PlatformVersionCompanies))
http.HandleFunc(webhook(client.PlatformVersionReleaseDates))
http.HandleFunc(webhook(client.PlatformVersions))
http.HandleFunc(webhook(client.PlatformWebsites))
http.HandleFunc(webhook(client.PlayerPerspectives))
http.HandleFunc(webhook(client.PopularityTypes))
http.HandleFunc(webhook(client.Regions))
http.HandleFunc(webhook(client.ReleaseDateRegions))
http.HandleFunc(webhook(client.ReleaseDates))
http.HandleFunc(webhook(client.ReleaseDateStatuses))
http.HandleFunc(webhook(client.Screenshots))
http.HandleFunc(webhook(client.Themes))
http.HandleFunc(webhook(client.Websites))
http.HandleFunc(webhook(client.WebsiteTypes))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
if _, err := w.Write([]byte("Hello World!")); err != nil {
log.Printf("failed to write response: %v", err)
}
})
serverStart := make(chan bool)
go func() {
defer close(serverStart)
log.Printf("starting webhook server on %s", config.C().Address)
err = http.ListenAndServe(config.C().Address, nil)
if err != nil {
log.Fatalf("failed to start webhook server: %v", err)
}
}()
enabledEndpoint := endpoint.AllNames
enabledEndpoint = slices.DeleteFunc(enabledEndpoint, func(e endpoint.Name) bool {
return e == endpoint.EPWebhooks || e == endpoint.EPSearch || e == endpoint.EPPopularityPrimitives
})
ip := net.ParseIP(baseUrl.Hostname())
if baseUrl.Hostname() == "localhost" || (ip != nil && ip.IsLoopback()) {
log.Printf("extral url is localhost. webhook will not be registered")
} else {
for _, ep := range enabledEndpoint {
Url := baseUrl.JoinPath(fmt.Sprintf("/webhook/%s", string(ep)))
log.Printf("registering webhook \"%s\" to \"%s\"", ep, Url.String())
err = client.Webhooks.Register(ep, config.C().WebhookSecret, Url.String())
if err != nil {
log.Fatalf("failed to register webhook \"%s\": %v", ep, err)
}
log.Printf("webhook \"%s\" registered", ep)
}
log.Printf("all webhook registered")
}
<-serverStart
}
func webhook[T any](
e endpoint.EntityEndpoint[T],
) (string, func(w http.ResponseWriter, r *http.Request)) {
return fmt.Sprintf("/webhook/%s", e.GetEndpointName()), func(w http.ResponseWriter, r *http.Request) {
secret := r.Header.Get("X-Secret")
if secret != config.C().WebhookSecret {
w.WriteHeader(401)
return
}
w.WriteHeader(200)
data := struct {
ID uint64 `json:"id"`
}{}
jsonBytes, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("failed to read request body: %v", err)
return
}
err = json.Unmarshal(jsonBytes, &data)
if err != nil {
log.Printf("failed to unmarshal request body: %v", err)
return
}
if data.ID == 0 {
return
}
item, err := e.GetByID(data.ID)
if err != nil {
log.Printf("failed to get %s: %v", e.GetEndpointName(), err)
return
}
if _, ok := any(e).(*endpoint.Games); ok {
game := any(item).(*pb.Game)
g, err := db.ConvertGame(game)
if err != nil {
log.Printf("failed to convert game: %v", err)
} else {
_ = db.SaveGame(g)
log.Printf("game %d aggregated", data.ID)
}
}
err = db.SaveItem(e.GetEndpointName(), item)
if err != nil {
log.Printf("failed to save %s: %v", e.GetEndpointName(), err)
return
}
// update associated game
type gameGetter interface {
GetGame() *pb.Game
}
if v, ok := any(item).(gameGetter); ok {
game, err := db.GetItemById[pb.Game](endpoint.EPGames, v.GetGame().Id)
if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("failed to get game: %v", err)
goto END
}
g, err := db.ConvertGame(game)
if err != nil {
log.Printf("failed to convert game: %v", err)
goto END
}
err = db.SaveGame(g)
if err != nil {
log.Printf("failed to save game: %v", err)
goto END
}
log.Printf("game %d aggregated", data.ID)
}
END:
log.Printf("%s %d saved", e.GetEndpointName(), data.ID)
}
}