diff --git a/autosocial/autosocial.go b/autosocial/autosocial.go index 8e7237e66..493b49909 100644 --- a/autosocial/autosocial.go +++ b/autosocial/autosocial.go @@ -41,8 +41,10 @@ func CoreInitServer(ctx context.Context) *gin.Engine { pgx := postgres.NewPgxClient() queries := coredb.New(pgx) + ney := farcaster.NewNeynarAPI(http.DefaultClient) router.Use(middleware.GinContextToContext(), middleware.Sentry(true), middleware.Tracing(), middleware.HandleCORS(), middleware.ErrLogger()) - router.POST("/process/users", processUsers(queries, farcaster.NewNeynarAPI(http.DefaultClient), lens.NewAPI(http.DefaultClient))) + router.POST("/process/users", processUsers(queries, ney, lens.NewAPI(http.DefaultClient))) + router.GET("/checkFarcasterApproval", checkFarcasterApproval(queries, ney)) return router } diff --git a/autosocial/handler.go b/autosocial/handler.go index 51d8d6911..68f20bdbb 100644 --- a/autosocial/handler.go +++ b/autosocial/handler.go @@ -65,6 +65,49 @@ func processUsers(q *coredb.Queries, n *farcaster.NeynarAPI, l *lens.LensAPI) gi } } +func checkFarcasterApproval(q *coredb.Queries, n *farcaster.NeynarAPI) gin.HandlerFunc { + return func(c *gin.Context) { + var in task.AutosocialPollFarcasterMessage + if err := c.ShouldBindQuery(&in); err != nil { + util.ErrResponse(c, http.StatusBadRequest, err) + return + } + + s, err := n.GetSignerByUUID(c, in.SignerUUID) + if err != nil { + util.ErrResponse(c, http.StatusInternalServerError, err) + return + } + + if s.Status != "approved" { + util.ErrResponse(c, http.StatusInternalServerError, err) + return + } + + user, err := q.GetSocialsByUserID(c, in.UserID) + if err != nil { + util.ErrResponse(c, http.StatusInternalServerError, err) + return + } + far, ok := user[persist.SocialProviderFarcaster] + if !ok { + util.ErrResponse(c, http.StatusInternalServerError, err) + return + } + + far.Metadata["signer_status"] = s.Status + + err = q.AddSocialToUser(c, coredb.AddSocialToUserParams{ + UserID: in.UserID, + Socials: persist.Socials{ + persist.SocialProviderFarcaster: far, + }, + }) + + c.JSON(http.StatusOK, util.SuccessResponse{Success: true}) + } +} + func addLensProfileToUser(ctx context.Context, l *lens.LensAPI, address []persist.ChainAddress, q *coredb.Queries, userID persist.DBID) error { for _, a := range address { if a.Address() == "" { diff --git a/db/queries/core/query.sql b/db/queries/core/query.sql index 4c7439e70..d719dd5cd 100644 --- a/db/queries/core/query.sql +++ b/db/queries/core/query.sql @@ -52,6 +52,8 @@ SELECT * FROM users WHERE (traits->$1::string) IS NOT NULL AND deleted = false; -- name: GetUsersWithTraitBatch :batchmany SELECT * FROM users WHERE (traits->$1::string) IS NOT NULL AND deleted = false; +-- name: + -- name: GetGalleryById :one SELECT * FROM galleries WHERE id = $1 AND deleted = false; diff --git a/docker-compose.yml b/docker-compose.yml index b8d2d073e..c850dabea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,8 @@ services: 'projects/gallery-local/locations/here/queues/email', '-queue', 'projects/gallery-local/locations/here/queues/autosocial', + '-queue', + 'projects/gallery-local/locations/here/queues/autosocial-poll', ] pubsub-emulator: image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators diff --git a/graphql/generated/generated.go b/graphql/generated/generated.go index 926c7b6da..a751bb50e 100644 --- a/graphql/generated/generated.go +++ b/graphql/generated/generated.go @@ -10376,6 +10376,7 @@ input FarcasterAuth { input LensAuth { address: Address! + signature: String @scrub # lens only supports ETH addresses currently so no need to specify a chain } @@ -61424,7 +61425,7 @@ func (ec *executionContext) unmarshalInputLensAuth(ctx context.Context, obj inte asMap[k] = v } - fieldsInOrder := [...]string{"address"} + fieldsInOrder := [...]string{"address", "signature"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -61440,6 +61441,15 @@ func (ec *executionContext) unmarshalInputLensAuth(ctx context.Context, obj inte return it, err } it.Address = data + case "signature": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("signature")) + data, err := ec.unmarshalOString2áš–string(ctx, v) + if err != nil { + return it, err + } + it.Signature = data } } diff --git a/graphql/generated_test.go b/graphql/generated_test.go index 6b9cfbbaa..20aa47692 100644 --- a/graphql/generated_test.go +++ b/graphql/generated_test.go @@ -310,12 +310,16 @@ func (v *IntervalInput) GetStart() int { return v.Start } func (v *IntervalInput) GetLength() int { return v.Length } type LensAuth struct { - Address string `json:"address"` + Address string `json:"address"` + Signature *string `json:"signature"` } // GetAddress returns LensAuth.Address, and is useful for accessing the field via an interface. func (v *LensAuth) GetAddress() string { return v.Address } +// GetSignature returns LensAuth.Signature, and is useful for accessing the field via an interface. +func (v *LensAuth) GetSignature() *string { return v.Signature } + type MagicLinkAuth struct { Token string `json:"token"` } diff --git a/graphql/model/models_gen.go b/graphql/model/models_gen.go index fcabb8935..f93de28f8 100644 --- a/graphql/model/models_gen.go +++ b/graphql/model/models_gen.go @@ -1584,7 +1584,8 @@ func (JSONMedia) IsMediaSubtype() {} func (JSONMedia) IsMedia() {} type LensAuth struct { - Address persist.Address `json:"address"` + Address persist.Address `json:"address"` + Signature *string `json:"signature"` } type LensSocialAccount struct { diff --git a/graphql/resolver/schema.resolvers.helpers.go b/graphql/resolver/schema.resolvers.helpers.go index 821ae07fe..b2d545bf1 100644 --- a/graphql/resolver/schema.resolvers.helpers.go +++ b/graphql/resolver/schema.resolvers.helpers.go @@ -9,10 +9,11 @@ import ( "database/sql" "errors" "fmt" - "golang.org/x/net/html" "strings" "time" + "golang.org/x/net/html" + "github.com/gammazero/workerpool" "github.com/magiclabs/magic-admin-go/token" @@ -216,7 +217,11 @@ func (r *Resolver) socialAuthMechanismToAuthenticator(ctx context.Context, m mod } if m.Lens != nil { - return publicapi.For(ctx).Social.NewLensAuthenticator(authedUserID, m.Lens.Address), nil + sig := "" + if m.Lens.Signature != nil { + sig = *m.Lens.Signature + } + return publicapi.For(ctx).Social.NewLensAuthenticator(authedUserID, m.Lens.Address, sig), nil } return nil, errNoAuthMechanismFound diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 843d3253c..d8c57db2c 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -1790,6 +1790,7 @@ input FarcasterAuth { input LensAuth { address: Address! + signature: String @scrub # lens only supports ETH addresses currently so no need to specify a chain } diff --git a/publicapi/socials.go b/publicapi/socials.go index 1444901d4..5851649dc 100644 --- a/publicapi/socials.go +++ b/publicapi/socials.go @@ -46,12 +46,13 @@ func (s SocialAPI) NewFarcasterAuthenticator(userID persist.DBID, address persis } } -func (s SocialAPI) NewLensAuthenticator(userID persist.DBID, address persist.Address) *socialauth.LensAuthenticator { +func (s SocialAPI) NewLensAuthenticator(userID persist.DBID, address persist.Address, sig string) *socialauth.LensAuthenticator { return &socialauth.LensAuthenticator{ HTTPClient: s.httpClient, UserID: userID, Queries: s.queries, Address: address, + Signature: sig, } } diff --git a/secrets/dev/backend-env.yaml b/secrets/dev/backend-env.yaml index 86072a8c3..0ec19e0e2 100644 --- a/secrets/dev/backend-env.yaml +++ b/secrets/dev/backend-env.yaml @@ -75,6 +75,7 @@ GCLOUD_USER_PREF_BUCKET: ENC[AES256_GCM,data:xRr1aKffotk+aaqjCQ==,iv:SWp8vm2Tb5G EMAILS_QUEUE: ENC[AES256_GCM,data:ZOUviMU+qtyGcHYbuVagc5vYzbATV1wRm9YBLzKWQdTnQeTQJeQ1+n8KUZZSeeWzJgazgv/ZpkdR/sY=,iv:TXo6iW9hneOMTTHUJJkBh7t9PpEoA778tVPfDmLxll8=,tag:iO7RJDNjUguvsEkC8TFUvA==,type:str] EMAILS_TASK_SECRET: ENC[AES256_GCM,data:bOV1Bp1WidXFGkprWGZl1cr8zRuz2YlVZ9jIlx5lBaE=,iv:VAmyf21miDHuwWqXbe/fVNA7Nk43Q+F2y9zroE7qB6w=,tag:6ervW0J9B6YLEQPLkNDuXw==,type:str] AUTOSOCIAL_QUEUE: ENC[AES256_GCM,data:Drwp2QJvFRGleEfhCwcqcUIWmiGE03QHhDfDitWhrBq/dCjHGYww26u4YJRrg344nAU254EUb+rmsH5qXEgzyg==,iv:J63wP8pIDHyqbHoPJR14npxg8gplZ0ztScBsuAP4D+k=,tag:OJzAdrI+VMjBvbAm3TKXLA==,type:str] +AUTOSOCIAL_POLL_QUEUE: ENC[AES256_GCM,data:5FQ4Ffp8cWbiGfbEEjn1zblBddof7+oTbRAbYx30EUPGCt11aIWKKGkLB7AM4mSgl7ln91b8BFqhJJ9Cg+60ACyL5q3j,iv:abJBRMtaGRdsy72O+8795bfu+EK7YXEKy9E2Ejjzzn4=,tag:i6APKPkF9RNPhRXyIwGduQ==,type:str] AUTOSOCIAL_URL: ENC[AES256_GCM,data:4t2yAxgNVw42WqeSzHiXcITANTHSWQIh9TsEGW62xBnhFsizxfbh7UsW,iv:oW4x0ydvjTXk7lkNfhCscY4tYZJhyxXHzIo33C+IMCo=,tag:R3aZ3bDpB/wjbzRPRBgFCg==,type:str] sops: kms: [] @@ -85,8 +86,8 @@ sops: azure_kv: [] hc_vault: [] age: [] - lastmodified: "2023-10-03T19:45:45Z" - mac: ENC[AES256_GCM,data:3RdMYUN9gFoKvvpUFakn0Dp1nOWunFswVvw3CIeCunbp5mKU+gbS1ohOmNtbyBc3xE3kzU6AUBP36rr9fK3s5Gzj2JK8XDpEcXfkWnFJInIyVo3ClwjjNIRuqwVRysAaoLZ1GAQvq5XZ1modayCWQkrnkTf+cBAudSuweH3XF/w=,iv:Z1RM027rrzbnO73pxJPK4VTaImZ5Ce6Xcslc/EYWD3w=,tag:QDNwaBXeKa2wRYqqu0SLrg==,type:str] + lastmodified: "2023-10-24T06:26:41Z" + mac: ENC[AES256_GCM,data:cTzTvWf4Etmq95FHSLGjNCNSPiq+0wWmkLzXm6o5UkX0JD7ZXn8iEz5JmVhlWZYYweBLfSdFCRyxpBvvpCYOvWKivMHxnUIL0pup/eCcawu9iV4eN2HO8P7FYA2BqTIR4r+SpPfavc1r+ZePCi8uFlMw2Zs8iAIAGphu7elprN0=,iv:rrDBWe1KPl/XvM2XvpyBPdujoQ1Ej60IlYe70aDp6F0=,tag:xHqfx6SUiXC2koqITEYHKQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.7.3 diff --git a/secrets/dev/local/app-dev-backend.yaml b/secrets/dev/local/app-dev-backend.yaml index cab302ad6..ee1badcc0 100644 --- a/secrets/dev/local/app-dev-backend.yaml +++ b/secrets/dev/local/app-dev-backend.yaml @@ -49,6 +49,7 @@ EMAILS_QUEUE: ENC[AES256_GCM,data:alOYj1WuVvGjetBCFtFhN2NqmdxDifbxHCUNw2TkSGhD14 EMAILS_TASK_SECRET: ENC[AES256_GCM,data:s27svCF9CtqvDddlkqpbpchN/TKXRLTK3fs1IuIF2FQ=,iv:OrzkHkUFK0r6UPb7IZkmTqtxRlsilAQ4OUK8YRLjqg8=,tag:he3wH/Nsv8NEDYDV0FlgPw==,type:str] AUTOSOCIAL_QUEUE: ENC[AES256_GCM,data:+BTIQbwpXJudqIX9MRHOOoqHyoJxzGi+RWpIwVZIb3AnXixQUsDMgLK68VHiuLbJRbs39b1X1Q==,iv:LOw/wc6qZNildKhwzgHa1plpGwF80e/3vCMWcemMAoU=,tag:F7HZ2tn3c45SmyqBWV/JZg==,type:str] AUTOSOCIAL_URL: ENC[AES256_GCM,data:2B6jfL92LdHhuWRpI/o=,iv:kJr//brJh3Thno9FHbkyEgJh9HQ4vMkLP26c8Lcnxbc=,tag:QLsaigs+YowjgMtLLIm69w==,type:str] +AUTOSOCIAL_POLL_QUEUE: ENC[AES256_GCM,data:KoTHV1WcXWiZl9AH1N9hFbLPKcLJtu6t7VU1AOxVIrZKz3OB2Ml3YYFLNYjQmJJuZp6vcZJcgxgOG/VO,iv:QFiI0UR6FAIp8fqdb4/8Vg+V6XV6dytriQ/aw82dBKw=,tag:6wHxd0XoYD2jPIolgDWaqQ==,type:str] FEEDBOT_URL: ENC[AES256_GCM,data:381swg3qLYO6UfGdWz65pwv6dnC3MrD4hrcYHrvfq3lKF/ySAAkH3/IKwg==,iv:n06Bc+VsJZKvV9RZAqxlGL8+EqboXdBOHbR8xqv4G8I=,tag:0ZjRuTOGrR4D+fQ49/2hYw==,type:str] sops: kms: [] @@ -59,8 +60,8 @@ sops: azure_kv: [] hc_vault: [] age: [] - lastmodified: "2023-10-03T19:45:47Z" - mac: ENC[AES256_GCM,data:iPjrLuzM1d0dWIdLU9shrYhHy3Tlwez5Ia7GwrW3pTUeOvduNku9wEqH8MsDKjuPk8ezt/QmR+78J0tXKHLOSWzV9BhULYIv2DZtxBwPbxkVOyebb7rD0X/7/6ZjpBbWI2TNtIhXEt070mF9K1HBdTZB8ZsWUHmrlaMeShduo4Y=,iv:09xzk/dcBVQD96ZFSKzsi8FfK7PV2zxs0M1qkSTQjdI=,tag:7aZyKrlanqUahv/GiYuOTw==,type:str] + lastmodified: "2023-10-24T06:26:57Z" + mac: ENC[AES256_GCM,data:e9WQ5c7hWO3rURwD331M+pVcuRuFMdFKBTN8jcaXXT7rgeXkwPEO9L/lqKWL64nl+L0/N/7RIoV4zmG5/9Xk2Gw+yayTWBA4H7iK7Y0WhOLCVldOynCRAfrDowzL5L+EPFl8q+LR6J5jp39OiFVNzOTh+E/89KBKsdBgenWdbM0=,iv:rIdnpM7Tzx2cLC8OwahIJ1be44wBfL6pcTz312bKA4s=,tag:y9abCY5Z9diOciy9OlDnKw==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.7.3 diff --git a/secrets/local/local/app-local-backend.yaml b/secrets/local/local/app-local-backend.yaml index 72a5afb88..6e8ed14c1 100644 --- a/secrets/local/local/app-local-backend.yaml +++ b/secrets/local/local/app-local-backend.yaml @@ -43,6 +43,7 @@ RPC_URL: ENC[AES256_GCM,data:Qt54Eru5hohZXpV2pUCx/0+dg7CpVK7bHYEGye/fej+WV2HxjGo GCLOUD_USER_PREF_BUCKET: ENC[AES256_GCM,data:+abxLn4GAtNR/PRAjg==,iv:pywnL1YJzPfgEyHVvCJeJc83WaScMhL81QNCJasTzac=,tag:+91RyaN4clWzmXURuXqxMA==,type:str] EMAIL_QUEUE: ENC[AES256_GCM,data:DZlQknhz+9UvLcin3j4iamhRp4y0ZrsRmcuhEArrXtBro1b58ov6T1PnSHw43pUgc5c=,iv:KrJQSR1FnwVA9Ue0cJTzqmLwz7DG/dF8LvI1aNWMic4=,tag:Bba+2d2hH3vaABLqPc1knQ==,type:str] AUTOSOCIAL_QUEUE: ENC[AES256_GCM,data:VPxQ8izFihl26rbrTe/sBgb3x+IvIobtPWKcDTUGuqAXlwgT4LNxVs4biB3fsP02nsJ7Z5SKug==,iv:CzPRmkSIt6rxkQ9GmWQl1UEbFTnuKZzqO92Jp+gf3MI=,tag:sFqz7HLDHc/6+CFDJZSJLg==,type:str] +AUTOSOCIAL_POLL_QUEUE: ENC[AES256_GCM,data:lJcSmOd4k8zk09H4tS6LpC3dv9o3xWb4syW7ITnuFZpAry1V0c0KzV8McaLx1L7MQNYwC8s3nuUZTM5y,iv:wB0H3kHl0kofMe/TE25czNrlCekGgPfZhxjhs3KtDJ8=,tag:K7RixYUmheMYn3FakPZhoA==,type:str] AUTOSOCIAL_URL: ENC[AES256_GCM,data:Jx2v/E4x841LeDjmiUUmTT70IBfJmmzbhmCUTd/aqUM=,iv:n8NkhZosTCwo0AEXuRvQoVvlzBcEdWa1Swa2gWIA8yU=,tag:BH5wARsJ4w31HFkVfMBN7w==,type:str] sops: kms: [] @@ -53,8 +54,8 @@ sops: azure_kv: [] hc_vault: [] age: [] - lastmodified: "2023-10-23T18:41:58Z" - mac: ENC[AES256_GCM,data:8BrdTVeD9/Gb4YZF8ds94zTFb1UrU/eCtZ4POBUJYdYLgddFCXezomJQNr0QJrOI/pAc6ObKa31yuVpZTkYGc9yC6uNMWF+oVMdtzlJkXuRIG6zlBmaBGZr0MIWWk3P5RY6zGGpBIHbHTCIFrMscXRxIizss63XxTzIsqPTpev8=,iv:epX4hH8qJWhDtSdIfhD2IeLIZUPf8/LrPJPpe9p6Wng=,tag:y31wz30w9UDBgaeub9VW6Q==,type:str] + lastmodified: "2023-10-24T06:25:40Z" + mac: ENC[AES256_GCM,data:vr+FisofpJ5WdBcMd+S+1BD31VPjZ5PYEmZfkXwZkKUdYrpAPGR48hmQBlJuMh33CbWUNipJuTmHSWyCpcu1LEkXwZvev2WZ8stHhNFpPWSBU5C9iW682w0Uel8UolGvzFY9F0+NkF/H3o5ZoeMwMePvnVEoQFpnqUoPpdc8Z+M=,iv:A4Et5/ud0IJCxnbCXRAqyZ9VD39LvKfSsSk4K7zHLnU=,tag:Pn/BUpc4ScvIafcKvi7tAQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.7.3 diff --git a/secrets/prod/backend-env.yaml b/secrets/prod/backend-env.yaml index 1709e36b4..3f61d8de3 100644 --- a/secrets/prod/backend-env.yaml +++ b/secrets/prod/backend-env.yaml @@ -74,6 +74,7 @@ GOLDSKY_API_KEY: ENC[AES256_GCM,data:Cof9gRBnQ3OXV8Fo+aJhv7J34tgd/KyNQA==,iv:5CB GCLOUD_USER_PREF_BUCKET: ENC[AES256_GCM,data:d30SUr/1z/8u0XPCgb8=,iv:xKanYFb0ySLhZu0vrTcBycAqMuQb+RsSw5x/lwhYmTM=,tag:0rXKTpz2DUQkB1cacv906Q==,type:str] EMAILS_QUEUE: ENC[AES256_GCM,data:DxWePlmNdmVaDnV+qtNuoQl5OT2r9oFSwZGO0C19Ra4HhZm9ySl4dhZ+0kpB40dsO61A43WzrWFuRbYK,iv:djCFFuJflxrulIHxE5AEpTgqVcQEHWvnQzP0z6vH0Yc=,tag:4mAAor7FKQfcpHhHBvjPzg==,type:str] AUTOSOCIAL_QUEUE: ENC[AES256_GCM,data:pOl1MmNzmzLkmVDfDL/ZXAX8mKIV2p/E25OKh46YdVSb4Ve0u4dPlGumJgWkpC9fFSCLRbCKMShwIOre+itrsv0=,iv:aLUhiFzmx2a8f577xT1G0mFlBFBttY1JoZsy2w0Ozzs=,tag:YEtSVNNEXQ08TZJxos/vXA==,type:str] +AUTOSOCIAL_POLL_QUEUE: ENC[AES256_GCM,data:JaRosMbk8lh5si61XYU/w/g/fTdIJgy5wDLeqtNqszW4fxVHleU88QTAWHwKpRjpe+adkNLyWFu/0JX0snLSy47cDkj6kw==,iv:EPFzS58+dfzs9dLFI4tsrVFIaxoQvmtvN/tQEcO6OBI=,tag:jEfm/XczkJXGDS/wgKJbaw==,type:str] AUTOSOCIAL_URL: ENC[AES256_GCM,data:1++2NVhPw2K6vfAJO/DYX6w+OeieJQKC7X/+yQOJ655d+wtrz+HGq713,iv:BS+dpFcn5G4WxbEG1C3sz2NSb5ro5LkLkgEkPJA8gyc=,tag:Gcg/lXOTJiAf0EkiM9TN8g==,type:str] EMAILS_TASK_SECRET: ENC[AES256_GCM,data:C+HWjJa5qu5Rs2xGjqek6tcYI8SnGhdziOSyq54W7Uk=,iv:qcek1g7fyDb3B8mOrDqowoYXGvS6WkAyuN9xABGkZfw=,tag:qvlHiesFIvUe8Il3INXzkQ==,type:str] sops: @@ -85,8 +86,8 @@ sops: azure_kv: [] hc_vault: [] age: [] - lastmodified: "2023-10-03T19:45:31Z" - mac: ENC[AES256_GCM,data:eM9VBYmr3icW5Pege444uYbWUOkzOmUMqZvStS/BqTlymvrg1AM4QMvJ+hVHEmlxSaxmhl0MAZ1LfoVRXCg3Yvwcyd8tg8k/W4qHmiXC7ltXWPiz0SEld9OPyzsuAxUOIfXiyEXHREDvwrqPkj7usVAmJiKN9tkjPTcYLvzDnRs=,iv:UGl2y0iT2iW2wXDGIPJPe80mI2JlQ9ICcUA6VUyRzRM=,tag:yxPCz1owqswnFFGN2IlOFw==,type:str] + lastmodified: "2023-10-24T06:26:12Z" + mac: ENC[AES256_GCM,data:fkA3vYODXXy+syViPfWE2yabvZ5tGFdiQIQQOukqUtlOnaTd8qYv03BDnjwrpGGKFtUp8WS1emTHEVdVU6WsSw34EmCSMRhfP92AXteYw3c7cmWjiiOVMZVwf0lCtHhcaP8nsiKWSUYPcACa5SDfbEr0QWt5Vsf+pEiKnPrkPQQ=,iv:eTOo82Rj6t8cHSi/MbYRFBiWY3BDSBZLpJt/SL6rV/w=,tag:NWJtvy6sRefgUqKzf4//ug==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.7.3 diff --git a/server/server.go b/server/server.go index b7947bac2..f6166df6c 100644 --- a/server/server.go +++ b/server/server.go @@ -233,6 +233,9 @@ func SetDefaults() { viper.SetDefault("EMAILS_TASK_SECRET", "emails-task-secret") viper.SetDefault("AUTOSOCIAL_URL", "") viper.SetDefault("AUTOSOCIAL_QUEUE", "") + viper.SetDefault("AUTOSOCIAL_POLL_QUEUE", "") + viper.SetDefault("FARCASTER_PRIVATE_KEY", "") + viper.SetDefault("FARCASTER_APP_ID", "") viper.AutomaticEnv() diff --git a/service/farcaster/neynar.go b/service/farcaster/neynar.go index 37e8239c9..8e2f3c2ac 100644 --- a/service/farcaster/neynar.go +++ b/service/farcaster/neynar.go @@ -1,12 +1,18 @@ package farcaster import ( + "bytes" "context" + "crypto/ed25519" + "encoding/hex" "encoding/json" "fmt" "io" + "math/big" "net/http" + "time" + "github.com/ethereum/go-ethereum/crypto" "github.com/mikeydub/go-gallery/env" "github.com/mikeydub/go-gallery/service/persist" ) @@ -168,3 +174,173 @@ func (n *NeynarAPI) FollowingByUserID(ctx context.Context, fid string) ([]Neynar return neynarResp.Result.Users, nil } + +type NeynarSigner struct { + SignerUUID string `json:"signer_uuid"` + PublicKey string `json:"public_key"` + Status string `json:"status"` + SignerApprovalURL string `json:"signer_approval_url"` + SignerApprovalFID string `json:"fid"` +} +type EIP712Domain struct { + Name string + Version string + ChainID *big.Int + VerifyingContract string +} + +type SignedKeyRequestType struct { + RequestFid *big.Int + Key []byte + Deadline *big.Int +} + +func (n *NeynarAPI) CreateSignerForUser(ctx context.Context, fid NeynarID) (NeynarSigner, error) { + su := fmt.Sprintf("%s/signer/?api_key=%s", neynarBaseURL, n.apiKey) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, su, nil) + if err != nil { + return NeynarSigner{}, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", n.apiKey) + + sResp, err := n.httpClient.Do(req) + if err != nil { + return NeynarSigner{}, err + } + + if sResp.StatusCode != http.StatusOK { + bs, err := io.ReadAll(sResp.Body) + if err != nil { + return NeynarSigner{}, err + } + return NeynarSigner{}, fmt.Errorf("neynar returned status %d (%s)", sResp.StatusCode, bs) + } + defer sResp.Body.Close() + + var curSigner NeynarSigner + if err := json.NewDecoder(sResp.Body).Decode(&curSigner); err != nil { + return NeynarSigner{}, err + } + + appFidStr := env.GetString("FARCASTER_APP_ID") + appFid := new(big.Int) + appFid.SetString(appFidStr, 10) + + domain := EIP712Domain{ + Name: "Farcaster SignedKeyRequestValidator", + Version: "1", + ChainID: big.NewInt(10), + VerifyingContract: "0x00000000fc700472606ed4fa22623acf62c60553", + } + + deadline := big.NewInt(time.Now().Unix() + 86400) + + privateKeyHex := env.GetString("FARCASTER_PRIVATE_KEY") + private, err := hex.DecodeString(privateKeyHex) + if err != nil { + return NeynarSigner{}, err + } + public := ed25519.PrivateKey(private).Public().(ed25519.PublicKey) + + typedData := SignedKeyRequestType{ + RequestFid: appFid, + Key: public, + Deadline: deadline, + } + + signature := signEIP712TypedData(private, domain, typedData) + + rsu := fmt.Sprintf("%s/signer/signed_key/?api_key=%s", neynarBaseURL, n.apiKey) + in := map[string]any{ + "signer_uuid": curSigner.SignerUUID, + "signature": signature, + "app_fid": appFid, + "deadline": deadline, + } + asJSON, err := json.Marshal(in) + if err != nil { + return NeynarSigner{}, err + } + buf := bytes.NewBuffer(asJSON) + req, err = http.NewRequestWithContext(ctx, http.MethodPost, rsu, buf) + if err != nil { + return NeynarSigner{}, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", n.apiKey) + + rsResp, err := n.httpClient.Do(req) + if err != nil { + return NeynarSigner{}, err + } + + if rsResp.StatusCode != http.StatusOK { + bs, err := io.ReadAll(rsResp.Body) + if err != nil { + return NeynarSigner{}, err + } + return NeynarSigner{}, fmt.Errorf("neynar returned status %d (%s)", rsResp.StatusCode, bs) + } + defer rsResp.Body.Close() + + err = json.NewDecoder(rsResp.Body).Decode(&curSigner) + if err != nil { + return NeynarSigner{}, err + } + + return curSigner, nil +} + +func signEIP712TypedData(privateKey ed25519.PrivateKey, domain EIP712Domain, typedData SignedKeyRequestType) []byte { + // Hashing and signing logic for EIP-712 + + // Hash domain separator + domainData := fmt.Sprintf("%s%s%d%s", domain.Name, domain.Version, domain.ChainID, domain.VerifyingContract) + domainHash := crypto.Keccak256Hash([]byte(domainData)) + + // Hash typedData + typedDataHash := crypto.Keccak256Hash([]byte(fmt.Sprintf("%d%s%d", typedData.RequestFid, string(typedData.Key), typedData.Deadline))) + + // Hash domain separator and typed data hash + dataToSign := append(domainHash.Bytes(), typedDataHash.Bytes()...) + dataToSignHash := crypto.Keccak256Hash(dataToSign) + + // Sign the final hash + signature := ed25519.Sign(privateKey, dataToSignHash.Bytes()) + return signature +} + +func (n *NeynarAPI) GetSignerByUUID(ctx context.Context, uuid string) (NeynarSigner, error) { + su := fmt.Sprintf("%s/signer/?api_key=%s&signer=%s", neynarBaseURL, n.apiKey, uuid) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, su, nil) + if err != nil { + return NeynarSigner{}, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", n.apiKey) + + sResp, err := n.httpClient.Do(req) + if err != nil { + return NeynarSigner{}, err + } + + if sResp.StatusCode != http.StatusOK { + bs, err := io.ReadAll(sResp.Body) + if err != nil { + return NeynarSigner{}, err + } + return NeynarSigner{}, fmt.Errorf("neynar returned status %d (%s)", sResp.StatusCode, bs) + } + defer sResp.Body.Close() + + var curSigner NeynarSigner + if err := json.NewDecoder(sResp.Body).Decode(&curSigner); err != nil { + return NeynarSigner{}, err + } + + return curSigner, nil +} diff --git a/service/lens/lens.go b/service/lens/lens.go index f443204fc..e9457e468 100644 --- a/service/lens/lens.go +++ b/service/lens/lens.go @@ -125,17 +125,130 @@ func (n *LensAPI) DefaultProfileByAddress(ctx context.Context, address persist.A defer resp.Body.Close() - var neynarResp DefaultProfileByAddressResponse - if err := json.NewDecoder(resp.Body).Decode(&neynarResp); err != nil { + var lensResp DefaultProfileByAddressResponse + if err := json.NewDecoder(resp.Body).Decode(&lensResp); err != nil { return User{}, err } - if neynarResp.Data == nil || neynarResp.Data.DefaultProfile == nil { + if lensResp.Data == nil || lensResp.Data.DefaultProfile == nil { var errStr string - if neynarResp.Error != nil { - errStr = *neynarResp.Error + if lensResp.Error != nil { + errStr = *lensResp.Error } return User{}, fmt.Errorf("no result for %s (err %s)", address, errStr) } - return *neynarResp.Data.DefaultProfile, nil + return *lensResp.Data.DefaultProfile, nil +} + +type AuthResponse struct { + Data *struct { + Authenticate *struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + } `json:"authenticate"` + } `json:"data"` + Error *string `json:"error"` +} + +func (n *LensAPI) AuthenticateWithSignature(ctx context.Context, address persist.Address, sig string) (string, string, error) { + gqlQuery := fmt.Sprintf(`mutation { + authenticate(request: { + address: "%s", + signature: "%s" + }) { + accessToken + refreshToken + } + }`, address, sig) + + body, err := json.Marshal(map[string]string{ + "query": gqlQuery, + }) + if err != nil { + return "", "", err + } + buf := bytes.NewBuffer(body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, buf) + if err != nil { + return "", "", err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := n.httpClient.Do(req) + if err != nil { + return "", "", err + } + + defer resp.Body.Close() + + var lensResp AuthResponse + if err := json.NewDecoder(resp.Body).Decode(&lensResp); err != nil { + return "", "", err + } + if lensResp.Data == nil || lensResp.Data.Authenticate == nil { + var errStr string + if lensResp.Error != nil { + errStr = *lensResp.Error + } + return "", "", fmt.Errorf("no result for %s (err %s)", address, errStr) + } + + return lensResp.Data.Authenticate.AccessToken, lensResp.Data.Authenticate.RefreshToken, nil +} + +type RefreshResponse struct { + Data *struct { + Refresh *struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + } `json:"Refresh"` + } `json:"data"` + Error *string `json:"error"` +} + +func (n *LensAPI) RefreshAccessToken(ctx context.Context, refreshToken string) (string, string, error) { + gqlQuery := fmt.Sprintf(`mutation { + refresh(request: { + refreshToken: "%s" + }) { + accessToken + refreshToken + } + }`, refreshToken) + + body, err := json.Marshal(map[string]string{ + "query": gqlQuery, + }) + if err != nil { + return "", "", err + } + buf := bytes.NewBuffer(body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, buf) + if err != nil { + return "", "", err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := n.httpClient.Do(req) + if err != nil { + return "", "", err + } + + defer resp.Body.Close() + + var lensResp RefreshResponse + if err := json.NewDecoder(resp.Body).Decode(&lensResp); err != nil { + return "", "", err + } + if lensResp.Data == nil || lensResp.Data.Refresh == nil { + var errStr string + if lensResp.Error != nil { + errStr = *lensResp.Error + } + return "", "", fmt.Errorf("no result (err %s)", errStr) + } + + return lensResp.Data.Refresh.AccessToken, lensResp.Data.Refresh.RefreshToken, nil } diff --git a/service/socialauth/socialauth.go b/service/socialauth/socialauth.go index fd91163b1..0f70781bd 100644 --- a/service/socialauth/socialauth.go +++ b/service/socialauth/socialauth.go @@ -5,11 +5,13 @@ import ( "fmt" "net/http" + cloudtasks "cloud.google.com/go/cloudtasks/apiv2" "github.com/mikeydub/go-gallery/db/gen/coredb" "github.com/mikeydub/go-gallery/service/farcaster" "github.com/mikeydub/go-gallery/service/lens" "github.com/mikeydub/go-gallery/service/persist" "github.com/mikeydub/go-gallery/service/redis" + "github.com/mikeydub/go-gallery/service/task" "github.com/mikeydub/go-gallery/service/twitter" "github.com/mikeydub/go-gallery/util" ) @@ -65,9 +67,11 @@ func (a TwitterAuthenticator) Authenticate(ctx context.Context) (*SocialAuthResu type FarcasterAuthenticator struct { Queries *coredb.Queries HTTPClient *http.Client + TaskClient *cloudtasks.Client - UserID persist.DBID - Address persist.Address + UserID persist.DBID + Address persist.Address + WithSigner bool } func (a FarcasterAuthenticator) Authenticate(ctx context.Context) (*SocialAuthResult, error) { @@ -92,7 +96,7 @@ func (a FarcasterAuthenticator) Authenticate(ctx context.Context) (*SocialAuthRe return nil, fmt.Errorf("get user by address: %w", err) } - return &SocialAuthResult{ + res := &SocialAuthResult{ Provider: persist.SocialProviderFarcaster, ID: fu.Fid.String(), Metadata: map[string]interface{}{ @@ -101,7 +105,26 @@ func (a FarcasterAuthenticator) Authenticate(ctx context.Context) (*SocialAuthRe "profile_image_url": fu.Pfp.URL, "bio": fu.Profile.Bio.Text, }, - }, nil + } + + if a.WithSigner { + signer, err := api.CreateSignerForUser(ctx, fu.Fid) + if err != nil { + return nil, fmt.Errorf("get signer by address: %w", err) + } + + res.Metadata["signer_uuid"] = signer.SignerUUID + res.Metadata["signer_status"] = signer.Status + err = task.CreateTaskForAutosocialPollFarcaster(ctx, task.AutosocialPollFarcasterMessage{ + SignerUUID: signer.SignerUUID, + UserID: a.UserID, + }, a.TaskClient) + if err != nil { + return nil, fmt.Errorf("create task for autosocial poll farcaster: %w", err) + } + } + + return res, nil } @@ -109,8 +132,9 @@ type LensAuthenticator struct { Queries *coredb.Queries HTTPClient *http.Client - UserID persist.DBID - Address persist.Address + UserID persist.DBID + Address persist.Address + Signature string } func (a LensAuthenticator) Authenticate(ctx context.Context) (*SocialAuthResult, error) { @@ -135,6 +159,23 @@ func (a LensAuthenticator) Authenticate(ctx context.Context) (*SocialAuthResult, return nil, err } + if a.Signature != "" { + access, refresh, err := api.AuthenticateWithSignature(ctx, a.Address, a.Signature) + if err != nil { + return nil, err + } + err = a.Queries.UpsertSocialOAuth(ctx, coredb.UpsertSocialOAuthParams{ + ID: persist.GenerateID(), + UserID: a.UserID, + Provider: persist.SocialProviderLens, + AccessToken: util.ToNullString(access, false), + RefreshToken: util.ToNullString(refresh, false), + }) + if err != nil { + return nil, err + } + } + return &SocialAuthResult{ Provider: persist.SocialProviderFarcaster, ID: lu.ID, diff --git a/service/task/cloudtask.go b/service/task/cloudtask.go index 57ac7987e..b80cd5334 100644 --- a/service/task/cloudtask.go +++ b/service/task/cloudtask.go @@ -66,6 +66,11 @@ type AutosocialProcessUsersMessage struct { Users map[persist.DBID]map[persist.SocialProvider][]persist.ChainAddress `json:"users" binding:"required"` } +type AutosocialPollFarcasterMessage struct { + SignerUUID string `form:"signer_uuid" binding:"required"` + UserID persist.DBID `form:"user_id" binding:"required"` +} + type TokenIdentifiersQuantities map[persist.TokenUniqueIdentifiers]persist.HexString func (t TokenIdentifiersQuantities) MarshalJSON() ([]byte, error) { @@ -218,6 +223,14 @@ func CreateTaskForAutosocialProcessUsers(ctx context.Context, message Autosocial return submitTask(ctx, client, queue, url, withJSON(message), withTrace(span)) } +func CreateTaskForAutosocialPollFarcaster(ctx context.Context, message AutosocialPollFarcasterMessage, client *gcptasks.Client) error { + span, ctx := tracing.StartSpan(ctx, "cloudtask.create", "createTaskForAutosocialPollFarcaster") + defer tracing.FinishSpan(span) + queue := env.GetString("AUTOSOCIAL_POLL_QUEUE") + url := fmt.Sprintf("%s/checkFarcasterApproval/signer_uuid=%s&user_id=%s", env.GetString("AUTOSOCIAL_URL"), message.SignerUUID, message.UserID) + return submitTask(ctx, client, queue, url, withTrace(span)) +} + func CreateTaskForSlackPostFeedBot(ctx context.Context, message FeedbotSlackPostMessage, client *gcptasks.Client) error { span, ctx := tracing.StartSpan(ctx, "cloudtask.create", "createTaskForSlackPostFeedBot") defer tracing.FinishSpan(span)