1package github
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io/ioutil"
9 "net/http"
10 "os"
11 "strings"
12 "time"
13
14 "github.com/pkg/errors"
15 "github.com/shurcooL/githubv4"
16 "golang.org/x/sync/errgroup"
17
18 "github.com/MichaelMure/git-bug/bridge/core"
19 "github.com/MichaelMure/git-bug/bridge/core/auth"
20 "github.com/MichaelMure/git-bug/bug"
21 "github.com/MichaelMure/git-bug/cache"
22 "github.com/MichaelMure/git-bug/entity"
23 "github.com/MichaelMure/git-bug/identity"
24)
25
26var (
27 ErrMissingIdentityToken = errors.New("missing identity token")
28)
29
30// githubExporter implement the Exporter interface
31type githubExporter struct {
32 conf core.Configuration
33
34 // cache identities clients
35 identityClient map[entity.Id]*rateLimitHandlerClient
36
37 // the client to use for non user-specific queries
38 // it's the client associated to the "default-login" config
39 // used for the github V4 API (graphql)
40 defaultClient *rateLimitHandlerClient
41
42 // the token of the default user
43 // it's the token associated to the "default-login" config
44 // used for the github V3 API (REST)
45 defaultToken *auth.Token
46
47 // github repository ID
48 repositoryID string
49
50 // cache identifiers used to speed up exporting operations
51 // cleared for each bug
52 cachedOperationIDs map[entity.Id]string
53
54 // cache labels used to speed up exporting labels events
55 cachedLabels map[string]string
56
57 // channel to send export results
58 out chan<- core.ExportResult
59}
60
61// Init .
62func (ge *githubExporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
63 ge.conf = conf
64 ge.identityClient = make(map[entity.Id]*rateLimitHandlerClient)
65 ge.cachedOperationIDs = make(map[entity.Id]string)
66 ge.cachedLabels = make(map[string]string)
67
68 // preload all clients
69 err := ge.cacheAllClient(repo)
70 if err != nil {
71 return err
72 }
73
74 return nil
75}
76
77func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error {
78 creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
79 if err != nil {
80 return err
81 }
82
83 for _, cred := range creds {
84 login, ok := cred.GetMetadata(auth.MetaKeyLogin)
85 if !ok {
86 _, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with a Github login\n", cred.ID().Human())
87 continue
88 }
89
90 user, err := repo.ResolveIdentityImmutableMetadata(metaKeyGithubLogin, login)
91 if err == identity.ErrIdentityNotExist {
92 continue
93 }
94 if err != nil {
95 return nil
96 }
97
98 if _, ok := ge.identityClient[user.Id()]; ok {
99 continue
100 }
101
102 client := buildClient(creds[0].(*auth.Token))
103 ge.identityClient[user.Id()] = client
104
105 // assign the default client and token as well
106 if ge.defaultClient == nil && login == ge.conf[confKeyDefaultLogin] {
107 ge.defaultClient = client
108 ge.defaultToken = creds[0].(*auth.Token)
109 }
110 }
111
112 if ge.defaultClient == nil {
113 return fmt.Errorf("no token found for the default login \"%s\"", ge.conf[confKeyDefaultLogin])
114 }
115
116 return nil
117}
118
119// getClientForIdentity return a githubv4 API client configured with the access token of the given identity.
120func (ge *githubExporter) getClientForIdentity(userId entity.Id) (*rateLimitHandlerClient, error) {
121 client, ok := ge.identityClient[userId]
122 if ok {
123 return client, nil
124 }
125
126 return nil, ErrMissingIdentityToken
127}
128
129// ExportAll export all event made by the current user to Github
130func (ge *githubExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
131 out := make(chan core.ExportResult)
132 ge.out = out
133
134 var err error
135 // get repository node id
136 ge.repositoryID, err = getRepositoryNodeID(
137 ctx,
138 ge.defaultToken,
139 ge.conf[confKeyOwner],
140 ge.conf[confKeyProject],
141 )
142 if err != nil {
143 return nil, err
144 }
145
146 go func() {
147 defer close(out)
148
149 // query all labels
150 err = ge.cacheGithubLabels(ctx, ge.defaultClient)
151 if err != nil {
152 out <- core.NewExportError(errors.Wrap(err, "can't obtain Github labels"), "")
153 return
154 }
155
156 allIdentitiesIds := make([]entity.Id, 0, len(ge.identityClient))
157 for id := range ge.identityClient {
158 allIdentitiesIds = append(allIdentitiesIds, id)
159 }
160
161 allBugsIds := repo.AllBugsIds()
162
163 for _, id := range allBugsIds {
164 b, err := repo.ResolveBug(id)
165 if err != nil {
166 out <- core.NewExportError(errors.Wrap(err, "can't load bug"), id)
167 return
168 }
169
170 select {
171
172 case <-ctx.Done():
173 // stop iterating if context cancel function is called
174 return
175
176 default:
177 snapshot := b.Snapshot()
178
179 // ignore issues created before since date
180 // TODO: compare the Lamport time instead of using the unix time
181 if snapshot.CreateTime.Before(since) {
182 out <- core.NewExportNothing(b.Id(), "bug created before the since date")
183 continue
184 }
185
186 if snapshot.HasAnyActor(allIdentitiesIds...) {
187 // try to export the bug and it associated events
188 ge.exportBug(ctx, b, out)
189 }
190 }
191 }
192 }()
193
194 return out, nil
195}
196
197// exportBug publish bugs and related events
198func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out chan<- core.ExportResult) {
199 snapshot := b.Snapshot()
200 var bugUpdated bool
201
202 var bugGithubID string
203 var bugGithubURL string
204
205 // Special case:
206 // if a user try to export a bug that is not already exported to Github (or imported
207 // from Github) and we do not have the token of the bug author, there is nothing we can do.
208
209 // first operation is always createOp
210 createOp := snapshot.Operations[0].(*bug.CreateOperation)
211 author := snapshot.Author
212
213 // skip bug if origin is not allowed
214 origin, ok := snapshot.GetCreateMetadata(core.MetaKeyOrigin)
215 if ok && origin != target {
216 out <- core.NewExportNothing(b.Id(), fmt.Sprintf("issue tagged with origin: %s", origin))
217 return
218 }
219
220 // get github bug ID
221 githubID, ok := snapshot.GetCreateMetadata(metaKeyGithubId)
222 if ok {
223 githubURL, ok := snapshot.GetCreateMetadata(metaKeyGithubUrl)
224 if !ok {
225 // if we find github ID, github URL must be found too
226 err := fmt.Errorf("incomplete Github metadata: expected to find issue URL")
227 out <- core.NewExportError(err, b.Id())
228 }
229
230 // extract owner and project
231 owner, project, err := splitURL(githubURL)
232 if err != nil {
233 err := fmt.Errorf("bad project url: %v", err)
234 out <- core.NewExportError(err, b.Id())
235 return
236 }
237
238 // ignore issue coming from other repositories
239 if owner != ge.conf[confKeyOwner] && project != ge.conf[confKeyProject] {
240 out <- core.NewExportNothing(b.Id(), fmt.Sprintf("skipping issue from url:%s", githubURL))
241 return
242 }
243
244 // will be used to mark operation related to a bug as exported
245 bugGithubID = githubID
246 bugGithubURL = githubURL
247
248 } else {
249 // check that we have a token for operation author
250 client, err := ge.getClientForIdentity(author.Id())
251 if err != nil {
252 // if bug is still not exported and we do not have the author stop the execution
253 out <- core.NewExportNothing(b.Id(), fmt.Sprintf("missing author token"))
254 return
255 }
256
257 // create bug
258 id, url, err := ge.createGithubIssue(ctx, client, ge.repositoryID, createOp.Title, createOp.Message)
259 if err != nil {
260 err := errors.Wrap(err, "exporting github issue")
261 out <- core.NewExportError(err, b.Id())
262 return
263 }
264
265 out <- core.NewExportBug(b.Id())
266
267 // mark bug creation operation as exported
268 if err := markOperationAsExported(b, createOp.Id(), id, url); err != nil {
269 err := errors.Wrap(err, "marking operation as exported")
270 out <- core.NewExportError(err, b.Id())
271 return
272 }
273
274 // commit operation to avoid creating multiple issues with multiple pushes
275 if err := b.CommitAsNeeded(); err != nil {
276 err := errors.Wrap(err, "bug commit")
277 out <- core.NewExportError(err, b.Id())
278 return
279 }
280
281 // cache bug github ID and URL
282 bugGithubID = id
283 bugGithubURL = url
284 }
285
286 // cache operation github id
287 ge.cachedOperationIDs[createOp.Id()] = bugGithubID
288
289 for _, op := range snapshot.Operations[1:] {
290 // ignore SetMetadata operations
291 if _, ok := op.(*bug.SetMetadataOperation); ok {
292 continue
293 }
294
295 // ignore operations already existing in github (due to import or export)
296 // cache the ID of already exported or imported issues and events from Github
297 if id, ok := op.GetMetadata(metaKeyGithubId); ok {
298 ge.cachedOperationIDs[op.Id()] = id
299 continue
300 }
301
302 opAuthor := op.Author()
303 client, err := ge.getClientForIdentity(opAuthor.Id())
304 if err != nil {
305 continue
306 }
307
308 var id, url string
309 switch op := op.(type) {
310 case *bug.AddCommentOperation:
311 // send operation to github
312 id, url, err = ge.addCommentGithubIssue(ctx, client, bugGithubID, op.Message)
313 if err != nil {
314 err := errors.Wrap(err, "adding comment")
315 out <- core.NewExportError(err, b.Id())
316 return
317 }
318
319 out <- core.NewExportComment(op.Id())
320
321 // cache comment id
322 ge.cachedOperationIDs[op.Id()] = id
323
324 case *bug.EditCommentOperation:
325 // Since github doesn't consider the issue body as a comment
326 if op.Target == createOp.Id() {
327
328 // case bug creation operation: we need to edit the Github issue
329 if err := ge.updateGithubIssueBody(ctx, client, bugGithubID, op.Message); err != nil {
330 err := errors.Wrap(err, "editing issue")
331 out <- core.NewExportError(err, b.Id())
332 return
333 }
334
335 out <- core.NewExportCommentEdition(op.Id())
336
337 id = bugGithubID
338 url = bugGithubURL
339
340 } else {
341
342 // case comment edition operation: we need to edit the Github comment
343 commentID, ok := ge.cachedOperationIDs[op.Target]
344 if !ok {
345 panic("unexpected error: comment id not found")
346 }
347
348 eid, eurl, err := ge.editCommentGithubIssue(ctx, client, commentID, op.Message)
349 if err != nil {
350 err := errors.Wrap(err, "editing comment")
351 out <- core.NewExportError(err, b.Id())
352 return
353 }
354
355 out <- core.NewExportCommentEdition(op.Id())
356
357 // use comment id/url instead of issue id/url
358 id = eid
359 url = eurl
360 }
361
362 case *bug.SetStatusOperation:
363 if err := ge.updateGithubIssueStatus(ctx, client, bugGithubID, op.Status); err != nil {
364 err := errors.Wrap(err, "editing status")
365 out <- core.NewExportError(err, b.Id())
366 return
367 }
368
369 out <- core.NewExportStatusChange(op.Id())
370
371 id = bugGithubID
372 url = bugGithubURL
373
374 case *bug.SetTitleOperation:
375 if err := ge.updateGithubIssueTitle(ctx, client, bugGithubID, op.Title); err != nil {
376 err := errors.Wrap(err, "editing title")
377 out <- core.NewExportError(err, b.Id())
378 return
379 }
380
381 out <- core.NewExportTitleEdition(op.Id())
382
383 id = bugGithubID
384 url = bugGithubURL
385
386 case *bug.LabelChangeOperation:
387 if err := ge.updateGithubIssueLabels(ctx, client, bugGithubID, op.Added, op.Removed); err != nil {
388 err := errors.Wrap(err, "updating labels")
389 out <- core.NewExportError(err, b.Id())
390 return
391 }
392
393 out <- core.NewExportLabelChange(op.Id())
394
395 id = bugGithubID
396 url = bugGithubURL
397
398 default:
399 panic("unhandled operation type case")
400 }
401
402 // mark operation as exported
403 if err := markOperationAsExported(b, op.Id(), id, url); err != nil {
404 err := errors.Wrap(err, "marking operation as exported")
405 out <- core.NewExportError(err, b.Id())
406 return
407 }
408
409 // commit at each operation export to avoid exporting same events multiple times
410 if err := b.CommitAsNeeded(); err != nil {
411 err := errors.Wrap(err, "bug commit")
412 out <- core.NewExportError(err, b.Id())
413 return
414 }
415
416 bugUpdated = true
417 }
418
419 if !bugUpdated {
420 out <- core.NewExportNothing(b.Id(), "nothing has been exported")
421 }
422}
423
424// getRepositoryNodeID request github api v3 to get repository node id
425func getRepositoryNodeID(ctx context.Context, token *auth.Token, owner, project string) (string, error) {
426 url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project)
427 client := &http.Client{}
428
429 req, err := http.NewRequest("GET", url, nil)
430 if err != nil {
431 return "", err
432 }
433
434 // need the token for private repositories
435 req.Header.Set("Authorization", fmt.Sprintf("token %s", token.Value))
436
437 ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
438 defer cancel()
439 req = req.WithContext(ctx)
440
441 resp, err := client.Do(req)
442 if err != nil {
443 return "", err
444 }
445
446 if resp.StatusCode != http.StatusOK {
447 return "", fmt.Errorf("HTTP error %v retrieving repository node id", resp.StatusCode)
448 }
449
450 aux := struct {
451 NodeID string `json:"node_id"`
452 }{}
453
454 data, _ := ioutil.ReadAll(resp.Body)
455 err = resp.Body.Close()
456 if err != nil {
457 return "", err
458 }
459
460 err = json.Unmarshal(data, &aux)
461 if err != nil {
462 return "", err
463 }
464
465 return aux.NodeID, nil
466}
467
468func markOperationAsExported(b *cache.BugCache, target entity.Id, githubID, githubURL string) error {
469 _, err := b.SetMetadata(
470 target,
471 map[string]string{
472 metaKeyGithubId: githubID,
473 metaKeyGithubUrl: githubURL,
474 },
475 )
476
477 return err
478}
479
480func (ge *githubExporter) cacheGithubLabels(ctx context.Context, gc *rateLimitHandlerClient) error {
481 variables := map[string]interface{}{
482 "owner": githubv4.String(ge.conf[confKeyOwner]),
483 "name": githubv4.String(ge.conf[confKeyProject]),
484 "first": githubv4.Int(10),
485 "after": (*githubv4.String)(nil),
486 }
487
488 q := labelsQuery{}
489
490 hasNextPage := true
491 for hasNextPage {
492 if err := gc.queryExport(ctx, &q, variables, ge.out); err != nil {
493 return err
494 }
495
496 for _, label := range q.Repository.Labels.Nodes {
497 ge.cachedLabels[label.Name] = label.ID
498 }
499
500 hasNextPage = q.Repository.Labels.PageInfo.HasNextPage
501 variables["after"] = q.Repository.Labels.PageInfo.EndCursor
502 }
503
504 return nil
505}
506
507func (ge *githubExporter) getLabelID(label string) (string, error) {
508 label = strings.ToLower(label)
509 for cachedLabel, ID := range ge.cachedLabels {
510 if label == strings.ToLower(cachedLabel) {
511 return ID, nil
512 }
513 }
514
515 return "", fmt.Errorf("didn't find label id in cache")
516}
517
518// create a new label and return it github id
519// NOTE: since createLabel mutation is still in preview mode we use github api v3 to create labels
520// see https://developer.github.com/v4/mutation/createlabel/ and https://developer.github.com/v4/previews/#labels-preview
521func (ge *githubExporter) createGithubLabel(ctx context.Context, label, color string) (string, error) {
522 url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[confKeyOwner], ge.conf[confKeyProject])
523 client := &http.Client{}
524
525 params := struct {
526 Name string `json:"name"`
527 Color string `json:"color"`
528 Description string `json:"description"`
529 }{
530 Name: label,
531 Color: color,
532 }
533
534 data, err := json.Marshal(params)
535 if err != nil {
536 return "", err
537 }
538
539 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
540 if err != nil {
541 return "", err
542 }
543
544 ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
545 defer cancel()
546 req = req.WithContext(ctx)
547
548 // need the token for private repositories
549 req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.defaultToken.Value))
550
551 resp, err := client.Do(req)
552 if err != nil {
553 return "", err
554 }
555
556 if resp.StatusCode != http.StatusCreated {
557 return "", fmt.Errorf("error creating label: response status %v", resp.StatusCode)
558 }
559
560 aux := struct {
561 ID int `json:"id"`
562 NodeID string `json:"node_id"`
563 Color string `json:"color"`
564 }{}
565
566 data, _ = ioutil.ReadAll(resp.Body)
567 defer resp.Body.Close()
568
569 err = json.Unmarshal(data, &aux)
570 if err != nil {
571 return "", err
572 }
573
574 return aux.NodeID, nil
575}
576
577/**
578// create github label using api v4
579func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelColor string) (string, error) {
580 m := createLabelMutation{}
581 input := createLabelInput{
582 RepositoryID: ge.repositoryID,
583 Name: githubv4.String(label),
584 Color: githubv4.String(labelColor),
585 }
586
587 ctx := context.Background()
588
589 if err := gc.mutate(ctx, &m, input, nil); err != nil {
590 return "", err
591 }
592
593 return m.CreateLabel.Label.ID, nil
594}
595*/
596
597func (ge *githubExporter) getOrCreateGithubLabelID(ctx context.Context, gc *rateLimitHandlerClient, repositoryID string, label bug.Label) (string, error) {
598 // try to get label id from cache
599 labelID, err := ge.getLabelID(string(label))
600 if err == nil {
601 return labelID, nil
602 }
603
604 // RGBA to hex color
605 rgba := label.Color().RGBA()
606 hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
607
608 ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
609 defer cancel()
610
611 labelID, err = ge.createGithubLabel(ctx, string(label), hexColor)
612 if err != nil {
613 return "", err
614 }
615
616 return labelID, nil
617}
618
619func (ge *githubExporter) getLabelsIDs(ctx context.Context, gc *rateLimitHandlerClient, repositoryID string, labels []bug.Label) ([]githubv4.ID, error) {
620 ids := make([]githubv4.ID, 0, len(labels))
621 var err error
622
623 // check labels ids
624 for _, label := range labels {
625 id, ok := ge.cachedLabels[string(label)]
626 if !ok {
627 // try to query label id
628 id, err = ge.getOrCreateGithubLabelID(ctx, gc, repositoryID, label)
629 if err != nil {
630 return nil, errors.Wrap(err, "get or create github label")
631 }
632
633 // cache label id
634 ge.cachedLabels[string(label)] = id
635 }
636
637 ids = append(ids, githubv4.ID(id))
638 }
639
640 return ids, nil
641}
642
643// create a github issue and return it ID
644func (ge *githubExporter) createGithubIssue(ctx context.Context, gc *rateLimitHandlerClient, repositoryID, title, body string) (string, string, error) {
645 m := &createIssueMutation{}
646 input := githubv4.CreateIssueInput{
647 RepositoryID: repositoryID,
648 Title: githubv4.String(title),
649 Body: (*githubv4.String)(&body),
650 }
651
652 if err := gc.mutate(ctx, m, input, nil, ge.out); err != nil {
653 return "", "", err
654 }
655
656 issue := m.CreateIssue.Issue
657 return issue.ID, issue.URL, nil
658}
659
660// add a comment to an issue and return it ID
661func (ge *githubExporter) addCommentGithubIssue(ctx context.Context, gc *rateLimitHandlerClient, subjectID string, body string) (string, string, error) {
662 m := &addCommentToIssueMutation{}
663 input := githubv4.AddCommentInput{
664 SubjectID: subjectID,
665 Body: githubv4.String(body),
666 }
667
668 if err := gc.mutate(ctx, m, input, nil, ge.out); err != nil {
669 return "", "", err
670 }
671
672 node := m.AddComment.CommentEdge.Node
673 return node.ID, node.URL, nil
674}
675
676func (ge *githubExporter) editCommentGithubIssue(ctx context.Context, gc *rateLimitHandlerClient, commentID, body string) (string, string, error) {
677 m := &updateIssueCommentMutation{}
678 input := githubv4.UpdateIssueCommentInput{
679 ID: commentID,
680 Body: githubv4.String(body),
681 }
682
683 if err := gc.mutate(ctx, m, input, nil, ge.out); err != nil {
684 return "", "", err
685 }
686
687 return commentID, m.UpdateIssueComment.IssueComment.URL, nil
688}
689
690func (ge *githubExporter) updateGithubIssueStatus(ctx context.Context, gc *rateLimitHandlerClient, id string, status bug.Status) error {
691 m := &updateIssueMutation{}
692
693 // set state
694 var state githubv4.IssueState
695
696 switch status {
697 case bug.OpenStatus:
698 state = githubv4.IssueStateOpen
699 case bug.ClosedStatus:
700 state = githubv4.IssueStateClosed
701 default:
702 panic("unknown bug state")
703 }
704
705 input := githubv4.UpdateIssueInput{
706 ID: id,
707 State: &state,
708 }
709
710 if err := gc.mutate(ctx, m, input, nil, ge.out); err != nil {
711 return err
712 }
713
714 return nil
715}
716
717func (ge *githubExporter) updateGithubIssueBody(ctx context.Context, gc *rateLimitHandlerClient, id string, body string) error {
718 m := &updateIssueMutation{}
719 input := githubv4.UpdateIssueInput{
720 ID: id,
721 Body: (*githubv4.String)(&body),
722 }
723
724 if err := gc.mutate(ctx, m, input, nil, ge.out); err != nil {
725 return err
726 }
727
728 return nil
729}
730
731func (ge *githubExporter) updateGithubIssueTitle(ctx context.Context, gc *rateLimitHandlerClient, id, title string) error {
732 m := &updateIssueMutation{}
733 input := githubv4.UpdateIssueInput{
734 ID: id,
735 Title: (*githubv4.String)(&title),
736 }
737
738 if err := gc.mutate(ctx, m, input, nil, ge.out); err != nil {
739 return err
740 }
741
742 return nil
743}
744
745// update github issue labels
746func (ge *githubExporter) updateGithubIssueLabels(ctx context.Context, gc *rateLimitHandlerClient, labelableID string, added, removed []bug.Label) error {
747
748 wg, ctx := errgroup.WithContext(ctx)
749 if len(added) > 0 {
750 wg.Go(func() error {
751 addedIDs, err := ge.getLabelsIDs(ctx, gc, labelableID, added)
752 if err != nil {
753 return err
754 }
755
756 m := &addLabelsToLabelableMutation{}
757 inputAdd := githubv4.AddLabelsToLabelableInput{
758 LabelableID: labelableID,
759 LabelIDs: addedIDs,
760 }
761
762 // add labels
763 if err := gc.mutate(ctx, m, inputAdd, nil, ge.out); err != nil {
764 return err
765 }
766 return nil
767 })
768 }
769
770 if len(removed) > 0 {
771 wg.Go(func() error {
772 removedIDs, err := ge.getLabelsIDs(ctx, gc, labelableID, removed)
773 if err != nil {
774 return err
775 }
776
777 m2 := &removeLabelsFromLabelableMutation{}
778 inputRemove := githubv4.RemoveLabelsFromLabelableInput{
779 LabelableID: labelableID,
780 LabelIDs: removedIDs,
781 }
782
783 // remove label labels
784 if err := gc.mutate(ctx, m2, inputRemove, nil, ge.out); err != nil {
785 return err
786 }
787 return nil
788 })
789 }
790
791 return wg.Wait()
792}