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