Implemented advanced compose note form.

This commit is contained in:
Nick Gerakines 2020-03-06 09:46:44 -05:00
parent 4ab38d7730
commit 1830f443e5
8 changed files with 554 additions and 110 deletions

View File

@ -32,7 +32,7 @@ func JSONString(document map[string]interface{}, key string) (string, bool) {
}
func JSONStrings(document map[string]interface{}, key string) ([]string, bool) {
var results []string
results := make([]string, 0)
value, ok := document[key]
if !ok {
return nil, false
@ -55,7 +55,7 @@ func JSONStrings(document map[string]interface{}, key string) ([]string, bool) {
}
func JSONMapList(document map[string]interface{}, key string) ([]map[string]interface{}, bool) {
var results []map[string]interface{}
results := make([]map[string]interface{}, 0)
value, ok := document[key]
if !ok {
return nil, false

View File

@ -5,11 +5,18 @@
<div class="col">
<h1 class="pb-1">Compose Note</h1>
<form method="POST" action="{{ url "compose_create_note" }}" id="create_note">
<div class="mt-1">
<p class="float-right">
<a href="{{ url "compose" }}?advanced=true">
Advanced
</a>
</p>
</div>
{{ if .conversation }}
<input type="hidden" name="conversation" value="{{ .conversation }}"/>
{{ end }}
{{ if .in_reply_to }}
<input type="hidden" name="in_reply_to" value="{{ .in_reply_to }}"/>
<input type="hidden" name="inReplyTo" value="{{ .in_reply_to }}"/>
{{ end }}
{{ range .recipients }}
<input type="hidden" name="recipient" value="{{ . }}"/>

View File

@ -0,0 +1,236 @@
{{define "head"}}{{end}}
{{define "footer_script"}}{{end}}
{{define "content"}}
<div class="row pt-3">
<div class="col">
<h1 class="pb-1">Compose Note</h1>
<div class="form-group">
<p class="lead text-danger">
Caution: This form allows you to deviate from the standard behavior of notes. Proceed with caution.
</p>
</div>
<form method="POST" action="{{ url "compose_create_note" }}?advanced=true" id="create_note">
<div class="mt-1">
<p class="float-right">
<a href="{{ url "compose" }}?advanced=false">
Simple
</a>
</p>
</div>
<div class="form-group">
<label for="createNoteContent">Content</label>
<textarea class="form-control" id="createNoteContent" name="content" required></textarea>
<small id="createNoteToHelp" class="form-text text-muted">
The content of the post.
</small>
</div>
<div class="form-row">
<div class="form-group col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="createNoteParseTags"
name="parseTags"
checked="checked">
<label class="form-check-label" for="createNoteParseTags">
Parse Tags
</label>
<small id="createNoteParseTagsHelp" class="form-text text-muted">
Parse hashtags from note content.
</small>
</div>
</div>
<div class="form-group col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="createNoteParseMentions"
name="parseMentions" checked="checked">
<label class="form-check-label" for="createNoteParseMentions">
Parse Mentions
</label>
<small id="createNoteParseTagsHelp" class="form-text text-muted">
Parse user mentions from note content.
</small>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="createNoteMediaType">Media Type</label>
<select id="createNoteMediaType" class="form-control" name="mediaType">
<option selected>text/html</option>
<option>markdown</option>
</select>
<small id="createNoteMediaTypeHelp" class="form-text text-muted">
Content is assumed to be text/html. If "text/html" is selected and the content has no HTML
markup, each line will be wrapped in <code>p</code> tags.
</small>
</div>
<div class="form-group col-md-6">
<label for="createNoteLanguage">Language</label>
<input type="text" class="form-control" id="createNoteLanguage" name="language" value="en">
<small id="createNoteLanguageHelp" class="form-text text-muted">
Content is presumed to be in english. Enter the language code to explicitly set it.
</small>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="createNoteTo">To</label>
<textarea class="form-control" id="createNoteTo" name="to">https://www.w3.org/ns/activitystreams#Public{{ range .recipients }}
{{ . }}{{ end }}</textarea>
<small id="createNoteToHelp" class="form-text text-muted">
List all of the inboxes that the note should be delivered to. One destination per line.
</small>
</div>
<div class="form-group col-md-6">
<label for="createNoteCc">CC</label>
<textarea class="form-control" id="createNoteCc"
name="cc">{{ .followers_collection }}</textarea>
<small id="createNoteCcHelp" class="form-text text-muted">
List all of the inboxes that the note should be delivered CC'd to. One destination per
line.
</small>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="createNoteBroadcastTo"
name="broadcastTo"
checked="checked">
<label class="form-check-label" for="createNoteBroadcastTo">
Broadcast To
</label>
<small id="createNoteBroadcastToHelp" class="form-text text-muted">
Publish the activity to all "To" destinations.
</small>
</div>
</div>
<div class="form-group col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="createNoteBroadcastCc"
name="broadcastCc"
checked="checked">
<label class="form-check-label" for="createNoteBroadcastCc">
Broadcast CC
</label>
<small id="createNoteBroadcastCcHelp" class="form-text text-muted">
Publish the activity to all "CC" destinations.
</small>
</div>
</div>
<div class="form-group col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="createNoteBroadcastPeers"
name="broadcastPeers"
checked="checked">
<label class="form-check-label" for="createNoteBroadcastPeers">
Broadcast Peers
</label>
<small id="createNoteBroadcastPeersHelp" class="form-text text-muted">
Publish the activity to all known peers.
</small>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="createNoteTags">Tags</label>
<input type="text" class="form-control" id="createNoteTags" name="tags">
<small id="createNoteTagsHelp" class="form-text text-muted">
Space separated list of tags to add to the note. Format is "#hashtag".
</small>
</div>
<div class="form-group col-md-6">
<label for="createNoteMentions">Mentions</label>
<input type="text" class="form-control" id="createNoteMentions" name="mentions">
<small id="createNoteMentionsHelp" class="form-text text-muted">
Space separated list of actors to reference. Format is "@user@domain".
</small>
</div>
</div>
<div class="form-group">
<label for="createNoteConversation">Conversation</label>
<input type="text" class="form-control" id="createNoteConversation"
name="conversation" {{ if .conversation }} value="{{ .conversation }}" {{ end }}>
<small id="createNoteConversationHelp" class="form-text text-muted">
The value of the "conversation" attribute associated with the note.
</small>
</div>
<div class="form-group">
<label for="createNoteInReplyTo">In Reply To</label>
<input type="text" class="form-control" id="createNoteInReplyTo"
name="inReplyTo" {{ if .in_reply_to }} value="{{ .in_reply_to }}" {{ end }} >
<small id="createNoteInReplyToHelp" class="form-text text-muted">
The object ID (fully qualified URL) that this note is a reply to.
</small>
</div>
<div class="form-group">
<label for="createNoteSummary">Summary</label>
<input type="text" class="form-control" id="createNoteSummary" name="summary">
<small id="createNoteSummaryHelp" class="form-text text-muted">
The summary associated with the activity. This is often left blank.
</small>
</div>
<div class="form-row">
<div class="form-group col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true" id="createNotePreviewJSON"
name="previewJSON">
<label class="form-check-label" for="createNotePreviewJSON">
Preview JSON
</label>
<small id="createNotePreviewJSONHelp" class="form-text text-muted">
Preview the activity JSON first.
</small>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="true"
id="createNotePreviewDestinations" name="previewDestinations">
<label class="form-check-label" for="createNotePreviewDestinations">
Preview Destinations
</label>
<small id="createNotePreviewDestinationsHelp" class="form-text text-muted">
Preview the activity destinations.
</small>
</div>
</div>
</div>
<input type="submit" class="btn btn-dark" name="submit" value="Create Note"/>
</form>
</div>
</div>
<div class="row mt-4 border-top">
<div class="col">
<h1>Help</h1>
<dl class="row">
<dt class="col-md-3">Public Notes</dt>
<dd class="col-md-9">
<p>
Omitting the
<span class="text-danger">https://www.w3.org/ns/activitystreams#Public</span>
destination from the TO field will make the note private.
</p>
</dd>
<dt class="col-md-3">Broadcast To Followers</dt>
<dd class="col-md-9">
<p>
To broadcast to your followers, you must address your followers collection:
<span class="text-primary">{{ .followers_collection }}</span>
</p>
</dd>
<dt class="col-md-3">TO and CC</dt>
<dd class="col-md-9">
<p>
All destinations must be actor IDs. The only exceptions are the standard public destination and
your own followers collection.
</p>
</dd>
<dt class="col-md-3">Mentions</dt>
<dd class="col-md-9">
<p>
Mentioned actors are expanded and placed in the CC field.
</p>
</dd>
</dl>
</div>
</div>
{{end}}

View File

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{.title}}</title>
<link rel="stylesheet" href="/public/bootstrap.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="/public/all.min.css">
{{template "head" .}}
</head>
@ -55,8 +55,9 @@
</main>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="/public/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="/public/clipboard.min.js"></script>
{{ template "footer_script" . }}
</body>

View File

@ -8,6 +8,28 @@
<li class="media mb-3 border-top border-secondary pt-2">
<img class="pr-1" src="{{- $actors.Lookup "icon" $note.author -}}?size=60" alt="icon"/>
<div class="media-body">
<div class="dropdown float-right">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button"
id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<i class="fas fa-tools"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
{{ if eq true (lookupBool $announcements $note.object_id false) }}
<a href="#" class="dropdown-item disabled"
data-object="{{ $note.object_id }}">Announce</a>
{{ else }}
<a href="#" class="dropdown-item announce"
data-object="{{ $note.object_id }}">Announce</a>
{{ end }}
{{- if $note.conversation -}}
<a class="dropdown-item"
href="{{ url "compose" }}?inReplyTo={{ urlEncode $note.object_id }}">
Reply
</a>
{{ end }}
</div>
</div>
<span class="text-muted d-block">
Boosted by
<img src="{{- $actors.Lookup "icon" $boost.announcer -}}?size=14" alt="icon"/>
@ -38,30 +60,6 @@
{{ end }}
</ul>
{{ end }}
<div class="card mt-1">
{{ if $note.tags }}
<div class="card-body m-1 p-1">
{{ if $note.tags}}
Tags:
{{ range $tag, $tagLink := $note.tags }}
<a href="{{ $tagLink }}">{{ $tag }}</a>
{{ end }}
{{ end }}
</div>
{{ end }}
<div class="card-body m-1 p-1">
{{ if eq true (lookupBool $announcements $note.object_id false) }}
<a href="#" class="btn btn-primary btn-sm disabled"
data-object="{{ $note.object_id }}">Announced</a>
{{ else }}
<a href="#" class="btn btn-primary btn-sm announce"
data-object="{{ $note.object_id }}">Announce</a>
{{ end }}
{{- if $note.conversation -}}
<a href="#" class="btn btn-primary btn-sm">Reply</a>
{{ end }}
</div>
</div>
</div>
</li>
{{ end }}

View File

@ -7,6 +7,28 @@
<li class="media mb-3 border-top border-secondary pt-2">
<img class="pr-1" src="{{- $actors.Lookup "icon" $note.author -}}?size=60" alt="icon"/>
<div class="media-body">
<div class="dropdown float-right">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button"
id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<i class="fas fa-tools"></i>
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
{{ if eq true (lookupBool $announcements $note.object_id false) }}
<a href="#" class="dropdown-item disabled"
data-object="{{ $note.object_id }}">Announce</a>
{{ else }}
<a href="#" class="dropdown-item announce"
data-object="{{ $note.object_id }}">Announce</a>
{{ end }}
{{- if $note.conversation -}}
<a class="dropdown-item"
href="{{ url "compose" }}?inReplyTo={{ urlEncode $note.object_id }}">
Reply
</a>
{{ end }}
</div>
</div>
<p>
{{ if $note.in_reply_to }}
<a href="{{ $note.author }}">
@ -43,31 +65,6 @@
{{ end }}
</ul>
{{ end }}
<div class="card mt-1">
{{ if $note.tags }}
<div class="card-body m-1 p-1">
{{ if $note.tags}}
Tags:
{{ range $tag, $tagLink := $note.tags }}
<a href="{{ $tagLink }}">{{ $tag }}</a>
{{ end }}
{{ end }}
</div>
{{ end }}
<div class="card-body m-1 p-1">
{{ if eq true (lookupBool $announcements $note.object_id false) }}
<a href="#" class="btn btn-primary btn-sm disabled"
data-object="{{ $note.object_id }}">Announced</a>
{{ else }}
<a href="#" class="btn btn-primary btn-sm announce"
data-object="{{ $note.object_id }}">Announce</a>
{{ end }}
{{- if $note.conversation -}}
<a href="{{ url "compose" }}?inReplyTo={{ urlEncode $note.object_id }}"
class="btn btn-primary btn-sm">Reply</a>
{{ end }}
</div>
</div>
</div>
</li>
{{ end }}

View File

@ -2,7 +2,6 @@ package web
import (
"github.com/gin-contrib/sessions"
"github.com/kr/pretty"
"github.com/ngerakines/tavern/errors"
)
@ -28,7 +27,6 @@ func getFlashes(session sessions.Session) flashes {
}
func (f flashes) Show() bool {
pretty.Println(f)
return len(f.Success) > 0 || len(f.Info) > 0 || len(f.Error) > 0
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -18,7 +19,7 @@ import (
)
func (h handler) compose(c *gin.Context) {
data, _, _, cont := h.loggedIn(c)
data, user, session, cont := h.loggedIn(c)
if !cont {
return
}
@ -53,7 +54,19 @@ func (h handler) compose(c *gin.Context) {
data["recipients"] = recipients
}
c.HTML(http.StatusOK, "compose", data)
view := "compose"
if ok, err := strconv.ParseBool(c.Query("advanced")); ok && err == nil {
view = "compose_advanced"
}
data["followers_collection"] = storage.NewActorID(user.Name, h.domain).Followers()
if err := session.Save(); err != nil {
h.hardFail(c, errors.NewCannotSaveSessionError(err))
return
}
c.HTML(http.StatusOK, view, data)
}
func (h handler) createNote(c *gin.Context) {
@ -70,70 +83,139 @@ func (h handler) createNote(c *gin.Context) {
return
}
content := c.PostForm("content")
if len(content) == 0 {
h.flashErrorOrFail(c, h.url("compose"), fmt.Errorf("note is empty"))
return
userActor := storage.NewActorID(user.Name, h.domain)
advanced := false
if ok, err := strconv.ParseBool(c.Query("advanced")); ok && err == nil {
advanced = true
}
if !advanced {
if ok, err := strconv.ParseBool(c.PostForm("advanced")); ok && err == nil {
advanced = true
}
}
actor := storage.NewActorID(user.Name, h.domain)
activityID := storage.NewV4()
activityURL := fmt.Sprintf("https://%s/activity/%s", h.domain, activityID)
now := time.Now()
publishedAt := now.Format("2006-01-02T15:04:05Z")
errorPage := h.url("compose")
if advanced {
errorPage = errorPage + "?advanced=true"
}
now := time.Now()
activityID := storage.NewV4()
to := make([]string, 0)
cc := make([]string, 0)
broadcastTo := true
broadcastCC := true
broadcastPeers := true
parseTags := true
parseMentions := true
mediaType := "text/html"
language := "en"
summary := ""
conversation := c.PostForm("conversation")
inReplyTo := c.PostForm("inReplyTo")
content := c.PostForm("content")
if advanced {
broadcastTo, _ = strconv.ParseBool(c.PostForm("broadcastTo"))
broadcastCC, _ = strconv.ParseBool(c.PostForm("broadcastCc"))
broadcastPeers, _ = strconv.ParseBool(c.PostForm("broadcastPeers"))
parseTags, _ = strconv.ParseBool(c.PostForm("parseTags"))
parseMentions, _ = strconv.ParseBool(c.PostForm("parseMentions"))
mediaType = c.PostForm("mediaType")
language = c.PostForm("language")
summary = c.PostForm("summary")
}
if len(conversation) == 0 {
conversation = fmt.Sprintf("tag:%s,%s:objectId=%s:objectType=Conversation", h.domain, now.Format("2006-01-02"), activityID)
}
inReplyTo := c.PostForm("in_reply_to")
to := []string{
"https://www.w3.org/ns/activitystreams#Public",
}
cc := []string{
actor.Followers(),
}
for _, recipient := range c.PostFormArray("recipient") {
cc = append(cc, recipient)
if len(content) == 0 {
h.flashErrorOrFail(c, errorPage, fmt.Errorf("note is empty"))
return
}
allRecipients := make(map[string]storage.Actor, 0)
activityURL := fmt.Sprintf("https://%s/activity/%s", h.domain, activityID)
publishedAt := now.Format("2006-01-02T15:04:05Z")
if !advanced {
// The default behavior is to make the post public, send it to followers, and add any recipients to cc.
to = append(to, "https://www.w3.org/ns/activitystreams#Public")
cc = append(cc, userActor.Followers())
for _, recipient := range c.PostFormArray("recipient") {
cc = append(cc, recipient)
}
} else {
for _, dest := range strings.Fields(c.PostForm("to")) {
to = append(to, dest)
}
for _, dest := range strings.Fields(c.PostForm("cc")) {
cc = append(cc, dest)
}
}
mentionedActorNames := storage.FindMentionedActors(content)
mentionedActors := make(map[string]storage.Actor)
var mentionedActorNames []string
if parseMentions {
mentionedActorNames = storage.FindMentionedActors(content)
}
if formMentions := c.PostForm("mentions"); len(formMentions) > 0 {
moreMentionedActorNames := storage.FindMentionedActors(c.PostForm("mentions"))
mentionedActorNames = append(mentionedActorNames, moreMentionedActorNames...)
}
for _, mentionedActor := range mentionedActorNames {
if _, ok := mentionedActors[mentionedActor]; ok {
continue
}
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), mentionedActor)
if err == nil {
mentionedActors[mentionedActor] = foundActor
to = append(to, foundActor.GetID())
allRecipients[foundActor.GetID()] = foundActor
}
}
mentionedTags := storage.FindMentionedTags(content)
to = uniqueTo(to)
cc = uniqueCc(to, cc)
isPublic := true
var mentionedTags []string
if parseTags {
mentionedTags = storage.FindMentionedTags(content)
}
isPublic := (len(to) > 0 && to[0] == "https://www.w3.org/ns/activitystreams#Public") || (len(cc) > 0 && cc[0] == "https://www.w3.org/ns/activitystreams#Public")
firstTags := make([]string, 0)
createNoteID := storage.NewV4()
createNote := storage.EmptyPayload()
createNote["@context"] = "https://www.w3.org/ns/activitystreams"
createNote["actor"] = actor
createNote["actor"] = string(userActor)
createNote["id"] = activityURL
createNote["published"] = publishedAt
createNote["type"] = "Create"
createNote["to"] = to
createNote["cc"] = cc
note := storage.EmptyPayload()
note["attributedTo"] = actor
note["attributedTo"] = string(userActor)
note["content"] = content
if len(language) > 0 {
note["contentMap"] = map[string]string{
language: content,
}
}
note["context"] = conversation
note["conversation"] = conversation
noteURL := fmt.Sprintf("https://%s/object/%s", h.domain, createNoteID)
note["id"] = noteURL
note["published"] = publishedAt
note["summary"] = ""
note["summary"] = summary
note["mediaType"] = mediaType
note["to"] = to
if len(inReplyTo) > 0 {
note["inReplyTo"] = inReplyTo
@ -169,46 +251,79 @@ func (h handler) createNote(c *gin.Context) {
payload := createNote.Bytes()
objectPayload := note.Bytes()
objectEventID, err := h.storage.RecordCreateNoteEvent(c.Request.Context(), activityURL, noteURL, string(actor), string(objectPayload), now, to, cc)
if err != nil {
h.flashErrorOrFail(c, h.url("compose"), err)
return
if broadcastTo || broadcastCC || broadcastPeers {
h.logger.Debug("broadcasting")
}
err = h.storage.WatchThread(ctx, user.ID, conversation)
if err != nil {
h.flashErrorOrFail(c, h.url("compose"), err)
return
}
err = h.storage.RecordUserActivity(ctx, user.ID, objectEventID, isPublic, firstTags)
if err != nil {
h.flashErrorOrFail(c, h.url("compose"), err)
if ok, err := strconv.ParseBool(c.PostForm("previewJSON")); ok && err == nil {
c.JSON(http.StatusOK, createNote)
return
}
followerTotal, err := h.storage.RowCount(ctx, `SELECT COUNT(*) FROM followers WHERE user_id = $1`, user.ID)
if err != nil {
h.flashErrorOrFail(c, h.url("compose"), err)
h.flashErrorOrFail(c, errorPage, err)
return
}
followers, err := h.storage.ListAcceptedFollowers(ctx, user.ID, followerTotal, 0)
if err != nil {
h.flashErrorOrFail(c, h.url("compose"), err)
h.flashErrorOrFail(c, errorPage, err)
return
}
for _, follower := range followers {
_, ok := allRecipients[follower]
if ok {
var toDestinations []string
var ccDestinations []string
for _, dest := range to {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), follower)
if err != nil {
if dest == userActor.Followers() {
toDestinations = append(toDestinations, followers...)
continue
}
allRecipients[follower] = foundActor
toDestinations = append(toDestinations, dest)
}
for _, dest := range cc {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
if dest == userActor.Followers() {
ccDestinations = append(ccDestinations, followers...)
continue
}
ccDestinations = append(ccDestinations, dest)
}
toDestinations = uniqueTo(toDestinations, string(userActor))
ccDestinations = uniqueCc(toDestinations, ccDestinations, string(userActor))
if ok, err := strconv.ParseBool(c.PostForm("previewDestinations")); ok && err == nil {
c.JSON(http.StatusOK, gin.H{"to": toDestinations, "cc": ccDestinations})
return
}
objectEventID, err := h.storage.RecordCreateNoteEvent(c.Request.Context(), activityURL, noteURL, string(userActor), string(objectPayload), now, to, cc)
if err != nil {
h.flashErrorOrFail(c, errorPage, err)
return
}
err = h.storage.WatchThread(ctx, user.ID, conversation)
if err != nil {
h.flashErrorOrFail(c, errorPage, err)
return
}
err = h.storage.RecordUserActivity(ctx, user.ID, objectEventID, isPublic, firstTags)
if err != nil {
h.flashErrorOrFail(c, errorPage, err)
return
}
if !broadcastTo && broadcastCC && broadcastPeers {
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
nc := fed.ActorClient{
@ -217,10 +332,31 @@ func (h handler) createNote(c *gin.Context) {
}
localActor := storage.LocalActor{User: user, ActorID: storage.NewActorID(user.Name, h.domain)}
for _, recipient := range allRecipients {
err = nc.SendToInbox(ctx, localActor, recipient, payload)
if err != nil {
h.logger.Error("failed sending to mentioned actor", zap.String("target", recipient.GetID()), zap.String("activity", activityURL))
if broadcastTo {
for _, dest := range toDestinations {
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), dest)
if err != nil {
h.logger.Error("unable to get or fetch actor", zap.Error(err), zap.String("actor", dest))
continue
}
err = nc.SendToInbox(ctx, localActor, foundActor, payload)
if err != nil {
h.logger.Error("failed sending to mentioned actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
}
}
}
if broadcastCC {
for _, dest := range ccDestinations {
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, common.DefaultHTTPClient(), dest)
if err != nil {
h.logger.Error("unable to get or fetch actor", zap.Error(err), zap.String("actor", dest))
continue
}
err = nc.SendToInbox(ctx, localActor, foundActor, payload)
if err != nil {
h.logger.Error("failed sending to mentioned actor", zap.String("target", foundActor.GetID()), zap.String("activity", activityURL))
}
}
}
@ -301,3 +437,74 @@ func (h handler) announceNote(c *gin.Context) {
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func uniqueTo(input []string, exclude ...string) []string {
results := make([]string, 0)
if len(input) == 0 {
return results
}
hasPublic := false
imap := make(map[string]bool)
for _, i := range input {
if i == "https://www.w3.org/ns/activitystreams#Public" {
hasPublic = true
}
imap[i] = true
}
for _, ex := range exclude {
delete(imap, ex)
}
if hasPublic {
results = append(results, "https://www.w3.org/ns/activitystreams#Public")
}
for dest := range imap {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
results = append(results, dest)
}
return results
}
func uniqueCc(to []string, cc []string, exclude ...string) []string {
results := make([]string, 0)
if len(cc) == 0 {
return results
}
hasPublic := false
toMap := make(map[string]bool)
ccMap := make(map[string]bool)
for _, i := range to {
toMap[i] = true
}
for _, i := range cc {
if i == "https://www.w3.org/ns/activitystreams#Public" {
hasPublic = true
}
ccMap[i] = true
}
for _, ex := range exclude {
delete(ccMap, ex)
}
if hasPublic {
results = append(results, "https://www.w3.org/ns/activitystreams#Public")
}
for dest := range ccMap {
if dest == "https://www.w3.org/ns/activitystreams#Public" {
continue
}
if _, ok := toMap[dest]; ok {
continue
}
results = append(results, dest)
}
return results
}