package activitypub import ( "context" "encoding/json" "fmt" "net/http" "time" webutils "git.mstar.dev/mstar/goutils/http" "github.com/rs/zerolog/hlog" "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/config" "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) { id := r.PathValue("id") log := hlog.FromRequest(r) note, err := NoteFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) return case nil: note.Context = activitypub.BaseLdContext data, err := json.Marshal(note) if err != nil { log.Error().Err(err).Any("activity", note).Msg("Failed to marshal create activity") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } w.Header().Add("Content-Type", "application/activity+json") _, _ = fmt.Fprint(w, string(data)) default: if storage.HandleReconnectError(err) { log.Error().Err(err).Msg("Connection failed, restart attempt started") } else { log.Error().Err(err).Msg("Failed to get create activity from db") } _ = 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 }