tenseleyflow/shithub / 6b1f39a

Browse files

Add social follow graph

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6b1f39a769984171266bbaa6f49be01699d171f9
Parents
d6510ce
Tree
a45086f

26 changed files

StatusFile+-
M internal/actions/sqlc/models.go 101 0
M internal/admin/sqlc/models.go 101 0
M internal/auth/audit/audit.go 2 0
M internal/auth/policy/sqlc/models.go 101 0
M internal/checks/sqlc/models.go 101 0
M internal/issues/sqlc/models.go 101 0
M internal/meta/sqlc/models.go 101 0
A internal/migrationsfs/migrations/0055_social_feed_trending.sql 78 0
M internal/notif/sqlc/models.go 101 0
M internal/orgs/sqlc/models.go 101 0
M internal/pulls/sqlc/models.go 101 0
M internal/ratelimit/sqlc/models.go 101 0
M internal/repos/sqlc/models.go 101 0
A internal/social/feed.go 399 0
A internal/social/follows.go 213 0
A internal/social/queries/feed.sql 199 0
A internal/social/queries/follows.sql 109 0
M internal/social/social.go 2 0
M internal/social/social_test.go 115 0
A internal/social/sqlc/feed.sql.go 527 0
A internal/social/sqlc/follows.sql.go 382 0
M internal/social/sqlc/models.go 101 0
M internal/social/sqlc/querier.go 24 0
M internal/users/sqlc/models.go 101 0
M internal/webhook/sqlc/models.go 101 0
M internal/worker/sqlc/models.go 101 0
internal/actions/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/admin/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/auth/audit/audit.gomodified
@@ -69,6 +69,8 @@ const (
6969
 	ActionStarDeleted           Action = "star_deleted"
7070
 	ActionWatchSet              Action = "watch_set"
7171
 	ActionWatchUnset            Action = "watch_unset"
72
+	ActionFollowCreated         Action = "follow_created"
73
+	ActionFollowDeleted         Action = "follow_deleted"
7274
 	ActionRepoForked            Action = "repo_forked"
7375
 	ActionRepoForkSynced        Action = "repo_fork_synced"
7476
 
internal/auth/policy/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/checks/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/issues/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/meta/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/notif/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/orgs/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/pulls/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/ratelimit/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/repos/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/social/feed.goadded
@@ -0,0 +1,399 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package social
4
+
5
+import (
6
+	"context"
7
+	"encoding/json"
8
+	"fmt"
9
+	"strconv"
10
+	"strings"
11
+	"time"
12
+
13
+	"github.com/jackc/pgx/v5/pgtype"
14
+
15
+	socialdb "github.com/tenseleyFlow/shithub/internal/social/sqlc"
16
+)
17
+
18
+const (
19
+	defaultFeedLimit     int32 = 30
20
+	defaultTrendingLimit int32 = 10
21
+)
22
+
23
+// FeedCursor is S42's keyset cursor over (created_at, event_id).
24
+type FeedCursor struct {
25
+	BeforeCreatedAt time.Time
26
+	BeforeID        int64
27
+}
28
+
29
+type FeedRepo struct {
30
+	ID              int64
31
+	Owner           string
32
+	Name            string
33
+	Description     string
34
+	PrimaryLanguage string
35
+	StarCount       int64
36
+	ForkCount       int64
37
+}
38
+
39
+type FeedItem struct {
40
+	ID               int64
41
+	Kind             string
42
+	Verb             string
43
+	ActorUsername    string
44
+	ActorDisplayName string
45
+	CreatedAt        time.Time
46
+	Repo             *FeedRepo
47
+	RepoFullName     string
48
+	RepoURL          string
49
+	SourceName       string
50
+	SourceURL        string
51
+	ItemTitle        string
52
+	ItemURL          string
53
+}
54
+
55
+type DashboardRepo struct {
56
+	ID              int64
57
+	Name            string
58
+	Description     string
59
+	Visibility      string
60
+	PrimaryLanguage string
61
+	StarCount       int64
62
+	ForkCount       int64
63
+	UpdatedAt       time.Time
64
+}
65
+
66
+type TrendingRepo struct {
67
+	RepoID          int64  `json:"repo_id"`
68
+	Owner           string `json:"owner"`
69
+	Name            string `json:"name"`
70
+	Description     string `json:"description"`
71
+	PrimaryLanguage string `json:"primary_language,omitempty"`
72
+	StarCount       int64  `json:"star_count"`
73
+	ForkCount       int64  `json:"fork_count"`
74
+	Score           int64  `json:"score"`
75
+}
76
+
77
+type TrendingUser struct {
78
+	UserID        int64  `json:"user_id"`
79
+	Username      string `json:"username"`
80
+	DisplayName   string `json:"display_name"`
81
+	Score         int64  `json:"score"`
82
+	FollowerDelta int64  `json:"follower_delta"`
83
+	EventCount    int64  `json:"event_count"`
84
+}
85
+
86
+// DashboardFeed returns public feed rows from followed users, followed
87
+// orgs, watched repos, and the viewer's own public activity.
88
+func DashboardFeed(ctx context.Context, deps Deps, viewerUserID int64, cursor FeedCursor, limit int32) ([]FeedItem, error) {
89
+	if viewerUserID == 0 {
90
+		return nil, ErrNotLoggedIn
91
+	}
92
+	if limit <= 0 || limit > 100 {
93
+		limit = defaultFeedLimit
94
+	}
95
+	params := socialdb.ListDashboardFeedEventsParams{
96
+		ViewerUserID: viewerUserID,
97
+		LimitCount:   limit,
98
+	}
99
+	if !cursor.BeforeCreatedAt.IsZero() && cursor.BeforeID > 0 {
100
+		params.BeforeCreatedAt = pgtype.Timestamptz{Time: cursor.BeforeCreatedAt, Valid: true}
101
+		params.BeforeID = pgtype.Int8{Int64: cursor.BeforeID, Valid: true}
102
+	}
103
+	rows, err := socialdb.New().ListDashboardFeedEvents(ctx, deps.Pool, params)
104
+	if err != nil {
105
+		return nil, fmt.Errorf("dashboard feed: %w", err)
106
+	}
107
+	out := make([]FeedItem, 0, len(rows))
108
+	for _, row := range rows {
109
+		out = append(out, feedItemFromDashboardRow(row))
110
+	}
111
+	return out, nil
112
+}
113
+
114
+// PublicFeed returns the global public activity feed used by Explore.
115
+func PublicFeed(ctx context.Context, deps Deps, cursor FeedCursor, limit int32) ([]FeedItem, error) {
116
+	if limit <= 0 || limit > 100 {
117
+		limit = defaultFeedLimit
118
+	}
119
+	params := socialdb.ListPublicFeedEventsParams{LimitCount: limit}
120
+	if !cursor.BeforeCreatedAt.IsZero() && cursor.BeforeID > 0 {
121
+		params.BeforeCreatedAt = pgtype.Timestamptz{Time: cursor.BeforeCreatedAt, Valid: true}
122
+		params.BeforeID = pgtype.Int8{Int64: cursor.BeforeID, Valid: true}
123
+	}
124
+	rows, err := socialdb.New().ListPublicFeedEvents(ctx, deps.Pool, params)
125
+	if err != nil {
126
+		return nil, fmt.Errorf("public feed: %w", err)
127
+	}
128
+	out := make([]FeedItem, 0, len(rows))
129
+	for _, row := range rows {
130
+		out = append(out, feedItemFromPublicRow(row))
131
+	}
132
+	return out, nil
133
+}
134
+
135
+func DashboardRepos(ctx context.Context, deps Deps, viewerUserID int64, limit int32) ([]DashboardRepo, error) {
136
+	if limit <= 0 || limit > 20 {
137
+		limit = 8
138
+	}
139
+	rows, err := socialdb.New().ListDashboardReposForUser(ctx, deps.Pool, socialdb.ListDashboardReposForUserParams{
140
+		OwnerUserID: pgtype.Int8{Int64: viewerUserID, Valid: true},
141
+		Limit:       limit,
142
+	})
143
+	if err != nil {
144
+		return nil, fmt.Errorf("dashboard repos: %w", err)
145
+	}
146
+	out := make([]DashboardRepo, 0, len(rows))
147
+	for _, row := range rows {
148
+		out = append(out, DashboardRepo{
149
+			ID: row.RepoID, Name: row.Name, Description: row.Description,
150
+			Visibility: string(row.Visibility), PrimaryLanguage: row.PrimaryLanguage,
151
+			StarCount: row.StarCount, ForkCount: row.ForkCount,
152
+			UpdatedAt: timeFromPG(row.UpdatedAt),
153
+		})
154
+	}
155
+	return out, nil
156
+}
157
+
158
+func TrendingRepos(ctx context.Context, deps Deps, windowDays, limit int32) ([]TrendingRepo, error) {
159
+	if windowDays <= 0 {
160
+		windowDays = 7
161
+	}
162
+	if limit <= 0 || limit > 50 {
163
+		limit = defaultTrendingLimit
164
+	}
165
+	rows, err := socialdb.New().ListTrendingRepos(ctx, deps.Pool, socialdb.ListTrendingReposParams{
166
+		LimitCount: limit,
167
+		WindowDays: windowDays,
168
+	})
169
+	if err != nil {
170
+		return nil, fmt.Errorf("trending repos: %w", err)
171
+	}
172
+	out := make([]TrendingRepo, 0, len(rows))
173
+	for _, row := range rows {
174
+		out = append(out, TrendingRepo{
175
+			RepoID: row.RepoID, Owner: row.Owner, Name: row.Name,
176
+			Description: row.Description, PrimaryLanguage: row.PrimaryLanguage,
177
+			StarCount: row.StarCount, ForkCount: row.ForkCount, Score: row.Score,
178
+		})
179
+	}
180
+	return out, nil
181
+}
182
+
183
+func TrendingUsers(ctx context.Context, deps Deps, windowDays, limit int32) ([]TrendingUser, error) {
184
+	if windowDays <= 0 {
185
+		windowDays = 7
186
+	}
187
+	if limit <= 0 || limit > 50 {
188
+		limit = defaultTrendingLimit
189
+	}
190
+	rows, err := socialdb.New().ListTrendingUsers(ctx, deps.Pool, socialdb.ListTrendingUsersParams{
191
+		LimitCount: limit,
192
+		WindowDays: windowDays,
193
+	})
194
+	if err != nil {
195
+		return nil, fmt.Errorf("trending users: %w", err)
196
+	}
197
+	out := make([]TrendingUser, 0, len(rows))
198
+	for _, row := range rows {
199
+		out = append(out, TrendingUser{
200
+			UserID: row.UserID, Username: row.Username, DisplayName: row.DisplayName,
201
+			Score: row.Score, FollowerDelta: row.FollowerDelta, EventCount: row.EventCount,
202
+		})
203
+	}
204
+	return out, nil
205
+}
206
+
207
+// CaptureTrendingSnapshots computes the S42 denormalized rankings for
208
+// day/week/month windows. It is idempotent in behavior: inserting a new
209
+// snapshot never mutates prior rows, so stale readers still have a valid
210
+// last-known ranking.
211
+func CaptureTrendingSnapshots(ctx context.Context, deps Deps) error {
212
+	q := socialdb.New()
213
+	windows := []struct {
214
+		scope socialdb.TrendingScope
215
+		days  int32
216
+	}{
217
+		{scope: socialdb.TrendingScopeDay, days: 1},
218
+		{scope: socialdb.TrendingScopeWeek, days: 7},
219
+		{scope: socialdb.TrendingScopeMonth, days: 30},
220
+	}
221
+	for _, window := range windows {
222
+		repos, err := TrendingRepos(ctx, deps, window.days, 50)
223
+		if err != nil {
224
+			return err
225
+		}
226
+		body, err := json.Marshal(repos)
227
+		if err != nil {
228
+			return fmt.Errorf("marshal trending repos: %w", err)
229
+		}
230
+		if _, err := q.InsertTrendingSnapshot(ctx, deps.Pool, socialdb.InsertTrendingSnapshotParams{
231
+			Scope: window.scope, Kind: socialdb.TrendingKindRepos, Payload: body,
232
+		}); err != nil {
233
+			return fmt.Errorf("insert trending repos snapshot: %w", err)
234
+		}
235
+
236
+		users, err := TrendingUsers(ctx, deps, window.days, 50)
237
+		if err != nil {
238
+			return err
239
+		}
240
+		body, err = json.Marshal(users)
241
+		if err != nil {
242
+			return fmt.Errorf("marshal trending users: %w", err)
243
+		}
244
+		if _, err := q.InsertTrendingSnapshot(ctx, deps.Pool, socialdb.InsertTrendingSnapshotParams{
245
+			Scope: window.scope, Kind: socialdb.TrendingKindUsers, Payload: body,
246
+		}); err != nil {
247
+			return fmt.Errorf("insert trending users snapshot: %w", err)
248
+		}
249
+	}
250
+	return nil
251
+}
252
+
253
+func feedItemFromDashboardRow(row socialdb.ListDashboardFeedEventsRow) FeedItem {
254
+	return feedItemFromParts(feedParts{
255
+		id: row.ID, kind: row.Kind, actorUsername: row.ActorUsername,
256
+		actorDisplayName: row.ActorDisplayName, createdAt: row.CreatedAt,
257
+		repoID: row.RepoID, repoOwner: row.RepoOwner, repoName: row.RepoName,
258
+		repoDescription: row.RepoDescription, repoPrimaryLanguage: row.RepoPrimaryLanguage,
259
+		repoStarCount: row.RepoStarCount, repoForkCount: row.RepoForkCount,
260
+		sourceName: row.SourceName, payload: row.Payload,
261
+	})
262
+}
263
+
264
+func feedItemFromPublicRow(row socialdb.ListPublicFeedEventsRow) FeedItem {
265
+	return feedItemFromParts(feedParts{
266
+		id: row.ID, kind: row.Kind, actorUsername: row.ActorUsername,
267
+		actorDisplayName: row.ActorDisplayName, createdAt: row.CreatedAt,
268
+		repoID: row.RepoID, repoOwner: row.RepoOwner, repoName: row.RepoName,
269
+		repoDescription: row.RepoDescription, repoPrimaryLanguage: row.RepoPrimaryLanguage,
270
+		repoStarCount: row.RepoStarCount, repoForkCount: row.RepoForkCount,
271
+		sourceName: row.SourceName, payload: row.Payload,
272
+	})
273
+}
274
+
275
+type feedParts struct {
276
+	id                  int64
277
+	kind                string
278
+	actorUsername       string
279
+	actorDisplayName    string
280
+	createdAt           pgtype.Timestamptz
281
+	repoID              pgtype.Int8
282
+	repoOwner           string
283
+	repoName            string
284
+	repoDescription     string
285
+	repoPrimaryLanguage string
286
+	repoStarCount       int64
287
+	repoForkCount       int64
288
+	sourceName          string
289
+	payload             []byte
290
+}
291
+
292
+func feedItemFromParts(p feedParts) FeedItem {
293
+	item := FeedItem{
294
+		ID: p.id, Kind: p.kind, Verb: feedVerb(p.kind),
295
+		ActorUsername: p.actorUsername, ActorDisplayName: p.actorDisplayName,
296
+		CreatedAt: timeFromPG(p.createdAt), SourceName: p.sourceName,
297
+	}
298
+	if p.repoID.Valid && p.repoOwner != "" && p.repoName != "" {
299
+		item.Repo = &FeedRepo{
300
+			ID: p.repoID.Int64, Owner: p.repoOwner, Name: p.repoName,
301
+			Description: p.repoDescription, PrimaryLanguage: p.repoPrimaryLanguage,
302
+			StarCount: p.repoStarCount, ForkCount: p.repoForkCount,
303
+		}
304
+		item.RepoFullName = p.repoOwner + "/" + p.repoName
305
+		item.RepoURL = "/" + item.RepoFullName
306
+	}
307
+	item.ItemTitle, item.ItemURL = feedItemTarget(p.kind, p.payload, item)
308
+	if item.SourceName != "" {
309
+		item.SourceURL = "/" + item.SourceName
310
+	}
311
+	return item
312
+}
313
+
314
+func feedVerb(kind string) string {
315
+	switch kind {
316
+	case "star":
317
+		return "starred"
318
+	case "unstar":
319
+		return "unstarred"
320
+	case "forked":
321
+		return "forked"
322
+	case "push":
323
+		return "pushed to"
324
+	case "repo_created":
325
+		return "created"
326
+	case "issue_created":
327
+		return "opened an issue in"
328
+	case "issue_comment_created":
329
+		return "commented on an issue in"
330
+	case "issue_closed":
331
+		return "closed an issue in"
332
+	case "issue_reopened":
333
+		return "reopened an issue in"
334
+	case "pr_opened":
335
+		return "opened a pull request in"
336
+	case "pr_comment_created":
337
+		return "commented on a pull request in"
338
+	case "pr_closed":
339
+		return "closed a pull request in"
340
+	case "pr_reopened":
341
+		return "reopened a pull request in"
342
+	case "pr_merged":
343
+		return "merged a pull request in"
344
+	case "followed_user", "followed_org":
345
+		return "followed"
346
+	default:
347
+		return strings.ReplaceAll(kind, "_", " ")
348
+	}
349
+}
350
+
351
+func feedItemTarget(kind string, payload []byte, item FeedItem) (string, string) {
352
+	switch kind {
353
+	case "followed_user", "followed_org":
354
+		if item.SourceName != "" {
355
+			return item.SourceName, "/" + item.SourceName
356
+		}
357
+		return "a profile", ""
358
+	}
359
+	data := map[string]any{}
360
+	_ = json.Unmarshal(payload, &data)
361
+	if title, _ := data["issue_title"].(string); title != "" {
362
+		if item.RepoURL == "" {
363
+			return title, ""
364
+		}
365
+		if n, ok := jsonNumberToInt(data["issue_number"]); ok {
366
+			section := "issues"
367
+			if strings.HasPrefix(kind, "pr_") {
368
+				section = "pulls"
369
+			}
370
+			return title, item.RepoURL + "/" + section + "/" + strconv.FormatInt(n, 10)
371
+		}
372
+		return title, item.RepoURL
373
+	}
374
+	if item.RepoFullName != "" {
375
+		return item.RepoFullName, item.RepoURL
376
+	}
377
+	return "", ""
378
+}
379
+
380
+func jsonNumberToInt(v any) (int64, bool) {
381
+	switch n := v.(type) {
382
+	case float64:
383
+		return int64(n), true
384
+	case int64:
385
+		return n, true
386
+	case string:
387
+		i, err := strconv.ParseInt(n, 10, 64)
388
+		return i, err == nil
389
+	default:
390
+		return 0, false
391
+	}
392
+}
393
+
394
+func timeFromPG(t pgtype.Timestamptz) time.Time {
395
+	if !t.Valid {
396
+		return time.Time{}
397
+	}
398
+	return t.Time
399
+}
internal/social/follows.goadded
@@ -0,0 +1,213 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package social
4
+
5
+import (
6
+	"context"
7
+	"fmt"
8
+	"time"
9
+
10
+	"github.com/jackc/pgx/v5/pgtype"
11
+
12
+	"github.com/tenseleyFlow/shithub/internal/auth/audit"
13
+	"github.com/tenseleyFlow/shithub/internal/auth/throttle"
14
+	socialdb "github.com/tenseleyFlow/shithub/internal/social/sqlc"
15
+)
16
+
17
+const (
18
+	followRateMax    = 200
19
+	followRateWindow = time.Hour
20
+)
21
+
22
+// FollowUser idempotently records actorUserID following targetUserID.
23
+// On a new edge it emits a public user-scoped domain event so S42 feed
24
+// surfaces can show "alice followed bob" style rows.
25
+func FollowUser(ctx context.Context, deps Deps, actorUserID, targetUserID int64) error {
26
+	if actorUserID == 0 {
27
+		return ErrNotLoggedIn
28
+	}
29
+	if actorUserID == targetUserID {
30
+		return ErrCannotFollowSelf
31
+	}
32
+	if err := hitFollowLimit(ctx, deps, actorUserID); err != nil {
33
+		return err
34
+	}
35
+
36
+	tx, err := deps.Pool.Begin(ctx)
37
+	if err != nil {
38
+		return err
39
+	}
40
+	committed := false
41
+	defer func() {
42
+		if !committed {
43
+			_ = tx.Rollback(ctx)
44
+		}
45
+	}()
46
+
47
+	q := socialdb.New()
48
+	inserted, err := q.FollowUser(ctx, tx, socialdb.FollowUserParams{
49
+		FollowerUserID: actorUserID,
50
+		FolloweeUserID: pgtype.Int8{Int64: targetUserID, Valid: true},
51
+	})
52
+	if err != nil {
53
+		return fmt.Errorf("follow user: %w", err)
54
+	}
55
+	if inserted {
56
+		if _, err := q.InsertDomainEvent(ctx, tx, socialdb.InsertDomainEventParams{
57
+			ActorUserID: pgInt(actorUserID),
58
+			Kind:        "followed_user",
59
+			RepoID:      pgInt(0),
60
+			SourceKind:  "user",
61
+			SourceID:    targetUserID,
62
+			Public:      true,
63
+			Payload:     []byte("{}"),
64
+		}); err != nil {
65
+			return fmt.Errorf("follow event: %w", err)
66
+		}
67
+	}
68
+	if err := tx.Commit(ctx); err != nil {
69
+		return err
70
+	}
71
+	committed = true
72
+
73
+	if inserted && deps.Audit != nil {
74
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
75
+			audit.ActionFollowCreated, audit.TargetUser, targetUserID, nil)
76
+	}
77
+	return nil
78
+}
79
+
80
+// UnfollowUser removes the actor->user follow edge. The operation is
81
+// idempotent so duplicate form submits are harmless.
82
+func UnfollowUser(ctx context.Context, deps Deps, actorUserID, targetUserID int64) error {
83
+	if actorUserID == 0 {
84
+		return ErrNotLoggedIn
85
+	}
86
+	if err := hitFollowLimit(ctx, deps, actorUserID); err != nil {
87
+		return err
88
+	}
89
+	rows, err := socialdb.New().UnfollowUser(ctx, deps.Pool, socialdb.UnfollowUserParams{
90
+		FollowerUserID: actorUserID,
91
+		FolloweeUserID: pgtype.Int8{Int64: targetUserID, Valid: true},
92
+	})
93
+	if err != nil {
94
+		return fmt.Errorf("unfollow user: %w", err)
95
+	}
96
+	if rows > 0 && deps.Audit != nil {
97
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
98
+			audit.ActionFollowDeleted, audit.TargetUser, targetUserID, nil)
99
+	}
100
+	return nil
101
+}
102
+
103
+// FollowOrg idempotently records actorUserID following an organization.
104
+// This is an S42-compatible extension over the user-only sprint text so
105
+// org-owned repos can participate in the same social feed.
106
+func FollowOrg(ctx context.Context, deps Deps, actorUserID, orgID int64) error {
107
+	if actorUserID == 0 {
108
+		return ErrNotLoggedIn
109
+	}
110
+	if err := hitFollowLimit(ctx, deps, actorUserID); err != nil {
111
+		return err
112
+	}
113
+
114
+	tx, err := deps.Pool.Begin(ctx)
115
+	if err != nil {
116
+		return err
117
+	}
118
+	committed := false
119
+	defer func() {
120
+		if !committed {
121
+			_ = tx.Rollback(ctx)
122
+		}
123
+	}()
124
+
125
+	q := socialdb.New()
126
+	inserted, err := q.FollowOrg(ctx, tx, socialdb.FollowOrgParams{
127
+		FollowerUserID: actorUserID,
128
+		FolloweeOrgID:  pgtype.Int8{Int64: orgID, Valid: true},
129
+	})
130
+	if err != nil {
131
+		return fmt.Errorf("follow org: %w", err)
132
+	}
133
+	if inserted {
134
+		if _, err := q.InsertDomainEvent(ctx, tx, socialdb.InsertDomainEventParams{
135
+			ActorUserID: pgInt(actorUserID),
136
+			Kind:        "followed_org",
137
+			RepoID:      pgInt(0),
138
+			SourceKind:  "org",
139
+			SourceID:    orgID,
140
+			Public:      true,
141
+			Payload:     []byte("{}"),
142
+		}); err != nil {
143
+			return fmt.Errorf("follow event: %w", err)
144
+		}
145
+	}
146
+	if err := tx.Commit(ctx); err != nil {
147
+		return err
148
+	}
149
+	committed = true
150
+
151
+	if inserted && deps.Audit != nil {
152
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
153
+			audit.ActionFollowCreated, audit.TargetOrg, orgID, nil)
154
+	}
155
+	return nil
156
+}
157
+
158
+// UnfollowOrg removes an actor->org follow edge.
159
+func UnfollowOrg(ctx context.Context, deps Deps, actorUserID, orgID int64) error {
160
+	if actorUserID == 0 {
161
+		return ErrNotLoggedIn
162
+	}
163
+	if err := hitFollowLimit(ctx, deps, actorUserID); err != nil {
164
+		return err
165
+	}
166
+	rows, err := socialdb.New().UnfollowOrg(ctx, deps.Pool, socialdb.UnfollowOrgParams{
167
+		FollowerUserID: actorUserID,
168
+		FolloweeOrgID:  pgtype.Int8{Int64: orgID, Valid: true},
169
+	})
170
+	if err != nil {
171
+		return fmt.Errorf("unfollow org: %w", err)
172
+	}
173
+	if rows > 0 && deps.Audit != nil {
174
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
175
+			audit.ActionFollowDeleted, audit.TargetOrg, orgID, nil)
176
+	}
177
+	return nil
178
+}
179
+
180
+func IsFollowingUser(ctx context.Context, deps Deps, followerUserID, targetUserID int64) (bool, error) {
181
+	if followerUserID == 0 {
182
+		return false, nil
183
+	}
184
+	return socialdb.New().IsFollowingUser(ctx, deps.Pool, socialdb.IsFollowingUserParams{
185
+		FollowerUserID: followerUserID,
186
+		FolloweeUserID: pgtype.Int8{Int64: targetUserID, Valid: true},
187
+	})
188
+}
189
+
190
+func IsFollowingOrg(ctx context.Context, deps Deps, followerUserID, orgID int64) (bool, error) {
191
+	if followerUserID == 0 {
192
+		return false, nil
193
+	}
194
+	return socialdb.New().IsFollowingOrg(ctx, deps.Pool, socialdb.IsFollowingOrgParams{
195
+		FollowerUserID: followerUserID,
196
+		FolloweeOrgID:  pgtype.Int8{Int64: orgID, Valid: true},
197
+	})
198
+}
199
+
200
+func hitFollowLimit(ctx context.Context, deps Deps, userID int64) error {
201
+	if deps.Limiter == nil {
202
+		return nil
203
+	}
204
+	if err := deps.Limiter.Hit(ctx, deps.Pool, throttle.Limit{
205
+		Scope:      "follow",
206
+		Identifier: fmt.Sprintf("user:%d", userID),
207
+		Max:        followRateMax,
208
+		Window:     followRateWindow,
209
+	}); err != nil {
210
+		return ErrFollowRateLimit
211
+	}
212
+	return nil
213
+}
internal/social/queries/feed.sqladded
@@ -0,0 +1,199 @@
1
+-- ─── activity feed / trending ─────────────────────────────────────
2
+
3
+-- name: ListDashboardFeedEvents :many
4
+SELECT
5
+    de.id, de.actor_user_id, de.kind, de.repo_id, de.source_kind,
6
+    de.source_id, de.public, de.payload, de.created_at,
7
+    actor.username AS actor_username,
8
+    actor.display_name AS actor_display_name,
9
+    COALESCE(r.name::text, '')::text AS repo_name,
10
+    COALESCE(r.description, '') AS repo_description,
11
+    COALESCE(r.primary_language, '') AS repo_primary_language,
12
+    COALESCE(r.star_count, 0)::bigint AS repo_star_count,
13
+    COALESCE(r.fork_count, 0)::bigint AS repo_fork_count,
14
+    COALESCE(owner_user.username::text, owner_org.slug::text, '')::text AS repo_owner,
15
+    COALESCE(source_user.username::text, source_org.slug::text, '')::text AS source_name
16
+FROM domain_events de
17
+JOIN users actor ON actor.id = de.actor_user_id
18
+LEFT JOIN repos r ON r.id = de.repo_id AND r.deleted_at IS NULL
19
+LEFT JOIN users owner_user ON owner_user.id = r.owner_user_id
20
+LEFT JOIN orgs owner_org ON owner_org.id = r.owner_org_id
21
+LEFT JOIN users source_user ON de.source_kind = 'user' AND source_user.id = de.source_id
22
+LEFT JOIN orgs source_org ON de.source_kind = 'org' AND source_org.id = de.source_id
23
+WHERE de.public = true
24
+  AND actor.suspended_at IS NULL
25
+  AND actor.deleted_at IS NULL
26
+  AND (
27
+      de.repo_id IS NULL
28
+      OR (r.id IS NOT NULL AND r.visibility = 'public')
29
+  )
30
+  AND (
31
+      de.actor_user_id = sqlc.arg(viewer_user_id)::bigint
32
+      OR de.actor_user_id IN (
33
+          SELECT followee_user_id FROM follows
34
+          WHERE follower_user_id = sqlc.arg(viewer_user_id)::bigint
35
+            AND followee_user_id IS NOT NULL
36
+      )
37
+      OR de.repo_id IN (
38
+          SELECT repo_id FROM watches
39
+          WHERE user_id = sqlc.arg(viewer_user_id)::bigint
40
+            AND level <> 'ignore'
41
+      )
42
+      OR (
43
+          r.owner_org_id IN (
44
+              SELECT followee_org_id FROM follows
45
+              WHERE follower_user_id = sqlc.arg(viewer_user_id)::bigint
46
+                AND followee_org_id IS NOT NULL
47
+          )
48
+      )
49
+      OR (
50
+          de.source_kind = 'org'
51
+          AND de.source_id IN (
52
+              SELECT followee_org_id FROM follows
53
+              WHERE follower_user_id = sqlc.arg(viewer_user_id)::bigint
54
+                AND followee_org_id IS NOT NULL
55
+          )
56
+      )
57
+  )
58
+  AND (
59
+      sqlc.narg(before_created_at)::timestamptz IS NULL
60
+      OR (de.created_at, de.id) < (
61
+          sqlc.narg(before_created_at)::timestamptz,
62
+          sqlc.narg(before_id)::bigint
63
+      )
64
+  )
65
+ORDER BY de.created_at DESC, de.id DESC
66
+LIMIT sqlc.arg(limit_count)::int;
67
+
68
+-- name: ListPublicFeedEvents :many
69
+SELECT
70
+    de.id, de.actor_user_id, de.kind, de.repo_id, de.source_kind,
71
+    de.source_id, de.public, de.payload, de.created_at,
72
+    actor.username AS actor_username,
73
+    actor.display_name AS actor_display_name,
74
+    COALESCE(r.name::text, '')::text AS repo_name,
75
+    COALESCE(r.description, '') AS repo_description,
76
+    COALESCE(r.primary_language, '') AS repo_primary_language,
77
+    COALESCE(r.star_count, 0)::bigint AS repo_star_count,
78
+    COALESCE(r.fork_count, 0)::bigint AS repo_fork_count,
79
+    COALESCE(owner_user.username::text, owner_org.slug::text, '')::text AS repo_owner,
80
+    COALESCE(source_user.username::text, source_org.slug::text, '')::text AS source_name
81
+FROM domain_events de
82
+JOIN users actor ON actor.id = de.actor_user_id
83
+LEFT JOIN repos r ON r.id = de.repo_id AND r.deleted_at IS NULL
84
+LEFT JOIN users owner_user ON owner_user.id = r.owner_user_id
85
+LEFT JOIN orgs owner_org ON owner_org.id = r.owner_org_id
86
+LEFT JOIN users source_user ON de.source_kind = 'user' AND source_user.id = de.source_id
87
+LEFT JOIN orgs source_org ON de.source_kind = 'org' AND source_org.id = de.source_id
88
+WHERE de.public = true
89
+  AND actor.suspended_at IS NULL
90
+  AND actor.deleted_at IS NULL
91
+  AND (
92
+      de.repo_id IS NULL
93
+      OR (r.id IS NOT NULL AND r.visibility = 'public')
94
+  )
95
+  AND (
96
+      sqlc.narg(before_created_at)::timestamptz IS NULL
97
+      OR (de.created_at, de.id) < (
98
+          sqlc.narg(before_created_at)::timestamptz,
99
+          sqlc.narg(before_id)::bigint
100
+      )
101
+  )
102
+ORDER BY de.created_at DESC, de.id DESC
103
+LIMIT sqlc.arg(limit_count)::int;
104
+
105
+-- name: ListTrendingRepos :many
106
+WITH recent AS (
107
+    SELECT
108
+        repo_id,
109
+        (
110
+            COUNT(*) FILTER (WHERE kind = 'star') * 3
111
+          + COUNT(*) FILTER (WHERE kind = 'forked') * 2
112
+          + COUNT(DISTINCT actor_user_id) FILTER (WHERE kind = 'push')
113
+        )::bigint AS score
114
+    FROM domain_events
115
+    WHERE public = true
116
+      AND repo_id IS NOT NULL
117
+      AND created_at >= now() - make_interval(days => sqlc.arg(window_days)::int)
118
+    GROUP BY repo_id
119
+)
120
+SELECT
121
+    r.id AS repo_id,
122
+    COALESCE(owner_user.username::text, owner_org.slug::text, '')::text AS owner,
123
+    r.name::text AS name,
124
+    r.description,
125
+    COALESCE(r.primary_language, '') AS primary_language,
126
+    r.star_count,
127
+    r.fork_count,
128
+    COALESCE(recent.score, 0)::bigint AS score,
129
+    r.updated_at
130
+FROM repos r
131
+LEFT JOIN recent ON recent.repo_id = r.id
132
+LEFT JOIN users owner_user ON owner_user.id = r.owner_user_id
133
+LEFT JOIN orgs owner_org ON owner_org.id = r.owner_org_id
134
+WHERE r.visibility = 'public'
135
+  AND r.deleted_at IS NULL
136
+  AND r.is_archived = false
137
+ORDER BY COALESCE(recent.score, 0) DESC, r.star_count DESC, r.updated_at DESC
138
+LIMIT sqlc.arg(limit_count)::int;
139
+
140
+-- name: ListTrendingUsers :many
141
+WITH recent_events AS (
142
+    SELECT actor_user_id AS user_id, COUNT(*)::bigint AS event_count
143
+    FROM domain_events
144
+    WHERE public = true
145
+      AND actor_user_id IS NOT NULL
146
+      AND created_at >= now() - make_interval(days => sqlc.arg(window_days)::int)
147
+    GROUP BY actor_user_id
148
+),
149
+recent_followers AS (
150
+    SELECT followee_user_id AS user_id, COUNT(*)::bigint AS follower_count
151
+    FROM follows
152
+    WHERE followee_user_id IS NOT NULL
153
+      AND followed_at >= now() - make_interval(days => sqlc.arg(window_days)::int)
154
+    GROUP BY followee_user_id
155
+)
156
+SELECT
157
+    u.id AS user_id,
158
+    u.username,
159
+    u.display_name,
160
+    (COALESCE(recent_followers.follower_count, 0) * 2 + COALESCE(recent_events.event_count, 0))::bigint AS score,
161
+    COALESCE(recent_followers.follower_count, 0)::bigint AS follower_delta,
162
+    COALESCE(recent_events.event_count, 0)::bigint AS event_count
163
+FROM users u
164
+LEFT JOIN recent_events ON recent_events.user_id = u.id
165
+LEFT JOIN recent_followers ON recent_followers.user_id = u.id
166
+WHERE u.suspended_at IS NULL
167
+  AND u.deleted_at IS NULL
168
+  AND (COALESCE(recent_followers.follower_count, 0) > 0 OR COALESCE(recent_events.event_count, 0) > 0)
169
+ORDER BY score DESC, u.created_at DESC
170
+LIMIT sqlc.arg(limit_count)::int;
171
+
172
+-- name: ListDashboardReposForUser :many
173
+SELECT
174
+    r.id AS repo_id,
175
+    r.name::text AS name,
176
+    r.description,
177
+    r.visibility,
178
+    COALESCE(r.primary_language, '') AS primary_language,
179
+    r.star_count,
180
+    r.fork_count,
181
+    r.updated_at
182
+FROM repos r
183
+WHERE r.owner_user_id = $1
184
+  AND r.deleted_at IS NULL
185
+ORDER BY r.updated_at DESC
186
+LIMIT $2;
187
+
188
+-- name: InsertTrendingSnapshot :one
189
+INSERT INTO trending_snapshots (scope, kind, payload)
190
+VALUES ($1, $2, $3)
191
+RETURNING id, scope, kind, captured_at, payload;
192
+
193
+-- name: LatestTrendingSnapshot :one
194
+SELECT id, scope, kind, captured_at, payload
195
+FROM trending_snapshots
196
+WHERE scope = $1
197
+  AND kind = $2
198
+ORDER BY captured_at DESC
199
+LIMIT 1;
internal/social/queries/follows.sqladded
@@ -0,0 +1,109 @@
1
+-- ─── follows ───────────────────────────────────────────────────────
2
+
3
+-- name: FollowUser :one
4
+WITH inserted AS (
5
+    INSERT INTO follows (follower_user_id, followee_user_id)
6
+    VALUES ($1, $2)
7
+    ON CONFLICT (follower_user_id, followee_user_id)
8
+        WHERE followee_user_id IS NOT NULL
9
+    DO NOTHING
10
+    RETURNING 1
11
+)
12
+SELECT EXISTS (SELECT 1 FROM inserted) AS inserted;
13
+
14
+-- name: UnfollowUser :execrows
15
+DELETE FROM follows
16
+WHERE follower_user_id = $1
17
+  AND followee_user_id = $2;
18
+
19
+-- name: FollowOrg :one
20
+WITH inserted AS (
21
+    INSERT INTO follows (follower_user_id, followee_org_id)
22
+    VALUES ($1, $2)
23
+    ON CONFLICT (follower_user_id, followee_org_id)
24
+        WHERE followee_org_id IS NOT NULL
25
+    DO NOTHING
26
+    RETURNING 1
27
+)
28
+SELECT EXISTS (SELECT 1 FROM inserted) AS inserted;
29
+
30
+-- name: UnfollowOrg :execrows
31
+DELETE FROM follows
32
+WHERE follower_user_id = $1
33
+  AND followee_org_id = $2;
34
+
35
+-- name: IsFollowingUser :one
36
+SELECT EXISTS (
37
+    SELECT 1 FROM follows
38
+    WHERE follower_user_id = $1
39
+      AND followee_user_id = $2
40
+) AS following;
41
+
42
+-- name: IsFollowingOrg :one
43
+SELECT EXISTS (
44
+    SELECT 1 FROM follows
45
+    WHERE follower_user_id = $1
46
+      AND followee_org_id = $2
47
+) AS following;
48
+
49
+-- name: CountFollowersForUser :one
50
+SELECT COUNT(*) FROM follows f
51
+JOIN users u ON u.id = f.follower_user_id
52
+WHERE f.followee_user_id = $1
53
+  AND u.suspended_at IS NULL
54
+  AND u.deleted_at IS NULL;
55
+
56
+-- name: CountFollowingForUser :one
57
+SELECT COUNT(*) FROM follows f
58
+LEFT JOIN users u ON u.id = f.followee_user_id
59
+LEFT JOIN orgs o ON o.id = f.followee_org_id
60
+WHERE f.follower_user_id = $1
61
+  AND (f.followee_user_id IS NULL OR (u.suspended_at IS NULL AND u.deleted_at IS NULL))
62
+  AND (f.followee_org_id IS NULL OR (o.suspended_at IS NULL AND o.deleted_at IS NULL));
63
+
64
+-- name: CountFollowersForOrg :one
65
+SELECT COUNT(*) FROM follows f
66
+JOIN users u ON u.id = f.follower_user_id
67
+WHERE f.followee_org_id = $1
68
+  AND u.suspended_at IS NULL
69
+  AND u.deleted_at IS NULL;
70
+
71
+-- name: ListFollowersForUser :many
72
+SELECT f.follower_user_id AS user_id, f.followed_at, u.username, u.display_name
73
+FROM follows f
74
+JOIN users u ON u.id = f.follower_user_id
75
+WHERE f.followee_user_id = $1
76
+  AND u.suspended_at IS NULL
77
+  AND u.deleted_at IS NULL
78
+ORDER BY f.followed_at DESC, f.id DESC
79
+LIMIT $2 OFFSET $3;
80
+
81
+-- name: ListFollowingUsersForUser :many
82
+SELECT f.followee_user_id AS user_id, f.followed_at, u.username, u.display_name
83
+FROM follows f
84
+JOIN users u ON u.id = f.followee_user_id
85
+WHERE f.follower_user_id = $1
86
+  AND u.suspended_at IS NULL
87
+  AND u.deleted_at IS NULL
88
+ORDER BY f.followed_at DESC, f.id DESC
89
+LIMIT $2 OFFSET $3;
90
+
91
+-- name: ListFollowingOrgsForUser :many
92
+SELECT f.followee_org_id AS org_id, f.followed_at, o.slug, o.display_name
93
+FROM follows f
94
+JOIN orgs o ON o.id = f.followee_org_id
95
+WHERE f.follower_user_id = $1
96
+  AND o.suspended_at IS NULL
97
+  AND o.deleted_at IS NULL
98
+ORDER BY f.followed_at DESC, f.id DESC
99
+LIMIT $2 OFFSET $3;
100
+
101
+-- name: ListFollowersForOrg :many
102
+SELECT f.follower_user_id AS user_id, f.followed_at, u.username, u.display_name
103
+FROM follows f
104
+JOIN users u ON u.id = f.follower_user_id
105
+WHERE f.followee_org_id = $1
106
+  AND u.suspended_at IS NULL
107
+  AND u.deleted_at IS NULL
108
+ORDER BY f.followed_at DESC, f.id DESC
109
+LIMIT $2 OFFSET $3;
internal/social/social.gomodified
@@ -33,6 +33,8 @@ type Deps struct {
3333
 // Errors surfaced to handlers.
3434
 var (
3535
 	ErrNotLoggedIn       = errors.New("social: login required")
36
+	ErrCannotFollowSelf  = errors.New("social: cannot follow yourself")
3637
 	ErrInvalidWatchLevel = errors.New("social: watch level must be all, participating, or ignore")
3738
 	ErrStarRateLimit     = errors.New("social: star/unstar rate limit exceeded")
39
+	ErrFollowRateLimit   = errors.New("social: follow/unfollow rate limit exceeded")
3840
 )
internal/social/social_test.gomodified
@@ -4,6 +4,7 @@ package social_test
44
 
55
 import (
66
 	"context"
7
+	"errors"
78
 	"io"
89
 	"log/slog"
910
 	"testing"
@@ -11,6 +12,7 @@ import (
1112
 	"github.com/jackc/pgx/v5/pgtype"
1213
 	"github.com/jackc/pgx/v5/pgxpool"
1314
 
15
+	orgsdb "github.com/tenseleyFlow/shithub/internal/orgs/sqlc"
1416
 	reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
1517
 	"github.com/tenseleyFlow/shithub/internal/social"
1618
 	socialdb "github.com/tenseleyFlow/shithub/internal/social/sqlc"
@@ -65,6 +67,20 @@ func mustCreateUser(t *testing.T, pool *pgxpool.Pool, username string) int64 {
6567
 	return u.ID
6668
 }
6769
 
70
+func mustCreateOrg(t *testing.T, pool *pgxpool.Pool, slug string, creatorID int64) int64 {
71
+	t.Helper()
72
+	o, err := orgsdb.New().CreateOrg(context.Background(), pool, orgsdb.CreateOrgParams{
73
+		Slug:            slug,
74
+		DisplayName:     slug,
75
+		BillingEmail:    slug + "@example.test",
76
+		CreatedByUserID: pgtype.Int8{Int64: creatorID, Valid: creatorID != 0},
77
+	})
78
+	if err != nil {
79
+		t.Fatalf("CreateOrg %s: %v", slug, err)
80
+	}
81
+	return o.ID
82
+}
83
+
6884
 func repoStarCount(t *testing.T, pool *pgxpool.Pool, repoID int64) int64 {
6985
 	t.Helper()
7086
 	r, err := reposdb.New().GetRepoByID(context.Background(), pool, repoID)
@@ -104,6 +120,105 @@ func TestStar_IncrementsCount_AndIsIdempotent(t *testing.T) {
104120
 	}
105121
 }
106122
 
123
+func TestFollowUser_IdempotentCountsAndEvent(t *testing.T) {
124
+	pool, deps, targetID, _ := setup(t)
125
+	followerID := mustCreateUser(t, pool, "bob")
126
+	ctx := context.Background()
127
+
128
+	if err := social.FollowUser(ctx, deps, followerID, targetID); err != nil {
129
+		t.Fatalf("FollowUser: %v", err)
130
+	}
131
+	if err := social.FollowUser(ctx, deps, followerID, targetID); err != nil {
132
+		t.Fatalf("FollowUser duplicate: %v", err)
133
+	}
134
+	q := socialdb.New()
135
+	followers, err := q.CountFollowersForUser(ctx, pool, pgtype.Int8{Int64: targetID, Valid: true})
136
+	if err != nil {
137
+		t.Fatalf("CountFollowersForUser: %v", err)
138
+	}
139
+	if followers != 1 {
140
+		t.Fatalf("followers = %d, want 1", followers)
141
+	}
142
+	following, err := q.CountFollowingForUser(ctx, pool, followerID)
143
+	if err != nil {
144
+		t.Fatalf("CountFollowingForUser: %v", err)
145
+	}
146
+	if following != 1 {
147
+		t.Fatalf("following = %d, want 1", following)
148
+	}
149
+	var eventCount int
150
+	if err := pool.QueryRow(ctx,
151
+		`SELECT count(*) FROM domain_events WHERE actor_user_id = $1 AND kind = 'followed_user'`,
152
+		followerID,
153
+	).Scan(&eventCount); err != nil {
154
+		t.Fatalf("count follow events: %v", err)
155
+	}
156
+	if eventCount != 1 {
157
+		t.Fatalf("event count = %d, want 1", eventCount)
158
+	}
159
+}
160
+
161
+func TestFollowUser_RejectsSelf(t *testing.T) {
162
+	_, deps, userID, _ := setup(t)
163
+	if err := social.FollowUser(context.Background(), deps, userID, userID); !errors.Is(err, social.ErrCannotFollowSelf) {
164
+		t.Fatalf("FollowUser self err = %v, want ErrCannotFollowSelf", err)
165
+	}
166
+}
167
+
168
+func TestFollowOrg_IdempotentCountsAndEvent(t *testing.T) {
169
+	pool, deps, creatorID, _ := setup(t)
170
+	followerID := mustCreateUser(t, pool, "bob")
171
+	orgID := mustCreateOrg(t, pool, "octo-org", creatorID)
172
+	ctx := context.Background()
173
+
174
+	if err := social.FollowOrg(ctx, deps, followerID, orgID); err != nil {
175
+		t.Fatalf("FollowOrg: %v", err)
176
+	}
177
+	if err := social.FollowOrg(ctx, deps, followerID, orgID); err != nil {
178
+		t.Fatalf("FollowOrg duplicate: %v", err)
179
+	}
180
+	followers, err := socialdb.New().CountFollowersForOrg(ctx, pool, pgtype.Int8{Int64: orgID, Valid: true})
181
+	if err != nil {
182
+		t.Fatalf("CountFollowersForOrg: %v", err)
183
+	}
184
+	if followers != 1 {
185
+		t.Fatalf("org followers = %d, want 1", followers)
186
+	}
187
+	var eventCount int
188
+	if err := pool.QueryRow(ctx,
189
+		`SELECT count(*) FROM domain_events WHERE actor_user_id = $1 AND kind = 'followed_org'`,
190
+		followerID,
191
+	).Scan(&eventCount); err != nil {
192
+		t.Fatalf("count org follow events: %v", err)
193
+	}
194
+	if eventCount != 1 {
195
+		t.Fatalf("event count = %d, want 1", eventCount)
196
+	}
197
+}
198
+
199
+func TestUnfollowUser_Idempotent(t *testing.T) {
200
+	pool, deps, targetID, _ := setup(t)
201
+	followerID := mustCreateUser(t, pool, "bob")
202
+	ctx := context.Background()
203
+
204
+	if err := social.FollowUser(ctx, deps, followerID, targetID); err != nil {
205
+		t.Fatalf("FollowUser: %v", err)
206
+	}
207
+	if err := social.UnfollowUser(ctx, deps, followerID, targetID); err != nil {
208
+		t.Fatalf("UnfollowUser: %v", err)
209
+	}
210
+	if err := social.UnfollowUser(ctx, deps, followerID, targetID); err != nil {
211
+		t.Fatalf("UnfollowUser duplicate: %v", err)
212
+	}
213
+	followers, err := socialdb.New().CountFollowersForUser(ctx, pool, pgtype.Int8{Int64: targetID, Valid: true})
214
+	if err != nil {
215
+		t.Fatalf("CountFollowersForUser: %v", err)
216
+	}
217
+	if followers != 0 {
218
+		t.Fatalf("followers = %d, want 0", followers)
219
+	}
220
+}
221
+
107222
 func TestUnstar_DecrementsCount_AndIsIdempotent(t *testing.T) {
108223
 	pool, deps, _, repoID := setup(t)
109224
 	uid := mustCreateUser(t, pool, "bob")
internal/social/sqlc/feed.sql.goadded
@@ -0,0 +1,527 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: feed.sql
5
+
6
+package socialdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const insertTrendingSnapshot = `-- name: InsertTrendingSnapshot :one
15
+INSERT INTO trending_snapshots (scope, kind, payload)
16
+VALUES ($1, $2, $3)
17
+RETURNING id, scope, kind, captured_at, payload
18
+`
19
+
20
+type InsertTrendingSnapshotParams struct {
21
+	Scope   TrendingScope
22
+	Kind    TrendingKind
23
+	Payload []byte
24
+}
25
+
26
+func (q *Queries) InsertTrendingSnapshot(ctx context.Context, db DBTX, arg InsertTrendingSnapshotParams) (TrendingSnapshot, error) {
27
+	row := db.QueryRow(ctx, insertTrendingSnapshot, arg.Scope, arg.Kind, arg.Payload)
28
+	var i TrendingSnapshot
29
+	err := row.Scan(
30
+		&i.ID,
31
+		&i.Scope,
32
+		&i.Kind,
33
+		&i.CapturedAt,
34
+		&i.Payload,
35
+	)
36
+	return i, err
37
+}
38
+
39
+const latestTrendingSnapshot = `-- name: LatestTrendingSnapshot :one
40
+SELECT id, scope, kind, captured_at, payload
41
+FROM trending_snapshots
42
+WHERE scope = $1
43
+  AND kind = $2
44
+ORDER BY captured_at DESC
45
+LIMIT 1
46
+`
47
+
48
+type LatestTrendingSnapshotParams struct {
49
+	Scope TrendingScope
50
+	Kind  TrendingKind
51
+}
52
+
53
+func (q *Queries) LatestTrendingSnapshot(ctx context.Context, db DBTX, arg LatestTrendingSnapshotParams) (TrendingSnapshot, error) {
54
+	row := db.QueryRow(ctx, latestTrendingSnapshot, arg.Scope, arg.Kind)
55
+	var i TrendingSnapshot
56
+	err := row.Scan(
57
+		&i.ID,
58
+		&i.Scope,
59
+		&i.Kind,
60
+		&i.CapturedAt,
61
+		&i.Payload,
62
+	)
63
+	return i, err
64
+}
65
+
66
+const listDashboardFeedEvents = `-- name: ListDashboardFeedEvents :many
67
+
68
+SELECT
69
+    de.id, de.actor_user_id, de.kind, de.repo_id, de.source_kind,
70
+    de.source_id, de.public, de.payload, de.created_at,
71
+    actor.username AS actor_username,
72
+    actor.display_name AS actor_display_name,
73
+    COALESCE(r.name::text, '')::text AS repo_name,
74
+    COALESCE(r.description, '') AS repo_description,
75
+    COALESCE(r.primary_language, '') AS repo_primary_language,
76
+    COALESCE(r.star_count, 0)::bigint AS repo_star_count,
77
+    COALESCE(r.fork_count, 0)::bigint AS repo_fork_count,
78
+    COALESCE(owner_user.username::text, owner_org.slug::text, '')::text AS repo_owner,
79
+    COALESCE(source_user.username::text, source_org.slug::text, '')::text AS source_name
80
+FROM domain_events de
81
+JOIN users actor ON actor.id = de.actor_user_id
82
+LEFT JOIN repos r ON r.id = de.repo_id AND r.deleted_at IS NULL
83
+LEFT JOIN users owner_user ON owner_user.id = r.owner_user_id
84
+LEFT JOIN orgs owner_org ON owner_org.id = r.owner_org_id
85
+LEFT JOIN users source_user ON de.source_kind = 'user' AND source_user.id = de.source_id
86
+LEFT JOIN orgs source_org ON de.source_kind = 'org' AND source_org.id = de.source_id
87
+WHERE de.public = true
88
+  AND actor.suspended_at IS NULL
89
+  AND actor.deleted_at IS NULL
90
+  AND (
91
+      de.repo_id IS NULL
92
+      OR (r.id IS NOT NULL AND r.visibility = 'public')
93
+  )
94
+  AND (
95
+      de.actor_user_id = $1::bigint
96
+      OR de.actor_user_id IN (
97
+          SELECT followee_user_id FROM follows
98
+          WHERE follower_user_id = $1::bigint
99
+            AND followee_user_id IS NOT NULL
100
+      )
101
+      OR de.repo_id IN (
102
+          SELECT repo_id FROM watches
103
+          WHERE user_id = $1::bigint
104
+            AND level <> 'ignore'
105
+      )
106
+      OR (
107
+          r.owner_org_id IN (
108
+              SELECT followee_org_id FROM follows
109
+              WHERE follower_user_id = $1::bigint
110
+                AND followee_org_id IS NOT NULL
111
+          )
112
+      )
113
+      OR (
114
+          de.source_kind = 'org'
115
+          AND de.source_id IN (
116
+              SELECT followee_org_id FROM follows
117
+              WHERE follower_user_id = $1::bigint
118
+                AND followee_org_id IS NOT NULL
119
+          )
120
+      )
121
+  )
122
+  AND (
123
+      $2::timestamptz IS NULL
124
+      OR (de.created_at, de.id) < (
125
+          $2::timestamptz,
126
+          $3::bigint
127
+      )
128
+  )
129
+ORDER BY de.created_at DESC, de.id DESC
130
+LIMIT $4::int
131
+`
132
+
133
+type ListDashboardFeedEventsParams struct {
134
+	ViewerUserID    int64
135
+	BeforeCreatedAt pgtype.Timestamptz
136
+	BeforeID        pgtype.Int8
137
+	LimitCount      int32
138
+}
139
+
140
+type ListDashboardFeedEventsRow struct {
141
+	ID                  int64
142
+	ActorUserID         pgtype.Int8
143
+	Kind                string
144
+	RepoID              pgtype.Int8
145
+	SourceKind          string
146
+	SourceID            int64
147
+	Public              bool
148
+	Payload             []byte
149
+	CreatedAt           pgtype.Timestamptz
150
+	ActorUsername       string
151
+	ActorDisplayName    string
152
+	RepoName            string
153
+	RepoDescription     string
154
+	RepoPrimaryLanguage string
155
+	RepoStarCount       int64
156
+	RepoForkCount       int64
157
+	RepoOwner           string
158
+	SourceName          string
159
+}
160
+
161
+// ─── activity feed / trending ─────────────────────────────────────
162
+func (q *Queries) ListDashboardFeedEvents(ctx context.Context, db DBTX, arg ListDashboardFeedEventsParams) ([]ListDashboardFeedEventsRow, error) {
163
+	rows, err := db.Query(ctx, listDashboardFeedEvents,
164
+		arg.ViewerUserID,
165
+		arg.BeforeCreatedAt,
166
+		arg.BeforeID,
167
+		arg.LimitCount,
168
+	)
169
+	if err != nil {
170
+		return nil, err
171
+	}
172
+	defer rows.Close()
173
+	items := []ListDashboardFeedEventsRow{}
174
+	for rows.Next() {
175
+		var i ListDashboardFeedEventsRow
176
+		if err := rows.Scan(
177
+			&i.ID,
178
+			&i.ActorUserID,
179
+			&i.Kind,
180
+			&i.RepoID,
181
+			&i.SourceKind,
182
+			&i.SourceID,
183
+			&i.Public,
184
+			&i.Payload,
185
+			&i.CreatedAt,
186
+			&i.ActorUsername,
187
+			&i.ActorDisplayName,
188
+			&i.RepoName,
189
+			&i.RepoDescription,
190
+			&i.RepoPrimaryLanguage,
191
+			&i.RepoStarCount,
192
+			&i.RepoForkCount,
193
+			&i.RepoOwner,
194
+			&i.SourceName,
195
+		); err != nil {
196
+			return nil, err
197
+		}
198
+		items = append(items, i)
199
+	}
200
+	if err := rows.Err(); err != nil {
201
+		return nil, err
202
+	}
203
+	return items, nil
204
+}
205
+
206
+const listDashboardReposForUser = `-- name: ListDashboardReposForUser :many
207
+SELECT
208
+    r.id AS repo_id,
209
+    r.name::text AS name,
210
+    r.description,
211
+    r.visibility,
212
+    COALESCE(r.primary_language, '') AS primary_language,
213
+    r.star_count,
214
+    r.fork_count,
215
+    r.updated_at
216
+FROM repos r
217
+WHERE r.owner_user_id = $1
218
+  AND r.deleted_at IS NULL
219
+ORDER BY r.updated_at DESC
220
+LIMIT $2
221
+`
222
+
223
+type ListDashboardReposForUserParams struct {
224
+	OwnerUserID pgtype.Int8
225
+	Limit       int32
226
+}
227
+
228
+type ListDashboardReposForUserRow struct {
229
+	RepoID          int64
230
+	Name            string
231
+	Description     string
232
+	Visibility      RepoVisibility
233
+	PrimaryLanguage string
234
+	StarCount       int64
235
+	ForkCount       int64
236
+	UpdatedAt       pgtype.Timestamptz
237
+}
238
+
239
+func (q *Queries) ListDashboardReposForUser(ctx context.Context, db DBTX, arg ListDashboardReposForUserParams) ([]ListDashboardReposForUserRow, error) {
240
+	rows, err := db.Query(ctx, listDashboardReposForUser, arg.OwnerUserID, arg.Limit)
241
+	if err != nil {
242
+		return nil, err
243
+	}
244
+	defer rows.Close()
245
+	items := []ListDashboardReposForUserRow{}
246
+	for rows.Next() {
247
+		var i ListDashboardReposForUserRow
248
+		if err := rows.Scan(
249
+			&i.RepoID,
250
+			&i.Name,
251
+			&i.Description,
252
+			&i.Visibility,
253
+			&i.PrimaryLanguage,
254
+			&i.StarCount,
255
+			&i.ForkCount,
256
+			&i.UpdatedAt,
257
+		); err != nil {
258
+			return nil, err
259
+		}
260
+		items = append(items, i)
261
+	}
262
+	if err := rows.Err(); err != nil {
263
+		return nil, err
264
+	}
265
+	return items, nil
266
+}
267
+
268
+const listPublicFeedEvents = `-- name: ListPublicFeedEvents :many
269
+SELECT
270
+    de.id, de.actor_user_id, de.kind, de.repo_id, de.source_kind,
271
+    de.source_id, de.public, de.payload, de.created_at,
272
+    actor.username AS actor_username,
273
+    actor.display_name AS actor_display_name,
274
+    COALESCE(r.name::text, '')::text AS repo_name,
275
+    COALESCE(r.description, '') AS repo_description,
276
+    COALESCE(r.primary_language, '') AS repo_primary_language,
277
+    COALESCE(r.star_count, 0)::bigint AS repo_star_count,
278
+    COALESCE(r.fork_count, 0)::bigint AS repo_fork_count,
279
+    COALESCE(owner_user.username::text, owner_org.slug::text, '')::text AS repo_owner,
280
+    COALESCE(source_user.username::text, source_org.slug::text, '')::text AS source_name
281
+FROM domain_events de
282
+JOIN users actor ON actor.id = de.actor_user_id
283
+LEFT JOIN repos r ON r.id = de.repo_id AND r.deleted_at IS NULL
284
+LEFT JOIN users owner_user ON owner_user.id = r.owner_user_id
285
+LEFT JOIN orgs owner_org ON owner_org.id = r.owner_org_id
286
+LEFT JOIN users source_user ON de.source_kind = 'user' AND source_user.id = de.source_id
287
+LEFT JOIN orgs source_org ON de.source_kind = 'org' AND source_org.id = de.source_id
288
+WHERE de.public = true
289
+  AND actor.suspended_at IS NULL
290
+  AND actor.deleted_at IS NULL
291
+  AND (
292
+      de.repo_id IS NULL
293
+      OR (r.id IS NOT NULL AND r.visibility = 'public')
294
+  )
295
+  AND (
296
+      $1::timestamptz IS NULL
297
+      OR (de.created_at, de.id) < (
298
+          $1::timestamptz,
299
+          $2::bigint
300
+      )
301
+  )
302
+ORDER BY de.created_at DESC, de.id DESC
303
+LIMIT $3::int
304
+`
305
+
306
+type ListPublicFeedEventsParams struct {
307
+	BeforeCreatedAt pgtype.Timestamptz
308
+	BeforeID        pgtype.Int8
309
+	LimitCount      int32
310
+}
311
+
312
+type ListPublicFeedEventsRow struct {
313
+	ID                  int64
314
+	ActorUserID         pgtype.Int8
315
+	Kind                string
316
+	RepoID              pgtype.Int8
317
+	SourceKind          string
318
+	SourceID            int64
319
+	Public              bool
320
+	Payload             []byte
321
+	CreatedAt           pgtype.Timestamptz
322
+	ActorUsername       string
323
+	ActorDisplayName    string
324
+	RepoName            string
325
+	RepoDescription     string
326
+	RepoPrimaryLanguage string
327
+	RepoStarCount       int64
328
+	RepoForkCount       int64
329
+	RepoOwner           string
330
+	SourceName          string
331
+}
332
+
333
+func (q *Queries) ListPublicFeedEvents(ctx context.Context, db DBTX, arg ListPublicFeedEventsParams) ([]ListPublicFeedEventsRow, error) {
334
+	rows, err := db.Query(ctx, listPublicFeedEvents, arg.BeforeCreatedAt, arg.BeforeID, arg.LimitCount)
335
+	if err != nil {
336
+		return nil, err
337
+	}
338
+	defer rows.Close()
339
+	items := []ListPublicFeedEventsRow{}
340
+	for rows.Next() {
341
+		var i ListPublicFeedEventsRow
342
+		if err := rows.Scan(
343
+			&i.ID,
344
+			&i.ActorUserID,
345
+			&i.Kind,
346
+			&i.RepoID,
347
+			&i.SourceKind,
348
+			&i.SourceID,
349
+			&i.Public,
350
+			&i.Payload,
351
+			&i.CreatedAt,
352
+			&i.ActorUsername,
353
+			&i.ActorDisplayName,
354
+			&i.RepoName,
355
+			&i.RepoDescription,
356
+			&i.RepoPrimaryLanguage,
357
+			&i.RepoStarCount,
358
+			&i.RepoForkCount,
359
+			&i.RepoOwner,
360
+			&i.SourceName,
361
+		); err != nil {
362
+			return nil, err
363
+		}
364
+		items = append(items, i)
365
+	}
366
+	if err := rows.Err(); err != nil {
367
+		return nil, err
368
+	}
369
+	return items, nil
370
+}
371
+
372
+const listTrendingRepos = `-- name: ListTrendingRepos :many
373
+WITH recent AS (
374
+    SELECT
375
+        repo_id,
376
+        (
377
+            COUNT(*) FILTER (WHERE kind = 'star') * 3
378
+          + COUNT(*) FILTER (WHERE kind = 'forked') * 2
379
+          + COUNT(DISTINCT actor_user_id) FILTER (WHERE kind = 'push')
380
+        )::bigint AS score
381
+    FROM domain_events
382
+    WHERE public = true
383
+      AND repo_id IS NOT NULL
384
+      AND created_at >= now() - make_interval(days => $2::int)
385
+    GROUP BY repo_id
386
+)
387
+SELECT
388
+    r.id AS repo_id,
389
+    COALESCE(owner_user.username::text, owner_org.slug::text, '')::text AS owner,
390
+    r.name::text AS name,
391
+    r.description,
392
+    COALESCE(r.primary_language, '') AS primary_language,
393
+    r.star_count,
394
+    r.fork_count,
395
+    COALESCE(recent.score, 0)::bigint AS score,
396
+    r.updated_at
397
+FROM repos r
398
+LEFT JOIN recent ON recent.repo_id = r.id
399
+LEFT JOIN users owner_user ON owner_user.id = r.owner_user_id
400
+LEFT JOIN orgs owner_org ON owner_org.id = r.owner_org_id
401
+WHERE r.visibility = 'public'
402
+  AND r.deleted_at IS NULL
403
+  AND r.is_archived = false
404
+ORDER BY COALESCE(recent.score, 0) DESC, r.star_count DESC, r.updated_at DESC
405
+LIMIT $1::int
406
+`
407
+
408
+type ListTrendingReposParams struct {
409
+	LimitCount int32
410
+	WindowDays int32
411
+}
412
+
413
+type ListTrendingReposRow struct {
414
+	RepoID          int64
415
+	Owner           string
416
+	Name            string
417
+	Description     string
418
+	PrimaryLanguage string
419
+	StarCount       int64
420
+	ForkCount       int64
421
+	Score           int64
422
+	UpdatedAt       pgtype.Timestamptz
423
+}
424
+
425
+func (q *Queries) ListTrendingRepos(ctx context.Context, db DBTX, arg ListTrendingReposParams) ([]ListTrendingReposRow, error) {
426
+	rows, err := db.Query(ctx, listTrendingRepos, arg.LimitCount, arg.WindowDays)
427
+	if err != nil {
428
+		return nil, err
429
+	}
430
+	defer rows.Close()
431
+	items := []ListTrendingReposRow{}
432
+	for rows.Next() {
433
+		var i ListTrendingReposRow
434
+		if err := rows.Scan(
435
+			&i.RepoID,
436
+			&i.Owner,
437
+			&i.Name,
438
+			&i.Description,
439
+			&i.PrimaryLanguage,
440
+			&i.StarCount,
441
+			&i.ForkCount,
442
+			&i.Score,
443
+			&i.UpdatedAt,
444
+		); err != nil {
445
+			return nil, err
446
+		}
447
+		items = append(items, i)
448
+	}
449
+	if err := rows.Err(); err != nil {
450
+		return nil, err
451
+	}
452
+	return items, nil
453
+}
454
+
455
+const listTrendingUsers = `-- name: ListTrendingUsers :many
456
+WITH recent_events AS (
457
+    SELECT actor_user_id AS user_id, COUNT(*)::bigint AS event_count
458
+    FROM domain_events
459
+    WHERE public = true
460
+      AND actor_user_id IS NOT NULL
461
+      AND created_at >= now() - make_interval(days => $2::int)
462
+    GROUP BY actor_user_id
463
+),
464
+recent_followers AS (
465
+    SELECT followee_user_id AS user_id, COUNT(*)::bigint AS follower_count
466
+    FROM follows
467
+    WHERE followee_user_id IS NOT NULL
468
+      AND followed_at >= now() - make_interval(days => $2::int)
469
+    GROUP BY followee_user_id
470
+)
471
+SELECT
472
+    u.id AS user_id,
473
+    u.username,
474
+    u.display_name,
475
+    (COALESCE(recent_followers.follower_count, 0) * 2 + COALESCE(recent_events.event_count, 0))::bigint AS score,
476
+    COALESCE(recent_followers.follower_count, 0)::bigint AS follower_delta,
477
+    COALESCE(recent_events.event_count, 0)::bigint AS event_count
478
+FROM users u
479
+LEFT JOIN recent_events ON recent_events.user_id = u.id
480
+LEFT JOIN recent_followers ON recent_followers.user_id = u.id
481
+WHERE u.suspended_at IS NULL
482
+  AND u.deleted_at IS NULL
483
+  AND (COALESCE(recent_followers.follower_count, 0) > 0 OR COALESCE(recent_events.event_count, 0) > 0)
484
+ORDER BY score DESC, u.created_at DESC
485
+LIMIT $1::int
486
+`
487
+
488
+type ListTrendingUsersParams struct {
489
+	LimitCount int32
490
+	WindowDays int32
491
+}
492
+
493
+type ListTrendingUsersRow struct {
494
+	UserID        int64
495
+	Username      string
496
+	DisplayName   string
497
+	Score         int64
498
+	FollowerDelta int64
499
+	EventCount    int64
500
+}
501
+
502
+func (q *Queries) ListTrendingUsers(ctx context.Context, db DBTX, arg ListTrendingUsersParams) ([]ListTrendingUsersRow, error) {
503
+	rows, err := db.Query(ctx, listTrendingUsers, arg.LimitCount, arg.WindowDays)
504
+	if err != nil {
505
+		return nil, err
506
+	}
507
+	defer rows.Close()
508
+	items := []ListTrendingUsersRow{}
509
+	for rows.Next() {
510
+		var i ListTrendingUsersRow
511
+		if err := rows.Scan(
512
+			&i.UserID,
513
+			&i.Username,
514
+			&i.DisplayName,
515
+			&i.Score,
516
+			&i.FollowerDelta,
517
+			&i.EventCount,
518
+		); err != nil {
519
+			return nil, err
520
+		}
521
+		items = append(items, i)
522
+	}
523
+	if err := rows.Err(); err != nil {
524
+		return nil, err
525
+	}
526
+	return items, nil
527
+}
internal/social/sqlc/follows.sql.goadded
@@ -0,0 +1,382 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: follows.sql
5
+
6
+package socialdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const countFollowersForOrg = `-- name: CountFollowersForOrg :one
15
+SELECT COUNT(*) FROM follows f
16
+JOIN users u ON u.id = f.follower_user_id
17
+WHERE f.followee_org_id = $1
18
+  AND u.suspended_at IS NULL
19
+  AND u.deleted_at IS NULL
20
+`
21
+
22
+func (q *Queries) CountFollowersForOrg(ctx context.Context, db DBTX, followeeOrgID pgtype.Int8) (int64, error) {
23
+	row := db.QueryRow(ctx, countFollowersForOrg, followeeOrgID)
24
+	var count int64
25
+	err := row.Scan(&count)
26
+	return count, err
27
+}
28
+
29
+const countFollowersForUser = `-- name: CountFollowersForUser :one
30
+SELECT COUNT(*) FROM follows f
31
+JOIN users u ON u.id = f.follower_user_id
32
+WHERE f.followee_user_id = $1
33
+  AND u.suspended_at IS NULL
34
+  AND u.deleted_at IS NULL
35
+`
36
+
37
+func (q *Queries) CountFollowersForUser(ctx context.Context, db DBTX, followeeUserID pgtype.Int8) (int64, error) {
38
+	row := db.QueryRow(ctx, countFollowersForUser, followeeUserID)
39
+	var count int64
40
+	err := row.Scan(&count)
41
+	return count, err
42
+}
43
+
44
+const countFollowingForUser = `-- name: CountFollowingForUser :one
45
+SELECT COUNT(*) FROM follows f
46
+LEFT JOIN users u ON u.id = f.followee_user_id
47
+LEFT JOIN orgs o ON o.id = f.followee_org_id
48
+WHERE f.follower_user_id = $1
49
+  AND (f.followee_user_id IS NULL OR (u.suspended_at IS NULL AND u.deleted_at IS NULL))
50
+  AND (f.followee_org_id IS NULL OR (o.suspended_at IS NULL AND o.deleted_at IS NULL))
51
+`
52
+
53
+func (q *Queries) CountFollowingForUser(ctx context.Context, db DBTX, followerUserID int64) (int64, error) {
54
+	row := db.QueryRow(ctx, countFollowingForUser, followerUserID)
55
+	var count int64
56
+	err := row.Scan(&count)
57
+	return count, err
58
+}
59
+
60
+const followOrg = `-- name: FollowOrg :one
61
+WITH inserted AS (
62
+    INSERT INTO follows (follower_user_id, followee_org_id)
63
+    VALUES ($1, $2)
64
+    ON CONFLICT (follower_user_id, followee_org_id)
65
+        WHERE followee_org_id IS NOT NULL
66
+    DO NOTHING
67
+    RETURNING 1
68
+)
69
+SELECT EXISTS (SELECT 1 FROM inserted) AS inserted
70
+`
71
+
72
+type FollowOrgParams struct {
73
+	FollowerUserID int64
74
+	FolloweeOrgID  pgtype.Int8
75
+}
76
+
77
+func (q *Queries) FollowOrg(ctx context.Context, db DBTX, arg FollowOrgParams) (bool, error) {
78
+	row := db.QueryRow(ctx, followOrg, arg.FollowerUserID, arg.FolloweeOrgID)
79
+	var inserted bool
80
+	err := row.Scan(&inserted)
81
+	return inserted, err
82
+}
83
+
84
+const followUser = `-- name: FollowUser :one
85
+
86
+WITH inserted AS (
87
+    INSERT INTO follows (follower_user_id, followee_user_id)
88
+    VALUES ($1, $2)
89
+    ON CONFLICT (follower_user_id, followee_user_id)
90
+        WHERE followee_user_id IS NOT NULL
91
+    DO NOTHING
92
+    RETURNING 1
93
+)
94
+SELECT EXISTS (SELECT 1 FROM inserted) AS inserted
95
+`
96
+
97
+type FollowUserParams struct {
98
+	FollowerUserID int64
99
+	FolloweeUserID pgtype.Int8
100
+}
101
+
102
+// ─── follows ───────────────────────────────────────────────────────
103
+func (q *Queries) FollowUser(ctx context.Context, db DBTX, arg FollowUserParams) (bool, error) {
104
+	row := db.QueryRow(ctx, followUser, arg.FollowerUserID, arg.FolloweeUserID)
105
+	var inserted bool
106
+	err := row.Scan(&inserted)
107
+	return inserted, err
108
+}
109
+
110
+const isFollowingOrg = `-- name: IsFollowingOrg :one
111
+SELECT EXISTS (
112
+    SELECT 1 FROM follows
113
+    WHERE follower_user_id = $1
114
+      AND followee_org_id = $2
115
+) AS following
116
+`
117
+
118
+type IsFollowingOrgParams struct {
119
+	FollowerUserID int64
120
+	FolloweeOrgID  pgtype.Int8
121
+}
122
+
123
+func (q *Queries) IsFollowingOrg(ctx context.Context, db DBTX, arg IsFollowingOrgParams) (bool, error) {
124
+	row := db.QueryRow(ctx, isFollowingOrg, arg.FollowerUserID, arg.FolloweeOrgID)
125
+	var following bool
126
+	err := row.Scan(&following)
127
+	return following, err
128
+}
129
+
130
+const isFollowingUser = `-- name: IsFollowingUser :one
131
+SELECT EXISTS (
132
+    SELECT 1 FROM follows
133
+    WHERE follower_user_id = $1
134
+      AND followee_user_id = $2
135
+) AS following
136
+`
137
+
138
+type IsFollowingUserParams struct {
139
+	FollowerUserID int64
140
+	FolloweeUserID pgtype.Int8
141
+}
142
+
143
+func (q *Queries) IsFollowingUser(ctx context.Context, db DBTX, arg IsFollowingUserParams) (bool, error) {
144
+	row := db.QueryRow(ctx, isFollowingUser, arg.FollowerUserID, arg.FolloweeUserID)
145
+	var following bool
146
+	err := row.Scan(&following)
147
+	return following, err
148
+}
149
+
150
+const listFollowersForOrg = `-- name: ListFollowersForOrg :many
151
+SELECT f.follower_user_id AS user_id, f.followed_at, u.username, u.display_name
152
+FROM follows f
153
+JOIN users u ON u.id = f.follower_user_id
154
+WHERE f.followee_org_id = $1
155
+  AND u.suspended_at IS NULL
156
+  AND u.deleted_at IS NULL
157
+ORDER BY f.followed_at DESC, f.id DESC
158
+LIMIT $2 OFFSET $3
159
+`
160
+
161
+type ListFollowersForOrgParams struct {
162
+	FolloweeOrgID pgtype.Int8
163
+	Limit         int32
164
+	Offset        int32
165
+}
166
+
167
+type ListFollowersForOrgRow struct {
168
+	UserID      int64
169
+	FollowedAt  pgtype.Timestamptz
170
+	Username    string
171
+	DisplayName string
172
+}
173
+
174
+func (q *Queries) ListFollowersForOrg(ctx context.Context, db DBTX, arg ListFollowersForOrgParams) ([]ListFollowersForOrgRow, error) {
175
+	rows, err := db.Query(ctx, listFollowersForOrg, arg.FolloweeOrgID, arg.Limit, arg.Offset)
176
+	if err != nil {
177
+		return nil, err
178
+	}
179
+	defer rows.Close()
180
+	items := []ListFollowersForOrgRow{}
181
+	for rows.Next() {
182
+		var i ListFollowersForOrgRow
183
+		if err := rows.Scan(
184
+			&i.UserID,
185
+			&i.FollowedAt,
186
+			&i.Username,
187
+			&i.DisplayName,
188
+		); err != nil {
189
+			return nil, err
190
+		}
191
+		items = append(items, i)
192
+	}
193
+	if err := rows.Err(); err != nil {
194
+		return nil, err
195
+	}
196
+	return items, nil
197
+}
198
+
199
+const listFollowersForUser = `-- name: ListFollowersForUser :many
200
+SELECT f.follower_user_id AS user_id, f.followed_at, u.username, u.display_name
201
+FROM follows f
202
+JOIN users u ON u.id = f.follower_user_id
203
+WHERE f.followee_user_id = $1
204
+  AND u.suspended_at IS NULL
205
+  AND u.deleted_at IS NULL
206
+ORDER BY f.followed_at DESC, f.id DESC
207
+LIMIT $2 OFFSET $3
208
+`
209
+
210
+type ListFollowersForUserParams struct {
211
+	FolloweeUserID pgtype.Int8
212
+	Limit          int32
213
+	Offset         int32
214
+}
215
+
216
+type ListFollowersForUserRow struct {
217
+	UserID      int64
218
+	FollowedAt  pgtype.Timestamptz
219
+	Username    string
220
+	DisplayName string
221
+}
222
+
223
+func (q *Queries) ListFollowersForUser(ctx context.Context, db DBTX, arg ListFollowersForUserParams) ([]ListFollowersForUserRow, error) {
224
+	rows, err := db.Query(ctx, listFollowersForUser, arg.FolloweeUserID, arg.Limit, arg.Offset)
225
+	if err != nil {
226
+		return nil, err
227
+	}
228
+	defer rows.Close()
229
+	items := []ListFollowersForUserRow{}
230
+	for rows.Next() {
231
+		var i ListFollowersForUserRow
232
+		if err := rows.Scan(
233
+			&i.UserID,
234
+			&i.FollowedAt,
235
+			&i.Username,
236
+			&i.DisplayName,
237
+		); err != nil {
238
+			return nil, err
239
+		}
240
+		items = append(items, i)
241
+	}
242
+	if err := rows.Err(); err != nil {
243
+		return nil, err
244
+	}
245
+	return items, nil
246
+}
247
+
248
+const listFollowingOrgsForUser = `-- name: ListFollowingOrgsForUser :many
249
+SELECT f.followee_org_id AS org_id, f.followed_at, o.slug, o.display_name
250
+FROM follows f
251
+JOIN orgs o ON o.id = f.followee_org_id
252
+WHERE f.follower_user_id = $1
253
+  AND o.suspended_at IS NULL
254
+  AND o.deleted_at IS NULL
255
+ORDER BY f.followed_at DESC, f.id DESC
256
+LIMIT $2 OFFSET $3
257
+`
258
+
259
+type ListFollowingOrgsForUserParams struct {
260
+	FollowerUserID int64
261
+	Limit          int32
262
+	Offset         int32
263
+}
264
+
265
+type ListFollowingOrgsForUserRow struct {
266
+	OrgID       pgtype.Int8
267
+	FollowedAt  pgtype.Timestamptz
268
+	Slug        string
269
+	DisplayName string
270
+}
271
+
272
+func (q *Queries) ListFollowingOrgsForUser(ctx context.Context, db DBTX, arg ListFollowingOrgsForUserParams) ([]ListFollowingOrgsForUserRow, error) {
273
+	rows, err := db.Query(ctx, listFollowingOrgsForUser, arg.FollowerUserID, arg.Limit, arg.Offset)
274
+	if err != nil {
275
+		return nil, err
276
+	}
277
+	defer rows.Close()
278
+	items := []ListFollowingOrgsForUserRow{}
279
+	for rows.Next() {
280
+		var i ListFollowingOrgsForUserRow
281
+		if err := rows.Scan(
282
+			&i.OrgID,
283
+			&i.FollowedAt,
284
+			&i.Slug,
285
+			&i.DisplayName,
286
+		); err != nil {
287
+			return nil, err
288
+		}
289
+		items = append(items, i)
290
+	}
291
+	if err := rows.Err(); err != nil {
292
+		return nil, err
293
+	}
294
+	return items, nil
295
+}
296
+
297
+const listFollowingUsersForUser = `-- name: ListFollowingUsersForUser :many
298
+SELECT f.followee_user_id AS user_id, f.followed_at, u.username, u.display_name
299
+FROM follows f
300
+JOIN users u ON u.id = f.followee_user_id
301
+WHERE f.follower_user_id = $1
302
+  AND u.suspended_at IS NULL
303
+  AND u.deleted_at IS NULL
304
+ORDER BY f.followed_at DESC, f.id DESC
305
+LIMIT $2 OFFSET $3
306
+`
307
+
308
+type ListFollowingUsersForUserParams struct {
309
+	FollowerUserID int64
310
+	Limit          int32
311
+	Offset         int32
312
+}
313
+
314
+type ListFollowingUsersForUserRow struct {
315
+	UserID      pgtype.Int8
316
+	FollowedAt  pgtype.Timestamptz
317
+	Username    string
318
+	DisplayName string
319
+}
320
+
321
+func (q *Queries) ListFollowingUsersForUser(ctx context.Context, db DBTX, arg ListFollowingUsersForUserParams) ([]ListFollowingUsersForUserRow, error) {
322
+	rows, err := db.Query(ctx, listFollowingUsersForUser, arg.FollowerUserID, arg.Limit, arg.Offset)
323
+	if err != nil {
324
+		return nil, err
325
+	}
326
+	defer rows.Close()
327
+	items := []ListFollowingUsersForUserRow{}
328
+	for rows.Next() {
329
+		var i ListFollowingUsersForUserRow
330
+		if err := rows.Scan(
331
+			&i.UserID,
332
+			&i.FollowedAt,
333
+			&i.Username,
334
+			&i.DisplayName,
335
+		); err != nil {
336
+			return nil, err
337
+		}
338
+		items = append(items, i)
339
+	}
340
+	if err := rows.Err(); err != nil {
341
+		return nil, err
342
+	}
343
+	return items, nil
344
+}
345
+
346
+const unfollowOrg = `-- name: UnfollowOrg :execrows
347
+DELETE FROM follows
348
+WHERE follower_user_id = $1
349
+  AND followee_org_id = $2
350
+`
351
+
352
+type UnfollowOrgParams struct {
353
+	FollowerUserID int64
354
+	FolloweeOrgID  pgtype.Int8
355
+}
356
+
357
+func (q *Queries) UnfollowOrg(ctx context.Context, db DBTX, arg UnfollowOrgParams) (int64, error) {
358
+	result, err := db.Exec(ctx, unfollowOrg, arg.FollowerUserID, arg.FolloweeOrgID)
359
+	if err != nil {
360
+		return 0, err
361
+	}
362
+	return result.RowsAffected(), nil
363
+}
364
+
365
+const unfollowUser = `-- name: UnfollowUser :execrows
366
+DELETE FROM follows
367
+WHERE follower_user_id = $1
368
+  AND followee_user_id = $2
369
+`
370
+
371
+type UnfollowUserParams struct {
372
+	FollowerUserID int64
373
+	FolloweeUserID pgtype.Int8
374
+}
375
+
376
+func (q *Queries) UnfollowUser(ctx context.Context, db DBTX, arg UnfollowUserParams) (int64, error) {
377
+	result, err := db.Exec(ctx, unfollowUser, arg.FollowerUserID, arg.FolloweeUserID)
378
+	if err != nil {
379
+		return 0, err
380
+	}
381
+	return result.RowsAffected(), nil
382
+}
internal/social/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/social/sqlc/querier.gomodified
@@ -6,9 +6,14 @@ package socialdb
66
 
77
 import (
88
 	"context"
9
+
10
+	"github.com/jackc/pgx/v5/pgtype"
911
 )
1012
 
1113
 type Querier interface {
14
+	CountFollowersForOrg(ctx context.Context, db DBTX, followeeOrgID pgtype.Int8) (int64, error)
15
+	CountFollowersForUser(ctx context.Context, db DBTX, followeeUserID pgtype.Int8) (int64, error)
16
+	CountFollowingForUser(ctx context.Context, db DBTX, followerUserID int64) (int64, error)
1217
 	CountStargazersForRepo(ctx context.Context, db DBTX, repoID int64) (int64, error)
1318
 	CountStarsForUser(ctx context.Context, db DBTX, userID int64) (int64, error)
1419
 	CountWatchersForRepo(ctx context.Context, db DBTX, repoID int64) (int64, error)
@@ -17,6 +22,9 @@ type Querier interface {
1722
 	// implicit `participating` default). Trigger drops the watcher_count
1823
 	// when the prior level wasn't 'ignore'.
1924
 	DeleteWatch(ctx context.Context, db DBTX, arg DeleteWatchParams) error
25
+	FollowOrg(ctx context.Context, db DBTX, arg FollowOrgParams) (bool, error)
26
+	// ─── follows ───────────────────────────────────────────────────────
27
+	FollowUser(ctx context.Context, db DBTX, arg FollowUserParams) (bool, error)
2028
 	// ─── watches ───────────────────────────────────────────────────────
2129
 	GetWatch(ctx context.Context, db DBTX, arg GetWatchParams) (Watch, error)
2230
 	HasStar(ctx context.Context, db DBTX, arg HasStarParams) (bool, error)
@@ -29,18 +37,30 @@ type Querier interface {
2937
 	// already-starred repo doesn't double-increment the count (the
3038
 	// AFTER INSERT trigger only fires on actual insert).
3139
 	InsertStar(ctx context.Context, db DBTX, arg InsertStarParams) error
40
+	InsertTrendingSnapshot(ctx context.Context, db DBTX, arg InsertTrendingSnapshotParams) (TrendingSnapshot, error)
3241
 	// Auto-watch flow: only insert if the user doesn't already have a
3342
 	// preference. ON CONFLICT DO NOTHING preserves the user's chosen
3443
 	// level when the trigger fires repeatedly.
3544
 	InsertWatchIfAbsent(ctx context.Context, db DBTX, arg InsertWatchIfAbsentParams) error
45
+	IsFollowingOrg(ctx context.Context, db DBTX, arg IsFollowingOrgParams) (bool, error)
46
+	IsFollowingUser(ctx context.Context, db DBTX, arg IsFollowingUserParams) (bool, error)
47
+	LatestTrendingSnapshot(ctx context.Context, db DBTX, arg LatestTrendingSnapshotParams) (TrendingSnapshot, error)
48
+	// ─── activity feed / trending ─────────────────────────────────────
49
+	ListDashboardFeedEvents(ctx context.Context, db DBTX, arg ListDashboardFeedEventsParams) ([]ListDashboardFeedEventsRow, error)
50
+	ListDashboardReposForUser(ctx context.Context, db DBTX, arg ListDashboardReposForUserParams) ([]ListDashboardReposForUserRow, error)
3651
 	// Repo-scoped events, recency-sorted. No visibility filter — the
3752
 	// caller has already established read access to the repo.
3853
 	ListEventsForRepo(ctx context.Context, db DBTX, arg ListEventsForRepoParams) ([]DomainEvent, error)
54
+	ListFollowersForOrg(ctx context.Context, db DBTX, arg ListFollowersForOrgParams) ([]ListFollowersForOrgRow, error)
55
+	ListFollowersForUser(ctx context.Context, db DBTX, arg ListFollowersForUserParams) ([]ListFollowersForUserRow, error)
56
+	ListFollowingOrgsForUser(ctx context.Context, db DBTX, arg ListFollowingOrgsForUserParams) ([]ListFollowingOrgsForUserRow, error)
57
+	ListFollowingUsersForUser(ctx context.Context, db DBTX, arg ListFollowingUsersForUserParams) ([]ListFollowingUsersForUserRow, error)
3958
 	// Public activity-feed slice for a user's profile. Returns only
4059
 	// public rows, recency-sorted. The handler additionally filters by
4160
 	// repo visibility against the viewer (a public event row on a repo
4261
 	// whose visibility flipped to private must not leak).
4362
 	ListPublicEventsForActor(ctx context.Context, db DBTX, arg ListPublicEventsForActorParams) ([]DomainEvent, error)
63
+	ListPublicFeedEvents(ctx context.Context, db DBTX, arg ListPublicFeedEventsParams) ([]ListPublicFeedEventsRow, error)
4464
 	// S29 notification-routing consumer: for fan-out, get every watcher
4565
 	// of a repo at the requested level (e.g. `level='all'` for new-issue
4666
 	// events). This is the cross-package read; expose the user_ids
@@ -55,9 +75,13 @@ type Querier interface {
5575
 	// user starred and lets the handler decide what to render. Sort axis
5676
 	// is the spec's day-1 lean: most-recently-starred first.
5777
 	ListStarsForUser(ctx context.Context, db DBTX, arg ListStarsForUserParams) ([]ListStarsForUserRow, error)
78
+	ListTrendingRepos(ctx context.Context, db DBTX, arg ListTrendingReposParams) ([]ListTrendingReposRow, error)
79
+	ListTrendingUsers(ctx context.Context, db DBTX, arg ListTrendingUsersParams) ([]ListTrendingUsersRow, error)
5880
 	// Watchers list. `level <> 'ignore'` excludes users who have actively
5981
 	// muted the repo. Excludes suspended users from public surfaces.
6082
 	ListWatchersForRepo(ctx context.Context, db DBTX, arg ListWatchersForRepoParams) ([]ListWatchersForRepoRow, error)
83
+	UnfollowOrg(ctx context.Context, db DBTX, arg UnfollowOrgParams) (int64, error)
84
+	UnfollowUser(ctx context.Context, db DBTX, arg UnfollowUserParams) (int64, error)
6185
 	// Always-write upsert. The AFTER trigger handles the watcher_count
6286
 	// delta on transition into / out of `ignore`.
6387
 	UpsertWatch(ctx context.Context, db DBTX, arg UpsertWatchParams) error
internal/users/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/webhook/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string
internal/worker/sqlc/models.gomodified
@@ -1095,6 +1095,91 @@ func (ns NullTransferStatus) Value() (driver.Value, error) {
10951095
 	return string(ns.TransferStatus), nil
10961096
 }
10971097
 
1098
+type TrendingKind string
1099
+
1100
+const (
1101
+	TrendingKindRepos TrendingKind = "repos"
1102
+	TrendingKindUsers TrendingKind = "users"
1103
+)
1104
+
1105
+func (e *TrendingKind) Scan(src interface{}) error {
1106
+	switch s := src.(type) {
1107
+	case []byte:
1108
+		*e = TrendingKind(s)
1109
+	case string:
1110
+		*e = TrendingKind(s)
1111
+	default:
1112
+		return fmt.Errorf("unsupported scan type for TrendingKind: %T", src)
1113
+	}
1114
+	return nil
1115
+}
1116
+
1117
+type NullTrendingKind struct {
1118
+	TrendingKind TrendingKind
1119
+	Valid        bool // Valid is true if TrendingKind is not NULL
1120
+}
1121
+
1122
+// Scan implements the Scanner interface.
1123
+func (ns *NullTrendingKind) Scan(value interface{}) error {
1124
+	if value == nil {
1125
+		ns.TrendingKind, ns.Valid = "", false
1126
+		return nil
1127
+	}
1128
+	ns.Valid = true
1129
+	return ns.TrendingKind.Scan(value)
1130
+}
1131
+
1132
+// Value implements the driver Valuer interface.
1133
+func (ns NullTrendingKind) Value() (driver.Value, error) {
1134
+	if !ns.Valid {
1135
+		return nil, nil
1136
+	}
1137
+	return string(ns.TrendingKind), nil
1138
+}
1139
+
1140
+type TrendingScope string
1141
+
1142
+const (
1143
+	TrendingScopeDay   TrendingScope = "day"
1144
+	TrendingScopeWeek  TrendingScope = "week"
1145
+	TrendingScopeMonth TrendingScope = "month"
1146
+)
1147
+
1148
+func (e *TrendingScope) Scan(src interface{}) error {
1149
+	switch s := src.(type) {
1150
+	case []byte:
1151
+		*e = TrendingScope(s)
1152
+	case string:
1153
+		*e = TrendingScope(s)
1154
+	default:
1155
+		return fmt.Errorf("unsupported scan type for TrendingScope: %T", src)
1156
+	}
1157
+	return nil
1158
+}
1159
+
1160
+type NullTrendingScope struct {
1161
+	TrendingScope TrendingScope
1162
+	Valid         bool // Valid is true if TrendingScope is not NULL
1163
+}
1164
+
1165
+// Scan implements the Scanner interface.
1166
+func (ns *NullTrendingScope) Scan(value interface{}) error {
1167
+	if value == nil {
1168
+		ns.TrendingScope, ns.Valid = "", false
1169
+		return nil
1170
+	}
1171
+	ns.Valid = true
1172
+	return ns.TrendingScope.Scan(value)
1173
+}
1174
+
1175
+// Value implements the driver Valuer interface.
1176
+func (ns NullTrendingScope) Value() (driver.Value, error) {
1177
+	if !ns.Valid {
1178
+		return nil, nil
1179
+	}
1180
+	return string(ns.TrendingScope), nil
1181
+}
1182
+
10981183
 type WatchLevel string
10991184
 
11001185
 const (
@@ -1605,6 +1690,14 @@ type EmailVerification struct {
16051690
 	CreatedAt   pgtype.Timestamptz
16061691
 }
16071692
 
1693
+type Follow struct {
1694
+	ID             int64
1695
+	FollowerUserID int64
1696
+	FolloweeUserID pgtype.Int8
1697
+	FolloweeOrgID  pgtype.Int8
1698
+	FollowedAt     pgtype.Timestamptz
1699
+}
1700
+
16081701
 type Issue struct {
16091702
 	ID                int64
16101703
 	RepoID            int64
@@ -2098,6 +2191,14 @@ type TransactionalEmailLog struct {
20982191
 	DeliveredAt     pgtype.Timestamptz
20992192
 }
21002193
 
2194
+type TrendingSnapshot struct {
2195
+	ID         int64
2196
+	Scope      TrendingScope
2197
+	Kind       TrendingKind
2198
+	CapturedAt pgtype.Timestamptz
2199
+	Payload    []byte
2200
+}
2201
+
21012202
 type User struct {
21022203
 	ID                int64
21032204
 	Username          string