go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/chirp/pkg/model/manager.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package model 9 10 import ( 11 "context" 12 "fmt" 13 "time" 14 15 "go.charczuk.com/sdk/apputil" 16 "go.charczuk.com/sdk/db/dbutil" 17 "go.charczuk.com/sdk/uuid" 18 "go.charczuk.com/sdk/web" 19 ) 20 21 var qf = new(dbutil.QueryFormatter). 22 WithModels( 23 Audit{}, 24 Chirp{}, 25 ChirpStats{}, 26 ChirpStatus{}, 27 ChirpLike{}, 28 apputil.User{}, 29 UserInfo{}, 30 Feed{}, 31 Graph{}, 32 Notification{}, 33 ).WithTemplates( 34 templateUserColumns, 35 templateUserJoins, 36 templateChirpColumns, 37 templateChirpAuxJoins, 38 templateChirpBlockChecks, 39 ) 40 41 var f = qf.MustFormat 42 43 // queries live here in all their glory. 44 var ( 45 templateUserColumns = `{{ define "user_columns" }} 46 {{ columns_prefix_alias "User" "user_" "u" }} 47 , {{ columns_prefix_alias "UserInfo" "userinfo_" "ui" }} 48 , u.id as userstatus_user_id 49 , case when following.user_id is not null then 1 else 0 end as userstatus_is_following 50 , case when u.id = $1 then 1 else 0 end as userstatus_is_self 51 {{ end }}` 52 templateUserJoins = `{{ define "user_joins" }} 53 {{ table "User" }} u 54 JOIN {{ table "UserInfo" }} ui on ui.user_id = u.id 55 LEFT JOIN {{ table "Graph" }} following ON following.user_id = $1 AND following.target_id = u.id AND type = 'follow' 56 {{ end }}` 57 58 templateChirpColumns = `{{ define "chirp_columns" }} 59 {{ columns_prefix_alias "Chirp" "chirp_" "c" }} 60 , {{ columns_prefix_alias "ChirpStats" "chirp_stats_" "cs" }} 61 , {{ columns_prefix_alias "User" "chirp_user_" "u" }} 62 , {{ columns_prefix_alias "UserInfo" "chirp_userinfo_" "ui" }} 63 64 , case when cl.chirp_id is not null then 1 else 0 end as chirp_status_is_liked 65 , case when cr.id is not null then 1 else 0 end as chirp_status_is_quoted 66 67 , {{ columns_prefix_alias "Chirp" "quoted_" "qc" }} 68 , {{ columns_prefix_alias "ChirpStats" "quoted_stats_" "qcs" }} 69 , {{ columns_prefix_alias "User" "quoted_user_" "qu" }} 70 , {{ columns_prefix_alias "UserInfo" "quoted_userinfo_" "qui" }} 71 72 , {{ columns_prefix_alias "Chirp" "reply_" "rc" }} 73 , {{ columns_prefix_alias "ChirpStats" "reply_stats_" "rcs" }} 74 , {{ columns_prefix_alias "User" "reply_user_" "ru" }} 75 , {{ columns_prefix_alias "UserInfo" "reply_userinfo_" "rui" }} 76 {{ end }}` 77 78 templateChirpAuxJoins = `{{ define "chirp_aux_joins" }} 79 JOIN {{ table "ChirpStats" }} cs on cs.chirp_id = c.id 80 JOIN {{ table "User" }} u on c.user_id = u.id 81 JOIN {{ table "UserInfo" }} ui on c.user_id = ui.user_id 82 83 LEFT JOIN {{ table "ChirpLike" }} cl on cl.chirp_id = c.id AND cl.user_id = $1 84 LEFT JOIN {{ table "Chirp" }} cr on cr.id = c.quoted_id AND cl.user_id = $1 85 86 LEFT JOIN {{ table "Chirp" }} qc on qc.id = c.quoted_id 87 LEFT JOIN {{ table "ChirpStats" }} qcs on qcs.chirp_id = c.quoted_id 88 LEFT JOIN {{ table "User" }} qu on qu.id = qc.user_id 89 LEFT JOIN {{ table "UserInfo" }} qui on qui.user_id = qc.user_id 90 91 LEFT JOIN {{ table "Chirp" }} rc on rc.id = c.reply_id 92 LEFT JOIN {{ table "ChirpStats" }} rcs on rcs.chirp_id = c.reply_id 93 LEFT JOIN {{ table "User" }} ru on ru.id = rc.user_id 94 LEFT JOIN {{ table "UserInfo" }} rui on rui.user_id = rc.user_id 95 {{ end }}` 96 97 templateChirpBlockChecks = `{{ define "chirp_block_checks" }} 98 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} g WHERE g.user_id = $1 AND g.target_id = c.user_id AND type IN ('block', 'mute')) 99 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} g WHERE g.target_id = $1 AND g.user_id= c.user_id AND type = 'block') 100 AND ( 101 c.quoted_id IS NULL 102 OR ( 103 c.quoted_id IS NOT NULL 104 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} g WHERE g.user_id = $1 AND g.target_id = qc.user_id AND type IN ('block', 'mute')) 105 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} g WHERE g.target_id = $1 AND g.user_id= qc.user_id AND type = 'block') 106 ) 107 ) 108 AND ( 109 c.reply_id IS NULL 110 OR ( 111 c.reply_id IS NOT NULL 112 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} g WHERE g.user_id = $1 AND g.target_id = rc.user_id AND type IN ('block', 'mute')) 113 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} g WHERE g.target_id = $1 AND g.user_id= rc.user_id AND type = 'block') 114 ) 115 ) 116 {{ end }}` 117 118 queryUser = f(` 119 SELECT 120 {{ template "user_columns" . }} 121 FROM 122 {{ template "user_joins" . }} 123 WHERE 124 u.id = $2`, nil) 125 126 queryUserByUsername = f(` 127 SELECT 128 {{ template "user_columns" . }} 129 FROM 130 {{ template "user_joins" . }} 131 WHERE 132 u.email = $2`, nil) 133 134 querySearchUsers = f(` 135 SELECT 136 {{ template "user_columns" . }} 137 FROM 138 {{ template "user_joins" . }} 139 WHERE 140 u.email ilike $2 141 OR u.given_name ilike $2 142 OR u.family_name ilike $2 143 AND u.id <> $1 144 AND NOT EXISTS (SELECT 1 FROM {{ table "Graph" }} blocked WHERE blocked.target_id = $1 and blocked.user_id = u.id AND type = 'block') 145 `, nil) 146 147 queryChirp = f(` 148 SELECT 149 {{ template "chirp_columns" . }} 150 FROM 151 {{ table "Chirp" }} c 152 {{ template "chirp_aux_joins" }} 153 WHERE 154 c.id = $2 155 {{ template "chirp_block_checks" }} 156 `, nil) 157 158 queryChirpReplies = f(` 159 SELECT 160 {{ template "chirp_columns" . }} 161 FROM 162 {{ table "Chirp" }} c 163 {{ template "chirp_aux_joins" }} 164 WHERE 165 c.reply_id = $2 166 {{ template "chirp_block_checks" }} 167 `, nil) 168 169 queryFeedForUserID = f(` 170 ; WITH feed_data as ( 171 SELECT 172 {{ template "chirp_columns" . }} 173 , ROW_NUMBER() OVER (ORDER BY f.timestamp_utc desc) as feed_rank 174 FROM 175 {{ table "Feed" }} f 176 JOIN {{ table "Chirp" }} c on c.id = f.chirp_id 177 {{ template "chirp_aux_joins" }} 178 WHERE 179 f.user_id = $1 180 {{ template "chirp_block_checks" }} 181 ) 182 SELECT 183 * 184 FROM 185 feed_data 186 WHERE 187 $3::uuid IS NULL 188 OR ( 189 feed_rank > (SELECT feed_rank from feed_data WHERE chirp_id = $3 LIMIT 1) 190 ) 191 ORDER BY feed_rank ASC 192 LIMIT $2 193 `, nil) 194 195 queryProfilePosts = f(` 196 ; WITH profile_data as ( 197 SELECT 198 {{ template "chirp_columns" . }} 199 , ROW_NUMBER() OVER (ORDER BY c.published_utc desc) as profile_rank 200 FROM 201 {{ table "Chirp" }} c 202 {{ template "chirp_aux_joins" }} 203 WHERE 204 c.user_id = $2 205 AND c.published_utc is not null 206 AND c.reply_id is null 207 {{ template "chirp_block_checks" }} 208 ) 209 SELECT 210 * 211 FROM 212 profile_data 213 WHERE 214 $4::uuid IS NULL 215 OR ( 216 profile_rank > (SELECT profile_rank from profile_data WHERE chirp_id = $4 LIMIT 1) 217 ) 218 219 ORDER BY profile_rank ASC 220 LIMIT $3 221 `, nil) 222 223 queryProfileReplies = f(` 224 ; WITH profile_data as ( 225 SELECT 226 {{ template "chirp_columns" . }} 227 , ROW_NUMBER() OVER (ORDER BY c.published_utc desc) as profile_rank 228 FROM 229 {{ table "Chirp" }} c 230 {{ template "chirp_aux_joins" }} 231 WHERE 232 c.user_id = $2 233 AND c.published_utc is not null 234 AND c.reply_id is not null 235 {{ template "chirp_block_checks" }} 236 ) 237 SELECT 238 * 239 FROM 240 profile_data 241 WHERE 242 $4::uuid IS NULL 243 OR ( 244 profile_rank > (SELECT profile_rank from profile_data WHERE chirp_id = $4 LIMIT 1) 245 ) 246 ORDER BY profile_rank ASC 247 LIMIT $3 248 `, nil) 249 250 queryProfileLikes = f(` 251 ; WITH profile_data as ( 252 SELECT 253 {{ template "chirp_columns" . }} 254 , ROW_NUMBER() OVER (ORDER BY c.published_utc desc) as profile_rank 255 FROM 256 {{ table "ChirpLike" }} clike 257 JOIN {{ table "Chirp" }} c ON clike.chirp_id = c.id 258 {{ template "chirp_aux_joins" }} 259 WHERE 260 clike.user_id = $2 261 {{ template "chirp_block_checks" }} 262 ) 263 SELECT 264 * 265 FROM 266 profile_data 267 WHERE 268 $4::uuid IS NULL 269 OR ( 270 profile_rank > (SELECT profile_rank from profile_data WHERE chirp_id = $4 LIMIT 1) 271 ) 272 ORDER BY profile_rank ASC 273 LIMIT $3 274 `, nil) 275 276 queryNotifications = f(` 277 ; WITH feed_data as ( 278 SELECT 279 {{ columns_prefix_alias "Notification" "notification_" "n" }} 280 , {{ columns_prefix_alias "User" "user_" "u" }} 281 , {{ columns_prefix_alias "UserInfo" "userinfo_" "ui" }} 282 , {{ columns_prefix_alias "Chirp" "chirp_" "c" }} 283 , u.id as userstatus_user_id 284 , case when following.user_id is not null then 1 else 0 end as userstatus_is_following 285 , case when u.id = $1 then 1 else 0 end as userstatus_is_self 286 , ROW_NUMBER() OVER (ORDER BY n.created_utc desc) as feed_rank 287 FROM 288 {{ table "Notification" }} n 289 JOIN {{ table "User" }} u ON u.id = n.acting_user_id 290 JOIN {{ table "UserInfo" }} ui ON ui.user_id = n.acting_user_id 291 LEFT JOIN {{ table "Chirp" }} c ON c.id = n.chirp_id 292 LEFT JOIN {{ table "Graph" }} following ON following.user_id = $1 AND following.target_id = u.id AND following.type = 'follow' 293 WHERE 294 n.user_id = $1 295 ) 296 SELECT 297 * 298 FROM 299 feed_data 300 WHERE 301 $3::uuid IS NULL 302 OR ( 303 feed_rank > (SELECT feed_rank from feed_data WHERE notification_id = $3 LIMIT 1) 304 ) 305 ORDER BY 306 feed_rank ASC 307 LIMIT $2`, nil) 308 queryNotificationsUnread = f(`SELECT count(id) FROM {{ table "Notification" }} WHERE read_utc is null AND user_id = $1`, nil) 309 310 execIncrementChirpReplies = f(`UPDATE {{ table "ChirpStats" }} SET replies = replies + 1 WHERE chirp_id = $1`, nil) 311 execIncrementChirpRechirps = f(`UPDATE {{ table "ChirpStats" }} SET rechirps = rechirps + 1 WHERE chirp_id = $1`, nil) 312 execDeleteChirpFromFeeds = f(`DELETE FROM {{ table "Feed" }} WHERE chirp_id = $1`, nil) 313 execDeleteChirpFromLikes = f(`DELETE FROM {{ table "ChirpLike" }} WHERE chirp_id = $1`, nil) 314 execDeleteChirpFromNotifications = f(`DELETE FROM {{ table "Notification" }} WHERE chirp_id = $1`, nil) 315 execDecementChirpReplies = f(`UPDATE {{ table "ChirpStats" }} SET replies = replies - 1 WHERE chirp_id = $1`, nil) 316 execDecementChirpRechirps = f(`UPDATE {{ table "ChirpStats" }} SET rechirps = rechirps - 1 WHERE chirp_id = $1`, nil) 317 execIncrementChirpLikes = f(`UPDATE {{ table "ChirpStats" }} SET likes = chirp_stats.likes + 1 WHERE chirp_id = $1`, nil) 318 execDecrementChirpLikes = f(`UPDATE {{ table "ChirpStats" }} SET likes = chirp_stats.likes - 1 WHERE chirp_id = $1`, nil) 319 execAddUserChirpsToFeed = f(`INSERT INTO {{ table "Feed" }} (user_id, timestamp_utc, chirp_id) 320 SELECT 321 $1 322 , published_utc 323 , id 324 FROM 325 {{ table "Chirp" }} c 326 WHERE 327 c.user_id = $2 328 AND NOT EXISTS (SELECT 1 FROM {{ table "Feed" }} f WHERE f.user_id = $1 and f.chirp_id = c.id) 329 `, nil) 330 execRemoveUserChirpsFromFeed = f(`DELETE FROM {{ table "Feed" }} WHERE user_id = $1 AND EXISTS (select 1 from chirp c where c.id = {{ table "Feed" }}.chirp_id AND c.user_id = $2)`, nil) 331 execMarkNotificationsRead = f(`UPDATE {{ table "Notification" }} SET read_utc = $3 WHERE user_id = $1 and id = any ($2)`, nil) 332 ) 333 334 // Manager is the datumsbase manager. 335 type Manager struct { 336 dbutil.BaseManager 337 } 338 339 // EnsureUserInfoOnCreate creates the rest of the user components on login. 340 func (m Manager) EnsureUserInfoOnCreate(ctx context.Context, user *apputil.User) error { 341 return m.Invoke(ctx).Create(&UserInfo{ 342 UserID: user.ID, 343 }) 344 } 345 346 // SessionState is an application specific session state. 347 type SessionState struct { 348 apputil.SessionState 349 UnreadNotifications int 350 UserInfo UserInfo 351 } 352 353 // EnsureUserInfoOnFetch fetches the rest of the user components. 354 func (m Manager) EnsureUserInfoOnFetch(ctx context.Context, session *web.Session) error { 355 existingState, ok := session.State.(apputil.SessionState) 356 if !ok { 357 return fmt.Errorf("invalid session state; expected apputil.SessionState") 358 } 359 user := existingState.User 360 var userInfo UserInfo 361 if _, err := m.Invoke(ctx).Get(&userInfo, user.ID); err != nil { 362 return err 363 } 364 unreadNotifications, err := m.NotificationsUnread(ctx, user.ID) 365 if err != nil { 366 return err 367 } 368 session.State = SessionState{ 369 SessionState: existingState, 370 UnreadNotifications: unreadNotifications, 371 UserInfo: userInfo, 372 } 373 return nil 374 } 375 376 // Audit creates a new audit log record. 377 func (m Manager) Audit(ctx context.Context, userID uuid.UUID, verb, noun, subject string) error { 378 return m.Invoke(ctx).Create(&Audit{ 379 TimestampUTC: time.Now().UTC(), 380 UserID: userID, 381 Verb: verb, 382 Noun: noun, 383 Subject: subject, 384 }) 385 } 386 387 // FollowsForUserID gets the users that follow a given user. 388 func (m Manager) FollowsForUserID(ctx context.Context, userID uuid.UUID) (output []uuid.UUID, err error) { 389 err = m.Invoke(ctx).Query(`select user_id from graph where target_id = $1 and type = $2`, userID, EdgeTypeFollow).OutMany(&output) 390 return 391 } 392 393 // FollowingForUserID gets the users that a given user follows. 394 func (m Manager) FollowingForUserID(ctx context.Context, userID uuid.UUID) (output []uuid.UUID, err error) { 395 err = m.Invoke(ctx).Query(`select target_id from graph where user_id = $1 and type = $2`, userID, EdgeTypeFollow).OutMany(&output) 396 return 397 } 398 399 // BlocksForUserID gets the users a given user blocks. 400 func (m Manager) BlocksForUserID(ctx context.Context, userID uuid.UUID) (output []uuid.UUID, err error) { 401 err = m.Invoke(ctx).Query(`select target_id from graph where user_id = $1 and type = $2`, userID, EdgeTypeBlock).OutMany(&output) 402 return 403 } 404 405 // BlocksForUserID gets the users a given user has muted. 406 func (m Manager) MutesForUserID(ctx context.Context, userID uuid.UUID) (output []uuid.UUID, err error) { 407 err = m.Invoke(ctx).Query(`select target_id from graph where user_id = $1 and type = $2`, userID, EdgeTypeMute).OutMany(&output) 408 return 409 } 410 411 // User returns a user by ID. 412 func (m Manager) User(ctx context.Context, selfUserID, userID uuid.UUID) (output UserFull, found bool, err error) { 413 found, err = m.Invoke(ctx).Query(queryUser, selfUserID, userID).Out(&output) 414 return 415 } 416 417 // UserByUsername returns a user by username. 418 func (m Manager) UserByUsername(ctx context.Context, selfUserID uuid.UUID, username string) (output UserFull, found bool, err error) { 419 found, err = m.Invoke(ctx).Query(queryUserByUsername, selfUserID, username).Out(&output) 420 return 421 } 422 423 // SearchUsers returns a list of users that match a search query. 424 func (m Manager) SearchUsers(ctx context.Context, userID uuid.UUID, query string) (output []UserFull, err error) { 425 err = m.Invoke(ctx).Query(querySearchUsers, userID, "%"+query+"%").OutMany(&output) 426 return 427 } 428 429 // Chirp gets a chirp by ID (and infers status based on the provided userID). 430 func (m Manager) Chirp(ctx context.Context, userID, id uuid.UUID) (c ChirpFull, found bool, err error) { 431 found, err = m.Invoke(ctx).Query(queryChirp, userID, id).Out(&c) 432 return 433 } 434 435 // FeedForUserID returns the feed for a given user, optionally after a given chirpID. 436 func (m Manager) FeedForUserID(ctx context.Context, userID uuid.UUID, take int, afterChirpID *uuid.UUID) (output []ChirpFull, err error) { 437 err = m.Invoke(ctx).Query(queryFeedForUserID, userID, take, afterChirpID).OutMany(&output) 438 return 439 } 440 441 // ProfilePosts returns the posts (non-reply, non-likes) for a given user, optionally after a given chirpID. 442 func (m Manager) ProfilePosts(ctx context.Context, selfUserID, userID uuid.UUID, take int, afterChirpID *uuid.UUID) (output []ChirpFull, err error) { 443 err = m.Invoke(ctx).Query(queryProfilePosts, selfUserID, userID, take, afterChirpID).OutMany(&output) 444 return 445 } 446 447 // ProfileReplies returns the posts (non-reply, non-likes) for a given user, optionally after a given chirpID. 448 func (m Manager) ProfileReplies(ctx context.Context, selfUserID, userID uuid.UUID, take int, afterChirpID *uuid.UUID) (output []ChirpFull, err error) { 449 err = m.Invoke(ctx).Query(queryProfileReplies, selfUserID, userID, take, afterChirpID).OutMany(&output) 450 return 451 } 452 453 // ProfileLikes returns the liked chirps for a given user. 454 func (m Manager) ProfileLikes(ctx context.Context, selfUserID, targetUserID uuid.UUID, take int, afterChirpID *uuid.UUID) (output []ChirpFull, err error) { 455 err = m.Invoke(ctx).Query(queryProfileLikes, selfUserID, targetUserID, take, afterChirpID).OutMany(&output) 456 return 457 } 458 459 // Notifications yields the last N notifications for a user 460 func (m Manager) Notifications(ctx context.Context, userID uuid.UUID, take int, afterNotificationID *uuid.UUID) (output []NotificationFull, err error) { 461 err = m.Invoke(ctx).Query(queryNotifications, userID, take, afterNotificationID).OutMany(&output) 462 return 463 } 464 465 // NotificationsUnread returns the unread notification count. 466 func (m Manager) NotificationsUnread(ctx context.Context, userID uuid.UUID) (output int, err error) { 467 _, err = m.Invoke(ctx).Query(queryNotificationsUnread, userID).Scan(&output) 468 return 469 } 470 471 // MarkNotificationsRead marks a list of notifications as read. 472 func (m Manager) MarkNotificationsRead(ctx context.Context, userID uuid.UUID, notificationIDs []uuid.UUID) error { 473 _, err := m.Invoke(ctx).Exec(execMarkNotificationsRead, userID, notificationIDs, time.Now().UTC()) 474 return err 475 } 476 477 // ChirpReplies returns the replies for a given chirp id. 478 func (m Manager) ChirpReplies(ctx context.Context, selfUserID, chirpID uuid.UUID) (output []ChirpFull, err error) { 479 err = m.Invoke(ctx).Query(queryChirpReplies, selfUserID, chirpID).OutMany(&output) 480 return 481 } 482 483 // CreateChirp creates the chirp staging it for publishing. 484 func (m Manager) CreateChirp(ctx context.Context, c *Chirp) (err error) { 485 c.CreatedUTC = time.Now().UTC() 486 c.PublishedUTC = nil 487 if err = m.Invoke(ctx).Create(c); err != nil { 488 return 489 } 490 if err = m.Invoke(ctx).Create(&ChirpStats{ 491 UserID: c.UserID, 492 ChirpID: c.ID, 493 }); err != nil { 494 return 495 } 496 return 497 } 498 499 // PublishChirp publishes the chirp, adding attachments and forwarding to the feed table. 500 func (m Manager) PublishChirp(ctx context.Context, c *Chirp) (err error) { 501 t := time.Now().UTC() 502 c.PublishedUTC = &t 503 if _, err = m.Invoke(ctx).Update(c); err != nil { 504 return 505 } 506 if c.ReplyID != nil && !c.ReplyID.IsZero() { 507 if _, err = m.Invoke(ctx).Exec(execIncrementChirpReplies, c.ReplyID); err != nil { 508 return 509 } 510 } 511 if c.QuotedID != nil && !c.QuotedID.IsZero() { 512 if _, err = m.Invoke(ctx).Exec(execIncrementChirpRechirps, c.QuotedID); err != nil { 513 return 514 } 515 } 516 517 // Publish for self 518 if err = m.Invoke(ctx).Create(Feed{ 519 UserID: c.UserID, 520 ChirpID: c.ID, 521 TimestampUTC: t, 522 }); err != nil { 523 return 524 } 525 526 // Publish for those that are following 527 var follows []uuid.UUID 528 follows, err = m.FollowsForUserID(ctx, c.UserID) 529 if err != nil { 530 return 531 } 532 for _, f := range follows { 533 if err = m.Invoke(ctx).Create(Feed{ 534 UserID: f, 535 ChirpID: c.ID, 536 TimestampUTC: t, 537 }); err != nil { 538 return 539 } 540 } 541 return 542 } 543 544 // DeleteChirp deletes a chirp and removes it from any referenced table(s). 545 func (m Manager) DeleteChirp(ctx context.Context, c Chirp) (deleted bool, err error) { 546 _, err = m.Invoke(ctx).Exec(execDeleteChirpFromFeeds, c.ID) 547 if err != nil { 548 return 549 } 550 _, err = m.Invoke(ctx).Exec(execDeleteChirpFromLikes, c.ID) 551 if err != nil { 552 return 553 } 554 _, err = m.Invoke(ctx).Exec(execDeleteChirpFromNotifications, c.ID) 555 if err != nil { 556 return 557 } 558 _, err = m.Invoke(ctx).Delete(ChirpStats{ChirpID: c.ID}) 559 if err != nil { 560 return 561 } 562 deleted, err = m.Invoke(ctx).Delete(c) 563 if err != nil { 564 return 565 } 566 if deleted { 567 if c.ReplyID != nil && !c.ReplyID.IsZero() { 568 if _, err = m.Invoke(ctx).Exec(execDecementChirpReplies, c.ReplyID); err != nil { 569 return 570 } 571 } 572 if c.QuotedID != nil && !c.QuotedID.IsZero() { 573 if _, err = m.Invoke(ctx).Exec(execDecementChirpRechirps, c.QuotedID); err != nil { 574 return 575 } 576 } 577 } 578 return 579 } 580 581 // LikeChirp applies a like to a chirp. 582 func (m Manager) LikeChirp(ctx context.Context, userID uuid.UUID, chirp Chirp) error { 583 if _, err := m.Invoke(ctx).Exec(execIncrementChirpLikes, chirp.ID); err != nil { 584 return err 585 } 586 if err := m.Invoke(ctx).Create(&ChirpLike{ 587 ChirpID: chirp.ID, 588 UserID: userID, 589 TimestampUTC: time.Now().UTC(), 590 }); err != nil { 591 return err 592 } 593 if !chirp.UserID.Equal(userID) { 594 if err := m.Invoke(ctx).Create(&Notification{ 595 CreatedUTC: time.Now().UTC(), 596 ActingUserID: userID, 597 UserID: chirp.UserID, 598 ChirpID: &chirp.ID, 599 Type: NotificationTypeLike, 600 }); err != nil { 601 return err 602 } 603 } 604 return nil 605 } 606 607 // UnlikeChirp undoes a like to a chirp. 608 func (m Manager) UnlikeChirp(ctx context.Context, userID, chirpID uuid.UUID) error { 609 deleted, err := m.Invoke(ctx).Delete(&ChirpLike{ 610 UserID: userID, 611 ChirpID: chirpID, 612 }) 613 if err != nil { 614 return err 615 } 616 if deleted { 617 if _, err := m.Invoke(ctx).Exec(execDecrementChirpLikes, chirpID); err != nil { 618 return err 619 } 620 } 621 return nil 622 } 623 624 // Follow adds a graph record that a given user follows a target user as well 625 // as adds that target user's chirps to the given user's feed. 626 func (m Manager) Follow(ctx context.Context, userID, targetUserID uuid.UUID) error { 627 if err := m.Invoke(ctx).Create(&Graph{ 628 TimestampUTC: time.Now().UTC(), 629 UserID: userID, 630 TargetID: targetUserID, 631 Type: EdgeTypeFollow, 632 }); err != nil { 633 return err 634 } 635 if _, err := m.Invoke(ctx).Exec(execAddUserChirpsToFeed, userID /* who's feed we're adding to */, targetUserID /* add this users chirps to my feed */); err != nil { 636 return err 637 } 638 if err := m.Invoke(ctx).Create(&Notification{ 639 CreatedUTC: time.Now().UTC(), 640 UserID: targetUserID, 641 ActingUserID: userID, 642 Type: NotificationTypeFollow, 643 }); err != nil { 644 return err 645 } 646 return nil 647 } 648 649 // Unfollow is the reverse of follow and removes a target user's chirps from a given 650 // user's feed and removes the graph record that the given user follows the target user. 651 func (m Manager) Unfollow(ctx context.Context, userID, targetUserID uuid.UUID) error { 652 if _, err := m.Invoke(ctx).Delete(&Graph{ 653 UserID: userID, 654 TargetID: targetUserID, 655 Type: EdgeTypeFollow, 656 }); err != nil { 657 return err 658 } 659 if _, err := m.Invoke(ctx).Exec(execRemoveUserChirpsFromFeed, userID /* who's feed we're removing from*/, targetUserID /*remove this users chirps from my feed */); err != nil { 660 return err 661 } 662 return nil 663 } 664 665 // Mute adds a graph record to indicate we should not show a target user's chirps 666 // in a given user's feed but we should not block the target user from following the 667 // given user or seeing their chirps. 668 func (m Manager) Mute(ctx context.Context, userID, targetUserID uuid.UUID) error { 669 if err := m.Invoke(ctx).Create(&Graph{ 670 TimestampUTC: time.Now().UTC(), 671 UserID: userID, 672 TargetID: targetUserID, 673 Type: EdgeTypeMute, 674 }); err != nil { 675 return err 676 } 677 return nil 678 } 679 680 // Unmute is the reverse of mute and allows a target user's chirps to appear 681 // in a given user's feed. 682 func (m Manager) Unmute(ctx context.Context, userID, targetUserID uuid.UUID) error { 683 if _, err := m.Invoke(ctx).Delete(&Graph{ 684 UserID: userID, 685 TargetID: targetUserID, 686 Type: EdgeTypeMute, 687 }); err != nil { 688 return err 689 } 690 return nil 691 } 692 693 // Block blocks a user. 694 func (m Manager) Block(ctx context.Context, userID, targetUserID uuid.UUID) error { 695 if err := m.Invoke(ctx).Create(&Graph{ 696 TimestampUTC: time.Now().UTC(), 697 UserID: userID, 698 TargetID: targetUserID, 699 Type: EdgeTypeBlock, 700 }); err != nil { 701 return err 702 } 703 return nil 704 } 705 706 // Unblock unblocks a user. 707 func (m Manager) Unblock(ctx context.Context, userID, targetUserID uuid.UUID) error { 708 if _, err := m.Invoke(ctx).Delete(&Graph{ 709 UserID: userID, 710 TargetID: targetUserID, 711 Type: EdgeTypeBlock, 712 }); err != nil { 713 return err 714 } 715 return nil 716 }