mirror of https://gitlab.com/ngerakines/tavern.git
Implemented group profile and manage group features. Closes #68.
This commit is contained in:
parent
04d1423670
commit
e8315dc13b
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div class="col">
|
||||
<h1>@{{ .group.Name }}@{{ .domain }}</h1>
|
||||
{{ if .group.About }}
|
||||
<p class="lead">{{ .group.About }}</p>
|
||||
{{ .group.About | toHTML }}
|
||||
{{ else }}
|
||||
<p class="text-muted">This group does not have a description.</p>
|
||||
{{ end }}
|
||||
|
@ -24,8 +24,8 @@
|
|||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h3>Update Description</h3>
|
||||
<form method="POST" action="{{ url "update_group" }}" id="update_about">
|
||||
<input type="hidden" name="actor_id" value="{{ .group_actor.ActorID }}"/>
|
||||
<form method="POST" action="{{ url "group" $group.Name }}" id="update_about">
|
||||
<input type="hidden" name="action" value="update_about"/>
|
||||
<div class="form-group">
|
||||
<label for="updateGroupAbout">About</label>
|
||||
<input type="text" class="form-control" id="updateGroupAbout" name="about" required>
|
||||
|
@ -36,15 +36,58 @@
|
|||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h3>Invite</h3>
|
||||
<form method="POST" action="{{ url "group_invite" }}" id="update_about">
|
||||
<input type="hidden" name="group" value="{{ .group_actor.ActorID }}"/>
|
||||
<div class="form-group">
|
||||
<label for="groupInviteActor">Actor</label>
|
||||
<input type="text" class="form-control" id="groupInviteActor" name="actor"
|
||||
placeholder="@nick@tavern.town" required>
|
||||
<h3>Auto-Accept</h3>
|
||||
<form method="POST" action="{{ url "group" $group.Name }}" id="update_auto_accept">
|
||||
<input type="hidden" name="action" value="update_auto_accept"/>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="updateGroupAutoAccept" name="auto_follow"
|
||||
{{ if $group.AcceptFollowers }}
|
||||
checked="checked"
|
||||
{{ end }}
|
||||
value="true">
|
||||
<label class="form-check-label" for="updateGroupAutoAccept">Auto-Accept Followers</label>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Invite"/>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Update"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h3>Allow Remote</h3>
|
||||
<form method="POST" action="{{ url "group" $group.Name }}" id="update_allow_remote">
|
||||
<input type="hidden" name="action" value="update_allow_remote"/>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="updateGroupAllowRemote" name="allow_remote"
|
||||
{{ if $group.AllowRemote }}
|
||||
checked="checked"
|
||||
{{ end }}
|
||||
value="true">
|
||||
<label class="form-check-label" for="updateGroupAllowRemote">Allow Remote Followers</label>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Update"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h3>Follower Starting Role</h3>
|
||||
<form method="POST" action="{{ url "group" $group.Name }}" id="update_default_role">
|
||||
<input type="hidden" name="action" value="update_default_role"/>
|
||||
<div class="form-group">
|
||||
<label for="updateGroupDefaultRole">Role</label>
|
||||
<select class="form-control" id="updateGroupDefaultRole" name="default_role">
|
||||
<option value="0" {{ if eq 0 $group.DefaultMemberRole }} selected="selected"{{ end }}>
|
||||
Viewer
|
||||
</option>
|
||||
<option value="1" {{ if eq 1 $group.DefaultMemberRole }} selected="selected"{{ end }}>
|
||||
Contributor
|
||||
</option>
|
||||
<option value="2" {{ if eq 2 $group.DefaultMemberRole }} selected="selected"{{ end }}>
|
||||
Manager
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-dark" name="submit" value="Update"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -62,7 +105,7 @@
|
|||
{{ range $m := .members }}
|
||||
<tr {{- if eq $m.ActorID $userActor.ActorID }} class="table-info" {{- end -}}>
|
||||
<td>
|
||||
{{ $m.ActorID }}
|
||||
<a href="{{ $m.ActorID }}">{{ $m.ActorID }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ if not (eq $m.ActorID $userActor.ActorID) }}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
{{ range .groups }}
|
||||
<tr>
|
||||
<td>
|
||||
{{ .ActorID }}
|
||||
<a href="{{ .ActorID }}">{{ .ActorID }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
|
|
@ -32,7 +32,9 @@
|
|||
<tbody>
|
||||
{{ range .following }}
|
||||
<tr>
|
||||
<td>{{ . }}</td>
|
||||
<td>
|
||||
<a href="{{ . }}">{{ . }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{{ url "network_unfollow" }}">
|
||||
<input type="hidden" name="actor" value="{{ . }}"/>
|
||||
|
@ -43,14 +45,20 @@
|
|||
{{ end }}
|
||||
{{ range .pending_following }}
|
||||
<tr>
|
||||
<td><span class="badge badge-danger">Pending</span> {{ . }}</td>
|
||||
<td>
|
||||
<span class="badge badge-danger">Pending</span>
|
||||
<a href="{{ . }}">{{ . }}</a>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .groups }}
|
||||
<tr>
|
||||
<td><span class="badge badge-success">Group</span> {{ . }}</td>
|
||||
<td>
|
||||
<span class="badge badge-success">Group</span>
|
||||
<a href="{{ . }}">{{ . }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{{ url "network_unfollow" }}">
|
||||
<input type="hidden" name="actor" value="{{ . }}"/>
|
||||
|
@ -64,7 +72,7 @@
|
|||
<td>
|
||||
<span class="badge badge-success">Group</span>
|
||||
<span class="badge badge-danger">Pending</span>
|
||||
{{ . }}
|
||||
<a href="{{ . }}">{{ . }}</a>
|
||||
</td>
|
||||
<td class="d-flex">
|
||||
<div>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue