mirror of https://gitlab.com/ngerakines/tavern.git
Improving groups and network manage pages.
This commit is contained in:
parent
fa13e8827f
commit
50316102fc
|
@ -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
2
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)...)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"))
|
||||
|
|
Loading…
Reference in New Issue