diff --git a/job/webfinger.go b/job/webfinger.go index 2a7c309..589b1e2 100644 --- a/job/webfinger.go +++ b/job/webfinger.go @@ -71,5 +71,8 @@ func (job *webfinger) work() error { } _, err = fed.GetOrFetchActor(context.Background(), job.storage, job.logger, job.httpClient, work) - return err + if err != nil { + job.logger.Warn("webfinger error", zap.Error(err), zap.String("location", work)) + } + return nil } diff --git a/storage/actor.go b/storage/actor.go index 6cc9679..453ff23 100644 --- a/storage/actor.go +++ b/storage/actor.go @@ -30,6 +30,7 @@ type ActorStorage interface { ActorSubjects(ctx context.Context, actors []uuid.UUID) ([]ActorAlias, error) ActorAliasSubjectExists(ctx context.Context, alias string) (bool, error) FilterGroupsByActorID(ctx context.Context, actorIDs []string) ([]string, error) + UpdateActorPayload(ctx context.Context, actorRowID uuid.UUID, payload Payload) error } type Actor struct { @@ -376,3 +377,9 @@ func (s pgStorage) FilterGroupsByActorID(ctx context.Context, actorIDs []string) query := fmt.Sprintf(`SELECT actor_id FROM actors WHERE payload->>'type' = 'Group' AND actor_id in (%s)`, strings.Join(common.DollarForEach(len(actorIDs)), ",")) return s.selectStrings(errors.WrapActorQueryFailedError, ctx, query, common.StringsToInterfaces(actorIDs)...) } + +func (s pgStorage) UpdateActorPayload(ctx context.Context, actorRowID uuid.UUID, payload Payload) error { + now := s.now() + _, err := s.db.ExecContext(ctx, "UPDATE actors SET payload = $3, updated_at = $2 WHERE id = $1", actorRowID, now, payload) + return errors.WrapActorUpdateFailedError(err) +} \ No newline at end of file diff --git a/storage/groups.go b/storage/groups.go index 669135e..1982dc8 100644 --- a/storage/groups.go +++ b/storage/groups.go @@ -44,6 +44,7 @@ type GroupStorage interface { UpdateGroupDefaultRole(ctx context.Context, groupActorRowID uuid.UUID, role GroupRole) error UpdateGroupAcceptFollowers(ctx context.Context, groupRowID uuid.UUID, acceptFollowers bool) error UpdateGroupAllowRemote(ctx context.Context, groupRowID uuid.UUID, allowRemote bool) error + UpdateGroupAbout(ctx context.Context, groupRowID uuid.UUID, about string) error } type Group struct { @@ -325,17 +326,23 @@ func (s pgStorage) MinutesSinceGroupBoost(ctx context.Context, groupActorRowID, func (s pgStorage) UpdateGroupDefaultRole(ctx context.Context, groupRowID uuid.UUID, role GroupRole) error { now := s.now() _, err := s.db.ExecContext(ctx, "UPDATE groups SET default_member_role = $3, updated_at = $2 WHERE id = $1", groupRowID, now, role) - return errors.WrapUserUpdateFailedError(err) + return errors.WrapGroupUpdateFailedError(err) } func (s pgStorage) UpdateGroupAcceptFollowers(ctx context.Context, groupRowID uuid.UUID, acceptFollowers bool) error { now := s.now() _, err := s.db.ExecContext(ctx, "UPDATE groups SET accept_followers = $3, updated_at = $2 WHERE id = $1", groupRowID, now, acceptFollowers) - return errors.WrapUserUpdateFailedError(err) + return errors.WrapGroupUpdateFailedError(err) } func (s pgStorage) UpdateGroupAllowRemote(ctx context.Context, groupRowID uuid.UUID, allowRemote bool) error { now := s.now() _, err := s.db.ExecContext(ctx, "UPDATE groups SET allow_remote = $3, updated_at = $2 WHERE id = $1", groupRowID, now, allowRemote) - return errors.WrapUserUpdateFailedError(err) + return errors.WrapGroupUpdateFailedError(err) +} + +func (s pgStorage) UpdateGroupAbout(ctx context.Context, groupRowID uuid.UUID, about string) error { + now := s.now() + _, err := s.db.ExecContext(ctx, "UPDATE groups SET about = $3, updated_at = $2 WHERE id = $1", groupRowID, now, about) + return errors.WrapGroupUpdateFailedError(err) } diff --git a/templates/group_profile.html b/templates/group_profile.html index 91eaaed..ef80399 100644 --- a/templates/group_profile.html +++ b/templates/group_profile.html @@ -7,7 +7,7 @@

@{{ .group.Name }}@{{ .domain }}

{{ if .group.About }} -

{{ .group.About }}

+ {{ .group.About | toHTML }} {{ else }}

This group does not have a description.

{{ end }} @@ -24,8 +24,8 @@

Update Description

-
- + +
@@ -36,15 +36,58 @@
-

Invite

- - -
- - +

Auto-Accept

+ + +
+ +
- + + +
+
+
+
+

Allow Remote

+
+ +
+ + +
+ +
+
+
+
+
+

Follower Starting Role

+
+ +
+ + +
+
@@ -62,7 +105,7 @@ {{ range $m := .members }} - {{ $m.ActorID }} + {{ $m.ActorID }} {{ if not (eq $m.ActorID $userActor.ActorID) }} diff --git a/templates/groups.html b/templates/groups.html index e72014a..bf29ad2 100644 --- a/templates/groups.html +++ b/templates/groups.html @@ -61,7 +61,7 @@ {{ range .groups }} - {{ .ActorID }} + {{ .ActorID }} {{ end }} diff --git a/templates/network.html b/templates/network.html index 9157059..ce416c3 100644 --- a/templates/network.html +++ b/templates/network.html @@ -32,7 +32,9 @@ {{ range .following }} - {{ . }} + + {{ . }} +
@@ -43,14 +45,20 @@ {{ end }} {{ range .pending_following }} - Pending {{ . }} + + Pending + {{ . }} + {{ end }} {{ range .groups }} - Group {{ . }} + + Group + {{ . }} + @@ -64,7 +72,7 @@ Group Pending - {{ . }} + {{ . }}
diff --git a/web/command.go b/web/command.go index 721108b..df923d0 100644 --- a/web/command.go +++ b/web/command.go @@ -350,6 +350,7 @@ func serverCommandAction(cliCtx *cli.Context) error { groupActor := authenticated.Group("/groups") { groupActor.GET("/:name", h.groupActorInfo) + groupActor.POST("/:name", h.configureGroup) groupActor.POST("/:name/inbox", h.groupActorInbox) groupActor.GET("/:name/outbox", h.groupActorOutbox) groupActor.GET("/:name/following", h.groupActorFollowing) diff --git a/web/handler_auth.go b/web/handler_auth.go new file mode 100644 index 0000000..4dd5bd9 --- /dev/null +++ b/web/handler_auth.go @@ -0,0 +1,53 @@ +package web + +import ( + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + + "github.com/ngerakines/tavern/errors" + "github.com/ngerakines/tavern/storage" +) + +func (h handler) loggedIn(c *gin.Context, requireUser bool) (gin.H, *storage.User, sessions.Session, bool) { + session := sessions.Default(c) + ctx := c.Request.Context() + trans, transOK := c.Get("trans") + if !transOK { + panic(errors.NewTranslatorNotFoundError(nil)) + } + data := gin.H{ + "flashes": getFlashes(session), + "Trans": trans, + } + + user, err := h.storage.GetUserBySession(ctx, session) + if requireUser && err != nil && errors.Is(err, errors.UserSessionNotFoundError{}) { + h.flashErrorOrFail(c, h.url(), errors.NewAuthenticationRequiredError(err)) + return nil, nil, nil, false + } else if err != nil && !errors.Is(err, errors.UserSessionNotFoundError{}) { + h.hardFail(c, err) + return nil, nil, nil, false + } + + if user != nil { + data["user"] = user + data["authenticated"] = true + } + + return data, user, session, true +} + +func (h handler) loggedInAPI(c *gin.Context, requireUser bool) (*storage.User, sessions.Session, bool) { + session := sessions.Default(c) + ctx := c.Request.Context() + + user, err := h.storage.GetUserBySession(ctx, session) + if requireUser && err != nil && errors.Is(err, errors.UserSessionNotFoundError{}) { + h.flashErrorOrFail(c, h.url(), errors.NewAuthenticationRequiredError(err)) + return nil, nil, false + } else if err != nil && !errors.Is(err, errors.UserSessionNotFoundError{}) { + h.hardFail(c, err) + return nil, nil, false + } + return user, session, true +} diff --git a/web/handler_feed.go b/web/handler_feed.go index 87c3535..534b189 100644 --- a/web/handler_feed.go +++ b/web/handler_feed.go @@ -13,39 +13,9 @@ import ( "github.com/gofrs/uuid" "go.uber.org/zap" - "github.com/ngerakines/tavern/errors" "github.com/ngerakines/tavern/storage" ) -func (h handler) loggedIn(c *gin.Context, requireUser bool) (gin.H, *storage.User, sessions.Session, bool) { - session := sessions.Default(c) - ctx := c.Request.Context() - trans, transOK := c.Get("trans") - if !transOK { - panic(errors.NewTranslatorNotFoundError(nil)) - } - data := gin.H{ - "flashes": getFlashes(session), - "Trans": trans, - } - - user, err := h.storage.GetUserBySession(ctx, session) - if requireUser && err != nil && errors.Is(err, errors.UserSessionNotFoundError{}) { - h.flashErrorOrFail(c, h.url(), errors.NewAuthenticationRequiredError(err)) - return nil, nil, nil, false - } else if err != nil && !errors.Is(err, errors.UserSessionNotFoundError{}) { - h.hardFail(c, err) - return nil, nil, nil, false - } - - if user != nil { - data["user"] = user - data["authenticated"] = true - } - - return data, user, session, true -} - func (h handler) saveSession(c *gin.Context, session sessions.Session) bool { if err := session.Save(); err != nil { h.hardFail(c, err) @@ -442,7 +412,7 @@ func (h handler) viewFeed(c *gin.Context) { "self_url": h.url("feed_recent"), } page := intParam(c, "page", 1) - limit := 10 + limit := 20 h.displayObjectFeed(c, true, meta, func(user *storage.User) (i int, payloads []storage.Payload, err error) { total, err := h.storage.CountObjectEventPayloadsInFeed(c.Request.Context(), user.ID) if err != nil { diff --git a/web/handler_group_profile.go b/web/handler_group_profile.go index efff4e6..dd0f151 100644 --- a/web/handler_group_profile.go +++ b/web/handler_group_profile.go @@ -1,12 +1,18 @@ package web import ( + "fmt" "net/http" + "strconv" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday/v2" + "go.uber.org/zap" "github.com/ngerakines/tavern/errors" + "github.com/ngerakines/tavern/storage" ) func (h handler) viewGroup(c *gin.Context) { @@ -21,28 +27,84 @@ func (h handler) viewGroup(c *gin.Context) { "Trans": trans, } - authenticated := true - localUser, err := h.storage.GetUserBySession(ctx, session) - if err != nil { - if !errors.Is(err, errors.NewNotFoundError(nil)) { - h.internalServerErrorJSON(c, err) - return + txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error { + authenticated := true + + localUser, err := tx.GetUserBySession(ctx, session) + if err != nil { + if !errors.Is(err, errors.NewNotFoundError(nil)) { + return err + } + authenticated = false } - authenticated = false + + name := c.Param("name") + + group, err := tx.GetGroupByName(ctx, name) + if err != nil { + return err + } + + groupActor, err := tx.GetActor(ctx, group.ActorID) + if err != nil { + return err + } + + data["group"] = group + data["group_actor"] = groupActor + data["domain"] = h.domain + + totalMembers, err := tx.CountGroupMembers(ctx, group.ID) + if err != nil { + return err + } + data["members_total"] = totalMembers + + if !authenticated { + return nil + } + + data["authenticated"] = true + data["user"] = localUser + + userActor, err := tx.GetActor(ctx, localUser.ActorID) + if err != nil { + return err + } + data["user_actor"] = userActor + + members, err := tx.GroupMemberActorsForGroupActorID(ctx, group.ActorID) + if err != nil { + return err + } + + data["members"] = members + + return nil + }) + if txErr != nil { + h.hardFail(c, txErr) + return } - data["authenticated"] = authenticated - data["user"] = localUser + c.HTML(http.StatusOK, "group_profile", data) +} - name := c.Param("name") +func (h handler) configureGroup(c *gin.Context) { + user, _, cont := h.loggedInAPI(c, true) + if !cont { + return + } - group, err := h.storage.GetGroupByName(ctx, name) + ctx := c.Request.Context() + + group, err := h.storage.GetGroupByName(ctx, c.Param("name")) if err != nil { - if errors.Is(err, errors.NewNotFoundError(nil)) { - h.notFoundJSON(c, err) - return - } - h.internalServerErrorJSON(c, err) + h.hardFail(c, err) + return + } + if group.OwnerID != user.ID { + h.hardFail(c, err) return } @@ -52,29 +114,64 @@ func (h handler) viewGroup(c *gin.Context) { return } - data["group"] = group - data["group_actor"] = groupActor - data["domain"] = h.domain + action := c.PostForm("action") + switch action { + case "update_about": + txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error { + unsafe := blackfriday.Run([]byte(c.PostForm("about"))) + html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) - if !authenticated { - c.HTML(http.StatusOK, "group_profile", data) - return + err := tx.UpdateGroupAbout(ctx, group.ID, string(html)) + if err != nil { + return err + } + groupPayload := groupActor.Payload + groupPayload["summary"] = string(html) + + return tx.UpdateActorPayload(ctx, group.ActorID, groupPayload) + }) + if txErr != nil { + h.flashErrorOrFail(c, h.url("group", group.Name), txErr) + return + } + case "update_auto_accept": + txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error { + autoFollow, _ := strconv.ParseBool(c.PostForm("auto_follow")) + return tx.UpdateGroupAcceptFollowers(ctx, group.ID, autoFollow) + }) + if txErr != nil { + h.flashErrorOrFail(c, h.url("group", group.Name), txErr) + return + } + case "update_default_role": + h.logger.Debug("update_default_role", zap.String("value", c.PostForm("default_role"))) + txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error { + defaultMemberRole, err := strconv.Atoi(c.PostForm("default_role")) + if err != nil { + return err + } + switch storage.GroupRole(defaultMemberRole) { + case storage.GroupViewer, storage.GroupMember, storage.GroupOwner: + default: + return fmt.Errorf("invalid group role: %d", defaultMemberRole) + } + + return tx.UpdateGroupDefaultRole(ctx, group.ID, storage.GroupRole(defaultMemberRole)) + }) + if txErr != nil { + h.flashErrorOrFail(c, h.url("group", group.Name), txErr) + return + } + case "update_allow_remote": + txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error { + allowRemote, _ := strconv.ParseBool(c.PostForm("allow_remote")) + return tx.UpdateGroupAllowRemote(ctx, group.ID, allowRemote) + }) + if txErr != nil { + h.flashErrorOrFail(c, h.url("group", group.Name), txErr) + return + } } - userActor, err := h.storage.GetActor(ctx, localUser.ActorID) - if err != nil { - h.hardFail(c, err) - return - } - data["user_actor"] = userActor - - members, err := h.storage.GroupMemberActorsForGroupActorID(ctx, group.ActorID) - if err != nil { - h.hardFail(c, err) - return - } - - data["members"] = members - - c.HTML(http.StatusOK, "group_profile", data) + c.Redirect(http.StatusFound, h.url("group", group.Name)) }