1// Copyright 2014 The Gogs Authors. All rights reserved. 2// Copyright 2019 The Gitea Authors. All rights reserved. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package models 7 8import ( 9 "context" 10 "fmt" 11 "net/url" 12 "path" 13 "strconv" 14 "strings" 15 "time" 16 17 "code.gitea.io/gitea/models/db" 18 repo_model "code.gitea.io/gitea/models/repo" 19 "code.gitea.io/gitea/models/unit" 20 user_model "code.gitea.io/gitea/models/user" 21 "code.gitea.io/gitea/modules/base" 22 "code.gitea.io/gitea/modules/git" 23 "code.gitea.io/gitea/modules/log" 24 "code.gitea.io/gitea/modules/setting" 25 "code.gitea.io/gitea/modules/timeutil" 26 "code.gitea.io/gitea/modules/util" 27 28 "xorm.io/builder" 29) 30 31// ActionType represents the type of an action. 32type ActionType int 33 34// Possible action types. 35const ( 36 ActionCreateRepo ActionType = iota + 1 // 1 37 ActionRenameRepo // 2 38 ActionStarRepo // 3 39 ActionWatchRepo // 4 40 ActionCommitRepo // 5 41 ActionCreateIssue // 6 42 ActionCreatePullRequest // 7 43 ActionTransferRepo // 8 44 ActionPushTag // 9 45 ActionCommentIssue // 10 46 ActionMergePullRequest // 11 47 ActionCloseIssue // 12 48 ActionReopenIssue // 13 49 ActionClosePullRequest // 14 50 ActionReopenPullRequest // 15 51 ActionDeleteTag // 16 52 ActionDeleteBranch // 17 53 ActionMirrorSyncPush // 18 54 ActionMirrorSyncCreate // 19 55 ActionMirrorSyncDelete // 20 56 ActionApprovePullRequest // 21 57 ActionRejectPullRequest // 22 58 ActionCommentPull // 23 59 ActionPublishRelease // 24 60 ActionPullReviewDismissed // 25 61 ActionPullRequestReadyForReview // 26 62) 63 64// Action represents user operation type and other information to 65// repository. It implemented interface base.Actioner so that can be 66// used in template render. 67type Action struct { 68 ID int64 `xorm:"pk autoincr"` 69 UserID int64 `xorm:"INDEX"` // Receiver user id. 70 OpType ActionType 71 ActUserID int64 `xorm:"INDEX"` // Action user id. 72 ActUser *user_model.User `xorm:"-"` 73 RepoID int64 `xorm:"INDEX"` 74 Repo *repo_model.Repository `xorm:"-"` 75 CommentID int64 `xorm:"INDEX"` 76 Comment *Comment `xorm:"-"` 77 IsDeleted bool `xorm:"INDEX NOT NULL DEFAULT false"` 78 RefName string 79 IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"` 80 Content string `xorm:"TEXT"` 81 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 82} 83 84func init() { 85 db.RegisterModel(new(Action)) 86} 87 88// GetOpType gets the ActionType of this action. 89func (a *Action) GetOpType() ActionType { 90 return a.OpType 91} 92 93// LoadActUser loads a.ActUser 94func (a *Action) LoadActUser() { 95 if a.ActUser != nil { 96 return 97 } 98 var err error 99 a.ActUser, err = user_model.GetUserByID(a.ActUserID) 100 if err == nil { 101 return 102 } else if user_model.IsErrUserNotExist(err) { 103 a.ActUser = user_model.NewGhostUser() 104 } else { 105 log.Error("GetUserByID(%d): %v", a.ActUserID, err) 106 } 107} 108 109func (a *Action) loadRepo() { 110 if a.Repo != nil { 111 return 112 } 113 var err error 114 a.Repo, err = repo_model.GetRepositoryByID(a.RepoID) 115 if err != nil { 116 log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) 117 } 118} 119 120// GetActFullName gets the action's user full name. 121func (a *Action) GetActFullName() string { 122 a.LoadActUser() 123 return a.ActUser.FullName 124} 125 126// GetActUserName gets the action's user name. 127func (a *Action) GetActUserName() string { 128 a.LoadActUser() 129 return a.ActUser.Name 130} 131 132// ShortActUserName gets the action's user name trimmed to max 20 133// chars. 134func (a *Action) ShortActUserName() string { 135 return base.EllipsisString(a.GetActUserName(), 20) 136} 137 138// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. 139func (a *Action) GetDisplayName() string { 140 if setting.UI.DefaultShowFullName { 141 trimmedFullName := strings.TrimSpace(a.GetActFullName()) 142 if len(trimmedFullName) > 0 { 143 return trimmedFullName 144 } 145 } 146 return a.ShortActUserName() 147} 148 149// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME 150func (a *Action) GetDisplayNameTitle() string { 151 if setting.UI.DefaultShowFullName { 152 return a.ShortActUserName() 153 } 154 return a.GetActFullName() 155} 156 157// GetRepoUserName returns the name of the action repository owner. 158func (a *Action) GetRepoUserName() string { 159 a.loadRepo() 160 return a.Repo.OwnerName 161} 162 163// ShortRepoUserName returns the name of the action repository owner 164// trimmed to max 20 chars. 165func (a *Action) ShortRepoUserName() string { 166 return base.EllipsisString(a.GetRepoUserName(), 20) 167} 168 169// GetRepoName returns the name of the action repository. 170func (a *Action) GetRepoName() string { 171 a.loadRepo() 172 return a.Repo.Name 173} 174 175// ShortRepoName returns the name of the action repository 176// trimmed to max 33 chars. 177func (a *Action) ShortRepoName() string { 178 return base.EllipsisString(a.GetRepoName(), 33) 179} 180 181// GetRepoPath returns the virtual path to the action repository. 182func (a *Action) GetRepoPath() string { 183 return path.Join(a.GetRepoUserName(), a.GetRepoName()) 184} 185 186// ShortRepoPath returns the virtual path to the action repository 187// trimmed to max 20 + 1 + 33 chars. 188func (a *Action) ShortRepoPath() string { 189 return path.Join(a.ShortRepoUserName(), a.ShortRepoName()) 190} 191 192// GetRepoLink returns relative link to action repository. 193func (a *Action) GetRepoLink() string { 194 // path.Join will skip empty strings 195 return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName()), url.PathEscape(a.GetRepoName())) 196} 197 198// GetRepositoryFromMatch returns a *repo_model.Repository from a username and repo strings 199func GetRepositoryFromMatch(ownerName, repoName string) (*repo_model.Repository, error) { 200 var err error 201 refRepo, err := repo_model.GetRepositoryByOwnerAndName(ownerName, repoName) 202 if err != nil { 203 if repo_model.IsErrRepoNotExist(err) { 204 log.Warn("Repository referenced in commit but does not exist: %v", err) 205 return nil, err 206 } 207 log.Error("repo_model.GetRepositoryByOwnerAndName: %v", err) 208 return nil, err 209 } 210 return refRepo, nil 211} 212 213// GetCommentLink returns link to action comment. 214func (a *Action) GetCommentLink() string { 215 return a.getCommentLink(db.DefaultContext) 216} 217 218func (a *Action) getCommentLink(ctx context.Context) string { 219 if a == nil { 220 return "#" 221 } 222 e := db.GetEngine(ctx) 223 if a.Comment == nil && a.CommentID != 0 { 224 a.Comment, _ = getCommentByID(e, a.CommentID) 225 } 226 if a.Comment != nil { 227 return a.Comment.HTMLURL() 228 } 229 if len(a.GetIssueInfos()) == 0 { 230 return "#" 231 } 232 // Return link to issue 233 issueIDString := a.GetIssueInfos()[0] 234 issueID, err := strconv.ParseInt(issueIDString, 10, 64) 235 if err != nil { 236 return "#" 237 } 238 239 issue, err := getIssueByID(e, issueID) 240 if err != nil { 241 return "#" 242 } 243 244 if err = issue.loadRepo(ctx); err != nil { 245 return "#" 246 } 247 248 return issue.HTMLURL() 249} 250 251// GetBranch returns the action's repository branch. 252func (a *Action) GetBranch() string { 253 return strings.TrimPrefix(a.RefName, git.BranchPrefix) 254} 255 256// GetRefLink returns the action's ref link. 257func (a *Action) GetRefLink() string { 258 switch { 259 case strings.HasPrefix(a.RefName, git.BranchPrefix): 260 return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix)) 261 case strings.HasPrefix(a.RefName, git.TagPrefix): 262 return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix)) 263 case len(a.RefName) == 40 && git.SHAPattern.MatchString(a.RefName): 264 return a.GetRepoLink() + "/src/commit/" + a.RefName 265 default: 266 // FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here. 267 return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix)) 268 } 269} 270 271// GetTag returns the action's repository tag. 272func (a *Action) GetTag() string { 273 return strings.TrimPrefix(a.RefName, git.TagPrefix) 274} 275 276// GetContent returns the action's content. 277func (a *Action) GetContent() string { 278 return a.Content 279} 280 281// GetCreate returns the action creation time. 282func (a *Action) GetCreate() time.Time { 283 return a.CreatedUnix.AsTime() 284} 285 286// GetIssueInfos returns a list of issues associated with 287// the action. 288func (a *Action) GetIssueInfos() []string { 289 return strings.SplitN(a.Content, "|", 3) 290} 291 292// GetIssueTitle returns the title of first issue associated 293// with the action. 294func (a *Action) GetIssueTitle() string { 295 index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) 296 issue, err := GetIssueByIndex(a.RepoID, index) 297 if err != nil { 298 log.Error("GetIssueByIndex: %v", err) 299 return "500 when get issue" 300 } 301 return issue.Title 302} 303 304// GetIssueContent returns the content of first issue associated with 305// this action. 306func (a *Action) GetIssueContent() string { 307 index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) 308 issue, err := GetIssueByIndex(a.RepoID, index) 309 if err != nil { 310 log.Error("GetIssueByIndex: %v", err) 311 return "500 when get issue" 312 } 313 return issue.Content 314} 315 316// GetFeedsOptions options for retrieving feeds 317type GetFeedsOptions struct { 318 RequestedUser *user_model.User // the user we want activity for 319 RequestedTeam *Team // the team we want activity for 320 Actor *user_model.User // the user viewing the activity 321 IncludePrivate bool // include private actions 322 OnlyPerformedBy bool // only actions performed by requested user 323 IncludeDeleted bool // include deleted actions 324 Date string // the day we want activity for: YYYY-MM-DD 325} 326 327// GetFeeds returns actions according to the provided options 328func GetFeeds(opts GetFeedsOptions) ([]*Action, error) { 329 if !activityReadable(opts.RequestedUser, opts.Actor) { 330 return make([]*Action, 0), nil 331 } 332 333 cond, err := activityQueryCondition(opts) 334 if err != nil { 335 return nil, err 336 } 337 338 actions := make([]*Action, 0, setting.UI.FeedPagingNum) 339 340 if err := db.GetEngine(db.DefaultContext).Limit(setting.UI.FeedPagingNum).Desc("created_unix").Where(cond).Find(&actions); err != nil { 341 return nil, fmt.Errorf("Find: %v", err) 342 } 343 344 if err := ActionList(actions).LoadAttributes(); err != nil { 345 return nil, fmt.Errorf("LoadAttributes: %v", err) 346 } 347 348 return actions, nil 349} 350 351func activityReadable(user, doer *user_model.User) bool { 352 var doerID int64 353 if doer != nil { 354 doerID = doer.ID 355 } 356 if doer == nil || !doer.IsAdmin { 357 if user.KeepActivityPrivate && doerID != user.ID { 358 return false 359 } 360 } 361 return true 362} 363 364func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) { 365 cond := builder.NewCond() 366 367 var repoIDs []int64 368 var actorID int64 369 if opts.Actor != nil { 370 actorID = opts.Actor.ID 371 } 372 373 // check readable repositories by doer/actor 374 if opts.Actor == nil || !opts.Actor.IsAdmin { 375 if opts.RequestedUser.IsOrganization() { 376 env, err := OrgFromUser(opts.RequestedUser).AccessibleReposEnv(actorID) 377 if err != nil { 378 return nil, fmt.Errorf("AccessibleReposEnv: %v", err) 379 } 380 if repoIDs, err = env.RepoIDs(1, opts.RequestedUser.NumRepos); err != nil { 381 return nil, fmt.Errorf("GetUserRepositories: %v", err) 382 } 383 cond = cond.And(builder.In("repo_id", repoIDs)) 384 } else { 385 cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor))) 386 } 387 } 388 389 if opts.RequestedTeam != nil { 390 env := OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(opts.RequestedTeam) 391 teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) 392 if err != nil { 393 return nil, fmt.Errorf("GetTeamRepositories: %v", err) 394 } 395 cond = cond.And(builder.In("repo_id", teamRepoIDs)) 396 } 397 398 cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID}) 399 400 if opts.OnlyPerformedBy { 401 cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID}) 402 } 403 if !opts.IncludePrivate { 404 cond = cond.And(builder.Eq{"is_private": false}) 405 } 406 if !opts.IncludeDeleted { 407 cond = cond.And(builder.Eq{"is_deleted": false}) 408 } 409 410 if opts.Date != "" { 411 dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation) 412 if err != nil { 413 log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err) 414 } else { 415 dateHigh := dateLow.Add(86399000000000) // 23h59m59s 416 417 cond = cond.And(builder.Gte{"created_unix": dateLow.Unix()}) 418 cond = cond.And(builder.Lte{"created_unix": dateHigh.Unix()}) 419 } 420 } 421 422 return cond, nil 423} 424 425// DeleteOldActions deletes all old actions from database. 426func DeleteOldActions(olderThan time.Duration) (err error) { 427 if olderThan <= 0 { 428 return nil 429 } 430 431 _, err = db.GetEngine(db.DefaultContext).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{}) 432 return 433} 434 435func notifyWatchers(ctx context.Context, actions ...*Action) error { 436 var watchers []*repo_model.Watch 437 var repo *repo_model.Repository 438 var err error 439 var permCode []bool 440 var permIssue []bool 441 var permPR []bool 442 443 e := db.GetEngine(ctx) 444 445 for _, act := range actions { 446 repoChanged := repo == nil || repo.ID != act.RepoID 447 448 if repoChanged { 449 // Add feeds for user self and all watchers. 450 watchers, err = repo_model.GetWatchers(ctx, act.RepoID) 451 if err != nil { 452 return fmt.Errorf("get watchers: %v", err) 453 } 454 } 455 456 // Add feed for actioner. 457 act.UserID = act.ActUserID 458 if _, err = e.Insert(act); err != nil { 459 return fmt.Errorf("insert new actioner: %v", err) 460 } 461 462 if repoChanged { 463 act.loadRepo() 464 repo = act.Repo 465 466 // check repo owner exist. 467 if err := act.Repo.GetOwner(ctx); err != nil { 468 return fmt.Errorf("can't get repo owner: %v", err) 469 } 470 } else if act.Repo == nil { 471 act.Repo = repo 472 } 473 474 // Add feed for organization 475 if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { 476 act.ID = 0 477 act.UserID = act.Repo.Owner.ID 478 if _, err = e.InsertOne(act); err != nil { 479 return fmt.Errorf("insert new actioner: %v", err) 480 } 481 } 482 483 if repoChanged { 484 permCode = make([]bool, len(watchers)) 485 permIssue = make([]bool, len(watchers)) 486 permPR = make([]bool, len(watchers)) 487 for i, watcher := range watchers { 488 user, err := user_model.GetUserByIDEngine(e, watcher.UserID) 489 if err != nil { 490 permCode[i] = false 491 permIssue[i] = false 492 permPR[i] = false 493 continue 494 } 495 perm, err := getUserRepoPermission(ctx, repo, user) 496 if err != nil { 497 permCode[i] = false 498 permIssue[i] = false 499 permPR[i] = false 500 continue 501 } 502 permCode[i] = perm.CanRead(unit.TypeCode) 503 permIssue[i] = perm.CanRead(unit.TypeIssues) 504 permPR[i] = perm.CanRead(unit.TypePullRequests) 505 } 506 } 507 508 for i, watcher := range watchers { 509 if act.ActUserID == watcher.UserID { 510 continue 511 } 512 act.ID = 0 513 act.UserID = watcher.UserID 514 act.Repo.Units = nil 515 516 switch act.OpType { 517 case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: 518 if !permCode[i] { 519 continue 520 } 521 case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: 522 if !permIssue[i] { 523 continue 524 } 525 case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest: 526 if !permPR[i] { 527 continue 528 } 529 } 530 531 if _, err = e.InsertOne(act); err != nil { 532 return fmt.Errorf("insert new action: %v", err) 533 } 534 } 535 } 536 return nil 537} 538 539// NotifyWatchers creates batch of actions for every watcher. 540func NotifyWatchers(actions ...*Action) error { 541 return notifyWatchers(db.DefaultContext, actions...) 542} 543 544// NotifyWatchersActions creates batch of actions for every watcher. 545func NotifyWatchersActions(acts []*Action) error { 546 ctx, committer, err := db.TxContext() 547 if err != nil { 548 return err 549 } 550 defer committer.Close() 551 for _, act := range acts { 552 if err := notifyWatchers(ctx, act); err != nil { 553 return err 554 } 555 } 556 return committer.Commit() 557} 558