diff --git a/go.mod b/go.mod index 7ef3fc5..e8546a8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.mstar.dev/mstar/linstrom go 1.24.2 require ( + golang.org/x/sync v0.15.0 git.mstar.dev/mstar/goutils v1.16.1 github.com/BurntSushi/toml v1.5.0 github.com/PeerDB-io/gluabit32 v1.0.2 @@ -80,7 +81,6 @@ require ( go.uber.org/mock v0.5.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.15.0 // indirect golang.org/x/text v0.24.0 // indirect golang.org/x/tools v0.32.0 // indirect gorm.io/datatypes v1.2.5 // indirect diff --git a/storage-new/dbgen/user_to_user_relations.gen.go b/storage-new/dbgen/user_to_user_relations.gen.go index dd9b0ca..14dc727 100644 --- a/storage-new/dbgen/user_to_user_relations.gen.go +++ b/storage-new/dbgen/user_to_user_relations.gen.go @@ -594,6 +594,7 @@ type IUserToUserRelationDo interface { GetFollowingApLinksPagedForId(id string, page int) (result []string, err error) CountFollowersForId(id string) (result int, err error) CountFollowingForId(id string) (result int, err error) + GetLocalFollowerIdsOfId(id string) (result []string, err error) } // Get all inbox links for accounts following the user with the specified id @@ -720,6 +721,34 @@ func (u userToUserRelationDo) CountFollowingForId(id string) (result int, err er return } +// Get the ids of all local accounts following the user with the target id +// +// SELECT +// r.user_id +// FROM +// +// user_to_user_relations r +// LEFT JOIN users u ON r.user_id = u.id +// LEFT JOIN remote_servers s ON u.server_id = s.id +// +// WHERE +// +// s.is_self = true +// AND r.target_user_id = @id; +func (u userToUserRelationDo) GetLocalFollowerIdsOfId(id string) (result []string, err error) { + var params []interface{} + + var generateSQL strings.Builder + params = append(params, id) + generateSQL.WriteString("SELECT r.user_id FROM user_to_user_relations r LEFT JOIN users u ON r.user_id = u.id LEFT JOIN remote_servers s ON u.server_id = s.id WHERE s.is_self = true AND r.target_user_id = ?; ") + + var executeSQL *gorm.DB + executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Find(&result) // ignore_security_alert + err = executeSQL.Error + + return +} + func (u userToUserRelationDo) Debug() IUserToUserRelationDo { return u.withDO(u.DO.Debug()) } diff --git a/storage-new/models/Feed.go b/storage-new/models/Feed.go index ccb8f36..d727183 100644 --- a/storage-new/models/Feed.go +++ b/storage-new/models/Feed.go @@ -34,3 +34,4 @@ type Feed struct { // Suffix added to feeds created as the default feed for a user const FeedDefaultSuffix = "-default" +const GlobalFeedName = "global" diff --git a/storage-new/models/UserToUserRelation.go b/storage-new/models/UserToUserRelation.go index b41b69e..2b683ee 100644 --- a/storage-new/models/UserToUserRelation.go +++ b/storage-new/models/UserToUserRelation.go @@ -71,4 +71,17 @@ type IUserToUserRelation interface { // r.user_id = @id AND // r.relation = 'follow' CountFollowingForId(id string) (int, error) + + // Get the ids of all local accounts following the user with the target id + // + // SELECT + // r.user_id + // FROM + // user_to_user_relations r + // LEFT JOIN users u ON r.user_id = u.id + // LEFT JOIN remote_servers s ON u.server_id = s.id + // WHERE + // s.is_self = true + // AND r.target_user_id = @id; + GetLocalFollowerIdsOfId(id string) ([]string, error) } diff --git a/storage-new/self.go b/storage-new/self.go index 6c2bdab..375779f 100644 --- a/storage-new/self.go +++ b/storage-new/self.go @@ -43,6 +43,9 @@ func InsertSelf() error { if err = attachUserToRole(user); err != nil { return other.Error("storage", "failed to save/update self user to full admin role", err) } + if err = insertGlobalFeed(user); err != nil { + return other.Error("storage", "failed to ensure that the global feed exists", err) + } return nil } @@ -215,3 +218,26 @@ func attachUserToRole(user *models.User) error { } return nil } + +func insertGlobalFeed(serverActor *models.User) error { + globalFeed, err := dbgen.Feed.Where(dbgen.Feed.Name.Eq(models.GlobalFeedName)).First() + switch err { + case nil: + return nil + case gorm.ErrRecordNotFound: + globalFeed = &models.Feed{ + Owner: *serverActor, + OwnerId: serverActor.ID, + IsDefault: true, + Name: models.GlobalFeedName, + PublicKey: sql.NullString{Valid: false}, + } + err = dbgen.Feed.Create(globalFeed) + if err != nil { + return err + } + return nil + default: + return err + } +} diff --git a/web/debug/server.go b/web/debug/server.go index 67197e4..22c36f7 100644 --- a/web/debug/server.go +++ b/web/debug/server.go @@ -24,22 +24,29 @@ type Server struct { func New(addr string) *Server { handler := http.NewServeMux() - handler.HandleFunc("GET /non-deleted", getNonDeletedUsers) + handler.HandleFunc("POST /local-user", createLocalUser) - handler.HandleFunc("GET /delete", deleteUser) handler.HandleFunc("POST /post-as", postAs) - handler.HandleFunc("GET /notes-for", notesFrom) - handler.HandleFunc("GET /import-user", issueUserImport) handler.HandleFunc("GET /keys-for", returnKeypair) - handler.HandleFunc("GET /import-server", importServerInfo) + handler.HandleFunc("GET /import-user", issueUserImport) handler.HandleFunc("GET /request-follow", requestFollow) handler.HandleFunc("POST /send-as", proxyMessageToTarget) + handler.HandleFunc("POST /follow", requestFollow) + handler.HandleFunc("POST /update-user", updateUser) + + handler.HandleFunc("GET /non-deleted", getNonDeletedUsers) + handler.HandleFunc("GET /delete", deleteUser) + handler.HandleFunc("GET /notes-for", notesFrom) + handler.HandleFunc("GET /import-server", importServerInfo) handler.HandleFunc("GET /replies-to/{id}", inReplyTo) handler.HandleFunc("POST /fetch", requestAs) - handler.HandleFunc("POST /follow", requestFollow) + handler.HandleFunc("POST /like-note", likeNote) + handler.HandleFunc("POST /boost-note", boostNote) + + handler.HandleFunc("GET /files-owned-by", getOwnedFiles) handler.HandleFunc("POST /upload-file", uploadMedia) handler.HandleFunc("/force-media-sync", forceMediaSync) - handler.HandleFunc("GET /files-owned-by", getOwnedFiles) + web := http.Server{ Addr: addr, Handler: webutils.ChainMiddlewares( diff --git a/web/debug/users.go b/web/debug/users.go index 2fc8887..d0ed270 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -448,3 +448,53 @@ func requestAs(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(res.Body) _, _ = fmt.Fprint(w, string(body)) } + +func updateUser(w http.ResponseWriter, r *http.Request) { + type Inbound struct { + UserId string + Displayname *string + Description *string + RestrictedFollow *bool + } + log := hlog.FromRequest(r) + var data Inbound + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + _ = webutils.ProblemDetailsStatusOnly(w, http.StatusBadRequest) + return + } + queryStart := dbgen.User.Where(dbgen.User.ID.Eq(data.UserId)) + user, err := queryStart.First() + switch err { + case gorm.ErrRecordNotFound: + _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) + return + case nil: + default: + log.Error().Err(err).Msg("Db error while trying to fetch user for updating") + _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + updateNeeded := false + if data.Displayname != nil { + user.DisplayName = *data.Displayname + updateNeeded = true + } + if data.Description != nil { + user.Description = *data.Description + updateNeeded = true + } + if data.RestrictedFollow != nil { + user.RestrictedFollow = *data.RestrictedFollow + updateNeeded = true + } + if !updateNeeded { + return + } + err = queryStart.Save(user) + if err != nil { + log.Error().Err(err).Msg("Failed to update user with new data") + _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } +}