feat: public feed

master
Xeronith 2022-08-15 16:52:17 +04:30
rodzic 9a9e47a3f2
commit c3a8ff5726
8 zmienionych plików z 225 dodań i 55 usunięć

Wyświetl plik

@ -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"`
}

Wyświetl plik

@ -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,
}
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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())
}

Wyświetl plik

@ -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))
})

Wyświetl plik

@ -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)
})

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>