package main import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" "github.com/meilisearch/meilisearch-go" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/plugins/migratecmd" "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/security" "github.com/spf13/cast" "pocketbase/federation" "pocketbase/integrations/komoot" "pocketbase/integrations/strava" _ "pocketbase/migrations" "pocketbase/util" pub "github.com/go-ap/activitypub" "github.com/microcosm-cc/bluemonday" ) const defaultMeiliMasterKey = "vODkljPcfFANYNepCHyDyGjzAMPcdHnrb6X5KyXQPWo" // verifySettings checks if the required environment variables are set. // If they are not set, it logs a warning. func verifySettings(app core.App) { encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY") if len(encryptionKey) == 0 || len(encryptionKey) < 32 { app.Logger().Warn("POCKETBASE_ENCRYPTION_KEY not set or is shorter than 32 bytes") } meiliMasterKey := os.Getenv("MEILI_MASTER_KEY") if len(meiliMasterKey) == 0 || len(meiliMasterKey) < 32 { app.Logger().Warn("MEILI_MASTER_KEY not set or is shorter than 32 bytes") } if meiliMasterKey == defaultMeiliMasterKey { app.Logger().Warn("MEILI_MASTER_KEY is still set to the default value. Please change it to a secure value.") } } func main() { app := pocketbase.New() client := initializeMeiliSearch() verifySettings(app) registerMigrations(app) setupEventHandlers(app, client) if err := app.Start(); err != nil { log.Fatal(err) } } func initializeMeiliSearch() meilisearch.ServiceManager { return meilisearch.New( os.Getenv("MEILI_URL"), meilisearch.WithAPIKey(os.Getenv("MEILI_MASTER_KEY")), ) } func registerMigrations(app *pocketbase.PocketBase) { migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{ Dir: "migrations", Automigrate: true, }) } func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceManager) { app.OnRecordAfterCreateSuccess("users").BindFunc(createUserHandler(client)) app.OnRecordAfterCreateSuccess("trails").BindFunc(createTrailHandler(client)) app.OnRecordAfterUpdateSuccess("trails").BindFunc(updateTrailHandler(client)) app.OnRecordAfterDeleteSuccess("trails").BindFunc(deleteTrailHandler(client)) app.OnRecordCreateRequest("summit_logs").BindFunc(createSummitLogHandler()) app.OnRecordUpdateRequest("summit_logs").BindFunc(updateSummitLogHandler()) app.OnRecordDeleteRequest("summit_logs").BindFunc(deleteSummitLogHandler()) app.OnRecordCreateRequest("comments").BindFunc(createCommentHandler()) app.OnRecordUpdateRequest("comments").BindFunc(updateCommentHandler()) app.OnRecordDeleteRequest("comments").BindFunc(deleteCommentHandler(client)) app.OnRecordCreateRequest("trail_share").BindFunc(createTrailShareHandler(client)) app.OnRecordDeleteRequest("trail_share").BindFunc(deleteTrailShareHandler(client)) app.OnRecordAfterCreateSuccess("trail_like").BindFunc(createTrailLikeHandler(client)) app.OnRecordAfterDeleteSuccess("trail_like").BindFunc(deleteTrailLikeHandler(client)) app.OnRecordAfterCreateSuccess("lists").BindFunc(createListHandler(client)) app.OnRecordAfterUpdateSuccess("lists").BindFunc(updateListHandler(client)) app.OnRecordAfterDeleteSuccess("lists").BindFunc(deleteListHandler(client)) app.OnRecordCreateRequest("list_share").BindFunc(createListShareHandler(client)) app.OnRecordDeleteRequest("list_share").BindFunc(deleteListShareHandler(client)) app.OnRecordCreateRequest("follows").BindFunc(createFollowHandler()) app.OnRecordDeleteRequest("follows").BindFunc(deleteFollowHandler()) app.OnRecordsListRequest("integrations").BindFunc(listIntegrationHandler()) app.OnRecordCreate("integrations").BindFunc(createIntegrationHandler()) app.OnRecordAfterCreateSuccess("integrations").BindFunc(createUpdateIntegrationSuccessHandler()) app.OnRecordUpdate("integrations").BindFunc(updateIntegrationHandler()) app.OnRecordAfterUpdateSuccess("integrations").BindFunc(createUpdateIntegrationSuccessHandler()) app.OnRecordCreateRequest().BindFunc(sanitizeHTML()) app.OnRecordUpdateRequest().BindFunc(sanitizeHTML()) app.OnRecordRequestEmailChangeRequest("users").BindFunc(changeUserEmailHandler()) app.OnServe().BindFunc(onBeforeServeHandler(client)) app.OnBootstrap().BindFunc(onBootstrapHandler()) } func sanitizeHTML() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { fieldsToSanitize := map[string][]string{ "lists": {"description"}, "settings": {"bio"}, "summit_logs": {"text"}, "trails": {"description"}, "comments": {"text"}, "waypoints": {"description"}, } collection := e.Collection.Name fields, ok := fieldsToSanitize[collection] if !ok { return e.Next() } p := bluemonday.NewPolicy() p.AllowStandardAttributes() p.AllowStandardURLs() p.AllowLists() p.AllowElements("br", "div", "hr", "p", "span", "wbr") p.AllowElements("b", "strong", "em", "u", "blockquote", "a") p.AllowAttrs("href").OnElements("a") p.AllowAttrs("target").OnElements("a") p.AllowAttrs("class").OnElements("a") for _, field := range fields { if val, ok := e.Record.Get(field).(string); ok { sanitizedValue := p.Sanitize(val) e.Record.Set(field, sanitizedValue) } } return e.Next() } } func createUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { userId := e.Record.Id err := createDefaultUserSettings(e.App, e.Record.Id) if err != nil { return err } actor, err := util.ActorFromUser(e.App, e.Record) if err != nil { return err } searchRules := map[string]interface{}{ "lists": map[string]string{ "filter": "public = true OR author = " + actor.Id + " OR shares = " + userId, }, "trails": map[string]string{ "filter": "public = true OR author = " + actor.Id + " OR shares = " + userId, }, } token, err := util.GenerateMeilisearchToken(searchRules, client) if err != nil { return err } e.Record.Set("token", token) if err := e.App.Save(e.Record); err != nil { return err } return e.Next() } } func createDefaultUserSettings(app core.App, userId string) error { collection, err := app.FindCollectionByNameOrId("settings") if err != nil { return err } settings := core.NewRecord(collection) settings.Set("language", "en") settings.Set("unit", "metric") settings.Set("mapFocus", "trails") settings.Set("user", userId) return app.Save(settings) } func createTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author"))) if err != nil { return err } if err := util.IndexTrail(e.App, record, author, client); err != nil { return err } if !author.GetBool("isLocal") { // this happens if someone fetches a remote trail // we create a stub trail record for later reference // no need to create an activity for that return e.Next() } err = e.Next() if err != nil { return err } err = federation.CreateTrailActivity(e.App, e.Record, pub.CreateType) if err != nil { return err } return nil } } func updateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author"))) if err != nil { return err } err = util.UpdateTrail(e.App, record, author, client) if err != nil { return err } if !author.GetBool("isLocal") { // this happens if someone fetches a remote trail // we create a stub trail record for later reference // no need to create an activity for that return e.Next() } err = e.Next() if err != nil { return err } err = federation.CreateTrailActivity(e.App, e.Record, pub.UpdateType) if err != nil { return err } return nil } } func deleteTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record task, err := client.Index("trails").DeleteDocument(record.Id) if err != nil { return err } interval := 500 * time.Millisecond _, err = client.WaitForTask(task.TaskUID, interval) if err != nil { log.Fatalf("Error waiting for task completion: %v", err) } err = federation.CreateTrailDeleteActivity(e.App, e.Record) if err != nil { return err } return e.Next() } } func createSummitLogHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := e.Next() if err != nil { return err } err = federation.CreateSummitLogActivity(e.App, e.Record, pub.CreateType) if err != nil { return err } return nil } } func updateSummitLogHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := e.Next() if err != nil { return err } err = federation.CreateSummitLogActivity(e.App, e.Record, pub.UpdateType) if err != nil { return err } return nil } } func deleteSummitLogHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := federation.CreateSummitLogDeleteActivity(e.App, e.Record) if err != nil { return err } return e.Next() } } func createCommentHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { e.Next() err := federation.CreateCommentActivity(e.App, e.Record, pub.CreateType) if err != nil { return err } return nil } } func updateCommentHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := federation.CreateCommentActivity(e.App, e.Record, pub.UpdateType) if err != nil { return err } return e.Next() } } func deleteCommentHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := federation.CreateCommentDeleteActivity(e.App, client, e.Record) if err != nil { return err } return e.Next() } } func createTrailShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := e.Next() if err != nil { return err } record := e.Record trailId := record.GetString("trail") shares, err := e.App.FindAllRecords("trail_share", dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trailId}), ) if err != nil { return err } actorIds := make([]string, len(shares)) for i, r := range shares { actorIds[i] = r.GetString("actor") } err = util.UpdateTrailShares(trailId, actorIds, client) if err != nil { return err } err = federation.CreateAnnounceActivity(e.App, record, federation.TrailAnnounceType) if err != nil { return err } return nil } } func deleteTrailShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { record := e.Record trailId := record.GetString("trail") err := util.UpdateTrailShares(trailId, []string{}, client) if err != nil { return err } return e.Next() } } func createTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { err := e.Next() if err != nil { return err } record := e.Record trailId := record.GetString("trail") actorId := record.GetString("actor") actor, err := e.App.FindRecordById("activitypub_actors", actorId) if err != nil { return err } trail, err := e.App.FindRecordById("trails", trailId) if err != nil { return err } likes, err := e.App.FindAllRecords("trail_like", dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trailId}), ) if err != nil { return err } trail.Set("like_count", len(likes)) err = e.App.UnsafeWithoutHooks().Save(trail) if err != nil { return err } actorIds := make([]string, len(likes)) for i, r := range likes { actorIds[i] = r.GetString("actor") } err = util.UpdateTrailLikes(trailId, actorIds, client) if err != nil { return err } if !actor.GetBool("isLocal") { // this happens if someone likes a remote trail // we create a local copy // no need to create an activity for that return nil } err = federation.CreateLikeActivity(e.App, record) if err != nil { return err } return nil } } func deleteTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record trailId := record.GetString("trail") actorId := record.GetString("actor") actor, err := e.App.FindRecordById("activitypub_actors", actorId) if err != nil { return err } // trail might deleted be already if this is called as part of a cascade trail, err := e.App.FindRecordById("trails", trailId) if err != nil && err == sql.ErrNoRows { return nil } else if err != nil { return err } likes, err := e.App.CountRecords("trail_like", dbx.NewExp("trail={:trail}", dbx.Params{"trail": trailId})) if err != nil { return err } trail.Set("like_count", likes) err = e.App.UnsafeWithoutHooks().Save(trail) if err != nil { return err } err = util.UpdateTrailLikes(trailId, []string{}, client) if err != nil { return err } if !actor.GetBool("isLocal") { // this happens if someone likes a remote trail // we create a local copy // no need to create an activity for that return nil } err = federation.CreateUnlikeActivity(e.App, record) if err != nil { return err } return e.Next() } } func createListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author"))) if err != nil { return err } if err := util.IndexList(e.App, record, author, client); err != nil { return err } if !author.GetBool("isLocal") { // this happens if someone fetches a remote list // we create a stub list record for later reference // no need to create an activity for that return e.Next() } err = e.Next() if err != nil { return err } err = federation.CreateListActivity(e.App, e.Record, pub.CreateType) if err != nil { return err } return nil } } func updateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record author, err := e.App.FindRecordById("activitypub_actors", record.GetString(("author"))) if err != nil { return err } err = util.UpdateList(e.App, record, author, client) if err != nil { return err } if !author.GetBool("isLocal") { // this happens if someone fetches a remote list // we create a stub list record for later reference // no need to create an activity for that return e.Next() } err = e.Next() if err != nil { return err } err = federation.CreateListActivity(e.App, e.Record, pub.CreateType) if err != nil { return err } return nil } } func deleteListHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { record := e.Record _, err := client.Index("lists").DeleteDocument(record.Id) if err != nil { return err } err = federation.CreateListDeleteActivity(e.App, record) if err != nil { return err } return e.Next() } } func createListShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { err := e.Next() if err != nil { return err } record := e.Record listId := record.GetString("list") shares, err := e.App.FindAllRecords("list_share", dbx.NewExp("list = {:listId}", dbx.Params{"listId": listId}), ) if err != nil { return err } actorIds := make([]string, len(shares)) for i, r := range shares { actorIds[i] = r.GetString("actor") } err = util.UpdateListShares(listId, actorIds, client) if err != nil { return err } err = federation.CreateAnnounceActivity(e.App, record, federation.ListAnnounceType) if err != nil { return err } return nil } } func deleteListShareHandler(client meilisearch.ServiceManager) func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { record := e.Record listId := record.GetString("list") err := util.UpdateListShares(listId, []string{}, client) if err != nil { return err } return e.Next() } } func createFollowHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { // record := e.Record // if errs := e.App.ExpandRecord(record, []string{"follower"}, nil); len(errs) > 0 { // return fmt.Errorf("failed to expand: %v", errs) // } // follower := record.ExpandedOne("follower") // notification := util.Notification{ // Type: util.NewFollower, // Metadata: map[string]string{ // "follower": follower.GetString("username"), // }, // Seen: false, // Author: record.GetString("follower"), // } // err := util.SendNotification(e.App, notification, record.GetString("followee")) // if err != nil { // return err // } e.Next() federation.CreateFollowActivity(e.App, e.Record) return nil } } func deleteFollowHandler() func(e *core.RecordRequestEvent) error { return func(e *core.RecordRequestEvent) error { // record := e.Record // if errs := e.App.ExpandRecord(record, []string{"follower"}, nil); len(errs) > 0 { // return fmt.Errorf("failed to expand: %v", errs) // } // follower := record.ExpandedOne("follower") // notification := util.Notification{ // Type: util.NewFollower, // Metadata: map[string]string{ // "follower": follower.GetString("username"), // }, // Seen: false, // Author: record.GetString("follower"), // } // err := util.SendNotification(e.App, notification, record.GetString("followee")) // if err != nil { // return err // } federation.CreateUnfollowActivity(e.App, e.Record) return e.Next() } } func listIntegrationHandler() func(e *core.RecordsListRequestEvent) error { return func(e *core.RecordsListRequestEvent) error { if e.HasSuperuserAuth() { return e.Next() } for _, r := range e.Records { err := censorIntegrationSecrets(r) if err != nil { return err } } return e.Next() } } func createIntegrationHandler() func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { err := encryptIntegrationSecrets(e.App, e.Record) if err != nil { return err } return e.Next() } } func createUpdateIntegrationSuccessHandler() func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { err := censorIntegrationSecrets(e.Record) if err != nil { return err } return e.Next() } } func updateIntegrationHandler() func(e *core.RecordEvent) error { return func(e *core.RecordEvent) error { err := encryptIntegrationSecrets(e.App, e.Record) if err != nil { return err } return e.Next() } } func censorIntegrationSecrets(r *core.Record) error { secrets := map[string][]string{ "strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"}, "komoot": {"password"}, } for key, secretKeys := range secrets { if integrationString := r.GetString(key); integrationString != "" { var integration map[string]interface{} if err := json.Unmarshal([]byte(integrationString), &integration); err != nil { return err } if integration == nil { continue } for _, secretKey := range secretKeys { integration[secretKey] = "" } b, err := json.Marshal(integration) if err != nil { return err } r.Set(key, string(b)) } } return nil } func encryptIntegrationSecrets(app core.App, r *core.Record) error { encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY") if len(encryptionKey) == 0 { return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil) } secrets := map[string][]string{ "strava": {"clientSecret", "refreshToken", "accessToken", "expiresAt"}, "komoot": {"password"}, } original, _ := app.FindRecordById("integrations", r.Id) for key, secretKeys := range secrets { if integrationString := r.GetString(key); integrationString != "" { var integration map[string]interface{} if err := json.Unmarshal([]byte(integrationString), &integration); err != nil { return err } for _, secretKey := range secretKeys { // If the secret is already encrypted, we don't re-encrypt it. // TODO: This is a bit of a hack, we should handle this in a more robust way (e.g. // storing flag on the record or prefixing encrypted strings with enc: or smilar). // Doing that would also potentially allow us to support key rotation in the future. if secret, ok := integration[secretKey].(string); ok && len(secret) > 0 && !util.CanDecryptSecret(secret) { encryptedSecret, err := security.Encrypt([]byte(secret), encryptionKey) if err != nil { return err } integration[secretKey] = encryptedSecret } else if original != nil { originalString := original.GetString(key) var originalIntegration map[string]interface{} if err := json.Unmarshal([]byte(originalString), &originalIntegration); err != nil { return err } if integration == nil { continue } integration[secretKey] = originalIntegration[secretKey] } } b, err := json.Marshal(integration) if err != nil { return err } r.Set(key, string(b)) } } return nil } func changeUserEmailHandler() func(e *core.RecordRequestEmailChangeRequestEvent) error { return func(e *core.RecordRequestEmailChangeRequestEvent) error { e.Record.Set("email", e.NewEmail) if err := e.App.Save(e.Record); err != nil { return err } return nil } } func onBeforeServeHandler(client meilisearch.ServiceManager) func(se *core.ServeEvent) error { return func(se *core.ServeEvent) error { registerRoutes(se, client) registerCronJobs(se.App) bootstrapData(se.App, client) return se.Next() } } func onBootstrapHandler() func(se *core.BootstrapEvent) error { return func(e *core.BootstrapEvent) error { if err := e.Next(); err != nil { return err } if v := os.Getenv("POCKETBASE_SMTP_SENDER_ADRESS"); v != "" { e.App.Settings().Meta.SenderAddress = v } if v := os.Getenv("POCKETBASE_SMTP_SENDER_NAME"); v != "" { e.App.Settings().Meta.SenderName = v } if v := os.Getenv("POCKETBASE_SMTP_ENABLED"); v != "" { e.App.Settings().SMTP.Enabled = cast.ToBool(v) } if v := os.Getenv("POCKETBASE_SMTP_HOST"); v != "" { e.App.Settings().SMTP.Host = v } if v := os.Getenv("POCKETBASE_SMTP_PORT"); v != "" { e.App.Settings().SMTP.Port = cast.ToInt(v) } if v := os.Getenv("POCKETBASE_SMTP_USERNAME"); v != "" { e.App.Settings().SMTP.Username = v } if v := os.Getenv("POCKETBASE_SMTP_PASSWORD"); v != "" { e.App.Settings().SMTP.Password = v } return e.App.Save(e.App.Settings()) } } func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) { se.Router.GET("/public/search/token", func(e *core.RequestEvent) error { searchRules := map[string]interface{}{ "lists": map[string]string{ "filter": "public = true", }, "trails": map[string]string{ "filter": "public = true", }, } token, err := util.GenerateMeilisearchToken(searchRules, client) if err != nil { return err } return e.JSON(http.StatusOK, map[string]string{"token": token}) }) se.Router.POST("/integration/strava/token", func(e *core.RequestEvent) error { encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY") if len(encryptionKey) == 0 { return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil) } var data strava.TokenRequest if err := e.BindBody(&data); err != nil { return apis.NewBadRequestError("Failed to read request data", err) } userId := "" if e.Auth != nil { userId = e.Auth.Id } integrations, err := e.App.FindAllRecords("integrations", dbx.NewExp("user = {:id}", dbx.Params{"id": userId})) if err != nil { return err } if len(integrations) == 0 { return apis.NewBadRequestError("user has no integration", nil) } integration := integrations[0] stravaString := integration.GetString("strava") if len(stravaString) == 0 { return apis.NewBadRequestError("strava integration missing", nil) } var stravaIntegration strava.StravaIntegration err = json.Unmarshal([]byte(stravaString), &stravaIntegration) if err != nil { return err } decryptedSecret, err := security.Decrypt(stravaIntegration.ClientSecret, encryptionKey) if err != nil { return err } request := strava.TokenRequest{ ClientID: stravaIntegration.ClientID, ClientSecret: string(decryptedSecret), Code: data.Code, GrantType: "authorization_code", } r, err := strava.GetStravaToken(request) if err != nil { return err } if r.AccessToken != "" { stravaIntegration.AccessToken = r.AccessToken } if r.RefreshToken != "" { stravaIntegration.RefreshToken = r.RefreshToken } if r.AccessToken != "" { stravaIntegration.ExpiresAt = r.ExpiresAt } stravaIntegration.Active = true b, err := json.Marshal(stravaIntegration) if err != nil { return err } integration.Set("strava", string(b)) err = e.App.Save(integration) if err != nil { return err } return e.JSON(http.StatusOK, nil) }) se.Router.GET("/integration/komoot/login", func(e *core.RequestEvent) error { encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY") if len(encryptionKey) == 0 { return apis.NewBadRequestError("POCKETBASE_ENCRYPTION_KEY not set", nil) } userId := "" if e.Auth != nil { userId = e.Auth.Id } integrations, err := e.App.FindAllRecords("integrations", dbx.NewExp("user = {:id}", dbx.Params{"id": userId})) if err != nil { return err } if len(integrations) == 0 { return apis.NewBadRequestError("user has no integration", nil) } integration := integrations[0] komootString := integration.GetString("komoot") if len(komootString) == 0 { return apis.NewBadRequestError("komoot integration missing", nil) } var komootIntegration komoot.KomootIntegration err = json.Unmarshal([]byte(komootString), &komootIntegration) if err != nil { return err } decryptedPassword, err := security.Decrypt(komootIntegration.Password, encryptionKey) if err != nil { return err } k := &komoot.KomootApi{} err = k.Login(komootIntegration.Email, string(decryptedPassword)) if err != nil { return apis.NewUnauthorizedError("invalid credentials", nil) } return e.JSON(http.StatusOK, nil) }) se.Router.POST("/activitypub/activity/process", federation.ProcessActivity) se.Router.GET("/activitypub/actor", func(e *core.RequestEvent) error { resource := e.Request.URL.Query().Get("resource") resource = strings.TrimPrefix(resource, "acct:") iri := e.Request.URL.Query().Get("iri") follows := e.Request.URL.Query().Get("follows") == "true" var actor *core.Record var err error if resource != "" { actor, err = federation.GetActorByHandle(e.App, resource, follows) } else { actor, err = federation.GetActorByIRI(e.App, iri, follows) } if err != nil && actor == nil { if strings.HasPrefix(err.Error(), "webfinger") { return e.NotFoundError("Not found", err) } return err } else if err != nil && actor != nil { if err.Error() == "profile is private" { // this is our own profile if actor.GetString("user") == e.Auth.Id { return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": nil}) } else { return e.JSON(http.StatusNotFound, map[string]any{"error": "profile is private"}) } } // we could not fetch the remote actor so we return our local cached copy return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": err.Error()}) } return e.JSON(http.StatusOK, map[string]any{"actor": actor, "error": nil}) }) se.Router.GET("/activitypub/trail/{id}", func(e *core.RequestEvent) error { id := e.Request.PathValue("id") trail, err := e.App.FindRecordById("trails", id) if err != nil { return err } trailObject, err := util.ObjectFromTrail(e.App, trail, nil) if err != nil { return err } return e.JSON(http.StatusOK, trailObject) }) se.Router.GET("/activitypub/comment/{id}", func(e *core.RequestEvent) error { id := e.Request.PathValue("id") comment, err := e.App.FindRecordById("comments", id) if err != nil { return err } commentObject, err := util.ObjectFromComment(e.App, comment, nil) if err != nil { return err } return e.JSON(http.StatusOK, commentObject) }) } func registerCronJobs(app core.App) { schedule := os.Getenv("POCKETBASE_CRON_SYNC_SCHEDULE") if len(schedule) == 0 { schedule = "0 2 * * *" } app.Cron().MustAdd("integrations", schedule, func() { err := strava.SyncStrava(app) if err != nil { warning := fmt.Sprintf("Error syncing with strava: %v", err) fmt.Println(warning) app.Logger().Error(warning) } err = komoot.SyncKomoot(app) if err != nil { warning := fmt.Sprintf("Error syncing with komoot: %v", err) fmt.Println(warning) app.Logger().Error(warning) } }) } func bootstrapData(app core.App, client meilisearch.ServiceManager) error { bootstrapCategories(app) go bootstrapMeilisearchDocuments(app, client) return nil } func bootstrapCategories(app core.App) error { query := app.RecordQuery("categories") records := []*core.Record{} if err := query.All(&records); err != nil { return err } if len(records) == 0 { collection, _ := app.FindCollectionByNameOrId("categories") categories := []string{"Hiking", "Walking", "Climbing", "Skiing", "Canoeing", "Biking"} for _, element := range categories { record := core.NewRecord(collection) record.Set("name", element) f, _ := filesystem.NewFileFromPath("migrations/initial_data/" + strings.ToLower(element) + ".jpg") record.Set("img", f) err := app.Save(record) if err != nil { return err } } } return nil } func bootstrapMeilisearchDocuments(app core.App, client meilisearch.ServiceManager) error { query := app.RecordQuery("trails") trails := []*core.Record{} if err := query.All(&trails); err != nil { return err } _, err := client.Index("trails").DeleteAllDocuments() if err != nil { return err } for _, trail := range trails { author, err := app.FindRecordById("activitypub_actors", trail.GetString(("author"))) if err != nil { return err } if err := util.IndexTrail(app, trail, author, client); err != nil { app.Logger().Warn(fmt.Sprintf("Unable to index trail '%s': %v", trail.GetString("name"), err)) continue } shares, err := app.FindAllRecords("trail_share", dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trail.Id}), ) if err != nil { return err } actorIds := make([]string, len(shares)) for i, r := range shares { actorIds[i] = r.GetString("actor") } err = util.UpdateTrailShares(trail.Id, actorIds, client) if err != nil { app.Logger().Warn(fmt.Sprintf("Unable to update trail shares '%s': %v", trail.GetString("name"), err)) continue } likes, err := app.FindAllRecords("trail_like", dbx.NewExp("trail = {:trailId}", dbx.Params{"trailId": trail.Id}), ) if err != nil { return err } actorIds = make([]string, len(likes)) for i, r := range likes { actorIds[i] = r.GetString("actor") } err = util.UpdateTrailLikes(trail.Id, actorIds, client) if err != nil { app.Logger().Warn(fmt.Sprintf("Unable to update trail likes '%s': %v", trail.GetString("name"), err)) continue } } lists, err := app.FindAllRecords("lists") if err != nil { return err } _, err = client.Index("lists").DeleteAllDocuments() if err != nil { return err } for _, list := range lists { author, err := app.FindRecordById("activitypub_actors", list.GetString(("author"))) if err != nil { return err } if err := util.IndexList(app, list, author, client); err != nil { app.Logger().Warn(fmt.Sprintf("Unable to index list '%s': %v", list.GetString("name"), err)) continue } shares, err := app.FindAllRecords("list_share", dbx.NewExp("list = {:listId}", dbx.Params{"listId": list.Id}), ) if err != nil { return err } actorIds := make([]string, len(shares)) for i, r := range shares { actorIds[i] = r.GetString("actor") } err = util.UpdateListShares(list.Id, actorIds, client) if err != nil { app.Logger().Warn(fmt.Sprintf("Unable to update list shares '%s': %v", list.GetString("name"), err)) continue } } return nil }