Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/borg/borg.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"context"
"fmt"

"github.com/kibirisu/borg/internal/config"
"github.com/kibirisu/borg/internal/server"
Expand All @@ -10,6 +11,12 @@
func main() {
ctx := context.Background()
conf := config.GetConfig()
fmt.Println("--- Config Test ---")

Check failure on line 14 in cmd/borg/borg.go

View workflow job for this annotation

GitHub Actions / check-golang

use of `fmt.Println` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
fmt.Printf("Env: %s\n", conf.AppEnv)

Check failure on line 15 in cmd/borg/borg.go

View workflow job for this annotation

GitHub Actions / check-golang

use of `fmt.Printf` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
fmt.Printf("Host: %s\n", conf.ListenHost)

Check failure on line 16 in cmd/borg/borg.go

View workflow job for this annotation

GitHub Actions / check-golang

use of `fmt.Printf` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
fmt.Printf("Port: %s\n", conf.ListenPort)

Check failure on line 17 in cmd/borg/borg.go

View workflow job for this annotation

GitHub Actions / check-golang

use of `fmt.Printf` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
fmt.Printf("DB: %s\n", conf.DatabaseURL)
fmt.Println("-------------------")

Check failure on line 19 in cmd/borg/borg.go

View workflow job for this annotation

GitHub Actions / check-golang

use of `fmt.Println` forbidden by pattern `^(fmt\.Print(|f|ln)|print|println)$` (forbidigo)
s := server.New(ctx, conf)
panic(s.ListenAndServe())
}
134 changes: 91 additions & 43 deletions internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

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"
)
Expand Down Expand Up @@ -57,8 +60,10 @@
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
Expand All @@ -74,52 +79,95 @@
}
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()

Check failure on line 119 in internal/server/api.go

View workflow job for this annotation

GitHub Actions / check-golang

Error return value of `actorResp.Body.Close` is not checked (errcheck)
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(), &params)
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.
Expand Down
53 changes: 44 additions & 9 deletions internal/service/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -196,15 +211,25 @@ 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)
}

// 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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -345,4 +380,4 @@ func (s *appService) GetTimelinePostsByAccountId(
accountID int,
) ([]db.GetTimelinePostsByAccountIdRow, error) {
return s.store.Statuses().GetTimelinePostsByAccountId(ctx, accountID)
}
}
10 changes: 8 additions & 2 deletions internal/util/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
7 changes: 6 additions & 1 deletion web/src/components/common/PostItem.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -149,6 +150,10 @@ export const PostItem = ({
event.stopPropagation();
if (onCommentClick) {
onCommentClick(post);
return;
}
if ("id" in post.data) {
navigate(`/post/${post.data.id}`);
}
}}
>
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/common/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ export default function Sidebar({ onPostClick }: SidebarProps) {

<button
type="button"
className="mt-8 w-full bg-indigo-500 text-white py-2 rounded-full font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:cursor-pointer"
className="mt-8 w-full bg-indigo-500 text-white py-2 rounded-full font-semibold disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:cursor-not-allowed hover:cursor-pointer"
disabled={!isAuthenticated}
onClick={handlePostClick}
>
{isAuthenticated ? "Post" : "Sign in to post"}
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/feed/CommentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function CommentForm() {
disabled={isSubmitting || !isAuthenticated}
className="self-end rounded-full bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm disabled:opacity-50"
>
{isSubmitting ? "Posting…" : "Post"}
{isSubmitting ? "Commenting…" : "Comment"}
</button>
</Form>
);
Expand Down
Loading
Loading