actual initial thoughts
This commit is contained in:
parent
9330ca0e2d
commit
dce1006228
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
config.json
|
49
finger.go
Normal file
49
finger.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
|
Links []Links `json:"links,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links struct {
|
||||||
|
Rel string `json:"rel"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Href string `json:"href,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Webfinger struct {
|
||||||
|
domain string
|
||||||
|
username string
|
||||||
|
accountName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebfinger(username string, domain string, aliases ...string) (*Webfinger, error) {
|
||||||
|
return &Webfinger{
|
||||||
|
username: username,
|
||||||
|
domain: domain,
|
||||||
|
accountName: fmt.Sprintf("acct:%s@%s", username, domain),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webfinger) FingerResource(name string) (Resource, error) {
|
||||||
|
if !strings.EqualFold(w.accountName, name) {
|
||||||
|
return Resource{}, fmt.Errorf("account not found")
|
||||||
|
}
|
||||||
|
return Resource{
|
||||||
|
Subject: w.accountName,
|
||||||
|
Links: []Links{
|
||||||
|
{
|
||||||
|
Rel: "self",
|
||||||
|
Type: "application/activity+json",
|
||||||
|
Href: fmt.Sprintf("https://%s/users/%s", w.domain, w.username),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -1,3 +1,5 @@
|
|||||||
module git.yetaga.in/alazyreader/friend
|
module git.yetaga.in/alazyreader/friend
|
||||||
|
|
||||||
go 1.18
|
go 1.19
|
||||||
|
|
||||||
|
require github.com/go-chi/chi/v5 v5.0.7
|
||||||
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
44
handlers.go
Normal file
44
handlers.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WebFingerHandler(wf *Webfinger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestedResource := r.URL.Query().Get("resource")
|
||||||
|
res, err := wf.FingerResource(requestedResource)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte(`{"error": "account not found"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProfileHandler(pp *ProfileProvider) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := chi.URLParam(r, "user")
|
||||||
|
res, err := pp.Get(userID)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte(`{"error": "account not found"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
}
|
29
main.go
Normal file
29
main.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
wf, err := NewWebfinger("dma", "hello.yetaga.in")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
pp, err := NewProfileProvider("hello.yetaga.in")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Get("/.well-known/webfinger", WebFingerHandler(wf))
|
||||||
|
r.Get("/users/{user}", ProfileHandler(pp))
|
||||||
|
|
||||||
|
log.Println("listening on http://0.0.0.0:8008")
|
||||||
|
err = http.ListenAndServe(":8008", r)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
68
profile.go
Normal file
68
profile.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Actor struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PreferredUsername string `json:"preferredUsername,omitempty"`
|
||||||
|
Summary string `json:"summary,omitempty"`
|
||||||
|
|
||||||
|
PublicKey PublicKey `json:"publicKey,omitempty"`
|
||||||
|
|
||||||
|
Inbox string `json:"inbox,omitempty"`
|
||||||
|
Outbox string `json:"outbo,omitempty"`
|
||||||
|
Followers string `json:"followers,omitempty"`
|
||||||
|
Following string `json:"following,omitempty"`
|
||||||
|
|
||||||
|
Icon *Link `json:"icon,omitempty"`
|
||||||
|
Image *Link `json:"image,omitempty"`
|
||||||
|
|
||||||
|
Endpoints map[string]string `json:"endpoints,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicKey struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
PublicKeyPem string `json:"publicKeyPem"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileProvider struct {
|
||||||
|
domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfileProvider(domain string) (*ProfileProvider, error) {
|
||||||
|
return &ProfileProvider{
|
||||||
|
domain: domain,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *ProfileProvider) Get(userID string) (*Actor, error) {
|
||||||
|
userRoot := fmt.Sprintf("https://%s/users/%s", pp.domain, userID)
|
||||||
|
return &Actor{
|
||||||
|
ID: userRoot,
|
||||||
|
Type: "Person",
|
||||||
|
URL: fmt.Sprintf("https://%s/@%s", pp.domain, userID),
|
||||||
|
Name: userID,
|
||||||
|
Inbox: userRoot + "/inbox",
|
||||||
|
Outbox: userRoot + "/outbox",
|
||||||
|
Followers: userRoot + "/followers",
|
||||||
|
Following: userRoot + "/following",
|
||||||
|
Endpoints: map[string]string{
|
||||||
|
"sharedInbox": fmt.Sprintf("https://%s/inbox", pp.domain),
|
||||||
|
},
|
||||||
|
PublicKey: PublicKey{
|
||||||
|
ID: userRoot + "#main-key",
|
||||||
|
Owner: userRoot,
|
||||||
|
PublicKeyPem: "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
30
signature.go
Normal file
30
signature.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Signer struct {
|
||||||
|
key crypto.Signer
|
||||||
|
keyID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign modifies the request, computing and setting the Signature header, and setting the Date header to the provided time.
|
||||||
|
func (s Signer) Sign(r *http.Request, date time.Time) error {
|
||||||
|
host := r.URL.Host
|
||||||
|
path := r.URL.Path
|
||||||
|
method := strings.ToLower(r.Method)
|
||||||
|
stringToSign := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", method, path, host, date.Format(time.RFC1123))
|
||||||
|
sig, err := s.key.Sign(rand.Reader, []byte(stringToSign), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Header.Set("Date", date.Format(time.RFC1123))
|
||||||
|
r.Header.Set("Signature", fmt.Sprintf(`keyId="%s",headers="(request-target) host date",signature="%s"`, s.keyID, sig))
|
||||||
|
return nil
|
||||||
|
}
|
38
streams.go
Normal file
38
streams.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Stream struct {
|
||||||
|
Context string `json:"@context"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TotalItems int `json:"totalItems"`
|
||||||
|
Items []struct{} `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Activity struct {
|
||||||
|
Context string `json:"@context"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Published time.Time `json:"published"`
|
||||||
|
Actor Actor `json:"actor"`
|
||||||
|
Object *Object `json:"object,omitempty"`
|
||||||
|
Target *Object `json:"target,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Object struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Streams struct{}
|
||||||
|
|
||||||
|
func (s *Streams) Inbox()
|
||||||
|
|
||||||
|
func (s *Streams) Outbox()
|
||||||
|
|
||||||
|
func (s *Streams) Followers()
|
||||||
|
|
||||||
|
func (s *Streams) Followed()
|
Loading…
Reference in New Issue
Block a user