Improving groups and network manage pages.

This commit is contained in:
Nick Gerakines 2020-04-03 15:25:45 -04:00
parent fa13e8827f
commit 50316102fc
No known key found for this signature in database
GPG Key ID: 33D43D854F96B2E4
8 changed files with 188 additions and 64 deletions

View File

@ -8,6 +8,8 @@ import (
"github.com/gofrs/uuid"
)
type FilterFunc func(string) bool
func IntRange(min, max int) []int {
a := make([]int, max-min+1)
for i := range a {
@ -79,3 +81,35 @@ func StringToUUIDMapValues(thing map[string]uuid.UUID) []uuid.UUID {
}
return results
}
func FilterStrings(values []string, includeFilter FilterFunc) []string {
filtered := make([]string, 0, len(values))
for _, value := range values {
if includeFilter(value) {
filtered = append(filtered, value)
}
}
return filtered
}
func StringsIncludeFF(values []string) FilterFunc {
return func(value string) bool {
for _, v := range values {
if v == value {
return true
}
}
return false
}
}
func StringsExcludeFF(values []string) FilterFunc {
return func(value string) bool {
for _, v := range values {
if v == value {
return false
}
}
return true
}
}

2
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/sslhound/herr v1.4.1 // indirect
github.com/stretchr/testify v1.4.0
github.com/teacat/noire v1.0.0
github.com/urfave/cli/v2 v2.1.1
github.com/urfave/cli/v2 v2.2.0
github.com/yukimochi/httpsig v0.1.3
go.uber.org/zap v1.13.0
golang.org/x/crypto v0.0.0-20200221170553-0f24fbd83dfb

2
go.sum
View File

@ -415,6 +415,8 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=

View File

@ -29,6 +29,7 @@ type ActorStorage interface {
GetActorByAlias(ctx context.Context, subject string) (*Actor, error)
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)
}
type Actor struct {
@ -367,3 +368,8 @@ func ActorFromGroupInfo(name, displayName, domain, publicKey string, privateKey
func (s pgStorage) ActorAliasSubjectExists(ctx context.Context, alias string) (bool, error) {
return s.wrappedExists(errors.WrapActorAliasQueryFailedError, ctx, `SELECT COUNT(*) FROM actor_aliases WHERE alias = $1 AND alias_type = 0`, alias)
}
func (s pgStorage) FilterGroupsByActorID(ctx context.Context, actorIDs []string) ([]string, error) {
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)...)
}

View File

@ -232,3 +232,23 @@ func (s pgStorage) toUUIDMultiMap(ew errors.ErrorWrapper, ctx context.Context, q
}
return results, nil
}
func (s pgStorage) selectStrings(ew errors.ErrorWrapper, ctx context.Context, query string, args ...interface{}) ([]string, error) {
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, ew(err)
}
defer rows.Close()
var results []string
for rows.Next() {
var value string
if err := rows.Scan(&value); err != nil {
return nil, ew(err)
}
results = append(results, value)
}
return results, nil
}

View File

@ -3,30 +3,38 @@
{{define "content"}}
<div class="row pt-3">
<div class="col">
<h1>Groups</h1>
<p class="lead">
Groups are special actors that announce posts by other members. Groups make it possible to broadcast
content to other actors without everyone in the group knowing about the other actors ahead of time. They
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>
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>)
in the message, but you can also use the advanced view of the compose note form to include the group in
the to, cc, bto, or bcc of the create activity. Alternatively, you can also create <code>Announce</code>
activities and publish them to the group to have the group do the same.
</p>
<p>
<span class="text-danger"><strong>Important!</strong></span>
Groups do not manage privacy or access controls for content. Please be mindful of who activities are
being sent to when publishing federated content.
</p>
</div>
</div>
<div class="row pt-3">
<div class="col">
<p class="lead">Use this form to follow a group.</p>
<form method="POST" action="{{ url "groups_follow" }}" id="follow">
<h3>Create Group</h3>
<form method="POST" action="{{ url "groups_create" }}" id="createGroup">
<div class="form-group">
<label for="followGroup">Group Actor ID</label>
<input type="text" class="form-control" id="followGroup" name="actor"
placeholder="@tavern-updates@tavern.town" required>
</div>
<input type="submit" class="btn btn-dark" name="submit" value="Follow"/>
</form>
</div>
</div>
<div class="row pt-3">
<div class="col">
<p class="lead">Use this form to create a group.</p>
<form method="POST" action="{{ url "groups_create" }}" id="follow">
<div class="form-group">
<label for="createGroup">Name</label>
<label for="createGroup">Group Name</label>
<input type="text" class="form-control" id="createGroup" name="name"
placeholder="tavern-updates" required>
<small class="form-text text-muted">
Groups use the same namespace as users. You cannot create a group that has the same "name" as an
existing user, and inversely, you cannot create a user with the same name as an existing group.
</small>
</div>
<input type="submit" class="btn btn-dark" name="submit" value="Create"/>
</form>
@ -34,12 +42,19 @@
</div>
<div class="row pt-3">
<div class="col">
<h1>Your Groups</h1>
<p class="lead">
To follow a group, go to the <a href="{{ url "network" }}">Network</a> page and follow the group's
actor.
</p>
</div>
</div>
<div class="row pt-3">
<div class="col">
<h1>Groups You Own</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Group</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -48,14 +63,6 @@
<td>
{{ .ActorID }}
</td>
<td>
<a href="{{ url "group" .Name }}">
View
</a>
<a href="{{ url "group_manage" .Name }}">
Manage
</a>
</td>
</tr>
{{ end }}
</tbody>

View File

@ -48,6 +48,41 @@
</td>
</tr>
{{ end }}
{{ range .groups }}
<tr>
<td><span class="badge badge-success">Group</span> {{ . }}</td>
<td>
<form method="post" action="{{ url "network_unfollow" }}">
<input type="hidden" name="actor" value="{{ . }}"/>
<input class="btn btn-sm btn-danger" type="submit" name="submit" value="Unfollow"/>
</form>
</td>
</tr>
{{ end }}
{{ range .pending_groups }}
<tr>
<td>
<span class="badge badge-success">Group</span>
<span class="badge badge-danger">Pending</span>
{{ . }}
</td>
<td class="d-flex">
<div>
<form method="post" action="{{ url "network_follow" }}">
<input type="hidden" name="actor" value="{{ . }}"/>
<input class="btn btn-sm btn-outline-primary" type="submit" name="submit"
value="Retry"/>
</form>
</div>
<div class="pl-2">
<form method="post" action="{{ url "network_unfollow" }}">
<input type="hidden" name="actor" value="{{ . }}"/>
<input class="btn btn-sm btn-danger" type="submit" name="submit" value="Unfollow"/>
</form>
</div>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>

View File

@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
"github.com/ngerakines/tavern/fed"
"github.com/ngerakines/tavern/storage"
@ -85,8 +86,20 @@ func (h handler) dashboardNetwork(c *gin.Context) {
return
}
data["following"] = following
data["pending_following"] = pendingFollowing
groups, err := h.storage.FilterGroupsByActorID(ctx, append(following, pendingFollowing...))
if err != nil {
h.hardFail(c, err)
return
}
followingActors := common.FilterStrings(following, common.StringsExcludeFF(groups))
pendingFollowingActors := common.FilterStrings(pendingFollowing, common.StringsExcludeFF(groups))
data["following"] = followingActors
data["pending_following"] = pendingFollowingActors
data["groups"] = common.FilterStrings(groups, common.StringsIncludeFF(following))
data["pending_groups"] = common.FilterStrings(groups, common.StringsIncludeFF(pendingFollowing))
c.HTML(http.StatusOK, "network", data)
}
@ -126,15 +139,12 @@ func (h handler) networkFollow(c *gin.Context) {
h.flashErrorOrFail(c, h.url("network"), err)
return
}
if isFollowing {
c.Redirect(http.StatusFound, h.url("network"))
return
}
follow := storage.EmptyPayload()
activityID := storage.NewV4()
activityID := common.ActivityURL(h.domain, storage.NewV4())
follow["@context"] = "https://www.w3.org/ns/activitystreams"
follow["actor"] = storage.NewActorID(user.Name, h.domain)
follow["id"] = fmt.Sprintf("https://%s/activity/%s", h.domain, activityID)
follow["id"] = activityID
follow["object"] = actor.GetID()
follow["to"] = actor.GetID()
follow["type"] = "Follow"
@ -142,20 +152,29 @@ func (h handler) networkFollow(c *gin.Context) {
payload := follow.Bytes()
err = h.storage.CreatePendingFollowing(ctx, user.ID, actor.ID, follow)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
if !isFollowing {
err = h.storage.CreatePendingFollowing(ctx, user.ID, actor.ID, follow)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
}
}
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
err = nc.SendToInbox(ctx, h.userActor(user, userActor), actor, payload)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
if h.publisherClient != nil {
err = h.publisherClient.Send(ctx, actor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, activityID, string(payload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", actor.GetID()), zap.String("activity", activityID), zap.Error(err))
}
} else {
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
err = nc.SendToInbox(ctx, h.userActor(user, userActor), actor, payload)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
}
}
c.Redirect(http.StatusFound, h.url("network"))
@ -207,10 +226,10 @@ func (h handler) networkUnfollow(c *gin.Context) {
delete(requestActivity, "@context")
undoFollow := storage.EmptyPayload()
activityID := storage.NewV4()
activityID := common.ActivityURL(h.domain, storage.NewV4())
undoFollow["@context"] = "https://www.w3.org/ns/activitystreams"
undoFollow["actor"] = storage.NewActorID(user.Name, h.domain)
undoFollow["id"] = fmt.Sprintf("https://%s/activity/%s", h.domain, activityID)
undoFollow["id"] = activityID
undoFollow["object"] = requestActivity
undoFollow["to"] = to
undoFollow["type"] = "Undo"
@ -218,26 +237,27 @@ func (h handler) networkUnfollow(c *gin.Context) {
payload := undoFollow.Bytes()
actor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, h.httpClient, to)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
}
err = h.storage.RemoveFollowing(ctx, user.ID, targetActor.ID)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
}
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
err = nc.SendToInbox(ctx, h.userActor(user, userActor), actor, payload)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
if h.publisherClient != nil {
err = h.publisherClient.Send(ctx, targetActor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, activityID, string(payload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", targetActor.GetID()), zap.String("activity", activityID), zap.Error(err))
}
} else {
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
err = nc.SendToInbox(ctx, h.userActor(user, userActor), targetActor, payload)
if err != nil {
h.flashErrorOrFail(c, h.url("network"), err)
return
}
}
c.Redirect(http.StatusFound, h.url("network"))