Compare commits

...

6 commits

7 changed files with 194 additions and 16 deletions

View file

@ -1,3 +1,66 @@
package activitypub
func ImportRemoteNote(noteId string) {}
import (
"encoding/json"
"fmt"
"io"
"time"
"git.mstar.dev/mstar/goutils/other"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared"
)
func ImportRemoteNote(noteId string, requester *models.User) (string, error) {
type Note struct {
Type string
Id string
Summary *string
Content string
MkContent *string `json:"_misskey_content"`
Published time.Time
To []string
Cc []string
InReplyTo *string
Sensitive bool
AttributedTo string
}
res, _, err := webshared.RequestSigned("GET", noteId, nil, requester)
if err != nil {
return "", other.Error("activitypub", "failed to request object", err)
}
if res.StatusCode != 200 {
return "", fmt.Errorf("activitypub: invalid status code: %v", res.StatusCode)
}
body, _ := io.ReadAll(res.Body)
res.Body.Close()
data := Note{}
err = json.Unmarshal(body, &data)
if err != nil {
return "", other.Error("activitypub", "json unmarshalling failed", err)
}
if data.Type != "Note" {
return "", fmt.Errorf("activitypub: invalid object type: %q", data.Type)
}
// (Re-)Import account this note belongs to
_, err = ImportRemoteAccountByAPUrl(data.AttributedTo)
if err != nil {
return "", other.Error("activitypub", "failed to import note author", err)
}
dbNote, err := dbgen.Note.Where(dbgen.Note.ID.Eq(data.Id)).First()
switch err {
case nil:
case gorm.ErrRecordNotFound:
dbNote = &models.Note{
ID: data.Id,
CreatedAt: data.Published,
CreatorId: data.AttributedTo,
}
default:
return "", other.Error("activitypub", "failed to check db for note", err)
}
return dbNote.ID, nil
}

View file

@ -419,25 +419,14 @@ func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) {
// Reason: Implementations should be switching over from cavage to the final implementation
// (rfc9421) slowly, but might not support the latter. Double-knocking will work
// around this
response, err = webshared.RequestSignedRFC9421("GET", apUrl, nil, linstromActor)
response, _, err = webshared.RequestSigned("GET", apUrl, nil, linstromActor)
if err != nil {
return nil, other.Error("activitypub", "failed to complete rfc9421 signed request", err)
return nil, other.Error("activitypub", "failed to complete signed request", err)
}
body, _ := io.ReadAll(response.Body)
response.Body.Close()
if response.StatusCode != 200 {
log.Debug().
Int("status-code", response.StatusCode).
Msg("RFC9421 signed request failed, trying cavage signature")
response, err = webshared.RequestSignedCavage("GET", apUrl, nil, linstromActor)
if err != nil {
return nil, other.Error("activitypub", "failed to complete cavage signed request", err)
}
body, _ = io.ReadAll(response.Body)
response.Body.Close()
if response.StatusCode != 200 {
return nil, fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode)
}
return nil, fmt.Errorf("activitypub: invalid status code: %v", response.StatusCode)
}
var data inboundImportUser
err = json.Unmarshal(body, &data)

63
samples/mkNote.json Normal file
View file

@ -0,0 +1,63 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Key": "sec:Key",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"Hashtag": "as:Hashtag",
"quoteUrl": "as:quoteUrl",
"fedibird": "http://fedibird.com/ns#",
"quoteUri": "fedibird:quoteUri",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"featured": "toot:featured",
"discoverable": "toot:discoverable",
"indexable": "toot:indexable",
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"misskey": "https://misskey-hub.net/ns#",
"_misskey_content": "misskey:_misskey_content",
"_misskey_quote": "misskey:_misskey_quote",
"_misskey_reaction": "misskey:_misskey_reaction",
"_misskey_votes": "misskey:_misskey_votes",
"_misskey_summary": "misskey:_misskey_summary",
"_misskey_followedMessage": "misskey:_misskey_followedMessage",
"_misskey_requireSigninToViewContents": "misskey:_misskey_requireSigninToViewContents",
"_misskey_makeNotesFollowersOnlyBefore": "misskey:_misskey_makeNotesFollowersOnlyBefore",
"_misskey_makeNotesHiddenBefore": "misskey:_misskey_makeNotesHiddenBefore",
"_misskey_license": "misskey:_misskey_license",
"freeText": {
"@id": "misskey:freeText",
"@type": "schema:text"
},
"isCat": "misskey:isCat",
"firefish": "https://joinfirefish.org/ns#",
"speakAsCat": "firefish:speakAsCat",
"sharkey": "https://joinsharkey.org/ns#",
"hideOnlineStatus": "sharkey:hideOnlineStatus",
"backgroundUrl": "sharkey:backgroundUrl",
"listenbrainz": "sharkey:listenbrainz",
"enableRss": "sharkey:enableRss",
"vcard": "http://www.w3.org/2006/vcard/ns#"
}
],
"id": "https://sharkey.team/notes/a73pi9r9eehz006z",
"type": "Note",
"attributedTo": "https://sharkey.team/users/a2xsj3dufv930001",
"content": "<p><span>We've officially released version 2025.2.3 of Sharkey!<br><br></span><b>This update contains critical security fixes. Please update as soon as possible.</b> Disclosures for the relevant vulnerabilities will be made available once instances have been patched.</p>",
"_misskey_content": "We've officially released version 2025.2.3 of Sharkey!\n\n**This update contains critical security fixes. Please update as soon as possible.** Disclosures for the relevant vulnerabilities will be made available once instances have been patched.",
"source": {
"content": "We've officially released version 2025.2.3 of Sharkey!\n\n**This update contains critical security fixes. Please update as soon as possible.** Disclosures for the relevant vulnerabilities will be made available once instances have been patched.",
"mediaType": "text/x.misskeymarkdown"
},
"published": "2025-04-27T21:09:18.693Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://sharkey.team/users/a2xsj3dufv930001/followers"],
"inReplyTo": null,
"attachment": [],
"sensitive": false,
"tag": []
}

View file

@ -35,6 +35,7 @@ func New(addr string) *Server {
handler.HandleFunc("GET /request-follow", kickoffFollow)
handler.HandleFunc("POST /send-as", proxyMessageToTarget)
handler.HandleFunc("GET /replies-to/{id}", inReplyTo)
handler.HandleFunc("POST /fetch", requestAs)
web := http.Server{
Addr: addr,
Handler: webutils.ChainMiddlewares(

View file

@ -295,3 +295,36 @@ func kickoffFollow(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(r.Body)
dec.Decode(&data)
}
func requestAs(w http.ResponseWriter, r *http.Request) {
type Inbound struct {
Username string
TargetUrl string
}
log := hlog.FromRequest(r)
data := Inbound{}
dec := json.NewDecoder(r.Body)
err := dec.Decode(&data)
if err != nil {
log.Warn().Err(err).Msg("Failed to decode json body")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
user, err := dbgen.User.GetByUsername(data.Username)
if err != nil {
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
}
res, _, err := webshared.RequestSigned("GET", data.TargetUrl, nil, user)
if err != nil {
log.Warn().Err(err).Msg("Request failed")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
if res.StatusCode != 200 {
webutils.ProblemDetailsStatusOnly(w, res.StatusCode)
return
}
body, _ := io.ReadAll(res.Body)
fmt.Fprint(w, string(body))
}

View file

@ -694,7 +694,17 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
}
switch val := activity.Object.(type) {
case string:
activitypub.ImportRemoteNote(val)
actor, err := dbgen.User.GetById(r.PathValue("id"))
if err != nil {
log.Error().Err(err).Msg("Failed to get local actor for importing targeted note")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
_, err = activitypub.ImportRemoteNote(val, actor)
if err != nil {
log.Error().Err(err).Str("note-url", val).Msg("Failed to import remote note that landed as id in the inbox")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
}
return
case map[string]any:
default:

View file

@ -29,6 +29,25 @@ Links for home:
- https://datatracker.ietf.org/doc/html/rfc9421
*/
// Perform a signed request. Tries RFC9421 first and on fail cavage.
// This double-knocking is because implementations currently use cavage (a precursor to RFC9421).
// However, Cavage is deprecated now and the RFC should be used instead. And so
// implementations have slowly begun to implement the RFC in addition to cavage
//
// Returns the unmodified response, if the request completed with RFC signing and an error, if any
func RequestSigned(
method, target string,
body []byte,
actor *models.User,
) (response *http.Response, wasRfc9421 bool, err error) {
res, err := RequestSignedRFC9421(method, target, body, actor)
if err == nil {
return res, true, nil
}
res, err = RequestSignedCavage(method, target, body, actor)
return res, false, err
}
// Perform a request, signing it as specified in RFC 9421
func RequestSignedRFC9421(
method, target string,