Compare commits
7 commits
dff031397e
...
03ca524c99
Author | SHA1 | Date | |
---|---|---|---|
03ca524c99 | |||
c7f875a9c5 | |||
30e2702df6 | |||
e182949a8d | |||
412a8be600 | |||
796e393985 | |||
6df7a354f0 |
24 changed files with 343 additions and 67 deletions
|
@ -421,11 +421,6 @@ func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) {
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
body, _ := io.ReadAll(response.Body)
|
body, _ := io.ReadAll(response.Body)
|
||||||
log.Debug().
|
|
||||||
Int("status", response.StatusCode).
|
|
||||||
Bytes("body", body).
|
|
||||||
// Any("headers", response.Header).
|
|
||||||
Msg("Response information")
|
|
||||||
if response.StatusCode != 200 {
|
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)
|
||||||
}
|
}
|
||||||
|
@ -452,9 +447,20 @@ func ImportRemoteAccountByAPUrl(apUrl string) (*models.User, error) {
|
||||||
Preload(dbgen.User.InfoFields).
|
Preload(dbgen.User.InfoFields).
|
||||||
Preload(dbgen.User.BeingTypes).
|
Preload(dbgen.User.BeingTypes).
|
||||||
Preload(dbgen.User.Roles).
|
Preload(dbgen.User.Roles).
|
||||||
FirstOrCreate()
|
First()
|
||||||
if err != nil {
|
switch err {
|
||||||
return nil, other.Error("activitypub", "failed to find or create user in db", err)
|
case nil:
|
||||||
|
case gorm.ErrRecordNotFound:
|
||||||
|
user = &models.User{
|
||||||
|
ID: shared.NewId(),
|
||||||
|
Username: data.PreferredUsername + "@" + targetUrl.Host,
|
||||||
|
ServerId: hostId,
|
||||||
|
}
|
||||||
|
if err = dbgen.User.Select(dbgen.User.ID, dbgen.User.Username, dbgen.User.ServerId).Create(user); err != nil {
|
||||||
|
return nil, other.Error("activitypub", "failed to create user", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, other.Error("activitypub", "failed to find user", err)
|
||||||
}
|
}
|
||||||
user.Verified = true
|
user.Verified = true
|
||||||
user.FinishedRegistration = true
|
user.FinishedRegistration = true
|
||||||
|
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/goutils/other"
|
"git.mstar.dev/mstar/goutils/other"
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
|
@ -131,12 +132,12 @@ type ConfigExperimental struct {
|
||||||
// if the other server also requires authorized fetch for the server actor
|
// if the other server also requires authorized fetch for the server actor
|
||||||
// Changing this setting is not expected to cause permanent problems
|
// Changing this setting is not expected to cause permanent problems
|
||||||
AuthFetchForServerActor bool `toml:"auth_fetch_for_server_actor"`
|
AuthFetchForServerActor bool `toml:"auth_fetch_for_server_actor"`
|
||||||
// Use cuid2 for generation of new IDs that are expected to be used via Activitypub
|
// Set the provider for ID generation.
|
||||||
// They are shorter than the main method used (uuid v4) but should still provide enough
|
// Default is "xid".
|
||||||
// uniqueness such that collisions are not to be expected.
|
// Options are "uuid", "cuid" and "xid"
|
||||||
// Changing this option will only affect new ID generations, not update existing ones
|
// Changing this option will only affect new ID generations, not update existing ones
|
||||||
// As of now, even that doesn't work due to implementation details
|
// As of now, even that doesn't work due to implementation details
|
||||||
UseCuid2Ids bool `toml:"use_cuid2_ids"`
|
IdGenerator string `toml:"id_generator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -220,6 +221,7 @@ var defaultConfig Config = Config{
|
||||||
Experimental: ConfigExperimental{
|
Experimental: ConfigExperimental{
|
||||||
UseEd25519Keys: false,
|
UseEd25519Keys: false,
|
||||||
AuthFetchForServerActor: false,
|
AuthFetchForServerActor: false,
|
||||||
|
IdGenerator: "xid",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,6 +324,7 @@ func ReadAndWriteToGlobal(fileName string) error {
|
||||||
log.Error().Err(err).Bytes("config-data-raw", data).Msg("Failed to unmarshal config file")
|
log.Error().Err(err).Bytes("config-data-raw", data).Msg("Failed to unmarshal config file")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
config.Experimental.IdGenerator = strings.ToLower(config.Experimental.IdGenerator)
|
||||||
GlobalConfig = config
|
GlobalConfig = config
|
||||||
log.Info().Str("config-file", fileName).Msg("Read and applied config file")
|
log.Info().Str("config-file", fileName).Msg("Read and applied config file")
|
||||||
return nil
|
return nil
|
||||||
|
|
1
main.go
1
main.go
|
@ -65,6 +65,7 @@ func main() {
|
||||||
Str("config-file", *shared.FlagConfigFile).
|
Str("config-file", *shared.FlagConfigFile).
|
||||||
Msg("Failed to read config and couldn't write default")
|
Msg("Failed to read config and couldn't write default")
|
||||||
}
|
}
|
||||||
|
log.Trace().Any("config", config.GlobalConfig).Msg("Full config")
|
||||||
|
|
||||||
// Request to only check config
|
// Request to only check config
|
||||||
if *shared.FlagConfigOnly {
|
if *shared.FlagConfigOnly {
|
||||||
|
|
12
samples/mastoUndoLike.json
Normal file
12
samples/mastoUndoLike.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://activitypub.academy/users/eufrilia_vulbos#likes/1103/undo",
|
||||||
|
"type": "Undo",
|
||||||
|
"actor": "https://activitypub.academy/users/eufrilia_vulbos",
|
||||||
|
"object": {
|
||||||
|
"id": "https://activitypub.academy/users/eufrilia_vulbos#likes/1103",
|
||||||
|
"type": "Like",
|
||||||
|
"actor": "https://activitypub.academy/users/eufrilia_vulbos",
|
||||||
|
"object": "https://e1cda93c47d7aa.lhr.life/api/activitypub/note/d0d1uqitest2bchhadi0"
|
||||||
|
}
|
||||||
|
}
|
69
samples/mkLike.json
Normal file
69
samples/mkLike.json
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"@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#"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Like",
|
||||||
|
"id": "https://mk.absturztau.be/likes/a7g8thvezsng01os",
|
||||||
|
"actor": "https://mk.absturztau.be/users/a1xy7910yc8401z8",
|
||||||
|
"object": "https://fa1c08293da1646a0971c124f143d7f8.serveo.net/api/activitypub/note/d0d1uqitest2bchhadi0",
|
||||||
|
"content": ":celeste_hearts_progress_pride:",
|
||||||
|
"_misskey_reaction": ":celeste_hearts_progress_pride:",
|
||||||
|
"tag": [
|
||||||
|
{
|
||||||
|
"id": "https://mk.absturztau.be/emojis/celeste_hearts_progress_pride",
|
||||||
|
"type": "Emoji",
|
||||||
|
"name": ":celeste_hearts_progress_pride:",
|
||||||
|
"updated": "2024-12-03T12:12:41.676Z",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/gif",
|
||||||
|
"url": "https://misskey-taube.s3.eu-central-1.wasabisys.com/files/6823d4ba-110f-4186-abb4-d14cb7aedd76.gif"
|
||||||
|
},
|
||||||
|
"_misskey_license": {
|
||||||
|
"freeText": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -3,15 +3,21 @@ package shared
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/nrednav/cuid2"
|
"github.com/nrednav/cuid2"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate a new string ID
|
// Generate a new string ID
|
||||||
func NewId() string {
|
func NewId() string {
|
||||||
if config.GlobalConfig.Experimental.UseCuid2Ids {
|
switch config.GlobalConfig.Experimental.IdGenerator {
|
||||||
return cuid2.Generate()
|
case "uuid":
|
||||||
} else {
|
|
||||||
return uuid.NewString()
|
return uuid.NewString()
|
||||||
|
case "cuid":
|
||||||
|
return cuid2.Generate()
|
||||||
|
case "xid":
|
||||||
|
return xid.New().String()
|
||||||
|
default:
|
||||||
|
return xid.New().String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package models
|
||||||
|
|
||||||
type Activity struct {
|
type Activity struct {
|
||||||
Id string `gorm:"primarykey"`
|
Id string `gorm:"primarykey"`
|
||||||
Type string `gorm:"type:activitystreams_activity_type"`
|
Type string // `gorm:"type:activitystreams_activity_type"`
|
||||||
ObjectId string
|
ObjectId string
|
||||||
ObjectType uint32 // Target type: ActivitystreamsActivityTargetType
|
ObjectType uint32 // Target type: ActivitystreamsActivityTargetType
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
// Instead, they are either stored on the remote server they originated from
|
// Instead, they are either stored on the remote server they originated from
|
||||||
// or an s3 bucket if uploaded to Linstrom.
|
// or an s3 bucket if uploaded to Linstrom.
|
||||||
type MediaMetadata struct {
|
type MediaMetadata struct {
|
||||||
ID string `gorm:"primarykey;default:gen_random_uuid()"` // The unique ID of this media file
|
ID string `gorm:"primarykey"` // The unique ID of this media file
|
||||||
CreatedAt time.Time // When this entry was created
|
CreatedAt time.Time // When this entry was created
|
||||||
UpdatedAt time.Time // When this entry was last updated
|
UpdatedAt time.Time // When this entry was last updated
|
||||||
// When this entry was deleted (for soft deletions)
|
// When this entry was deleted (for soft deletions)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
// A note describes some user generated text content.
|
// A note describes some user generated text content.
|
||||||
type Note struct {
|
type Note struct {
|
||||||
ID string `gorm:"primarykey;default:gen_random_uuid()"` // Make ID a string (uuid) for other implementations
|
ID string `gorm:"primarykey"` // Make ID a string for other implementations
|
||||||
CreatedAt time.Time // When this entry was created
|
CreatedAt time.Time // When this entry was created
|
||||||
UpdatedAt time.Time // When this entry was last updated
|
UpdatedAt time.Time // When this entry was last updated
|
||||||
// When this entry was deleted (for soft deletions)
|
// When this entry was deleted (for soft deletions)
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
// A Reaction is a user liking a note using an emote
|
// A Reaction is a user liking a note using an emote
|
||||||
type Reaction struct {
|
type Reaction struct {
|
||||||
|
@ -9,6 +13,6 @@ type Reaction struct {
|
||||||
NoteId string
|
NoteId string
|
||||||
Reactor User
|
Reactor User
|
||||||
ReactorId string
|
ReactorId string
|
||||||
Emote Emote
|
Emote *Emote // Emote is optional. If not set, use the default emote of the server
|
||||||
EmoteId uint
|
EmoteId sql.NullInt64
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ type AccessToken struct {
|
||||||
User User // The account the token belongs to
|
User User // The account the token belongs to
|
||||||
UserId string
|
UserId string
|
||||||
// The token itself is a uuid value
|
// The token itself is a uuid value
|
||||||
Token string `gorm:"primarykey;type:uuid;default:gen_random_uuid()"`
|
Token string `gorm:"primarykey"`
|
||||||
Name string // Token name will be empty if autogenerated with sucessful login
|
Name string // Token name will be empty if autogenerated with sucessful login
|
||||||
// Every token expires, even if set to "not expire". If set to "not expire", it just expires
|
// Every token expires, even if set to "not expire". If set to "not expire", it just expires
|
||||||
// at a point in the future this server should never reach
|
// at a point in the future this server should never reach
|
||||||
|
|
|
@ -10,7 +10,7 @@ type LoginProcessToken struct {
|
||||||
ID uint64 `gorm:"primarykey"`
|
ID uint64 `gorm:"primarykey"`
|
||||||
User User
|
User User
|
||||||
UserId string `gorm:"unique"`
|
UserId string `gorm:"unique"`
|
||||||
Token string `gorm:"type:uuid;default:gen_random_uuid()"`
|
Token string
|
||||||
Name string
|
Name string
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ type User struct {
|
||||||
// Same also applies for other types that use a UUID as primary key
|
// Same also applies for other types that use a UUID as primary key
|
||||||
// TODO: Change this to generate via cuid or uuid, depending on config
|
// TODO: Change this to generate via cuid or uuid, depending on config
|
||||||
// Or remove autogeneration alltogether
|
// Or remove autogeneration alltogether
|
||||||
ID string `gorm:"primarykey;default:gen_random_uuid()"`
|
ID string `gorm:"primarykey"`
|
||||||
// Username of the user (eg "max" if the full username is @max@example.com)
|
// Username of the user (eg "max" if the full username is @max@example.com)
|
||||||
// Assume unchangable (once set by a user) to be kind to other implementations
|
// Assume unchangable (once set by a user) to be kind to other implementations
|
||||||
// Would be an easy avenue to fuck with them though
|
// Would be an easy avenue to fuck with them though
|
||||||
|
|
|
@ -129,6 +129,7 @@ func insertUser(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
user := models.User{
|
user := models.User{
|
||||||
|
ID: shared.NewId(),
|
||||||
Username: shared.ServerActorName,
|
Username: shared.ServerActorName,
|
||||||
Server: *server,
|
Server: *server,
|
||||||
ServerId: server.ID,
|
ServerId: server.ID,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[general]
|
[general]
|
||||||
protocol = "https"
|
protocol = "https"
|
||||||
domain = "lhr.life"
|
domain = "serveo.net"
|
||||||
subdomain = "e8adf969423370"
|
subdomain = "b2f4e7c5596220d4c4957b24f6954220"
|
||||||
private_port = 8080
|
private_port = 8080
|
||||||
public_port = 443
|
public_port = 443
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
n := dbgen.Note
|
n := dbgen.Note
|
||||||
note := models.Note{
|
note := models.Note{
|
||||||
|
ID: shared.NewId(),
|
||||||
Creator: *user,
|
Creator: *user,
|
||||||
CreatorId: user.ID,
|
CreatorId: user.ID,
|
||||||
RawContent: data.Content,
|
RawContent: data.Content,
|
||||||
|
@ -66,6 +67,7 @@ func postAs(w http.ResponseWriter, r *http.Request) {
|
||||||
OriginId: 1,
|
OriginId: 1,
|
||||||
}
|
}
|
||||||
err = n.Select(
|
err = n.Select(
|
||||||
|
n.ID,
|
||||||
n.CreatorId,
|
n.CreatorId,
|
||||||
n.RawContent,
|
n.RawContent,
|
||||||
n.Remote,
|
n.Remote,
|
||||||
|
|
|
@ -129,6 +129,7 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
u := dbgen.User
|
u := dbgen.User
|
||||||
query := u.Select(
|
query := u.Select(
|
||||||
|
u.ID,
|
||||||
u.Username,
|
u.Username,
|
||||||
u.DisplayName,
|
u.DisplayName,
|
||||||
u.Description,
|
u.Description,
|
||||||
|
@ -139,6 +140,8 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
||||||
u.PrivateKeyRsa,
|
u.PrivateKeyRsa,
|
||||||
u.PublicKeyRsa,
|
u.PublicKeyRsa,
|
||||||
u.PasskeyId,
|
u.PasskeyId,
|
||||||
|
u.Verified,
|
||||||
|
u.FinishedRegistration,
|
||||||
)
|
)
|
||||||
if data.Birthday != nil {
|
if data.Birthday != nil {
|
||||||
query = query.Select(u.Birthday)
|
query = query.Select(u.Birthday)
|
||||||
|
@ -147,16 +150,19 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
|
||||||
query = query.Select(u.Location)
|
query = query.Select(u.Location)
|
||||||
}
|
}
|
||||||
user := models.User{
|
user := models.User{
|
||||||
Username: data.Username,
|
ID: shared.NewId(),
|
||||||
DisplayName: data.Displayname,
|
Username: data.Username,
|
||||||
Description: data.Description,
|
DisplayName: data.Displayname,
|
||||||
IsBot: data.IsBot,
|
Description: data.Description,
|
||||||
ServerId: 1, // Hardcoded, Self is always first ID
|
IsBot: data.IsBot,
|
||||||
PublicKeyRsa: publicKeyRsaBytes,
|
ServerId: 1, // Hardcoded, Self is always first ID
|
||||||
PublicKeyEd: publicKeyEdBytes,
|
PublicKeyRsa: publicKeyRsaBytes,
|
||||||
PrivateKeyRsa: privateKeyRsaBytes,
|
PublicKeyEd: publicKeyEdBytes,
|
||||||
PrivateKeyEd: privateKeyEdBytes,
|
PrivateKeyRsa: privateKeyRsaBytes,
|
||||||
PasskeyId: pkeyId,
|
PrivateKeyEd: privateKeyEdBytes,
|
||||||
|
PasskeyId: pkeyId,
|
||||||
|
Verified: true,
|
||||||
|
FinishedRegistration: true,
|
||||||
}
|
}
|
||||||
if data.Birthday != nil {
|
if data.Birthday != nil {
|
||||||
user.Birthday = sql.NullString{Valid: true, String: data.Birthday.Format("2006-Jan-02")}
|
user.Birthday = sql.NullString{Valid: true, String: data.Birthday.Format("2006-Jan-02")}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package activitypub
|
package activitypub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,8 +14,5 @@ func BuildActivitypubRouter() http.Handler {
|
||||||
router.HandleFunc("/activity/reject/{id}", activityReject)
|
router.HandleFunc("/activity/reject/{id}", activityReject)
|
||||||
router.HandleFunc("/activity/update/{id}", activityUpdate)
|
router.HandleFunc("/activity/update/{id}", activityUpdate)
|
||||||
router.HandleFunc("/note/{id}", objectNote)
|
router.HandleFunc("/note/{id}", objectNote)
|
||||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, _ = fmt.Fprint(w, "in ap")
|
|
||||||
})
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
193
web/public/api/activitypub/inbox.go
Normal file
193
web/public/api/activitypub/inbox.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package activitypub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
|
"git.mstar.dev/mstar/goutils/other"
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"git.mstar.dev/mstar/linstrom/activitypub"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/dbgen"
|
||||||
|
"git.mstar.dev/mstar/linstrom/storage-new/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var objectIdRegex = regexp.MustCompile(
|
||||||
|
`https?://.+/api/activitypub/[a-z]+(?:/[a-z]+)?/([a-zA-Z0-9\-]+)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
func userInbox(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := hlog.FromRequest(r)
|
||||||
|
userId := r.PathValue("id")
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
log.Info().
|
||||||
|
Err(err).
|
||||||
|
Str("userId", userId).
|
||||||
|
Bytes("body", body).
|
||||||
|
Any("headers", r.Header).
|
||||||
|
Msg("Inbox message")
|
||||||
|
data := map[string]any{}
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer("Body to inbox needs to be json"),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := data["@context"]; !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer("Request data needs to contain context"),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
objectType, ok := data["type"].(string)
|
||||||
|
if !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Request data needs to contain a field "type" with a string value`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, ok = data["id"].(string)
|
||||||
|
if !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Request data needs to contain a field "id" with a string value`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch objectType {
|
||||||
|
case "Like":
|
||||||
|
handleLike(w, r, data)
|
||||||
|
default:
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) {
|
||||||
|
log := hlog.FromRequest(r)
|
||||||
|
activityId := object["id"].(string)
|
||||||
|
likerUrl, ok := object["actor"].(string)
|
||||||
|
if !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Request data needs to contain a field "actor" with a string value`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Account for case where object is embedded in like
|
||||||
|
targetUrl, ok := object["object"].(string)
|
||||||
|
if !ok {
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer(`Request data needs to contain a field "object" with a string value`),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetIdMatches := objectIdRegex.FindStringSubmatch(targetUrl)
|
||||||
|
if len(targetIdMatches) != 2 {
|
||||||
|
log.Error().
|
||||||
|
Strs("match-results", targetIdMatches).
|
||||||
|
Str("url", targetUrl).
|
||||||
|
Msg("Url didn't match id extractor regex")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetId := targetIdMatches[1]
|
||||||
|
// Assume likes can only happen on notes for now
|
||||||
|
// Thus check if a note with that Id exists at all
|
||||||
|
note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(targetId)).First()
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
case gorm.ErrRecordNotFound:
|
||||||
|
webutils.ProblemDetails(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"/errors/bad-request-data",
|
||||||
|
"Bad activity data",
|
||||||
|
other.IntoPointer("There is no note with the target id"),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
log.Error().Err(err).Str("note-id", targetId).Msg("Failed to get note from db")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Import liker after verifying that target note is correct
|
||||||
|
liker, err := activitypub.ImportRemoteAccountByAPUrl(likerUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Str("liker-url", likerUrl).
|
||||||
|
Msg("Failed to import liking remote account")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reaction := models.Reaction{
|
||||||
|
Note: *note,
|
||||||
|
NoteId: note.ID,
|
||||||
|
Reactor: *liker,
|
||||||
|
ReactorId: liker.ID,
|
||||||
|
Emote: nil,
|
||||||
|
EmoteId: sql.NullInt64{Valid: false},
|
||||||
|
}
|
||||||
|
tx := dbgen.Q.Begin()
|
||||||
|
|
||||||
|
err = tx.Reaction.Create(&reaction)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
log.Error().Err(err).Any("raw-reaction", reaction).Msg("Failed to store reaction in db")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
// TODO: Create corresponding activity too
|
||||||
|
activity := models.Activity{
|
||||||
|
Id: activityId,
|
||||||
|
Type: string(models.ActivityLike),
|
||||||
|
ObjectId: fmt.Sprint(reaction.ID),
|
||||||
|
ObjectType: uint32(models.ActivitystreamsActivityTargetReaction),
|
||||||
|
}
|
||||||
|
err = tx.Activity.Create(&activity)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
log.Error().Err(err).Any("raw-reaction", reaction).Msg("Failed to store reaction in db")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to commit reaction transaction to db")
|
||||||
|
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package activitypub
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -140,18 +139,6 @@ func users(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, string(encoded))
|
fmt.Fprint(w, string(encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
func userInbox(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log := hlog.FromRequest(r)
|
|
||||||
userId := r.PathValue("id")
|
|
||||||
data, err := io.ReadAll(r.Body)
|
|
||||||
log.Info().
|
|
||||||
Err(err).
|
|
||||||
Str("userId", userId).
|
|
||||||
Bytes("body", data).
|
|
||||||
Any("headers", r.Header).
|
|
||||||
Msg("Inbox message")
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
|
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
|
||||||
Fuck you.
|
Fuck you.
|
|
@ -70,15 +70,12 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
||||||
}
|
}
|
||||||
// Not an always open path, check methods
|
// Not an always open path, check methods
|
||||||
if r.Method == "GET" && !forGet {
|
if r.Method == "GET" && !forGet {
|
||||||
log.Debug().Msg("Get request to AP resources don't need signature")
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
} else if !forGet && !forNonGet {
|
} else if !forGet && !forNonGet {
|
||||||
log.Info().Msg("Requests to AP resources don't need signature")
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debug().Msg("Need signature for AP request")
|
|
||||||
rawDate := r.Header.Get("Date")
|
rawDate := r.Header.Get("Date")
|
||||||
date, err := http.ParseTime(rawDate)
|
date, err := http.ParseTime(rawDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -105,7 +102,8 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
||||||
}
|
}
|
||||||
signatureHeader := r.Header.Get("Signature")
|
signatureHeader := r.Header.Get("Signature")
|
||||||
if signatureHeader == "" {
|
if signatureHeader == "" {
|
||||||
log.Info().Msg("Received AP request without signature header where one is required")
|
log.Debug().
|
||||||
|
Msg("Received AP request without signature header where one is required")
|
||||||
webutils.ProblemDetails(
|
webutils.ProblemDetails(
|
||||||
w,
|
w,
|
||||||
http.StatusUnauthorized,
|
http.StatusUnauthorized,
|
||||||
|
@ -121,7 +119,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
||||||
Msg("Signature header of inbound AP request")
|
Msg("Signature header of inbound AP request")
|
||||||
match := signatureRegex.FindStringSubmatch(signatureHeader)
|
match := signatureRegex.FindStringSubmatch(signatureHeader)
|
||||||
if len(match) <= 1 {
|
if len(match) <= 1 {
|
||||||
log.Info().
|
log.Debug().
|
||||||
Str("header", signatureHeader).
|
Str("header", signatureHeader).
|
||||||
Msg("Received signature with invalid pattern")
|
Msg("Received signature with invalid pattern")
|
||||||
webutils.ProblemDetails(
|
webutils.ProblemDetails(
|
||||||
|
@ -167,7 +165,7 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
||||||
}
|
}
|
||||||
_, err = url.Parse(rawKeyId)
|
_, err = url.Parse(rawKeyId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Key id is not an url")
|
log.Debug().Err(err).Msg("Key id is not an url")
|
||||||
webutils.ProblemDetails(
|
webutils.ProblemDetails(
|
||||||
w,
|
w,
|
||||||
http.StatusUnauthorized,
|
http.StatusUnauthorized,
|
||||||
|
@ -181,7 +179,6 @@ func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuil
|
||||||
|
|
||||||
_ = rawAlgorithm
|
_ = rawAlgorithm
|
||||||
stringToCheck := buildStringToCheck(r, rawHeaders)
|
stringToCheck := buildStringToCheck(r, rawHeaders)
|
||||||
log.Warn().Str("string-to-check", stringToCheck).Send()
|
|
||||||
|
|
||||||
requestingActor, err := getRequestingActor(rawKeyId)
|
requestingActor, err := getRequestingActor(rawKeyId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -28,7 +28,6 @@ package webpublic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -44,10 +43,6 @@ type Server struct {
|
||||||
|
|
||||||
func New(addr string, duckImg *string, duckFs fs.FS) *Server {
|
func New(addr string, duckImg *string, duckFs fs.FS) *Server {
|
||||||
handler := http.NewServeMux()
|
handler := http.NewServeMux()
|
||||||
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, "not implemented")
|
|
||||||
})
|
|
||||||
handler.Handle("/api/", http.StripPrefix("/api", api.BuildApiRouter()))
|
handler.Handle("/api/", http.StripPrefix("/api", api.BuildApiRouter()))
|
||||||
handler.HandleFunc("GET /.well-known/webfinger", api.WellKnownWebfinger)
|
handler.HandleFunc("GET /.well-known/webfinger", api.WellKnownWebfinger)
|
||||||
handler.HandleFunc("GET /.well-known/nodeinfo", api.NodeInfoOverview)
|
handler.HandleFunc("GET /.well-known/nodeinfo", api.NodeInfoOverview)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yaronf/httpsign"
|
"github.com/yaronf/httpsign"
|
||||||
|
|
||||||
"git.mstar.dev/mstar/linstrom/config"
|
"git.mstar.dev/mstar/linstrom/config"
|
||||||
|
@ -108,7 +107,6 @@ func RequestSignedCavage(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Debug().Bytes("body", body).Any("headers", req.Header).Msg("Sending signed request")
|
|
||||||
return RequestClient.Do(req)
|
return RequestClient.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue