mirror of https://gitlab.com/ngerakines/tavern.git
Implemented advanced compose note form.
This commit is contained in:
parent
4ab38d7730
commit
1830f443e5
|
@ -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
|
||||
|
|
|
@ -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="{{ . }}"/>
|
||||
|
|
|
@ -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}}
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue