Skip to content

Commit 44b9caa

Browse files
committed
feat: implement following & unfollowing
1 parent 05ae758 commit 44b9caa

3 files changed

Lines changed: 247 additions & 9 deletions

File tree

bluesky/blueskyapi.go

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ type DeleteRecordPayload struct {
183183
}
184184

185185
type PostInteractionRecord struct {
186-
Type string `json:"$type"`
187-
CreatedAt string `json:"createdAt"`
188-
Subject Subject `json:"subject"`
186+
Type string `json:"$type"`
187+
CreatedAt string `json:"createdAt"`
188+
Subject interface{} `json:"subject"`
189189
}
190190

191191
type CreatePostRecord struct {
@@ -322,6 +322,30 @@ func GetUserInfo(pds string, token string, screen_name string) (*bridge.TwitterU
322322
return twitterUser, nil
323323
}
324324

325+
func GetUserInfoRaw(pds string, token string, screen_name string) (*User, error) {
326+
url := pds + "/xrpc/app.bsky.actor.getProfile" + "?actor=" + screen_name
327+
328+
resp, err := SendRequest(&token, http.MethodGet, url, nil)
329+
if err != nil {
330+
return nil, err
331+
}
332+
defer resp.Body.Close()
333+
if resp.StatusCode != http.StatusOK {
334+
bodyBytes, _ := io.ReadAll(resp.Body)
335+
bodyString := string(bodyBytes)
336+
fmt.Println("Response Status:", resp.StatusCode)
337+
fmt.Println("Response Body:", bodyString)
338+
return nil, errors.New("failed to fetch user info")
339+
}
340+
341+
author := User{}
342+
if err := json.NewDecoder(resp.Body).Decode(&author); err != nil {
343+
return nil, err
344+
}
345+
346+
return &author, nil
347+
}
348+
325349
func GetUsersInfo(pds string, token string, items []string, ignoreCache bool) ([]*bridge.TwitterUser, error) {
326350
var results []*bridge.TwitterUser
327351
var missing []string
@@ -791,7 +815,7 @@ func LikePost(pds string, token string, id string, my_did string) (error, *Threa
791815
bodyString := string(bodyBytes)
792816
fmt.Println("Response Status:", resp.StatusCode)
793817
fmt.Println("Response Body:", bodyString)
794-
return errors.New("failed to retweet: " + bodyString), nil
818+
return errors.New("failed to like: " + bodyString), nil
795819
}
796820

797821
likeRes := CreateRecordResult{}
@@ -834,19 +858,120 @@ func UnlikePost(pds string, token string, id string, my_did string) (error, *Thr
834858
bodyString := string(bodyBytes)
835859
fmt.Println("Response Status:", resp.StatusCode)
836860
fmt.Println("Response Body:", bodyString)
837-
return errors.New("failed to retweet: " + bodyString), nil
861+
return errors.New("failed to unlike: " + bodyString), nil
838862
}
839863

840864
likeRes := CreateRecordResult{}
841865
if err := json.NewDecoder(resp.Body).Decode(&likeRes); err != nil {
842866
return err, nil
843867
}
844868

845-
thread.Thread.Post.Viewer.Like = &likeRes.URI // maybe?
869+
emptyString := ""
870+
thread.Thread.Post.Viewer.Like = &emptyString
846871

847872
return nil, thread
848873
}
849874

875+
func FollowUser(pds string, token string, targetActor string, my_did string) (error, *User) {
876+
url := pds + "/xrpc/com.atproto.repo.createRecord"
877+
878+
targetUser, err := GetUserInfoRaw(pds, token, targetActor)
879+
if err != nil {
880+
return errors.New("failed to fetch post"), nil
881+
}
882+
883+
if targetUser.Viewer.Following != nil {
884+
return errors.New("already following user"), nil
885+
}
886+
887+
payload := CreateRecordPayload{
888+
Collection: "app.bsky.graph.follow",
889+
Repo: my_did,
890+
Record: PostInteractionRecord{
891+
Type: "app.bsky.graph.follow",
892+
CreatedAt: time.Now().UTC().Format(time.RFC3339),
893+
Subject: targetUser.DID,
894+
},
895+
}
896+
897+
reqBody, err := json.Marshal(payload)
898+
if err != nil {
899+
return errors.New("failed to marshal payload"), nil
900+
}
901+
902+
resp, err := SendRequest(&token, http.MethodPost, url, bytes.NewReader(reqBody))
903+
if err != nil {
904+
return err, nil
905+
}
906+
defer resp.Body.Close()
907+
908+
if resp.StatusCode != http.StatusOK {
909+
bodyBytes, _ := io.ReadAll(resp.Body)
910+
bodyString := string(bodyBytes)
911+
fmt.Println("Response Status:", resp.StatusCode)
912+
fmt.Println("Response Body:", bodyString)
913+
return errors.New("failed to retweet: " + bodyString), nil
914+
}
915+
916+
followRes := CreateRecordResult{}
917+
if err := json.NewDecoder(resp.Body).Decode(&followRes); err != nil {
918+
return err, nil
919+
}
920+
921+
targetUser.Viewer.Following = &strings.Split(followRes.URI, "/app.bsky.graph.follow/")[1]
922+
targetUser.FollowersCount++
923+
924+
return nil, targetUser
925+
}
926+
927+
func UnfollowUser(pds string, token string, targetActor string, my_did string) (error, *User) {
928+
url := pds + "/xrpc/com.atproto.repo.deleteRecord"
929+
930+
targetUser, err := GetUserInfoRaw(pds, token, targetActor)
931+
if err != nil {
932+
return errors.New("failed to fetch post"), nil
933+
}
934+
935+
if targetUser.Viewer.Following == nil {
936+
return errors.New("not following user"), nil
937+
}
938+
939+
payload := DeleteRecordPayload{
940+
Collection: "app.bsky.graph.follow",
941+
Repo: my_did,
942+
RKey: strings.Split(*targetUser.Viewer.Following, "/app.bsky.graph.follow/")[1],
943+
}
944+
945+
reqBody, err := json.Marshal(payload)
946+
if err != nil {
947+
return errors.New("failed to marshal payload"), nil
948+
}
949+
950+
resp, err := SendRequest(&token, http.MethodPost, url, bytes.NewReader(reqBody))
951+
if err != nil {
952+
return err, nil
953+
}
954+
defer resp.Body.Close()
955+
956+
if resp.StatusCode != http.StatusOK {
957+
bodyBytes, _ := io.ReadAll(resp.Body)
958+
bodyString := string(bodyBytes)
959+
fmt.Println("Response Status:", resp.StatusCode)
960+
fmt.Println("Response Body:", bodyString)
961+
return errors.New("failed to unfollow: " + bodyString), nil
962+
}
963+
964+
unfollowRes := CreateRecordResult{}
965+
if err := json.NewDecoder(resp.Body).Decode(&unfollowRes); err != nil {
966+
return err, nil
967+
}
968+
969+
emptyString := ""
970+
targetUser.Viewer.Following = &emptyString
971+
972+
return nil, targetUser
973+
}
974+
850975
func GetLikes(pds string, token string, uri string, limit int) (*Likes, error) {
851976
url := fmt.Sprintf(pds+"/xrpc/app.bsky.feed.getLikes?limit=%d&uri=%s", limit, uri)
852977

@@ -866,7 +991,7 @@ func GetLikes(pds string, token string, uri string, limit int) (*Likes, error) {
866991
bodyString := string(bodyBytes)
867992
fmt.Println("Response Status:", resp.StatusCode)
868993
fmt.Println("Response Body:", bodyString)
869-
return nil, errors.New("failed to fetch timeline")
994+
return nil, errors.New("failed to fetch likes")
870995
}
871996

872997
likes := Likes{}
@@ -896,7 +1021,7 @@ func GetRetweetAuthors(pds string, token string, uri string, limit int) (*Repost
8961021
bodyString := string(bodyBytes)
8971022
fmt.Println("Response Status:", resp.StatusCode)
8981023
fmt.Println("Response Body:", bodyString)
899-
return nil, errors.New("failed to fetch timeline")
1024+
return nil, errors.New("failed to fetch retweet authors")
9001025
}
9011026

9021027
retweetAuthors := RepostedBy{}

twitterv1/twitterv1.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ func InitServer() {
5050
app.Post("/1/users/lookup.json", UsersLookup)
5151
app.Get("/1/friendships/lookup.xml", UserRelationships)
5252
app.Get("/1/friendships/show.xml", GetUsersRelationship)
53+
app.Post("/1/friendships/create.xml", FollowUser)
54+
app.Post("/1/friendships/destroy.xml", UnfollowUserForm)
55+
app.Post("/1/friendships/destroy/:id.xml", UnfollowUserParams)
5356

5457
// Connect
5558
app.Get("/1/users/search.json", UserSearch)

twitterv1/user.go

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,8 @@ func GetUsersRelationship(c *fiber.Ctx) error {
224224
// auth
225225
_, pds, _, oauthToken, err := GetAuthFromReq(c)
226226
if err != nil {
227-
return c.Status(fiber.StatusUnauthorized).SendString("OAuth token not found in Authorization header")
227+
blankstring := "" // I. Hate. This.
228+
oauthToken = &blankstring
228229
}
229230

230231
// It looks like there's a bug where I can't pass handles into GetRelationships, but we need to get the handle anyways, so this shouldn't impact that much
@@ -274,3 +275,112 @@ func GetUsersRelationship(c *fiber.Ctx) error {
274275

275276
return c.SendString(*xml)
276277
}
278+
279+
// https://web.archive.org/web/20120407201029/https://dev.twitter.com/docs/api/1/post/friendships/create
280+
func FollowUser(c *fiber.Ctx) error {
281+
// auth
282+
my_did, pds, _, oauthToken, err := GetAuthFromReq(c)
283+
if err != nil {
284+
return c.Status(fiber.StatusUnauthorized).SendString("OAuth token not found in Authorization header")
285+
}
286+
287+
// lets get the user params
288+
actor := c.FormValue("user_id")
289+
if actor == "" {
290+
actor = c.FormValue("screen_name")
291+
if actor == "" {
292+
c.Status(fiber.StatusBadRequest).SendString("No user provided")
293+
}
294+
} else {
295+
id, ok := new(big.Int).SetString(actor, 10)
296+
if !ok {
297+
return c.Status(fiber.StatusBadRequest).SendString("Invalid user_id provided")
298+
}
299+
actor = bridge.TwitterIDToBlueSky(*id)
300+
}
301+
302+
// follow
303+
err, user := blueskyapi.FollowUser(*pds, *oauthToken, actor, *my_did)
304+
305+
if err != nil {
306+
if err.Error() == "already following user" {
307+
return c.Status(403).SendString("already following user")
308+
}
309+
fmt.Println("Error:", err)
310+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to follow user")
311+
}
312+
313+
// convert user into twitter format
314+
twitterUser := blueskyapi.AuthorTTB(*user)
315+
316+
// XML Encode
317+
xml, err := bridge.XMLEncoder(twitterUser, "TwitterUser", "user")
318+
if err != nil {
319+
fmt.Println("Error:", err)
320+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to encode user info")
321+
}
322+
323+
return c.SendString(*xml)
324+
}
325+
326+
// https://web.archive.org/web/20120407201029/https://dev.twitter.com/docs/api/1/post/friendships/create
327+
func UnfollowUser(c *fiber.Ctx, actor string) error {
328+
// auth
329+
my_did, pds, _, oauthToken, err := GetAuthFromReq(c)
330+
if err != nil {
331+
return c.Status(fiber.StatusUnauthorized).SendString("OAuth token not found in Authorization header")
332+
}
333+
334+
// follow
335+
err, user := blueskyapi.UnfollowUser(*pds, *oauthToken, actor, *my_did)
336+
337+
if err != nil {
338+
if err.Error() == "not following user" {
339+
return c.Status(403).SendString("not following user")
340+
}
341+
fmt.Println("Error:", err)
342+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to unfollow user")
343+
}
344+
345+
// convert user into twitter format
346+
twitterUser := blueskyapi.AuthorTTB(*user)
347+
348+
// XML Encode
349+
xml, err := bridge.XMLEncoder(twitterUser, "TwitterUser", "user")
350+
if err != nil {
351+
fmt.Println("Error:", err)
352+
return c.Status(fiber.StatusInternalServerError).SendString("Failed to encode user info")
353+
}
354+
355+
return c.SendString(*xml)
356+
}
357+
358+
func UnfollowUserForm(c *fiber.Ctx) error {
359+
// lets get the user params
360+
actor := c.FormValue("user_id")
361+
if actor == "" {
362+
actor = c.FormValue("screen_name")
363+
if actor == "" {
364+
c.Status(fiber.StatusBadRequest).SendString("No user provided")
365+
}
366+
} else {
367+
id, ok := new(big.Int).SetString(actor, 10)
368+
if !ok {
369+
return c.Status(fiber.StatusBadRequest).SendString("Invalid user_id provided")
370+
}
371+
actor = bridge.TwitterIDToBlueSky(*id)
372+
}
373+
return UnfollowUser(c, actor)
374+
}
375+
376+
func UnfollowUserParams(c *fiber.Ctx) error {
377+
// This should allow lookup with a handle, but tbh, i'm too lazy to implement that right now as i do not see it being used.
378+
actor := c.Params("id")
379+
actorID, ok := new(big.Int).SetString(actor, 10)
380+
if !ok {
381+
return c.Status(fiber.StatusBadRequest).SendString("Invalid user_id provided")
382+
}
383+
actor = bridge.TwitterIDToBlueSky(*actorID)
384+
385+
return UnfollowUser(c, actor)
386+
}

0 commit comments

Comments
 (0)