diff --git a/cmd/borg/borg.go b/cmd/borg/borg.go index 05e491e8..ed62cc7e 100644 --- a/cmd/borg/borg.go +++ b/cmd/borg/borg.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "github.com/kibirisu/borg/internal/config" "github.com/kibirisu/borg/internal/server" @@ -10,6 +11,12 @@ import ( func main() { ctx := context.Background() conf := config.GetConfig() + fmt.Println("--- Config Test ---") + fmt.Printf("Env: %s\n", conf.AppEnv) + fmt.Printf("Host: %s\n", conf.ListenHost) + fmt.Printf("Port: %s\n", conf.ListenPort) + fmt.Printf("DB: %s\n", conf.DatabaseURL) + fmt.Println("-------------------") s := server.New(ctx, conf) panic(s.ListenAndServe()) } diff --git a/internal/server/api.go b/internal/server/api.go index 8636c4aa..2696388e 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -2,13 +2,16 @@ package server import ( "database/sql" + "encoding/json/v2" "errors" "log" "net/http" + "time" "github.com/kibirisu/borg/internal/ap" "github.com/kibirisu/borg/internal/api" "github.com/kibirisu/borg/internal/db" + "github.com/kibirisu/borg/internal/domain" "github.com/kibirisu/borg/internal/server/mapper" "github.com/kibirisu/borg/internal/util" ) @@ -57,8 +60,10 @@ func (s *Server) GetApiAccountsLookup( r *http.Request, params api.GetApiAccountsLookupParams, ) { + // we must check if account is local or from other instance + // if from other instance we do webfinger lookup acct := params.Acct - handle, err := util.ParseHandle(acct, s.conf.ListenHost) + handle, err := util.ParseHandle(acct, s.conf.ListenHost, s.conf.ListenPort) if err != nil { util.WriteError(w, http.StatusBadRequest, err.Error()) return @@ -74,52 +79,95 @@ func (s *Server) GetApiAccountsLookup( } log.Printf("lookup: found local account %s", account.Username) util.WriteJSON(w, http.StatusOK, mapper.AccountToAPI(account)) - return - } + } else { + log.Printf("lookup: remote handle %s detected, checking local cache", acct) + account, err := s.service.App.GetAccount(r.Context(), db.GetAccountParams{ + Username: handle.Username, + Domain: sql.NullString{ + String: handle.Domain, + Valid: true, + }, + }) + if err == nil { + log.Printf("lookup: remote account %s@%s found locally", handle.Username, handle.Domain) + util.WriteJSON(w, http.StatusOK, mapper.AccountToAPI(account)) + return + } + if !errors.Is(err, sql.ErrNoRows) { + log.Println(err) + util.WriteError(w, http.StatusInternalServerError, err.Error()) + return + } - if handle.Domain == "" { - w.WriteHeader(http.StatusBadRequest) - return - } + client := http.Client{Timeout: 5 * time.Second} + actorURL := "http://" + handle.Domain + "/user/" + handle.Username + + reqActor, err := http.NewRequestWithContext(r.Context(), http.MethodGet, actorURL, nil) + if err != nil { + log.Println(err) + util.WriteError(w, http.StatusBadRequest, err.Error()) + return + } + + log.Printf("lookup: fetching remote actor %s", actorURL) + actorResp, err := client.Do(reqActor) + if err != nil { + log.Println(err) + util.WriteError(w, http.StatusBadGateway, err.Error()) + return + } + defer actorResp.Body.Close() + if actorResp.StatusCode != http.StatusOK { + util.WriteError(w, http.StatusBadGateway, "remote actor fetch failed") + return + } + + var object domain.ObjectOrLink + if err := json.UnmarshalRead(actorResp.Body, &object); err != nil { + log.Println(err) + util.WriteError(w, http.StatusBadGateway, err.Error()) + return + } + actor := ap.NewActor(&object) + actorData := actor.GetObject() + + params := db.CreateActorParams{ + Username: actorData.PreferredUsername, + Uri: actorData.ID, + Domain: sql.NullString{ + String: handle.Domain, + Valid: true, + }, + DisplayName: sql.NullString{}, + InboxUri: actorData.Inbox, + OutboxUri: actorData.Outbox, + FollowersUri: actorData.Followers, + FollowingUri: actorData.Following, + Url: actorData.ID, + } + + log.Printf("lookup: creating remote account %s@%s from actor %s", actorData.PreferredUsername, handle.Domain, actorData.ID) + account, err = s.service.App.AddRemoteAccount(r.Context(), ¶ms) + if err != nil { + log.Println(err) + // If already exists, fetch the stored record. + existing, getErr := s.service.App.GetAccount(r.Context(), db.GetAccountParams{ + Username: actorData.PreferredUsername, + Domain: sql.NullString{ + String: handle.Domain, + Valid: true, + }, + }) + if getErr != nil { + util.WriteError(w, http.StatusInternalServerError, err.Error()) + return + } + util.WriteJSON(w, http.StatusOK, mapper.AccountToAPI(existing)) + return + } - log.Printf("lookup: remote handle %s detected, checking local cache", acct) - account, err := s.service.App.GetAccount( - r.Context(), - db.GetAccountParams{ - Username: handle.Username, - Domain: sql.NullString{String: handle.Domain, Valid: true}, - }, - ) - if err == nil { - log.Printf("lookup: remote account %s@%s found locally", handle.Username, handle.Domain) util.WriteJSON(w, http.StatusOK, mapper.AccountToAPI(account)) - return } - if !errors.Is(err, sql.ErrNoRows) { - log.Println(err) - util.WriteError(w, http.StatusInternalServerError, err.Error()) - return - } - - log.Printf( - "lookup: remote account %s@%s not cached, performing WebFinger lookup", - handle.Username, - handle.Domain, - ) - util.WriteError(w, http.StatusInternalServerError, "unimplemented") - // actor, err := s.service.Federation.processor.LookupActor(r.Context(), handle) - // if err != nil { - // log.Println(err) - // util.WriteError(w, http.StatusBadGateway, err.Error()) - // return - // } - // if err != nil { - // log.Println(err) - // util.WriteError(w, http.StatusInternalServerError, err.Error()) - // return - // } - // log.Printf("lookup: remote actor stored with username=%s domain=%s", row.Username, row.Domain.String) - // util.WriteJSON(w, http.StatusOK, mapper.AccountToAPI(row)) } // PostApiAccountsIdFollow implements api.ServerInterface. diff --git a/internal/service/app.go b/internal/service/app.go index d5c3a393..bd2685b1 100644 --- a/internal/service/app.go +++ b/internal/service/app.go @@ -63,7 +63,7 @@ var _ AppService = (*appService)(nil) // Register implements AppService. func (s *appService) Register(ctx context.Context, form api.AuthForm) error { - uri := fmt.Sprintf("http://%s:%s/user/%s", s.conf.ListenHost, s.conf.ListenPort, form.Username) + uri := fmt.Sprintf("http://%s:%s/user/%s", s.conf.ListenHost, s.conf.ListenPort, form.Username) log.Printf("register: creating actor username=%s uri=%s", form.Username, uri) actor, err := s.store.Accounts().Create(ctx, db.CreateActorParams{ Username: form.Username, @@ -72,9 +72,24 @@ func (s *appService) Register(ctx context.Context, form api.AuthForm) error { Domain: sql.NullString{}, InboxUri: uri + "/inbox", OutboxUri: uri + "/outbox", - Url: fmt.Sprintf("http://%s:%s/profiles/%s", s.conf.ListenHost, s.conf.ListenPort, form.Username), - FollowersUri: fmt.Sprintf("http://%s:%s/user/%s/followers", s.conf.ListenHost, s.conf.ListenPort, form.Username), - FollowingUri: fmt.Sprintf("http://%s:%s/user/%s/following", s.conf.ListenHost, s.conf.ListenPort, form.Username), + Url: fmt.Sprintf( + "http://%s:%s/profiles/%s", + s.conf.ListenHost, + s.conf.ListenPort, + form.Username, + ), + FollowersUri: fmt.Sprintf( + "http://%s:%s/user/%s/followers", + s.conf.ListenHost, + s.conf.ListenPort, + form.Username, + ), + FollowingUri: fmt.Sprintf( + "http://%s:%s/user/%s/following", + s.conf.ListenHost, + s.conf.ListenPort, + form.Username, + ), }) if err != nil { log.Printf("register: failed to create actor username=%s err=%v", form.Username, err) @@ -196,7 +211,12 @@ func (s *appService) CreateFollow( follow *db.CreateFollowParams, ) (*db.Follow, error) { if follow.Uri == "" { - follow.Uri = fmt.Sprintf("http://%s:%s/follows/%s", s.conf.ListenHost, s.conf.ListenPort, uuid.NewString()) + follow.Uri = fmt.Sprintf( + "http://%s:%s/follows/%s", + s.conf.ListenHost, + s.conf.ListenPort, + uuid.NewString(), + ) } return s.store.Follows().Create(ctx, *follow) } @@ -204,7 +224,12 @@ func (s *appService) CreateFollow( // AddNote implements AppService. func (s *appService) AddNote(ctx context.Context, note db.CreateStatusParams) (db.Status, error) { if note.Uri == "" { - note.Uri = fmt.Sprintf("http://%s:%s/statuses/%s", s.conf.ListenHost, s.conf.ListenPort, uuid.NewString()) + note.Uri = fmt.Sprintf( + "http://%s:%s/statuses/%s", + s.conf.ListenHost, + s.conf.ListenPort, + uuid.NewString(), + ) } if note.Url == "" { note.Url = note.Uri @@ -235,7 +260,12 @@ func (s *appService) AddFavourite( params := db.CreateFavouriteParams{ AccountID: int32(accountID), StatusID: int32(postID), - Uri: fmt.Sprintf("http://%s:%s/likes/%s", s.conf.ListenHost, s.conf.ListenPort, uuid.NewString()), + Uri: fmt.Sprintf( + "http://%s:%s/likes/%s", + s.conf.ListenHost, + s.conf.ListenPort, + uuid.NewString(), + ), } return s.store.Favourites().Create(ctx, params) } @@ -290,7 +320,12 @@ func (s *appService) FollowAccount( followee int, ) (*db.Follow, error) { createParams := db.CreateFollowParams{ - Uri: fmt.Sprintf("http://%s:%s/follows/%s", s.conf.ListenHost, s.conf.ListenPort, uuid.NewString()), + Uri: fmt.Sprintf( + "http://%s:%s/follows/%s", + s.conf.ListenHost, + s.conf.ListenPort, + uuid.NewString(), + ), AccountID: int32(follower), TargetAccountID: int32(followee), } @@ -345,4 +380,4 @@ func (s *appService) GetTimelinePostsByAccountId( accountID int, ) ([]db.GetTimelinePostsByAccountIdRow, error) { return s.store.Statuses().GetTimelinePostsByAccountId(ctx, accountID) -} \ No newline at end of file +} diff --git a/internal/util/handle.go b/internal/util/handle.go index cb673ff0..83580016 100644 --- a/internal/util/handle.go +++ b/internal/util/handle.go @@ -12,12 +12,14 @@ type HandleInfo struct { Local bool // true if domain matches local host } -func ParseHandle(raw string, localHost string) (*HandleInfo, error) { +func ParseHandle(raw string, localHost string, localPort string) (*HandleInfo, error) { handle := strings.TrimSpace(raw) if handle == "" { return nil, errors.New("empty handle") } + // Accept "@user@host" with optional leading "@". handle = strings.TrimPrefix(handle, "@") + parts := strings.Split(handle, "@") if len(parts) != 2 { return nil, fmt.Errorf("invalid handle format: %s", raw) @@ -27,10 +29,14 @@ func ParseHandle(raw string, localHost string) (*HandleInfo, error) { if username == "" || domain == "" { return nil, fmt.Errorf("invalid handle format: %s", raw) } + localDomain := localHost + if localPort != "" { + localDomain = localHost + ":" + localPort + } info := &HandleInfo{ Username: username, Domain: domain, - Local: strings.EqualFold(domain, localHost), + Local: strings.EqualFold(domain, localHost) || strings.EqualFold(domain, localDomain), } return info, nil } diff --git a/web/src/components/common/PostItem.tsx b/web/src/components/common/PostItem.tsx index cd946a08..706f59ef 100644 --- a/web/src/components/common/PostItem.tsx +++ b/web/src/components/common/PostItem.tsx @@ -1,7 +1,7 @@ import { Heart, MessageCircle, Repeat, Share2 } from "lucide-react"; import { useContext } from "react"; import ReactMarkdown from "react-markdown"; -import { Link } from "react-router"; +import { Link, useNavigate } from "react-router"; import type { components } from "../../lib/api/v1"; import type { AppClient } from "../../lib/client"; import AppContext from "../../lib/state"; @@ -32,6 +32,7 @@ export const PostItem = ({ }: PostProps) => { const appState = useContext(AppContext); const currentUserId = appState?.userId ?? null; + const navigate = useNavigate(); const likeAction = async () => { if (!("id" in post.data)) return; @@ -149,6 +150,10 @@ export const PostItem = ({ event.stopPropagation(); if (onCommentClick) { onCommentClick(post); + return; + } + if ("id" in post.data) { + navigate(`/post/${post.data.id}`); } }} > diff --git a/web/src/components/common/Sidebar.tsx b/web/src/components/common/Sidebar.tsx index df6d1fc9..a1124c1b 100644 --- a/web/src/components/common/Sidebar.tsx +++ b/web/src/components/common/Sidebar.tsx @@ -230,7 +230,8 @@ export default function Sidebar({ onPostClick }: SidebarProps) { ); diff --git a/web/src/components/feed/CommentView.tsx b/web/src/components/feed/CommentView.tsx index 1163154a..b715acae 100644 --- a/web/src/components/feed/CommentView.tsx +++ b/web/src/components/feed/CommentView.tsx @@ -73,24 +73,22 @@ export default function CommentView() { return null; } return ( -
{searchError}
++ Warning! + {searchError === "Error during fetching client." + ? "Sorry, we coudn't find this profile. Are you sure you entered the username and host correctly?" + : searchError} +
+Search result
-- {searchResult.displayName || searchResult.username} -
-- {searchResult.acct || `@${searchResult.username}`} +
)}