Move translators db->ap to separate module
This commit is contained in:
parent
cfe5047433
commit
d86ad370df
16 changed files with 456 additions and 423 deletions
48
activitypub/translators/accept.go
Normal file
48
activitypub/translators/accept.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package translators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityAcceptOut struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Actor string `json:"actor"`
|
||||||
|
Object any `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AcceptFromStorage(ctx context.Context, id string) (*ActivityAcceptOut, error) {
|
||||||
|
a := dbgen.Activity
|
||||||
|
activity, err := a.Where(a.Id.Eq(id), a.Type.Eq(string(models.ActivityAccept))).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// switch activity.ObjectType {
|
||||||
|
// case models.ActivitystreamsActivityTargetFollow:
|
||||||
|
// default:
|
||||||
|
// return nil, errors.New("unknown activity target type")
|
||||||
|
// }
|
||||||
|
follow, err := FollowFromStorage(ctx, activity.ObjectId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var outId string
|
||||||
|
if strings.HasPrefix(id, "http") {
|
||||||
|
outId = id
|
||||||
|
} else {
|
||||||
|
outId = fmt.Sprintf("%s/api/activitypub/activity/accept/%s", config.GlobalConfig.General.GetFullPublicUrl(), id)
|
||||||
|
}
|
||||||
|
return &ActivityAcceptOut{
|
||||||
|
Id: outId,
|
||||||
|
Actor: follow.Object.(string),
|
||||||
|
Type: "Accept",
|
||||||
|
Object: follow,
|
||||||
|
}, nil
|
||||||
|
}
|
23
activitypub/translators/collection.go
Normal file
23
activitypub/translators/collection.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package translators
|
||||||
|
|
||||||
|
// Used for both unordered and ordered
|
||||||
|
type CollectionOut struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Items []any `json:"items,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
TotalItems int `json:"totalItems"`
|
||||||
|
First string `json:"first"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for both unordered and ordered
|
||||||
|
type CollectionPageOut struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
PartOf string `json:"partOf"`
|
||||||
|
Next string `json:"next,omitempty"`
|
||||||
|
Previous string `json:"prev,omitempty"`
|
||||||
|
Items []any `json:"items"`
|
||||||
|
}
|
58
activitypub/translators/create.go
Normal file
58
activitypub/translators/create.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package translators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityCreate struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Actor string `json:"actor"`
|
||||||
|
Object any `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a create activity from the db and format it for activitypub reads
|
||||||
|
// Does not set the context for the activity, in case the activity is embedded
|
||||||
|
// in another activity or object. That's the responsibility of the handler
|
||||||
|
// getting the final result
|
||||||
|
func CreateFromStorage(ctx context.Context, id string) (*ActivityCreate, error) {
|
||||||
|
// log := log.Ctx(ctx)
|
||||||
|
a := dbgen.Activity
|
||||||
|
activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))).
|
||||||
|
Where(a.Id.Eq(id)).
|
||||||
|
First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch models.ActivitystreamsActivityTargetType(activity.ObjectType) {
|
||||||
|
case models.ActivitystreamsActivityTargetNote:
|
||||||
|
note, err := NoteFromStorage(ctx, activity.ObjectId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := ActivityCreate{
|
||||||
|
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id,
|
||||||
|
Type: "Create",
|
||||||
|
Actor: note.AttributedTo,
|
||||||
|
Object: note,
|
||||||
|
}
|
||||||
|
return &out, nil
|
||||||
|
case models.ActivitystreamsActivityTargetBoost:
|
||||||
|
panic("Not implemented")
|
||||||
|
case models.ActivitystreamsActivityTargetReaction:
|
||||||
|
panic("Not implemented")
|
||||||
|
case models.ActivitystreamsActivityTargetActivity:
|
||||||
|
panic("Not implemented")
|
||||||
|
case models.ActivitystreamsActivityTargetUser:
|
||||||
|
panic("Not implemented")
|
||||||
|
case models.ActivitystreamsActivityTargetUnknown:
|
||||||
|
panic("Not implemented")
|
||||||
|
default:
|
||||||
|
panic("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
61
activitypub/translators/follow.go
Normal file
61
activitypub/translators/follow.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package translators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityFollowOut struct {
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Actor string `json:"actor"`
|
||||||
|
Object any `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FollowFromStorage(ctx context.Context, id string) (*ActivityFollowOut, error) {
|
||||||
|
ac := dbgen.Activity
|
||||||
|
u2u := dbgen.UserToUserRelation
|
||||||
|
u := dbgen.User
|
||||||
|
// log := log.Ctx(ctx)
|
||||||
|
activity, err := ac.Where(ac.Id.Eq(id), ac.Type.Eq(string(models.ActivityFollow))).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
followId, err := strconv.ParseUint(activity.ObjectId, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
relation, err := u2u.Where(u2u.ID.Eq(followId)).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
follower, err := u.Where(u.ID.Eq(relation.UserId)).Preload(u.RemoteInfo).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
followed, err := u.Where(u.ID.Eq(relation.TargetUserId)).Preload(u.RemoteInfo).First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := ActivityFollowOut{
|
||||||
|
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/activity/follow/" + id,
|
||||||
|
Type: "Follow",
|
||||||
|
}
|
||||||
|
if follower.RemoteInfo != nil {
|
||||||
|
out.Actor = follower.RemoteInfo.ApLink
|
||||||
|
} else {
|
||||||
|
out.Actor = activitypub.UserIdToApUrl(follower.ID)
|
||||||
|
}
|
||||||
|
if followed.RemoteInfo != nil {
|
||||||
|
out.Object = followed.RemoteInfo.ApLink
|
||||||
|
} else {
|
||||||
|
out.Object = activitypub.UserIdToApUrl(followed.ID)
|
||||||
|
}
|
||||||
|
return &out, nil
|
||||||
|
}
|
113
activitypub/translators/note.go
Normal file
113
activitypub/translators/note.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package translators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectNote struct {
|
||||||
|
// Context should be set, if needed, by the endpoint handler
|
||||||
|
Context any `json:"@context,omitempty"`
|
||||||
|
|
||||||
|
// Attributes below set from storage
|
||||||
|
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Summary *string `json:"summary"`
|
||||||
|
InReplyTo *string `json:"inReplyTo"`
|
||||||
|
Published time.Time `json:"published"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
AttributedTo string `json:"attributedTo"`
|
||||||
|
To []string `json:"to"`
|
||||||
|
CC []string `json:"cc"`
|
||||||
|
Sensitive bool `json:"sensitive"`
|
||||||
|
AtomUri string `json:"atomUri"`
|
||||||
|
InReplyToAtomUri *string `json:"inReplyToAtomUri"`
|
||||||
|
// Conversation string `json:"conversation"` // FIXME: Uncomment once understood what this field wants
|
||||||
|
Content string `json:"content"`
|
||||||
|
// ContentMap map[string]string `json:"content_map"` // TODO: Uncomment once/if support for multiple languages available
|
||||||
|
// Attachments []string `json:"attachments"` // FIXME: Change this to document type
|
||||||
|
Tags []Tag `json:"tag"`
|
||||||
|
// Replies any `json:"replies"` // FIXME: Change this to collection type embedding first page
|
||||||
|
// Likes any `json:"likes"` // FIXME: Change this to collection
|
||||||
|
// Shares any `json:"shares"` // FIXME: Change this to collection, is boosts
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoteFromStorage(ctx context.Context, id string) (*ObjectNote, error) {
|
||||||
|
note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)).
|
||||||
|
Preload(dbgen.Note.Creator).
|
||||||
|
Preload(dbgen.Note.PingRelations).
|
||||||
|
Preload(dbgen.Note.Tags).
|
||||||
|
First()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// TODO: Check access level, requires acting user to be included in function signature
|
||||||
|
publicUrlPrefix := config.GlobalConfig.General.GetFullPublicUrl()
|
||||||
|
data := &ObjectNote{
|
||||||
|
Id: publicUrlPrefix + "/api/activitypub/note/" + id,
|
||||||
|
Type: "Note",
|
||||||
|
Published: note.CreatedAt,
|
||||||
|
AttributedTo: publicUrlPrefix + "/api/activitypub/user/" + note.CreatorId,
|
||||||
|
Content: note.RawContent, // FIXME: Escape content
|
||||||
|
Url: publicUrlPrefix + "/@" + note.Creator.Username + "/" + id,
|
||||||
|
AtomUri: publicUrlPrefix + "/api/activitypub/object/" + id,
|
||||||
|
Tags: []Tag{},
|
||||||
|
}
|
||||||
|
switch note.AccessLevel {
|
||||||
|
case models.NOTE_TARGET_PUBLIC:
|
||||||
|
data.To = []string{
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public",
|
||||||
|
}
|
||||||
|
data.CC = []string{
|
||||||
|
fmt.Sprintf("%s/api/activitypub/user/%s/followers", publicUrlPrefix, note.CreatorId),
|
||||||
|
}
|
||||||
|
case models.NOTE_TARGET_HOME:
|
||||||
|
return nil, fmt.Errorf("home access level not implemented")
|
||||||
|
case models.NOTE_TARGET_FOLLOWERS:
|
||||||
|
return nil, fmt.Errorf("followers access level not implemented")
|
||||||
|
case models.NOTE_TARGET_DM:
|
||||||
|
return nil, fmt.Errorf("dm access level not implemented")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown access level %v", note.AccessLevel)
|
||||||
|
}
|
||||||
|
if note.RepliesTo.Valid {
|
||||||
|
data.InReplyTo = ¬e.RepliesTo.String
|
||||||
|
data.InReplyToAtomUri = ¬e.RepliesTo.String
|
||||||
|
}
|
||||||
|
if note.ContentWarning.Valid {
|
||||||
|
data.Summary = ¬e.ContentWarning.String
|
||||||
|
data.Sensitive = true
|
||||||
|
}
|
||||||
|
for _, ping := range note.PingRelations {
|
||||||
|
target, err := dbgen.User.GetById(ping.PingTargetId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data.Tags = append(data.Tags, Tag{
|
||||||
|
Type: "Mention",
|
||||||
|
Href: webshared.UserPublicUrl(target.ID),
|
||||||
|
Name: target.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, tag := range note.Tags {
|
||||||
|
data.Tags = append(data.Tags, Tag{
|
||||||
|
Type: "Hashtag",
|
||||||
|
Href: tag.TagUrl,
|
||||||
|
Name: tag.Tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
126
activitypub/translators/user.go
Normal file
126
activitypub/translators/user.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package translators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserKey struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Pem string `json:"publicKeyPem"`
|
||||||
|
}
|
||||||
|
type Media struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
}
|
||||||
|
type User struct {
|
||||||
|
Context []any `json:"@context"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
PreferredUsername string `json:"preferredUsername"`
|
||||||
|
Inbox string `json:"inbox"`
|
||||||
|
Outboux string `json:"outbox"`
|
||||||
|
PublicKey UserKey `json:"publicKey"`
|
||||||
|
Published time.Time `json:"published"`
|
||||||
|
DisplayName string `json:"name"`
|
||||||
|
Description *string `json:"summary,omitempty"`
|
||||||
|
PublicUrl string `json:"url"`
|
||||||
|
Icon *Media `json:"icon,omitempty"`
|
||||||
|
Banner *Media `json:"image,omitempty"`
|
||||||
|
Discoverable bool `json:"discoverable"`
|
||||||
|
Location *string `json:"vcard:Address,omitempty"`
|
||||||
|
Birthday *string `json:"vcard:bday,omitempty"`
|
||||||
|
SpeakAsCat bool `json:"speakAsCat"`
|
||||||
|
IsCat bool `json:"isCat"`
|
||||||
|
RestrictedFollow bool `json:"manuallyApprovesFollowers"`
|
||||||
|
Following string `json:"following"`
|
||||||
|
Followers string `json:"followers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserFromStorage(ctx context.Context, id string) (*User, error) {
|
||||||
|
user, err := dbgen.User.Where(dbgen.User.ID.Eq(id)).
|
||||||
|
Preload(dbgen.User.Icon).Preload(dbgen.User.Banner).
|
||||||
|
Preload(dbgen.User.BeingTypes).
|
||||||
|
First()
|
||||||
|
|
||||||
|
err = storage.EnsureLocalUserIdHasLinks(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to create links for local user")
|
||||||
|
}
|
||||||
|
apUrl := activitypub.UserIdToApUrl(user.ID)
|
||||||
|
var keyBytes string
|
||||||
|
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
||||||
|
keyBytes = shared.KeyBytesToPem(user.PublicKeyEd, true)
|
||||||
|
} else {
|
||||||
|
keyBytes = shared.KeyBytesToPem(user.PublicKeyRsa, false)
|
||||||
|
}
|
||||||
|
data := User{
|
||||||
|
Id: apUrl,
|
||||||
|
Type: "Person",
|
||||||
|
PreferredUsername: user.Username,
|
||||||
|
Inbox: apUrl + "/inbox",
|
||||||
|
Outboux: apUrl + "/outbox",
|
||||||
|
PublicKey: UserKey{
|
||||||
|
Id: apUrl + "#main-key",
|
||||||
|
Owner: apUrl,
|
||||||
|
Pem: keyBytes,
|
||||||
|
},
|
||||||
|
Published: user.CreatedAt,
|
||||||
|
DisplayName: user.DisplayName,
|
||||||
|
PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username,
|
||||||
|
Discoverable: user.Indexable,
|
||||||
|
RestrictedFollow: user.RestrictedFollow,
|
||||||
|
Following: apUrl + "/following",
|
||||||
|
Followers: apUrl + "/followers",
|
||||||
|
}
|
||||||
|
if user.Description != "" {
|
||||||
|
data.Description = &user.Description
|
||||||
|
}
|
||||||
|
if user.Icon != nil {
|
||||||
|
log.Debug().Msg("icon found")
|
||||||
|
data.Icon = &Media{
|
||||||
|
Type: "Image",
|
||||||
|
Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl(
|
||||||
|
user.Icon.Location,
|
||||||
|
),
|
||||||
|
MediaType: user.Icon.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user.Banner != nil {
|
||||||
|
log.Debug().Msg("icon banner")
|
||||||
|
data.Banner = &Media{
|
||||||
|
Type: "Image",
|
||||||
|
Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl(
|
||||||
|
user.Banner.Location,
|
||||||
|
),
|
||||||
|
MediaType: user.Banner.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
||||||
|
return t.Being == string(models.BEING_CAT)
|
||||||
|
}) {
|
||||||
|
data.IsCat = true
|
||||||
|
// data.SpeakAsCat = true // TODO: Move to check of separate field in db model
|
||||||
|
}
|
||||||
|
if user.Location.Valid {
|
||||||
|
data.Location = &user.Location.String
|
||||||
|
}
|
||||||
|
if user.Birthday.Valid {
|
||||||
|
data.Birthday = &user.Birthday.String
|
||||||
|
// data.Birthday = other.IntoPointer(user.Birthday.Time.Format("2006-Jan-02")) //YYYY-Month-DD
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ var AllTypes = []any{
|
||||||
&Activity{},
|
&Activity{},
|
||||||
&Collection{},
|
&Collection{},
|
||||||
&Emote{},
|
&Emote{},
|
||||||
|
&FailedOutboundRequest{},
|
||||||
&Feed{},
|
&Feed{},
|
||||||
&MediaMetadata{},
|
&MediaMetadata{},
|
||||||
&Note{},
|
&Note{},
|
||||||
|
|
|
@ -13,11 +13,11 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
webap "git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
|
|
||||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debug().Strs("links", links).Send()
|
log.Debug().Strs("links", links).Send()
|
||||||
act, err := webap.CreateFromStorage(r.Context(), activity.Id)
|
act, err := translators.CreateFromStorage(r.Context(), activity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to fetch and format new note")
|
log.Error().Err(err).Msg("Failed to fetch and format new note")
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
|
|
@ -19,11 +19,11 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
webap "git.mstar.dev/mstar/linstrom/web/public/api/activitypub"
|
|
||||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -377,7 +377,7 @@ func requestFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Error().Err(err).Msg("Failed to get target user with remote links")
|
log.Error().Err(err).Msg("Failed to get target user with remote links")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
activity, err := webap.FollowFromStorage(context.Background(), activity.Id)
|
activity, err := translators.FollowFromStorage(context.Background(), activity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to retrieve and format follow request")
|
log.Error().Err(err).Msg("Failed to retrieve and format follow request")
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,35 +1,23 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActivityAcceptOut struct {
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Actor string `json:"actor"`
|
|
||||||
Object any `json:"object"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityAccept(w http.ResponseWriter, r *http.Request) {
|
func activityAccept(w http.ResponseWriter, r *http.Request) {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
activity, err := CreateFromStorage(r.Context(), id)
|
activity, err := translators.CreateFromStorage(r.Context(), id)
|
||||||
switch err {
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
@ -52,32 +40,3 @@ func activityAccept(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AcceptFromStorage(ctx context.Context, id string) (*ActivityAcceptOut, error) {
|
|
||||||
a := dbgen.Activity
|
|
||||||
activity, err := a.Where(a.Id.Eq(id), a.Type.Eq(string(models.ActivityAccept))).First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// switch activity.ObjectType {
|
|
||||||
// case models.ActivitystreamsActivityTargetFollow:
|
|
||||||
// default:
|
|
||||||
// return nil, errors.New("unknown activity target type")
|
|
||||||
// }
|
|
||||||
follow, err := FollowFromStorage(ctx, activity.ObjectId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var outId string
|
|
||||||
if strings.HasPrefix(id, "http") {
|
|
||||||
outId = id
|
|
||||||
} else {
|
|
||||||
outId = fmt.Sprintf("%s/api/activitypub/activity/accept/%s", config.GlobalConfig.General.GetFullPublicUrl(), id)
|
|
||||||
}
|
|
||||||
return &ActivityAcceptOut{
|
|
||||||
Id: outId,
|
|
||||||
Actor: follow.Object.(string),
|
|
||||||
Type: "Accept",
|
|
||||||
Object: follow,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -11,24 +10,14 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActivityCreate struct {
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Actor string `json:"actor"`
|
|
||||||
Object any `json:"object"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityCreate(w http.ResponseWriter, r *http.Request) {
|
func activityCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
activity, err := CreateFromStorage(r.Context(), id)
|
activity, err := translators.CreateFromStorage(r.Context(), id)
|
||||||
switch err {
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
@ -51,44 +40,3 @@ func activityCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a create activity from the db and format it for activitypub reads
|
|
||||||
// Does not set the context for the activity, in case the activity is embedded
|
|
||||||
// in another activity or object. That's the responsibility of the handler
|
|
||||||
// getting the final result
|
|
||||||
func CreateFromStorage(ctx context.Context, id string) (*ActivityCreate, error) {
|
|
||||||
// log := log.Ctx(ctx)
|
|
||||||
a := dbgen.Activity
|
|
||||||
activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))).
|
|
||||||
Where(a.Id.Eq(id)).
|
|
||||||
First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch models.ActivitystreamsActivityTargetType(activity.ObjectType) {
|
|
||||||
case models.ActivitystreamsActivityTargetNote:
|
|
||||||
note, err := NoteFromStorage(ctx, activity.ObjectId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := ActivityCreate{
|
|
||||||
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id,
|
|
||||||
Type: "Create",
|
|
||||||
Actor: note.AttributedTo,
|
|
||||||
Object: note,
|
|
||||||
}
|
|
||||||
return &out, nil
|
|
||||||
case models.ActivitystreamsActivityTargetBoost:
|
|
||||||
panic("Not implemented")
|
|
||||||
case models.ActivitystreamsActivityTargetReaction:
|
|
||||||
panic("Not implemented")
|
|
||||||
case models.ActivitystreamsActivityTargetActivity:
|
|
||||||
panic("Not implemented")
|
|
||||||
case models.ActivitystreamsActivityTargetUser:
|
|
||||||
panic("Not implemented")
|
|
||||||
case models.ActivitystreamsActivityTargetUnknown:
|
|
||||||
panic("Not implemented")
|
|
||||||
default:
|
|
||||||
panic("Not implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,35 +1,23 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActivityFollowOut struct {
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Actor string `json:"actor"`
|
|
||||||
Object any `json:"object"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityFollow(w http.ResponseWriter, r *http.Request) {
|
func activityFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
activity, err := FollowFromStorage(r.Context(), id)
|
activity, err := translators.FollowFromStorage(r.Context(), id)
|
||||||
switch err {
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
@ -52,45 +40,3 @@ func activityFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FollowFromStorage(ctx context.Context, id string) (*ActivityFollowOut, error) {
|
|
||||||
ac := dbgen.Activity
|
|
||||||
u2u := dbgen.UserToUserRelation
|
|
||||||
u := dbgen.User
|
|
||||||
// log := log.Ctx(ctx)
|
|
||||||
activity, err := ac.Where(ac.Id.Eq(id), ac.Type.Eq(string(models.ActivityFollow))).First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
followId, err := strconv.ParseUint(activity.ObjectId, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
relation, err := u2u.Where(u2u.ID.Eq(followId)).First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
follower, err := u.Where(u.ID.Eq(relation.UserId)).Preload(u.RemoteInfo).First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
followed, err := u.Where(u.ID.Eq(relation.TargetUserId)).Preload(u.RemoteInfo).First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out := ActivityFollowOut{
|
|
||||||
Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/activity/follow/" + id,
|
|
||||||
Type: "Follow",
|
|
||||||
}
|
|
||||||
if follower.RemoteInfo != nil {
|
|
||||||
out.Actor = follower.RemoteInfo.ApLink
|
|
||||||
} else {
|
|
||||||
out.Actor = activitypub.UserIdToApUrl(follower.ID)
|
|
||||||
}
|
|
||||||
if followed.RemoteInfo != nil {
|
|
||||||
out.Object = followed.RemoteInfo.ApLink
|
|
||||||
} else {
|
|
||||||
out.Object = activitypub.UserIdToApUrl(followed.ID)
|
|
||||||
}
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,28 +2,6 @@ package activitypub
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
// Used for both unordered and ordered
|
|
||||||
type collectionOut struct {
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
Summary string `json:"summary,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Items []any `json:"items,omitempty"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
TotalItems int `json:"totalItems"`
|
|
||||||
First string `json:"first"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for both unordered and ordered
|
|
||||||
type collectionPageOut struct {
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
PartOf string `json:"partOf"`
|
|
||||||
Next string `json:"next,omitempty"`
|
|
||||||
Previous string `json:"prev,omitempty"`
|
|
||||||
Items []any `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unordered collections handler
|
// Unordered collections handler
|
||||||
func collections(w http.ResponseWriter, r *http.Request) {}
|
func collections(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
"git.mstar.dev/mstar/linstrom/shared"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
@ -682,7 +683,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
|
|
||||||
func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) bool {
|
func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) bool {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
activity := ActivityCreate{}
|
activity := translators.ActivityCreate{}
|
||||||
err := mapstructure.Decode(object, &activity)
|
err := mapstructure.Decode(object, &activity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
@ -735,7 +736,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
} else {
|
} else {
|
||||||
obj["published"] = tmpTime
|
obj["published"] = tmpTime
|
||||||
}
|
}
|
||||||
objectNote := ObjectNote{}
|
objectNote := translators.ObjectNote{}
|
||||||
err = mapstructure.Decode(obj, &objectNote)
|
err = mapstructure.Decode(obj, &objectNote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
@ -850,7 +851,7 @@ func AcceptFollow(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
webAccept, err := AcceptFromStorage(ctx, acceptActivity.Id)
|
webAccept, err := translators.AcceptFromStorage(ctx, acceptActivity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,23 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
"git.mstar.dev/mstar/linstrom/storage-new"
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
|
||||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tag struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Href string `json:"href"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ObjectNote struct {
|
|
||||||
// Context should be set, if needed, by the endpoint handler
|
|
||||||
Context any `json:"@context,omitempty"`
|
|
||||||
|
|
||||||
// Attributes below set from storage
|
|
||||||
|
|
||||||
Id string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Summary *string `json:"summary"`
|
|
||||||
InReplyTo *string `json:"inReplyTo"`
|
|
||||||
Published time.Time `json:"published"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
AttributedTo string `json:"attributedTo"`
|
|
||||||
To []string `json:"to"`
|
|
||||||
CC []string `json:"cc"`
|
|
||||||
Sensitive bool `json:"sensitive"`
|
|
||||||
AtomUri string `json:"atomUri"`
|
|
||||||
InReplyToAtomUri *string `json:"inReplyToAtomUri"`
|
|
||||||
// Conversation string `json:"conversation"` // FIXME: Uncomment once understood what this field wants
|
|
||||||
Content string `json:"content"`
|
|
||||||
// ContentMap map[string]string `json:"content_map"` // TODO: Uncomment once/if support for multiple languages available
|
|
||||||
// Attachments []string `json:"attachments"` // FIXME: Change this to document type
|
|
||||||
Tags []Tag `json:"tag"`
|
|
||||||
// Replies any `json:"replies"` // FIXME: Change this to collection type embedding first page
|
|
||||||
// Likes any `json:"likes"` // FIXME: Change this to collection
|
|
||||||
// Shares any `json:"shares"` // FIXME: Change this to collection, is boosts
|
|
||||||
}
|
|
||||||
|
|
||||||
func objectNote(w http.ResponseWriter, r *http.Request) {
|
func objectNote(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
note, err := NoteFromStorage(r.Context(), id)
|
note, err := translators.NoteFromStorage(r.Context(), id)
|
||||||
switch err {
|
switch err {
|
||||||
case gorm.ErrRecordNotFound:
|
case gorm.ErrRecordNotFound:
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
|
||||||
|
@ -80,70 +41,3 @@ func objectNote(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NoteFromStorage(ctx context.Context, id string) (*ObjectNote, error) {
|
|
||||||
note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)).
|
|
||||||
Preload(dbgen.Note.Creator).
|
|
||||||
Preload(dbgen.Note.PingRelations).
|
|
||||||
Preload(dbgen.Note.Tags).
|
|
||||||
First()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// TODO: Check access level, requires acting user to be included in function signature
|
|
||||||
publicUrlPrefix := config.GlobalConfig.General.GetFullPublicUrl()
|
|
||||||
data := &ObjectNote{
|
|
||||||
Id: publicUrlPrefix + "/api/activitypub/note/" + id,
|
|
||||||
Type: "Note",
|
|
||||||
Published: note.CreatedAt,
|
|
||||||
AttributedTo: publicUrlPrefix + "/api/activitypub/user/" + note.CreatorId,
|
|
||||||
Content: note.RawContent, // FIXME: Escape content
|
|
||||||
Url: publicUrlPrefix + "/@" + note.Creator.Username + "/" + id,
|
|
||||||
AtomUri: publicUrlPrefix + "/api/activitypub/object/" + id,
|
|
||||||
Tags: []Tag{},
|
|
||||||
}
|
|
||||||
switch note.AccessLevel {
|
|
||||||
case models.NOTE_TARGET_PUBLIC:
|
|
||||||
data.To = []string{
|
|
||||||
"https://www.w3.org/ns/activitystreams#Public",
|
|
||||||
}
|
|
||||||
data.CC = []string{
|
|
||||||
fmt.Sprintf("%s/api/activitypub/user/%s/followers", publicUrlPrefix, note.CreatorId),
|
|
||||||
}
|
|
||||||
case models.NOTE_TARGET_HOME:
|
|
||||||
return nil, fmt.Errorf("home access level not implemented")
|
|
||||||
case models.NOTE_TARGET_FOLLOWERS:
|
|
||||||
return nil, fmt.Errorf("followers access level not implemented")
|
|
||||||
case models.NOTE_TARGET_DM:
|
|
||||||
return nil, fmt.Errorf("dm access level not implemented")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown access level %v", note.AccessLevel)
|
|
||||||
}
|
|
||||||
if note.RepliesTo.Valid {
|
|
||||||
data.InReplyTo = ¬e.RepliesTo.String
|
|
||||||
data.InReplyToAtomUri = ¬e.RepliesTo.String
|
|
||||||
}
|
|
||||||
if note.ContentWarning.Valid {
|
|
||||||
data.Summary = ¬e.ContentWarning.String
|
|
||||||
data.Sensitive = true
|
|
||||||
}
|
|
||||||
for _, ping := range note.PingRelations {
|
|
||||||
target, err := dbgen.User.GetById(ping.PingTargetId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data.Tags = append(data.Tags, Tag{
|
|
||||||
Type: "Mention",
|
|
||||||
Href: webshared.UserPublicUrl(target.ID),
|
|
||||||
Name: target.Username,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for _, tag := range note.Tags {
|
|
||||||
data.Tags = append(data.Tags, Tag{
|
|
||||||
Type: "Hashtag",
|
|
||||||
Href: tag.TagUrl,
|
|
||||||
Name: tag.Tag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
"git.mstar.dev/mstar/goutils/other"
|
"git.mstar.dev/mstar/goutils/other"
|
||||||
|
@ -14,143 +13,21 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/activitypub"
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/activitypub/translators"
|
||||||
"git.mstar.dev/mstar/linstrom/shared"
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new"
|
|
||||||
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
"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 users(w http.ResponseWriter, r *http.Request) {
|
func users(w http.ResponseWriter, r *http.Request) {
|
||||||
type OutboundKey struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
Pem string `json:"publicKeyPem"`
|
|
||||||
}
|
|
||||||
type OutboundMedia struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
MediaType string `json:"mediaType"`
|
|
||||||
}
|
|
||||||
type Outbound struct {
|
|
||||||
Context []any `json:"@context"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
PreferredUsername string `json:"preferredUsername"`
|
|
||||||
Inbox string `json:"inbox"`
|
|
||||||
Outboux string `json:"outbox"`
|
|
||||||
PublicKey OutboundKey `json:"publicKey"`
|
|
||||||
Published time.Time `json:"published"`
|
|
||||||
DisplayName string `json:"name"`
|
|
||||||
Description *string `json:"summary,omitempty"`
|
|
||||||
PublicUrl string `json:"url"`
|
|
||||||
Icon *OutboundMedia `json:"icon,omitempty"`
|
|
||||||
Banner *OutboundMedia `json:"image,omitempty"`
|
|
||||||
Discoverable bool `json:"discoverable"`
|
|
||||||
Location *string `json:"vcard:Address,omitempty"`
|
|
||||||
Birthday *string `json:"vcard:bday,omitempty"`
|
|
||||||
SpeakAsCat bool `json:"speakAsCat"`
|
|
||||||
IsCat bool `json:"isCat"`
|
|
||||||
RestrictedFollow bool `json:"manuallyApprovesFollowers"`
|
|
||||||
Following string `json:"following"`
|
|
||||||
Followers string `json:"followers"`
|
|
||||||
}
|
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
userId := r.PathValue("id")
|
userId := r.PathValue("id")
|
||||||
user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).
|
|
||||||
Preload(dbgen.User.Icon).Preload(dbgen.User.Banner).
|
user, err := translators.UserFromStorage(r.Context(), userId)
|
||||||
Preload(dbgen.User.BeingTypes).
|
|
||||||
First()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = webutils.ProblemDetails(
|
log.Error().Err(err).Msg("Failed to get user from db")
|
||||||
w,
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
500,
|
|
||||||
"/errors/db-failure",
|
|
||||||
"internal database failure",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if storage.HandleReconnectError(err) {
|
|
||||||
log.Warn().Msg("Connection to db lost. Reconnect attempt started")
|
|
||||||
} else {
|
|
||||||
log.Error().Err(err).Msg("Failed to get total user count from db")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// FIXME: Remove this later
|
encoded, err := json.Marshal(user)
|
||||||
// (or rather move to dedicated module in storage for old migration stuff),
|
|
||||||
// temporary fix for old data. User creation locations are fixed already
|
|
||||||
err = storage.EnsureLocalUserIdHasLinks(userId)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("Failed to create links for local user")
|
|
||||||
}
|
|
||||||
|
|
||||||
apUrl := activitypub.UserIdToApUrl(user.ID)
|
|
||||||
var keyBytes string
|
|
||||||
if config.GlobalConfig.Experimental.UseEd25519Keys {
|
|
||||||
keyBytes = shared.KeyBytesToPem(user.PublicKeyEd, true)
|
|
||||||
} else {
|
|
||||||
keyBytes = shared.KeyBytesToPem(user.PublicKeyRsa, false)
|
|
||||||
}
|
|
||||||
data := Outbound{
|
|
||||||
Context: activitypub.BaseLdContext,
|
|
||||||
Id: apUrl,
|
|
||||||
Type: "Person",
|
|
||||||
PreferredUsername: user.Username,
|
|
||||||
Inbox: apUrl + "/inbox",
|
|
||||||
Outboux: apUrl + "/outbox",
|
|
||||||
PublicKey: OutboundKey{
|
|
||||||
Id: apUrl + "#main-key",
|
|
||||||
Owner: apUrl,
|
|
||||||
Pem: keyBytes,
|
|
||||||
},
|
|
||||||
Published: user.CreatedAt,
|
|
||||||
DisplayName: user.DisplayName,
|
|
||||||
PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username,
|
|
||||||
Discoverable: user.Indexable,
|
|
||||||
RestrictedFollow: user.RestrictedFollow,
|
|
||||||
Following: apUrl + "/following",
|
|
||||||
Followers: apUrl + "/followers",
|
|
||||||
}
|
|
||||||
if user.Description != "" {
|
|
||||||
data.Description = &user.Description
|
|
||||||
}
|
|
||||||
if user.Icon != nil {
|
|
||||||
log.Debug().Msg("icon found")
|
|
||||||
data.Icon = &OutboundMedia{
|
|
||||||
Type: "Image",
|
|
||||||
Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl(
|
|
||||||
user.Icon.Location,
|
|
||||||
),
|
|
||||||
MediaType: user.Icon.Type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if user.Banner != nil {
|
|
||||||
log.Debug().Msg("icon banner")
|
|
||||||
data.Banner = &OutboundMedia{
|
|
||||||
Type: "Image",
|
|
||||||
Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl(
|
|
||||||
user.Banner.Location,
|
|
||||||
),
|
|
||||||
MediaType: user.Banner.Type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool {
|
|
||||||
return t.Being == string(models.BEING_CAT)
|
|
||||||
}) {
|
|
||||||
data.IsCat = true
|
|
||||||
// data.SpeakAsCat = true // TODO: Move to check of separate field in db model
|
|
||||||
}
|
|
||||||
if user.Location.Valid {
|
|
||||||
data.Location = &user.Location.String
|
|
||||||
}
|
|
||||||
if user.Birthday.Valid {
|
|
||||||
data.Birthday = &user.Birthday.String
|
|
||||||
// data.Birthday = other.IntoPointer(user.Birthday.Time.Format("2006-Jan-02")) //YYYY-Month-DD
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to marshal response")
|
log.Error().Err(err).Msg("Failed to marshal response")
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
@ -183,7 +60,7 @@ func userFollowing(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pageNrStr == "" {
|
if pageNrStr == "" {
|
||||||
col := collectionOut{
|
col := translators.CollectionOut{
|
||||||
Context: "https://www.w3.org/ns/activitystreams",
|
Context: "https://www.w3.org/ns/activitystreams",
|
||||||
Type: "OrderedCollection",
|
Type: "OrderedCollection",
|
||||||
Id: apUrl + "/following",
|
Id: apUrl + "/following",
|
||||||
|
@ -222,7 +99,7 @@ func userFollowing(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page := collectionPageOut{
|
page := translators.CollectionPageOut{
|
||||||
Context: "https://www.w3.org/ns/activitystreams",
|
Context: "https://www.w3.org/ns/activitystreams",
|
||||||
Type: "OrderedCollectionPage",
|
Type: "OrderedCollectionPage",
|
||||||
Id: fmt.Sprintf("%s/following?page=%d", apUrl, pageNr),
|
Id: fmt.Sprintf("%s/following?page=%d", apUrl, pageNr),
|
||||||
|
@ -270,7 +147,7 @@ func userFollowers(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pageNrStr == "" {
|
if pageNrStr == "" {
|
||||||
col := collectionOut{
|
col := translators.CollectionOut{
|
||||||
Context: activitypub.BaseLdContext,
|
Context: activitypub.BaseLdContext,
|
||||||
Type: "OrderedCollection",
|
Type: "OrderedCollection",
|
||||||
Id: apUrl + "/followers",
|
Id: apUrl + "/followers",
|
||||||
|
@ -310,7 +187,7 @@ func userFollowers(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page := collectionPageOut{
|
page := translators.CollectionPageOut{
|
||||||
Context: activitypub.BaseLdContext,
|
Context: activitypub.BaseLdContext,
|
||||||
Type: "OrderedCollectionPage",
|
Type: "OrderedCollectionPage",
|
||||||
Id: fmt.Sprintf("%s/followers?page=%d", apUrl, pageNr),
|
Id: fmt.Sprintf("%s/followers?page=%d", apUrl, pageNr),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue