mirror of https://gitlab.com/ngerakines/tavern.git
Implemented groups directory. Closes #67.
This commit is contained in:
parent
e8315dc13b
commit
0503e01375
|
@ -9,6 +9,7 @@ type GroupConfig struct {
|
|||
AllowAutoAcceptGroupFollowers bool
|
||||
AllowRemoteGroupFollowers bool
|
||||
DefaultGroupMemberRole int
|
||||
DefaultDirectoryOptIn bool
|
||||
}
|
||||
|
||||
var EnableGroupsFlag = cli.BoolFlag{
|
||||
|
@ -39,12 +40,20 @@ var DefaultGroupMemberRoleFlag = cli.IntFlag{
|
|||
Value: 1,
|
||||
}
|
||||
|
||||
var DefaultDirectoryOptInFlag = cli.BoolFlag{
|
||||
Name: "default-directory-opt-in",
|
||||
Usage: "The default directory opt-in value for groups.",
|
||||
EnvVars: []string{"DEFAULT_GROUP_DIRECTORY_OPT_IN"},
|
||||
Value: true,
|
||||
}
|
||||
|
||||
func NewGroupConfig(cliCtx *cli.Context) (GroupConfig, error) {
|
||||
cfg := GroupConfig{
|
||||
EnableGroups: cliCtx.Bool("enable-groups"),
|
||||
AllowAutoAcceptGroupFollowers: cliCtx.Bool("allow-auto-accept-group-followers"),
|
||||
AllowRemoteGroupFollowers: cliCtx.Bool("allow-remote-group-followers"),
|
||||
DefaultGroupMemberRole: cliCtx.Int("default-group-member-role"),
|
||||
DefaultDirectoryOptIn: cliCtx.Bool("default-directory-opt-in"),
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by herr at 2020-04-03 22:46:50.892042017 -0400 EDT m=+0.007694263
|
||||
// This file was generated by herr at 2020-04-05 12:40:16.825087707 -0400 EDT m=+0.009599340
|
||||
package errors
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by herr at 2020-04-03 22:46:50.924488758 -0400 EDT m=+0.040140977
|
||||
// This file was generated by herr at 2020-04-05 12:40:16.85622214 -0400 EDT m=+0.040733718
|
||||
package errors
|
||||
|
||||
import (
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
alter table groups drop column directory_opt_in;
|
|
@ -0,0 +1,2 @@
|
|||
alter table groups
|
||||
add directory_opt_in bool default true not null;
|
|
@ -97,15 +97,6 @@ var ActorSubjectsFields = []string{
|
|||
"updated_at",
|
||||
}
|
||||
|
||||
var ActorKeysFields = []string{
|
||||
"id",
|
||||
"actor_id",
|
||||
"key_id",
|
||||
"pem",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
func actorsSelectQuery(join string, where []string) string {
|
||||
var query strings.Builder
|
||||
query.WriteString("SELECT ")
|
||||
|
@ -382,4 +373,4 @@ func (s pgStorage) UpdateActorPayload(ctx context.Context, actorRowID uuid.UUID,
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/common"
|
||||
"github.com/ngerakines/tavern/errors"
|
||||
)
|
||||
|
||||
|
@ -39,12 +40,15 @@ type GroupStorage interface {
|
|||
RecordGroupMemberAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, memberActorRowID uuid.UUID, activity Payload, status RelationshipStatus, role GroupRole) (uuid.UUID, error)
|
||||
RemoveGroupInvitation(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) error
|
||||
RemoveGroupMember(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) error
|
||||
UpdateGroupMemberRole(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, role GroupRole) error
|
||||
UpdateGroupMemberStatus(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, status RelationshipStatus) error
|
||||
UpdateGroupDefaultRole(ctx context.Context, groupActorRowID uuid.UUID, role GroupRole) error
|
||||
UpdateGroupAbout(ctx context.Context, groupRowID uuid.UUID, about string) 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
|
||||
UpdateGroupDefaultRole(ctx context.Context, groupActorRowID uuid.UUID, role GroupRole) error
|
||||
UpdateGroupDirectoryOptIn(ctx context.Context, groupRowID uuid.UUID, value bool) error
|
||||
UpdateGroupMemberRole(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, role GroupRole) error
|
||||
UpdateGroupMemberStatus(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, status RelationshipStatus) error
|
||||
ListGroupsPaginated(ctx context.Context, limit, offset int) ([]*Actor, error)
|
||||
CountGroupsMembers(ctx context.Context, groupActorRowIDs []uuid.UUID) ([]Count, error)
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
|
@ -59,6 +63,7 @@ type Group struct {
|
|||
AcceptFollowers bool
|
||||
DefaultMemberRole GroupRole
|
||||
AllowRemote bool
|
||||
DirectoryOptIn bool
|
||||
OwnerID uuid.UUID
|
||||
ActorID uuid.UUID
|
||||
}
|
||||
|
@ -86,6 +91,7 @@ var groupFields = []string{
|
|||
"accept_followers",
|
||||
"default_member_role",
|
||||
"allow_remote",
|
||||
"directory_opt_in",
|
||||
"owner",
|
||||
"actor_id",
|
||||
}
|
||||
|
@ -185,6 +191,7 @@ func (s pgStorage) getGroups(ctx context.Context, query string, args ...interfac
|
|||
&group.AcceptFollowers,
|
||||
&group.DefaultMemberRole,
|
||||
&group.AllowRemote,
|
||||
&group.DirectoryOptIn,
|
||||
&group.OwnerID,
|
||||
&group.ActorID)
|
||||
if err != nil {
|
||||
|
@ -218,6 +225,15 @@ func (s pgStorage) CountGroupMembers(ctx context.Context, groupActorRowID uuid.U
|
|||
return s.wrappedRowCount(errors.WrapGroupQueryFailedError, ctx, `SELECT COUNT(*) FROM group_members WHERE group_actor_id = $1`, groupActorRowID)
|
||||
}
|
||||
|
||||
func (s pgStorage) CountGroupsMembers(ctx context.Context, groupActorRowIDs []uuid.UUID) ([]Count, error) {
|
||||
if len(groupActorRowIDs) == 0 {
|
||||
return []Count{}, nil
|
||||
}
|
||||
placeholders := common.DollarForEach(len(groupActorRowIDs))
|
||||
query := fmt.Sprintf(`SELECT group_actor_id, COUNT(*) FROM group_members WHERE group_actor_id IN (%s) GROUP BY group_actor_id`, strings.Join(placeholders, ","))
|
||||
return s.keyedCount(errors.WrapObjectBoostQueryFailedError, ctx, query, common.UUIDsToInterfaces(groupActorRowIDs)...)
|
||||
}
|
||||
|
||||
func (s pgStorage) GroupMemberActorIDs(ctx context.Context, groupActorRowID uuid.UUID) ([]string, error) {
|
||||
query := `SELECT a.actor_id FROM group_members gm INNER JOIN actors a ON a.id = gm.member_actor_id WHERE gm.group_actor_id = $1 ORDER BY a.actor_id ASC`
|
||||
rows, err := s.db.QueryContext(ctx, query, groupActorRowID)
|
||||
|
@ -346,3 +362,14 @@ func (s pgStorage) UpdateGroupAbout(ctx context.Context, groupRowID uuid.UUID, a
|
|||
_, err := s.db.ExecContext(ctx, "UPDATE groups SET about = $3, updated_at = $2 WHERE id = $1", groupRowID, now, about)
|
||||
return errors.WrapGroupUpdateFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) UpdateGroupDirectoryOptIn(ctx context.Context, groupRowID uuid.UUID, value bool) error {
|
||||
now := s.now()
|
||||
_, err := s.db.ExecContext(ctx, "UPDATE groups SET directory_opt_in = $3, updated_at = $2 WHERE id = $1", groupRowID, now, value)
|
||||
return errors.WrapGroupUpdateFailedError(err)
|
||||
}
|
||||
|
||||
func (s pgStorage) ListGroupsPaginated(ctx context.Context, limit, offset int) ([]*Actor, error) {
|
||||
query := actorsSelectQuery("groups g ON a.id = g.actor_id", nil) + " ORDER BY g.name ASC LIMIT $1 OFFSET $2"
|
||||
return s.getActors(ctx, query, limit, offset)
|
||||
}
|
||||
|
|
|
@ -252,3 +252,11 @@ func (s pgStorage) selectStrings(ew errors.ErrorWrapper, ctx context.Context, qu
|
|||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func CountMap(counts []Count) map[string]int {
|
||||
results := make(map[string]int)
|
||||
for _, c := range counts {
|
||||
results[c.Key] = c.Count
|
||||
}
|
||||
return results
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{{define "head"}}{{end}}
|
||||
{{define "footer_script"}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Group Directory</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3 ">
|
||||
<div class="col">
|
||||
<table class="table table-responsive-sm table-borderless table-hover table-sm">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Members</th>
|
||||
<th scope="col">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $group := .groups }}
|
||||
<tr>
|
||||
<th scope="row">{{ $group.Name }}</th>
|
||||
<td>{{ $group.MemberCount }}</td>
|
||||
<td>{{ date $group.CreatedAt }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "paged" .paged }}
|
||||
{{end}}
|
|
@ -68,6 +68,23 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col">
|
||||
<h3>Public Directory</h3>
|
||||
<form method="POST" action="{{ url "group" $group.Name }}" id="update_directory">
|
||||
<input type="hidden" name="action" value="update_directory"/>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="updateGroupDirectoryOptIn" name="directory"
|
||||
{{ if $group.DirectoryOptIn }}
|
||||
checked="checked"
|
||||
{{ end }}
|
||||
value="true">
|
||||
<label class="form-check-label" for="updateGroupDirectoryOptIn">Display in the groups directory</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>
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
fill the need for providing context, similar to how tags work, audiance, because it rebroadcasts
|
||||
content, and distribution, similar to traditional mailing lists or user groups.
|
||||
</p>
|
||||
<p>
|
||||
Browse existing groups listed in the <a href="{{ url "group_directory" }}">Group Directory</a>.
|
||||
</p>
|
||||
<p>
|
||||
To send a message to a group, make sure the group's inbox is a recipient of the activity. The easy way
|
||||
to do this is to include the group's network identifier (like <code>@tavern-updates@tavern.town</code>)
|
||||
|
|
|
@ -79,6 +79,7 @@ var Command = cli.Command{
|
|||
&config.AllowAutoAcceptGroupFollowersFlag,
|
||||
&config.AllowRemoteFollowersFlag,
|
||||
&config.DefaultGroupMemberRoleFlag,
|
||||
&config.DefaultDirectoryOptInFlag,
|
||||
},
|
||||
Action: serverCommandAction,
|
||||
}
|
||||
|
@ -400,6 +401,8 @@ func serverCommandAction(cliCtx *cli.Context) error {
|
|||
authenticated.GET("/utilities", h.utilities)
|
||||
authenticated.POST("/utilities/webfinger", h.utilitiesWebfinger)
|
||||
authenticated.POST("/utilities/crawl", h.utilitiesCrawl)
|
||||
|
||||
authenticated.GET("/directory/groups", h.groupDirectory)
|
||||
}
|
||||
|
||||
var group run.Group
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ngerakines/tavern/storage"
|
||||
)
|
||||
|
||||
type groupDirectorySummary struct {
|
||||
Name string
|
||||
ActorID string
|
||||
MemberCount int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (h handler) groupDirectory(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
ctx := c.Request.Context()
|
||||
trans, transOK := c.Get("trans")
|
||||
if !transOK {
|
||||
panic("trans not found in context")
|
||||
}
|
||||
data := gin.H{
|
||||
"flashes": getFlashes(session),
|
||||
"Trans": trans,
|
||||
}
|
||||
|
||||
var total int
|
||||
var groups []groupDirectorySummary
|
||||
|
||||
page := intParam(c, "page", 1)
|
||||
limit := 20
|
||||
|
||||
txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error {
|
||||
var err error
|
||||
total, err = tx.RowCount(ctx, `SELECT COUNT(*) FROM groups WHERE directory_opt_in = $1`, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
paginatedGroups, err := tx.ListGroupsPaginated(ctx, limit, (page-1)*limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(paginatedGroups) > 0 {
|
||||
groupActorRowIDs := make([]uuid.UUID, len(paginatedGroups))
|
||||
for i, pg := range paginatedGroups {
|
||||
groupActorRowIDs[i] = pg.ID
|
||||
}
|
||||
|
||||
groupCounts, err := tx.CountGroupsMembers(ctx, groupActorRowIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyedGroupCounts := storage.CountMap(groupCounts)
|
||||
|
||||
for _, pg := range paginatedGroups {
|
||||
groups = append(groups, groupDirectorySummary{
|
||||
Name: pg.Name,
|
||||
ActorID: pg.ActorID,
|
||||
MemberCount: keyedGroupCounts[pg.ID.String()],
|
||||
CreatedAt: pg.CreatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
h.hardFail(c, txErr)
|
||||
return
|
||||
}
|
||||
|
||||
data["groups"] = groups
|
||||
|
||||
paged, err := createPaged(limit, page, total, h.url("group_directory"))
|
||||
if err == nil {
|
||||
data["paged"] = paged
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "group_directory", data)
|
||||
}
|
|
@ -132,6 +132,10 @@ func (h handler) dashboardGroupsCreate(c *gin.Context) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.UpdateGroupDirectoryOptIn(ctx, groupRowID, h.groupConfig.DefaultDirectoryOptIn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.RecordActorKey(ctx, actorRowID, groupKeyID, publicKey)
|
||||
if err != nil {
|
||||
|
|
|
@ -171,6 +171,15 @@ func (h handler) configureGroup(c *gin.Context) {
|
|||
h.flashErrorOrFail(c, h.url("group", group.Name), txErr)
|
||||
return
|
||||
}
|
||||
case "update_directory":
|
||||
txErr := storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error {
|
||||
directoryOptIn, _ := strconv.ParseBool(c.PostForm("directory"))
|
||||
return tx.UpdateGroupDirectoryOptIn(ctx, group.ID, directoryOptIn)
|
||||
})
|
||||
if txErr != nil {
|
||||
h.flashErrorOrFail(c, h.url("group", group.Name), txErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, h.url("group", group.Name))
|
||||
|
|
|
@ -140,6 +140,9 @@ func tmplUrlGen(siteBase string) func(parts ...interface{}) string {
|
|||
case "group_manage":
|
||||
return fmt.Sprintf("%s/groups/%s/manage", siteBase, parts[1])
|
||||
|
||||
case "group_directory":
|
||||
return fmt.Sprintf("%s/directory/groups", siteBase)
|
||||
|
||||
case "groups":
|
||||
return fmt.Sprintf("%s/manage/groups", siteBase)
|
||||
case "groups_create":
|
||||
|
|
Loading…
Reference in New Issue