kopia lustrzana https://github.com/reiver/greatape
feat: ✨ public feed
rodzic
9a9e47a3f2
commit
c3a8ff5726
|
@ -1,5 +1,9 @@
|
|||
package activitypub
|
||||
|
||||
import "time"
|
||||
|
||||
const Public = "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
type Activity struct {
|
||||
Context string `json:"@context"`
|
||||
ID string `json:"id,omitempty"`
|
||||
|
@ -10,4 +14,5 @@ type Activity struct {
|
|||
To interface{} `json:"to,omitempty"`
|
||||
InReplyTo string `json:"inReplyTo,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Published time.Time `json:"published,omitempty"`
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"config"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
@ -13,19 +14,20 @@ type Note struct {
|
|||
Id string `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
To []string `json:"to"`
|
||||
AttributedTo string `json:"attributedto"`
|
||||
AttributedTo string `json:"attributedTo"`
|
||||
InReplyTo string `json:"inReplyTo,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (note *Note) Wrap(username string) *Activity {
|
||||
return &Activity{
|
||||
Context: ActivityStreams,
|
||||
Type: TypeCreate,
|
||||
ID: fmt.Sprintf("%s://%s/u/%s/posts/%s", config.PROTOCOL, config.DOMAIN, username, uuid.New().String()),
|
||||
To: note.To,
|
||||
Actor: fmt.Sprintf("%s://%s/u/%s", config.PROTOCOL, config.DOMAIN, username),
|
||||
Object: note,
|
||||
Context: ActivityStreams,
|
||||
Type: TypeCreate,
|
||||
ID: fmt.Sprintf("%s://%s/u/%s/posts/%s", config.PROTOCOL, config.DOMAIN, username, uuid.New().String()),
|
||||
To: note.To,
|
||||
Actor: fmt.Sprintf("%s://%s/u/%s", config.PROTOCOL, config.DOMAIN, username),
|
||||
Published: time.Now(),
|
||||
Object: note,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package activitypub
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Outbox struct {
|
||||
Context string `json:"@context"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
OrderedItems interface{} `json:"orderedItems,omitempty"`
|
||||
}
|
||||
|
||||
func UnmarshalOutbox(data []byte) (Outbox, error) {
|
||||
var o Outbox
|
||||
err := json.Unmarshal(data, &o)
|
||||
return o, err
|
||||
}
|
||||
|
||||
func (o *Outbox) Marshal() ([]byte, error) {
|
||||
return json.Marshal(o)
|
||||
}
|
|
@ -4,7 +4,7 @@ import (
|
|||
"app/activitypub"
|
||||
"config"
|
||||
. "contracts"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"server/route"
|
||||
|
@ -26,7 +26,7 @@ var Follow = route.New(HttpGet, "/u/:name/follow", func(x IContext) error {
|
|||
x.InternalServerError(err.Error())
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
x.InternalServerError(err.Error())
|
||||
}
|
||||
|
|
|
@ -40,21 +40,27 @@ var OutboxPost = route.New(HttpPost, "/u/:username/outbox", func(x IContext) err
|
|||
|
||||
activity := note.Wrap(username)
|
||||
|
||||
recipient := &activitypub.Actor{}
|
||||
if err := x.GetActivityStream(activity.To.([]string)[0], keyId, key.PrivateKey, nil, recipient); err != nil {
|
||||
return x.InternalServerError(err.Error())
|
||||
}
|
||||
to := activity.To.([]string)[0]
|
||||
|
||||
data, _ := json.Marshal(activity)
|
||||
output := &struct{}{}
|
||||
if err := x.PostActivityStream(recipient.Inbox, keyId, key.PrivateKey, data, output); err != nil {
|
||||
return x.InternalServerError(err.Error())
|
||||
if to != activitypub.Public {
|
||||
recipient := &activitypub.Actor{}
|
||||
if err := x.GetActivityStream(to, keyId, key.PrivateKey, nil, recipient); err != nil {
|
||||
return x.InternalServerError(err.Error())
|
||||
}
|
||||
|
||||
to = recipient.ID
|
||||
|
||||
data, _ := json.Marshal(activity)
|
||||
output := &struct{}{}
|
||||
if err := x.PostActivityStream(recipient.Inbox, keyId, key.PrivateKey, data, output); err != nil {
|
||||
return x.InternalServerError(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
message := &repos.OutgoingActivity{
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
From: note.AttributedTo,
|
||||
To: recipient.ID,
|
||||
To: to,
|
||||
Guid: x.GUID(),
|
||||
Content: note.Content,
|
||||
}
|
||||
|
@ -71,8 +77,9 @@ var OutboxPost = route.New(HttpPost, "/u/:username/outbox", func(x IContext) err
|
|||
})
|
||||
|
||||
var OutboxGet = route.New(HttpGet, "/u/:username/outbox", func(x IContext) error {
|
||||
user := x.Request().Params("username")
|
||||
actor := x.StringUtil().Format("%s://%s/u/%s", config.PROTOCOL, config.DOMAIN, user)
|
||||
username := x.Request().Params("username")
|
||||
actor := x.StringUtil().Format("%s://%s/u/%s", config.PROTOCOL, config.DOMAIN, username)
|
||||
id := x.StringUtil().Format("%s://%s/u/%s/outbox", config.PROTOCOL, config.DOMAIN, username)
|
||||
|
||||
messages := &[]types.MessageResponse{}
|
||||
err := repos.FindOutgoingActivitiesByUser(messages, actor).Error
|
||||
|
@ -80,5 +87,31 @@ var OutboxGet = route.New(HttpGet, "/u/:username/outbox", func(x IContext) error
|
|||
x.InternalServerError("internal server error")
|
||||
}
|
||||
|
||||
return x.JSON(messages)
|
||||
items := []*activitypub.Activity{}
|
||||
for _, message := range *messages {
|
||||
note := &activitypub.Note{
|
||||
Context: "https://www.w3.org/ns/activitystreams",
|
||||
To: []string{
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
},
|
||||
Content: message.Content,
|
||||
Type: "Note",
|
||||
AttributedTo: actor,
|
||||
}
|
||||
|
||||
activity := note.Wrap(username)
|
||||
items = append(items, activity)
|
||||
}
|
||||
|
||||
outbox := &activitypub.Outbox{
|
||||
Context: "https://www.w3.org/ns/activitystreams",
|
||||
ID: id,
|
||||
Type: "OrderedCollection",
|
||||
TotalItems: len(items),
|
||||
OrderedItems: items,
|
||||
}
|
||||
|
||||
json, _ := outbox.Marshal()
|
||||
x.Response().Header("Content-Type", "application/activity+json; charset=utf-8")
|
||||
return x.WriteString(string(json))
|
||||
})
|
||||
|
|
|
@ -11,16 +11,16 @@ import (
|
|||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var User = route.New(HttpGet, "/u/:name", func(x IContext) error {
|
||||
name := x.Request().Params("name")
|
||||
if name == "" {
|
||||
var User = route.New(HttpGet, "/u/:username", func(x IContext) error {
|
||||
username := x.Request().Params("username")
|
||||
if username == "" {
|
||||
return x.BadRequest("Bad request")
|
||||
}
|
||||
|
||||
user := &repos.User{}
|
||||
err := repos.FindUserByUsername(user, name).Error
|
||||
err := repos.FindUserByUsername(user, username).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return x.NotFound("No record found for %s.", name)
|
||||
return x.NotFound("No record found for %s.", username)
|
||||
}
|
||||
|
||||
actor := createActor(user)
|
||||
|
@ -30,20 +30,22 @@ var User = route.New(HttpGet, "/u/:name", func(x IContext) error {
|
|||
return x.WriteString(string(json))
|
||||
} else {
|
||||
return x.Render("user", ViewData{
|
||||
"Actor": actor,
|
||||
"Title": fmt.Sprintf("%s's Public Profile", user.DisplayName),
|
||||
"Username": user.Username,
|
||||
"Actor": actor,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
var _Followers = route.New(HttpPost, "/u/:name/:followers", func(x IContext) error {
|
||||
name := x.Request().Params("name")
|
||||
if name == "" {
|
||||
var _ = route.New(HttpPost, "/u/:username/:followers", func(x IContext) error {
|
||||
username := x.Request().Params("username")
|
||||
if username == "" {
|
||||
return x.BadRequest("Bad request")
|
||||
}
|
||||
|
||||
storage := x.Storage()
|
||||
domain := x.Config().Get("domain")
|
||||
result := storage.Prepare("select followers from accounts where name = ?").Param(fmt.Sprintf("%s@%s", name, domain))
|
||||
result := storage.Prepare("select followers from accounts where name = ?").Param(fmt.Sprintf("%s@%s", username, domain))
|
||||
if result.Get("followers") == nil {
|
||||
result.Set("followers", "[]")
|
||||
}
|
||||
|
@ -63,7 +65,7 @@ var _Followers = route.New(HttpPost, "/u/:name/:followers", func(x IContext) err
|
|||
},
|
||||
"@context":["https://www.w3.org/ns/activitystreams"]
|
||||
}
|
||||
`, domain, name, followers.Length())
|
||||
`, domain, username, followers.Length())
|
||||
|
||||
return x.JSON(followersCollection)
|
||||
})
|
||||
|
|
|
@ -115,12 +115,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h2>Outbox</h2>
|
||||
<form id="frmPost" action="/" method="get">
|
||||
<div class="mb-3">
|
||||
<label for="to" class="form-label">To</label>
|
||||
<input type="text" name="to" id="to" class="form-control" />
|
||||
<label for="note" class="form-label">Message</label>
|
||||
<input type="text" name="to" id="to" class="form-control" value="https://www.w3.org/ns/activitystreams#Public" style="display:none;" />
|
||||
<textarea
|
||||
style="height: 200px"
|
||||
name="note"
|
||||
|
@ -137,14 +134,14 @@
|
|||
<input type="submit" value="Post" class="btn btn-primary" />
|
||||
</form>
|
||||
<hr />
|
||||
<h2>Inbox</h2>
|
||||
<ul class="list-group" id="inbox"></ul>
|
||||
<h2>Feed</h2>
|
||||
<ul class="list-group" id="feed"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
let host = "{{ .Protocol }}://{{ .Domain }}:{{ .Port }}";
|
||||
let host = "{{ .Protocol }}://{{ .Domain }}";
|
||||
let username;
|
||||
document.getElementById("frmProfile").onsubmit = function () {
|
||||
axios({
|
||||
|
@ -170,28 +167,27 @@
|
|||
return false;
|
||||
};
|
||||
|
||||
function refreshInbox() {
|
||||
function refreshFeed() {
|
||||
axios({
|
||||
method: "get",
|
||||
url: "/u/" + username + "/inbox",
|
||||
url: "/u/" + username + "/outbox",
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
},
|
||||
}).then(function (response) {
|
||||
const inbox = document.getElementById("inbox");
|
||||
inbox.innerHTML = "";
|
||||
for (let i = 0; i < response.data.length; i++) {
|
||||
console.log(response);
|
||||
const feed = document.getElementById("feed");
|
||||
feed.innerHTML = "";
|
||||
for (let i = 0; i < response.data.orderedItems.length; i++) {
|
||||
let item = document.createElement("li");
|
||||
item.innerHTML =
|
||||
"<article><em>From: " +
|
||||
response.data[i].from +
|
||||
"</em><br><em>To: " +
|
||||
response.data[i].to +
|
||||
response.data.orderedItems[i].object.attributedTo +
|
||||
"</em><hr><p>" +
|
||||
response.data[i].content +
|
||||
response.data.orderedItems[i].object.content +
|
||||
"</p></article>";
|
||||
item.className = "list-group-item";
|
||||
inbox.appendChild(item);
|
||||
feed.appendChild(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -207,11 +203,11 @@
|
|||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
type: "Note",
|
||||
to: [document.getElementById("to").value],
|
||||
attributedTo: host + "/u/" + username + "/",
|
||||
attributedTo: host + "/u/" + username,
|
||||
content: document.getElementById("note").value,
|
||||
},
|
||||
}).then(function (response) {
|
||||
refreshInbox();
|
||||
refreshFeed();
|
||||
});
|
||||
|
||||
return false;
|
||||
|
@ -239,7 +235,7 @@
|
|||
"/u/" + username + "/follow";
|
||||
document.getElementById("frmPost").action =
|
||||
"/u/" + username + "/outbox";
|
||||
refreshInbox();
|
||||
refreshFeed();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -6,20 +6,131 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ .Title }}</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
|
||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col mb-3">
|
||||
<h1 class="display-1">{{ .Title }} Public Profile</h1>
|
||||
<h1 class="display-3">{{ .Title }}</h1>
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link active"
|
||||
id="feed-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#feed-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="feed-tab-pane"
|
||||
aria-selected="true"
|
||||
>
|
||||
Feed
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link"
|
||||
id="followers-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#followers-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="followers-tab-pane"
|
||||
aria-selected="false"
|
||||
>
|
||||
Followers
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link"
|
||||
id="following-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#following-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="following-tab-pane"
|
||||
aria-selected="false"
|
||||
>
|
||||
Following
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div
|
||||
class="tab-pane fade show active"
|
||||
id="feed-tab-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="feed-tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<br />
|
||||
<ul id="feed"></ul>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
id="followers-tab-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="followers-tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="container">
|
||||
<br />
|
||||
Followers
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
id="following-tab-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="following-tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<br />
|
||||
Following
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function refreshFeed() {
|
||||
axios({
|
||||
method: "get",
|
||||
url: "/u/{{ .Username }}/outbox",
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
},
|
||||
}).then(function (response) {
|
||||
console.log(response);
|
||||
const feed = document.getElementById("feed");
|
||||
feed.innerHTML = "";
|
||||
for (let i = 0; i < response.data.orderedItems.length; i++) {
|
||||
let item = document.createElement("li");
|
||||
item.innerHTML =
|
||||
"<article><em>From: " +
|
||||
response.data.orderedItems[i].object.attributedTo +
|
||||
"</em><hr><p>" +
|
||||
response.data.orderedItems[i].object.content +
|
||||
"</p></article>";
|
||||
item.className = "list-group-item";
|
||||
feed.appendChild(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshFeed();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Ładowanie…
Reference in New Issue