forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at oauth 12 kB view raw
1package state 2 3import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/hex" 7 "fmt" 8 "log" 9 "net/http" 10 "slices" 11 "strings" 12 13 comatproto "github.com/bluesky-social/indigo/api/atproto" 14 "github.com/bluesky-social/indigo/atproto/identity" 15 "github.com/bluesky-social/indigo/atproto/syntax" 16 lexutil "github.com/bluesky-social/indigo/lex/util" 17 "github.com/go-chi/chi/v5" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages" 21) 22 23func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 24 tabVal := r.URL.Query().Get("tab") 25 switch tabVal { 26 case "": 27 s.profilePage(w, r) 28 case "repos": 29 s.reposPage(w, r) 30 } 31} 32 33func (s *State) profilePage(w http.ResponseWriter, r *http.Request) { 34 didOrHandle := chi.URLParam(r, "user") 35 if didOrHandle == "" { 36 http.Error(w, "Bad request", http.StatusBadRequest) 37 return 38 } 39 40 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 41 if !ok { 42 s.pages.Error404(w) 43 return 44 } 45 46 profile, err := db.GetProfile(s.db, ident.DID.String()) 47 if err != nil { 48 log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 49 } 50 51 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 52 if err != nil { 53 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 54 } 55 56 // filter out ones that are pinned 57 pinnedRepos := []db.Repo{} 58 for i, r := range repos { 59 // if this is a pinned repo, add it 60 if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 61 pinnedRepos = append(pinnedRepos, r) 62 } 63 64 // if there are no saved pins, add the first 4 repos 65 if profile.IsPinnedReposEmpty() && i < 4 { 66 pinnedRepos = append(pinnedRepos, r) 67 } 68 } 69 70 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 71 if err != nil { 72 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 73 } 74 75 pinnedCollaboratingRepos := []db.Repo{} 76 for _, r := range collaboratingRepos { 77 // if this is a pinned repo, add it 78 if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 79 pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 80 } 81 } 82 83 timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 84 if err != nil { 85 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) 86 } 87 88 var didsToResolve []string 89 for _, r := range collaboratingRepos { 90 didsToResolve = append(didsToResolve, r.Did) 91 } 92 for _, byMonth := range timeline.ByMonth { 93 for _, pe := range byMonth.PullEvents.Items { 94 didsToResolve = append(didsToResolve, pe.Repo.Did) 95 } 96 for _, ie := range byMonth.IssueEvents.Items { 97 didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 98 } 99 for _, re := range byMonth.RepoEvents { 100 didsToResolve = append(didsToResolve, re.Repo.Did) 101 if re.Source != nil { 102 didsToResolve = append(didsToResolve, re.Source.Did) 103 } 104 } 105 } 106 107 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 108 didHandleMap := make(map[string]string) 109 for _, identity := range resolvedIds { 110 if !identity.Handle.IsInvalidHandle() { 111 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 112 } else { 113 didHandleMap[identity.DID.String()] = identity.DID.String() 114 } 115 } 116 117 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 118 if err != nil { 119 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 120 } 121 122 loggedInUser := s.oauth.GetUser(r) 123 followStatus := db.IsNotFollowing 124 if loggedInUser != nil { 125 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 126 } 127 128 profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 129 s.pages.ProfilePage(w, pages.ProfilePageParams{ 130 LoggedInUser: loggedInUser, 131 Repos: pinnedRepos, 132 CollaboratingRepos: pinnedCollaboratingRepos, 133 DidHandleMap: didHandleMap, 134 Card: pages.ProfileCard{ 135 UserDid: ident.DID.String(), 136 UserHandle: ident.Handle.String(), 137 AvatarUri: profileAvatarUri, 138 Profile: profile, 139 FollowStatus: followStatus, 140 Followers: followers, 141 Following: following, 142 }, 143 ProfileTimeline: timeline, 144 }) 145} 146 147func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 148 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 149 if !ok { 150 s.pages.Error404(w) 151 return 152 } 153 154 profile, err := db.GetProfile(s.db, ident.DID.String()) 155 if err != nil { 156 log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 157 } 158 159 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 160 if err != nil { 161 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 162 } 163 164 loggedInUser := s.oauth.GetUser(r) 165 followStatus := db.IsNotFollowing 166 if loggedInUser != nil { 167 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 168 } 169 170 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 171 if err != nil { 172 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 173 } 174 175 profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 176 177 s.pages.ReposPage(w, pages.ReposPageParams{ 178 LoggedInUser: loggedInUser, 179 Repos: repos, 180 Card: pages.ProfileCard{ 181 UserDid: ident.DID.String(), 182 UserHandle: ident.Handle.String(), 183 AvatarUri: profileAvatarUri, 184 Profile: profile, 185 FollowStatus: followStatus, 186 Followers: followers, 187 Following: following, 188 }, 189 }) 190} 191 192func (s *State) GetAvatarUri(handle string) string { 193 secret := s.config.Avatar.SharedSecret 194 h := hmac.New(sha256.New, []byte(secret)) 195 h.Write([]byte(handle)) 196 signature := hex.EncodeToString(h.Sum(nil)) 197 return fmt.Sprintf("%s/%s/%s", s.config.Avatar.Host, signature, handle) 198} 199 200func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 201 user := s.oauth.GetUser(r) 202 203 err := r.ParseForm() 204 if err != nil { 205 log.Println("invalid profile update form", err) 206 s.pages.Notice(w, "update-profile", "Invalid form.") 207 return 208 } 209 210 profile, err := db.GetProfile(s.db, user.Did) 211 if err != nil { 212 log.Printf("getting profile data for %s: %s", user.Did, err) 213 } 214 215 profile.Description = r.FormValue("description") 216 profile.IncludeBluesky = r.FormValue("includeBluesky") == "on" 217 profile.Location = r.FormValue("location") 218 219 var links [5]string 220 for i := range 5 { 221 iLink := r.FormValue(fmt.Sprintf("link%d", i)) 222 links[i] = iLink 223 } 224 profile.Links = links 225 226 // Parse stats (exactly 2) 227 stat0 := r.FormValue("stat0") 228 stat1 := r.FormValue("stat1") 229 230 if stat0 != "" { 231 profile.Stats[0].Kind = db.VanityStatKind(stat0) 232 } 233 234 if stat1 != "" { 235 profile.Stats[1].Kind = db.VanityStatKind(stat1) 236 } 237 238 if err := db.ValidateProfile(s.db, profile); err != nil { 239 log.Println("invalid profile", err) 240 s.pages.Notice(w, "update-profile", err.Error()) 241 return 242 } 243 244 s.updateProfile(profile, w, r) 245 return 246} 247 248func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 249 user := s.oauth.GetUser(r) 250 251 err := r.ParseForm() 252 if err != nil { 253 log.Println("invalid profile update form", err) 254 s.pages.Notice(w, "update-profile", "Invalid form.") 255 return 256 } 257 258 profile, err := db.GetProfile(s.db, user.Did) 259 if err != nil { 260 log.Printf("getting profile data for %s: %s", user.Did, err) 261 } 262 263 i := 0 264 var pinnedRepos [6]syntax.ATURI 265 for key, values := range r.Form { 266 if i >= 6 { 267 log.Println("invalid pin update form", err) 268 s.pages.Notice(w, "update-profile", "Only 6 repositories can be pinned at a time.") 269 return 270 } 271 if strings.HasPrefix(key, "pinnedRepo") && len(values) > 0 && values[0] != "" && i < 6 { 272 aturi, err := syntax.ParseATURI(values[0]) 273 if err != nil { 274 log.Println("invalid profile update form", err) 275 s.pages.Notice(w, "update-profile", "Invalid form.") 276 return 277 } 278 pinnedRepos[i] = aturi 279 i++ 280 } 281 } 282 profile.PinnedRepos = pinnedRepos 283 284 s.updateProfile(profile, w, r) 285 return 286} 287 288func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *http.Request) { 289 user := s.oauth.GetUser(r) 290 tx, err := s.db.BeginTx(r.Context(), nil) 291 if err != nil { 292 log.Println("failed to start transaction", err) 293 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 294 return 295 } 296 297 client, err := s.oauth.AuthorizedClient(r) 298 if err != nil { 299 log.Println("failed to get authorized client", err) 300 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 301 return 302 } 303 304 // yeah... lexgen dose not support syntax.ATURI in the record for some reason, 305 // nor does it support exact size arrays 306 var pinnedRepoStrings []string 307 for _, r := range profile.PinnedRepos { 308 pinnedRepoStrings = append(pinnedRepoStrings, r.String()) 309 } 310 311 var vanityStats []string 312 for _, v := range profile.Stats { 313 vanityStats = append(vanityStats, string(v.Kind)) 314 } 315 316 ex, _ := client.RepoGetRecord(r.Context(), "", tangled.ActorProfileNSID, user.Did, "self") 317 var cid *string 318 if ex != nil { 319 cid = ex.Cid 320 } 321 322 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 323 Collection: tangled.ActorProfileNSID, 324 Repo: user.Did, 325 Rkey: "self", 326 Record: &lexutil.LexiconTypeDecoder{ 327 Val: &tangled.ActorProfile{ 328 Bluesky: &profile.IncludeBluesky, 329 Description: &profile.Description, 330 Links: profile.Links[:], 331 Location: &profile.Location, 332 PinnedRepositories: pinnedRepoStrings, 333 Stats: vanityStats[:], 334 }}, 335 SwapRecord: cid, 336 }) 337 if err != nil { 338 log.Println("failed to update profile", err) 339 s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.") 340 return 341 } 342 343 err = db.UpsertProfile(tx, profile) 344 if err != nil { 345 log.Println("failed to update profile", err) 346 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 347 return 348 } 349 350 s.pages.HxRedirect(w, "/"+user.Did) 351 return 352} 353 354func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 355 user := s.oauth.GetUser(r) 356 357 profile, err := db.GetProfile(s.db, user.Did) 358 if err != nil { 359 log.Printf("getting profile data for %s: %s", user.Did, err) 360 } 361 362 s.pages.EditBioFragment(w, pages.EditBioParams{ 363 LoggedInUser: user, 364 Profile: profile, 365 }) 366} 367 368func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 369 user := s.oauth.GetUser(r) 370 371 profile, err := db.GetProfile(s.db, user.Did) 372 if err != nil { 373 log.Printf("getting profile data for %s: %s", user.Did, err) 374 } 375 376 repos, err := db.GetAllReposByDid(s.db, user.Did) 377 if err != nil { 378 log.Printf("getting repos for %s: %s", user.Did, err) 379 } 380 381 collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did) 382 if err != nil { 383 log.Printf("getting collaborating repos for %s: %s", user.Did, err) 384 } 385 386 allRepos := []pages.PinnedRepo{} 387 388 for _, r := range repos { 389 isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 390 allRepos = append(allRepos, pages.PinnedRepo{ 391 IsPinned: isPinned, 392 Repo: r, 393 }) 394 } 395 for _, r := range collaboratingRepos { 396 isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 397 allRepos = append(allRepos, pages.PinnedRepo{ 398 IsPinned: isPinned, 399 Repo: r, 400 }) 401 } 402 403 var didsToResolve []string 404 for _, r := range allRepos { 405 didsToResolve = append(didsToResolve, r.Did) 406 } 407 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 408 didHandleMap := make(map[string]string) 409 for _, identity := range resolvedIds { 410 if !identity.Handle.IsInvalidHandle() { 411 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 412 } else { 413 didHandleMap[identity.DID.String()] = identity.DID.String() 414 } 415 } 416 417 s.pages.EditPinsFragment(w, pages.EditPinsParams{ 418 LoggedInUser: user, 419 Profile: profile, 420 AllRepos: allRepos, 421 DidHandleMap: didHandleMap, 422 }) 423}