diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index fe01c2db..9c60b771 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -210,7 +210,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService) templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService) templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController) - ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter) + connectorController := controller.NewConnectorController(siteInfoCommonService) + pluginAPIRouter := router.NewPluginAPIRouter(connectorController) + ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter, pluginAPIRouter) scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService) application := newApplication(serverConf, ginEngine, scheduledTaskManager) return application, func() { diff --git a/go.mod b/go.mod index 7e4559df..bc845d5f 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,11 @@ require ( github.com/go-playground/locales v0.14.0 github.com/go-playground/universal-translator v0.18.0 github.com/go-playground/validator/v10 v10.11.1 + github.com/go-resty/resty/v2 v2.7.0 github.com/go-sql-driver/mysql v1.6.0 github.com/goccy/go-json v0.9.11 github.com/golang/mock v1.4.4 - github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c + github.com/google/go-github/v48 v48.2.0 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 github.com/gosimple/slug v1.13.1 @@ -41,6 +42,7 @@ require ( github.com/yuin/goldmark v1.4.13 golang.org/x/crypto v0.1.0 golang.org/x/net v0.1.0 + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 xorm.io/builder v0.3.12 @@ -69,7 +71,9 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -114,6 +118,7 @@ require ( golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.5.0 // indirect golang.org/x/tools v0.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index cfb81da7..6ad4d654 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,8 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -252,12 +254,12 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8= -github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -271,7 +273,11 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE= +github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -830,7 +836,9 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= @@ -844,6 +852,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1043,6 +1053,7 @@ google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1113,6 +1124,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= diff --git a/internal/base/server/http.go b/internal/base/server/http.go index 1b699bce..f9ee46d4 100644 --- a/internal/base/server/http.go +++ b/internal/base/server/http.go @@ -20,6 +20,7 @@ func NewHTTPServer(debug bool, authUserMiddleware *middleware.AuthUserMiddleware, avatarMiddleware *middleware.AvatarMiddleware, templateRouter *router.TemplateRouter, + pluginAPIRouter *router.PluginAPIRouter, ) *gin.Engine { if debug { @@ -62,5 +63,8 @@ func NewHTTPServer(debug bool, answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1) templateRouter.RegisterTemplateRouter(rootGroup) + + // plugin routes + pluginAPIRouter.RegisterConnector(r) return r } diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go new file mode 100644 index 00000000..62f5a43d --- /dev/null +++ b/internal/controller/connector_controller.go @@ -0,0 +1,63 @@ +package controller + +import ( + "fmt" + + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/plugin" + _ "github.com/answerdev/answer/internal/plugin/connector" + "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/service/siteinfo_common" + "github.com/gin-gonic/gin" +) + +const ( + oauthRedirectRouterPrefix = "/answer/api/v1/oauth/redirect/" + oauthLoginRouterPrefix = "/answer/api/v1/oauth/login/" +) + +// ConnectorController comment controller +type ConnectorController struct { + siteInfoService *siteinfo_common.SiteInfoCommonService +} + +// NewConnectorController new controller +func NewConnectorController( + siteInfoService *siteinfo_common.SiteInfoCommonService, +) *ConnectorController { + return &ConnectorController{siteInfoService: siteInfoService} +} + +func (cc *ConnectorController) ConnectorRedirectRegisterRouters(r *gin.Engine) { + _ = plugin.CallConnector(func(fn plugin.Connector) error { + // user login url + r.GET(oauthLoginRouterPrefix+fn.ConnectorSlugName(), fn.ConnectorSender) + // oauth redirect url + r.GET(oauthRedirectRouterPrefix+fn.ConnectorSlugName(), fn.ConnectorReceiver) + return nil + }) + r.GET("/answer/api/v1/oauth/info", cc.ConnectorsInfo) +} + +func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) { + general, err := cc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + resp := make([]*schema.ConnectorInfoResp, 0) + err = plugin.CallConnector(func(fn plugin.Connector) error { + resp = append(resp, &schema.ConnectorInfoResp{ + Name: fn.ConnectorSlugName(), + Icon: fn.ConnectorLogo(), + Link: fmt.Sprintf("%s%s%s", general.SiteUrl, oauthLoginRouterPrefix, fn.ConnectorSlugName()), + }) + return nil + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, resp) +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index fcbca143..6a7a62b4 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -24,4 +24,5 @@ var ProviderSetController = wire.NewSet( NewUploadController, NewActivityController, NewTemplateController, + NewConnectorController, ) diff --git a/internal/plugin/base.go b/internal/plugin/base.go new file mode 100644 index 00000000..021c2812 --- /dev/null +++ b/internal/plugin/base.go @@ -0,0 +1,22 @@ +package plugin + +// Info presents the plugin information +type Info struct { + Name string + Description string + Author string + Version string + Disabled bool +} + +// Base is the base plugin +type Base interface { + // Info returns the plugin information + Info() Info +} + +var ( + // CallBase is a function that calls all registered base plugins + CallBase, + registerBase = MakePlugin[Base]() +) diff --git a/internal/plugin/connector.go b/internal/plugin/connector.go new file mode 100644 index 00000000..7388f845 --- /dev/null +++ b/internal/plugin/connector.go @@ -0,0 +1,54 @@ +package plugin + +import "time" + +type Connector interface { + Base + + // ConnectorLogo presents the logo binary data of the connector + ConnectorLogo() []byte + + // ConnectorLogoContentType presents the content type of the logo + // e.g. image/png, image/jpeg, image/gif + ConnectorLogoContentType() string + + // ConnectorName presents the name of the connector + // e.g. Facebook, Twitter, Instagram + ConnectorName() string + + // ConnectorSlugName presents the slug name of the connector + // Please use lowercase and hyphen as the separator + // e.g. facebook, twitter, instagram + ConnectorSlugName() string + + // ConnectorSender presents the sender of the connector + // It handles the start endpoint of the connector + ConnectorSender(ctx *GinContext) + ConnectorReceiver(ctx *GinContext) + + //ConnectorLoginURL() (loginURL string) + //ConnectorLoginUserInfo(code string) (userInfo *UserExternalLogin) +} + +type UserExternalLogin struct { + Provider string + ExternalID string + Email string + Name string + FirstName string + LastName string + NickName string + Description string + AvatarUrl string + Location string + AccessToken string + AccessTokenSecret string + RefreshToken string + ExpiresAt time.Time +} + +var ( + // CallConnector is a function that calls all registered connectors + CallConnector, + registerConnector = MakePlugin[Connector]() +) diff --git a/internal/plugin/connector/github.go b/internal/plugin/connector/github.go new file mode 100644 index 00000000..d6805f03 --- /dev/null +++ b/internal/plugin/connector/github.go @@ -0,0 +1,96 @@ +package connector + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/answerdev/answer/internal/plugin" + "github.com/go-resty/resty/v2" + "github.com/google/go-github/v48/github" + "golang.org/x/oauth2" + oauth2GitHub "golang.org/x/oauth2/github" +) + +type GitHub struct { + ClientID string + ClientSecret string +} + +func init() { + plugin.Register(&GitHub{ + ClientID: os.Getenv("GITHUB_CLIENT_ID"), + ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), + }) +} + +func (g *GitHub) Info() plugin.Info { + return plugin.Info{ + Name: "github connector", + Description: "github connector plugin", + Version: "0.0.1", + } +} + +func (g *GitHub) ConnectorLogo() []byte { + response, err := resty.New().R().Get("https://cdn-icons-png.flaticon.com/32/25/25231.png") + if err != nil { + return nil + } + return response.Body() +} + +func (g *GitHub) ConnectorLogoContentType() string { + return "image/png" +} + +func (g *GitHub) ConnectorName() string { + return "GitHubConnector" +} + +func (g *GitHub) ConnectorSlugName() string { + return "github" +} + +func (g *GitHub) ConnectorSender(ctx *plugin.GinContext) { + oauth2Config := &oauth2.Config{ + ClientID: g.ClientID, + ClientSecret: g.ClientSecret, + Endpoint: oauth2GitHub.Endpoint, + RedirectURL: "http://127.0.0.1:8080/answer/api/v1/oauth/redirect/github", + Scopes: nil, + } + ctx.Redirect(http.StatusFound, oauth2Config.AuthCodeURL("")) +} + +func (g *GitHub) ConnectorReceiver(ctx *plugin.GinContext) { + code := ctx.Query("code") + //state := ctx.Query("state") + + oauth2Config := &oauth2.Config{ + ClientID: g.ClientID, + ClientSecret: g.ClientSecret, + Endpoint: oauth2GitHub.Endpoint, + } + token, err := oauth2Config.Exchange(context.Background(), code+"1") + if err != nil { + ctx.Error(err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + cli := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token.AccessToken}, + ))) + userInfo, _, err := cli.Users.Get(context.Background(), "") + if err != nil { + ctx.Error(err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + fmt.Printf("user info is :%+v", userInfo) + ctx.Redirect(http.StatusFound, "/") + return +} diff --git a/internal/plugin/connector/google.go b/internal/plugin/connector/google.go new file mode 100644 index 00000000..259c7c01 --- /dev/null +++ b/internal/plugin/connector/google.go @@ -0,0 +1,47 @@ +package connector + +import ( + "net/http" + + "github.com/answerdev/answer/internal/plugin" +) + +type Google struct { +} + +func (g *Google) Info() plugin.Info { + return plugin.Info{ + Name: "google connector", + Description: "google connector plugin", + Version: "0.0.1", + } +} + +func (g *Google) ConnectorLogo() []byte { + return nil +} + +func (g *Google) ConnectorLogoContentType() string { + return "image/png" +} + +func (g *Google) ConnectorName() string { + return "google" +} + +func (g *Google) ConnectorSlugName() string { + return "google" +} + +func (g *Google) ConnectorSender(ctx *plugin.GinContext) { + //TODO implement me + panic("implement me") +} + +func (g *Google) ConnectorReceiver(ctx *plugin.GinContext) { + ctx.String(http.StatusOK, "OK123") +} + +func init() { + plugin.Register(&Google{}) +} diff --git a/internal/plugin/filter.go b/internal/plugin/filter.go new file mode 100644 index 00000000..0f4fa3df --- /dev/null +++ b/internal/plugin/filter.go @@ -0,0 +1,12 @@ +package plugin + +type Filter interface { + Base + FilterText(text string) (err error) +} + +var ( + // CallFilter is a function that calls all registered parsers + CallFilter, + registerFilter = MakePlugin[Filter]() +) diff --git a/internal/plugin/parser.go b/internal/plugin/parser.go new file mode 100644 index 00000000..e5206bcc --- /dev/null +++ b/internal/plugin/parser.go @@ -0,0 +1,12 @@ +package plugin + +type Parser interface { + Base + Parse(text string) (string, error) +} + +var ( + // CallParser is a function that calls all registered parsers + CallParser, + registerParser = MakePlugin[Parser]() +) diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go new file mode 100644 index 00000000..d680bad2 --- /dev/null +++ b/internal/plugin/plugin.go @@ -0,0 +1,69 @@ +package plugin + +import "github.com/gin-gonic/gin" + +// GinContext is a wrapper of gin.Context +// We export it to make it easy to use in plugins +type GinContext = gin.Context + +// Register registers a plugin +func Register(p Base) { + registerBase(p) + + switch pType := p.(type) { + case Connector: + registerConnector(pType) + case Parser: + registerParser(pType) + case Filter: + registerFilter(pType) + } +} + +// Dump returns all registered plugins infos +func Dump() []Info { + var infos []Info + + CallBase(func(p Base) error { + infos = append(infos, p.Info()) + return nil + }) + + return infos +} + +type Stack[T Base] struct { + plugins []T +} + +type RegisterFn[T Base] func(p T) +type Caller[T Base] func(p T) error +type CallFn[T Base] func(fn Caller[T]) error + +// MakePlugin creates a plugin caller and register stack manager +// It returns a register function and a caller function +// The register function is used to register a plugin, it will be called in the plugin's init function +// The caller function is used to call all registered plugins +func MakePlugin[T Base]() (CallFn[T], RegisterFn[T]) { + stack := Stack[T]{} + + call := func(fn Caller[T]) error { + for _, p := range stack.plugins { + // If the plugin is disabled, skip it + if p.Info().Disabled { + continue + } + + if err := fn(p); err != nil { + return err + } + } + return nil + } + + register := func(p T) { + stack.plugins = append(stack.plugins, p) + } + + return call, register +} diff --git a/internal/plugin/sample/filter_sample.go b/internal/plugin/sample/filter_sample.go new file mode 100644 index 00000000..e50218dd --- /dev/null +++ b/internal/plugin/sample/filter_sample.go @@ -0,0 +1,30 @@ +package sample + +import ( + "fmt" + "strings" + + "github.com/answerdev/answer/internal/plugin" +) + +type FilterSample struct { +} + +func (s *FilterSample) Info() plugin.Info { + return plugin.Info{ + Name: "filter sample", + Description: "filter sample plugin", + Version: "0.0.1", + } +} + +func (s *FilterSample) FilterText(data string) (err error) { + if strings.Contains(data, "violent") { + return fmt.Errorf("bloody and violent words cannot appear in this website") + } + return nil +} + +func init() { + plugin.Register(&FilterSample{}) +} diff --git a/internal/plugin/sample/filter_sample_test.go b/internal/plugin/sample/filter_sample_test.go new file mode 100644 index 00000000..ba72cb20 --- /dev/null +++ b/internal/plugin/sample/filter_sample_test.go @@ -0,0 +1,18 @@ +package sample + +import ( + "fmt" + "testing" + + "github.com/answerdev/answer/internal/plugin" + "github.com/stretchr/testify/assert" +) + +func TestFilterSample_FilterText(t *testing.T) { + // try to call filter plugin for filter text that are not allowed + err := plugin.CallFilter(func(fn plugin.Filter) error { + return fn.FilterText("bloody and violent words") + }) + assert.Error(t, err) + fmt.Println(err) +} diff --git a/internal/plugin/sample/sample.go b/internal/plugin/sample/sample.go new file mode 100644 index 00000000..42e2b7e9 --- /dev/null +++ b/internal/plugin/sample/sample.go @@ -0,0 +1,18 @@ +package sample + +import "github.com/answerdev/answer/internal/plugin" + +type Sample struct { +} + +func (s *Sample) Info() plugin.Info { + return plugin.Info{ + Name: "sample", + Description: "sample plugin", + Version: "0.0.1", + } +} + +func init() { + plugin.Register(&Sample{}) +} diff --git a/internal/router/plugin_api_router.go b/internal/router/plugin_api_router.go new file mode 100644 index 00000000..698fd9d3 --- /dev/null +++ b/internal/router/plugin_api_router.go @@ -0,0 +1,22 @@ +package router + +import ( + "github.com/answerdev/answer/internal/controller" + "github.com/gin-gonic/gin" +) + +type PluginAPIRouter struct { + connectorController *controller.ConnectorController +} + +func NewPluginAPIRouter( + connectorController *controller.ConnectorController, +) *PluginAPIRouter { + return &PluginAPIRouter{ + connectorController: connectorController, + } +} + +func (pr *PluginAPIRouter) RegisterConnector(r *gin.Engine) { + pr.connectorController.ConnectorRedirectRegisterRouters(r) +} diff --git a/internal/router/provider.go b/internal/router/provider.go index f705c973..c3665314 100644 --- a/internal/router/provider.go +++ b/internal/router/provider.go @@ -3,4 +3,11 @@ package router import "github.com/google/wire" // ProviderSetRouter is providers. -var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter, NewTemplateRouter) +var ProviderSetRouter = wire.NewSet( + NewAnswerAPIRouter, + NewSwaggerRouter, + NewStaticRouter, + NewUIRouter, + NewTemplateRouter, + NewPluginAPIRouter, +) diff --git a/internal/schema/connector_schema.go b/internal/schema/connector_schema.go new file mode 100644 index 00000000..712c0ae0 --- /dev/null +++ b/internal/schema/connector_schema.go @@ -0,0 +1,7 @@ +package schema + +type ConnectorInfoResp struct { + Name string `json:"name"` + Icon []byte `json:"icon"` + Link string `json:"link"` +}