diff --git a/.gitignore b/.gitignore index 478c433..0152739 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ config.yml .vscode +.idea diff --git a/model/account.go b/model/account.go index 221638c..06b21fb 100644 --- a/model/account.go +++ b/model/account.go @@ -8,7 +8,7 @@ import ( type Account struct { ID string `db:"id" cc:"id"` Account string `db:"account" cc:"account"` - Password string `db:"password" cc:"password"` + Password string `db:"password" cc:"-"` Ctime time.Time `db:"ctime" cc:"ctime"` Mtime time.Time `db:"mtime" cc:"ctime"` } diff --git a/model/twitch_channel.go b/model/twitch_channel.go index b5e2d5f..f446588 100644 --- a/model/twitch_channel.go +++ b/model/twitch_channel.go @@ -1,6 +1,9 @@ package model -import "time" +import ( + "errors" + "time" +) // TwitchGroup - type TwitchGroup struct { @@ -32,6 +35,24 @@ func GetJoinChatChannel() (channels []*TwitchChannel, err error) { return } +// GetTwitchChannelWithName - +func GetTwitchChannelWithName(name string) (ch *TwitchChannel, err error) { + if len(name) == 0 { + return nil, errors.New("name empty") + } + err = x.Get(&ch, `select * from "public"."twitch_channel" where "name" = $1`, name) + return +} + +// GetTwitchChannelWithID - +func GetTwitchChannelWithID(id string) (ch *TwitchChannel, err error) { + if len(id) == 0 { + return nil, errors.New("id empty") + } + err = x.Get(&ch, `select * from "public"."twitch_channel" where "id" = $1`, id) + return +} + // GetWithName - func (p *TwitchChannel) GetWithName() (err error) { stmt, err := x.PrepareNamed(`select * from "public"."twitch_channel" where "name" = :name`) @@ -45,7 +66,7 @@ func (p *TwitchChannel) GetWithName() (err error) { // Add - func (p *TwitchChannel) Add() (err error) { - stmt, err := x.PrepareNamed(`insert into "public"."twitch_channel" ("name", "laststream", "join", "opayid") values (:name, :laststream, :join, :opayid) returning *`) + stmt, err := x.PrepareNamed(`insert into "public"."twitch_channel" ("id", "name", "laststream", "join", "opayid") values (:id, :name, :laststream, :join, :opayid) returning *`) if err != nil { return err } @@ -64,6 +85,16 @@ func (p *TwitchChannel) UpdateStream(streamID string) (err error) { return } +// UpdateName - +func (p *TwitchChannel) UpdateName(name string) (err error) { + _, err = x.Exec(`update "public"."twitch_channel" set "name" = $1 where "id" = $2`, name, p.ID) + if err != nil { + return + } + p.Name = name + return +} + // GetGroups - func (p *TwitchChannel) GetGroups() (err error) { query := `select g.*, rt.tmpl as tmpl from "public"."twitch_channel" tw diff --git a/model/youtube_channel.go b/model/youtube_channel.go index 6a22d44..4eca8c0 100644 --- a/model/youtube_channel.go +++ b/model/youtube_channel.go @@ -25,6 +25,16 @@ func GetYoutubeChannelWithID(id string) (yt *YoutubeChannel, err error) { return } +// Add - +func (p *YoutubeChannel) Add() (err error) { + stmt, err := x.PrepareNamed(`insert into "public"."youtube_channel" ("id", "name") values (:id, :name) returning *`) + if err != nil { + return err + } + err = stmt.Get(p, p) + return +} + // UpdateLastVideo - func (p *YoutubeChannel) UpdateLastVideo(vid string) (err error) { p.LastVideo = vid diff --git a/module/apimsg/apimsg.go b/module/apimsg/apimsg.go index c16d708..0ea26f7 100644 --- a/module/apimsg/apimsg.go +++ b/module/apimsg/apimsg.go @@ -37,6 +37,12 @@ var objs = map[string]*ResObject{ "message": "input data format error", }, }, + "LoginFirst": &ResObject{ + Status: 401, + Obj: map[string]string{ + "message": "login first", + }, + }, } // GetRes - diff --git a/module/apis/google/google.go b/module/apis/google/google.go new file mode 100644 index 0000000..2e03d51 --- /dev/null +++ b/module/apis/google/google.go @@ -0,0 +1,157 @@ +package google + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "git.trj.tw/golang/mtfosbot/module/apis" + + "git.trj.tw/golang/mtfosbot/module/config" +) + +var baseURL = "https://www.googleapis.com" + +func getURL(p string, querystring ...interface{}) (string, bool) { + u, err := url.Parse(baseURL) + if err != nil { + return "", false + } + ref, err := u.Parse(p) + if err != nil { + return "", false + } + if len(querystring) > 0 { + switch querystring[0].(type) { + case string: + ref, err = ref.Parse(fmt.Sprintf("?%s", (querystring[0].(string)))) + if err != nil { + return "", false + } + break + default: + } + } + + str := ref.String() + return str, true +} + +func getHeaders(token ...interface{}) map[string]string { + m := make(map[string]string) + m["Content-Type"] = "application/json" + return m +} + +type channelItem struct { + ID string `json:"id"` + Sinppet channelSinppet `json:"sinppet"` +} +type channelSinppet struct { + Title string `json:"title"` + Description string `json:"description"` + CustomURL string `json:"customUrl"` +} + +// QueryYoutubeName - +func QueryYoutubeName(id string) (n string, err error) { + conf := config.GetConf() + if len(id) == 0 { + return "", errors.New("id is empty") + } + qs := url.Values{} + qs.Add("id", id) + qs.Add("key", conf.Google.APIKey) + qs.Add("part", "snippet") + + apiURL, ok := getURL("/youtube/v3/channels", qs.Encode()) + if !ok { + return "", errors.New("url parser fail") + } + reqObj := apis.RequestObj{ + Method: "GET", + URL: apiURL, + Headers: getHeaders(), + } + + req, err := apis.GetRequest(reqObj) + if err != nil { + return "", err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 || !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + return "", errors.New("api response fail") + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + apiRes := struct { + Items []channelItem `json:"items"` + }{} + + err = json.Unmarshal(bodyBytes, &apiRes) + if err != nil { + return "", err + } + + if len(apiRes.Items) == 0 { + return "", errors.New("channel data not found") + } + + for _, v := range apiRes.Items { + if v.ID == id { + return v.Sinppet.Title, nil + } + } + + return "", errors.New("channel data not found") +} + +// SubscribeYoutube - +func SubscribeYoutube(id string) { + if len(id) == 0 { + return + } + conf := config.GetConf() + apiURL := "https://pubsubhubbub.appspot.com/subscribe" + cbURL, err := url.Parse(conf.URL) + if err != nil { + return + } + cbURL, err = cbURL.Parse(fmt.Sprintf("/google/youtube/webhook?id=%s", id)) + if err != nil { + return + } + + qs := url.Values{} + qs.Add("hub.mode", "subscribe") + qs.Add("hub.verify", "async") + qs.Add("hub.topic", fmt.Sprintf("https://www.youtube.com/xml/feeds/videos.xml?channel_id=%s", id)) + qs.Add("hub.callback", cbURL.String()) + qs.Add("hub.lease_seconds", "86400") + + req, err := http.NewRequest("POST", apiURL, strings.NewReader(qs.Encode())) + if err != nil { + return + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + return +} diff --git a/module/apis/twitch/twitch.go b/module/apis/twitch/twitch.go index f66c815..cf04050 100644 --- a/module/apis/twitch/twitch.go +++ b/module/apis/twitch/twitch.go @@ -2,6 +2,7 @@ package twitch import ( "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -234,3 +235,66 @@ func GetUserStreamStatus(ids []string) (info []*StreamInfo) { return apiData.Data } + +// TwitchTokenData - +type TwitchTokenData struct { + AccessToken string `json:"access_token" cc:"access_token"` + RefreshToken string `json:"refresh_token" cc:"refresh_token"` + ExpiresIn int64 `json:"expires_in" cc:"expires_in"` + Scope string `json:"scope" cc:"scope"` + TokenType string `json:"token_type" cc:"token_type"` +} + +// GetTokenData - +func GetTokenData(code string) (token *TwitchTokenData, err error) { + if len(code) == 0 { + return nil, errors.New("code is empty") + } + conf := config.GetConf() + twitchURL := "https://id.twitch.tv/oauth2/token" + redirectTo := strings.TrimRight(conf.URL, "/") + "/twitch/oauth" + + qs := url.Values{} + qs.Add("client_id", conf.Twitch.ClientID) + qs.Add("client_secret", conf.Twitch.ClientSecret) + qs.Add("code", code) + qs.Add("grant_type", "authorization_code") + qs.Add("redirect_uri", redirectTo) + + u, err := url.Parse(twitchURL) + if err != nil { + return nil, err + } + u, err = u.Parse(qs.Encode()) + if err != nil { + return nil, err + } + + reqObj := apis.RequestObj{ + URL: u.String(), + Method: "POST", + } + req, err := apis.GetRequest(reqObj) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 || strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + return nil, errors.New("api response error") + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bodyBytes, &token) + + return +} diff --git a/module/context/context.go b/module/context/context.go index b3d0964..6c304c3 100644 --- a/module/context/context.go +++ b/module/context/context.go @@ -35,6 +35,12 @@ func (c *Context) CustomRes(status int, msg interface{}) { c.AbortWithStatusJSON(status, msg) } +// LoginFirst - +func (c *Context) LoginFirst(msg interface{}) { + obj := apimsg.GetRes("LoginFirst", msg) + c.AbortWithStatusJSON(obj.Status, obj.Obj) +} + // NotFound - func (c *Context) NotFound(msg interface{}) { obj := apimsg.GetRes("NotFound", msg) diff --git a/module/message-command/line-group.go b/module/message-command/line-group.go index a0c604f..5652e5a 100644 --- a/module/message-command/line-group.go +++ b/module/message-command/line-group.go @@ -7,6 +7,7 @@ import ( "git.trj.tw/golang/mtfosbot/module/apis/twitch" "git.trj.tw/golang/mtfosbot/model" + googleapi "git.trj.tw/golang/mtfosbot/module/apis/google" lineobj "git.trj.tw/golang/mtfosbot/module/line-message/line-object" ) @@ -173,13 +174,19 @@ func addTwitchChannel(sub, txt string, s *lineobj.SourceObject) (res string) { return "get twitch user id fail" } - ch := &model.TwitchChannel{ - ID: info.ID, - Name: info.DisplayName, - } - err = ch.Add() + ch, err := model.GetTwitchChannelWithName(args[0]) if err != nil { - return "add twitch channel fail" + return "check channel fail" + } + if ch == nil { + ch = &model.TwitchChannel{ + ID: info.ID, + Name: info.DisplayName, + } + err = ch.Add() + if err != nil { + return "add twitch channel fail" + } } rt := &model.LineTwitchRT{ @@ -239,3 +246,50 @@ func delTwitchChannel(sub, txt string, s *lineobj.SourceObject) (res string) { return "Success" } + +func addYoutubeChannel(sub, txt string, s *lineobj.SourceObject) (res string) { + // args = youtubeID tmpl + ok, err := checkGroupOwner(s) + if err != nil { + return "check group fail" + } + if !ok { + return "not owner" + } + + args := strings.Split(strings.Trim(txt, " "), " ") + if len(args) < 2 { + return "command arg not match" + } + ytName, err := googleapi.QueryYoutubeName(args[0]) + if err != nil || len(ytName) == 0 { + return "get youtube channel name fail" + } + + ytData, err := model.GetYoutubeChannelWithID(args[0]) + if err != nil { + return "check youtube fail" + } + if ytData == nil { + ytData = &model.YoutubeChannel{ + ID: args[0], + Name: ytName, + } + err = ytData.Add() + if err != nil { + return "add youtube channel fail" + } + } + + rt := &model.LineYoutubeRT{ + Line: s.GroupID, + Youtube: args[0], + Tmpl: strings.Join(args[1:], " "), + } + err = rt.AddRT() + if err != nil { + return "add youtube channel rt fail" + } + + return "Success" +} diff --git a/router/api/api.go b/router/api/api.go index 12110c5..b5f3ef8 100644 --- a/router/api/api.go +++ b/router/api/api.go @@ -2,12 +2,47 @@ package api import ( "git.trj.tw/golang/mtfosbot/model" + "git.trj.tw/golang/mtfosbot/module/apis/twitch" "git.trj.tw/golang/mtfosbot/module/context" - "git.trj.tw/golang/mtfosbot/module/utils" "github.com/gin-gonic/contrib/sessions" "golang.org/x/crypto/bcrypt" ) +// CheckSession - +func CheckSession(c *context.Context) { + session := sessions.Default(c.Context) + userData := session.Get("user") + loginType := session.Get("loginType") + if userData == nil || loginType == nil { + c.LoginFirst(nil) + return + } + var name string + var ltype string + var ok bool + switch userData.(type) { + case model.Account: + name = userData.(model.Account).Account + case twitch.UserInfo: + name = userData.(twitch.UserInfo).DisplayName + default: + c.LoginFirst(nil) + return + } + if ltype, ok = loginType.(string); !ok { + c.LoginFirst(nil) + return + } + + loginUser := map[string]string{ + "name": name, + "type": ltype, + } + session.Set("loginUser", loginUser) + session.Save() + c.Next() +} + // UserLogin - system user login func UserLogin(c *context.Context) { bodyArg := struct { @@ -32,11 +67,10 @@ func UserLogin(c *context.Context) { return } - accInt := utils.ToMap(acc) - delete(accInt, "password") session := sessions.Default(c.Context) - session.Set("user", accInt) + acc.Password = "" + session.Set("user", acc) session.Set("loginType", "system") session.Save() diff --git a/router/routes/routes.go b/router/routes/routes.go index 1d60866..04ba3cc 100644 --- a/router/routes/routes.go +++ b/router/routes/routes.go @@ -7,6 +7,7 @@ import ( "git.trj.tw/golang/mtfosbot/router/api" "git.trj.tw/golang/mtfosbot/router/google" "git.trj.tw/golang/mtfosbot/router/line" + "git.trj.tw/golang/mtfosbot/router/twitch" "github.com/gin-contrib/cors" "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" @@ -44,6 +45,7 @@ func SetRoutes(r *gin.Engine) { apiGroup := r.Group("/api") { apiGroup.POST("/login", context.PatchCtx(api.UserLogin)) + apiGroup.POST("/logout", context.PatchCtx(api.UserLogout)) } lineApis := r.Group("/line") @@ -56,4 +58,10 @@ func SetRoutes(r *gin.Engine) { googleApis.GET("/youtube/webhook", context.PatchCtx(google.VerifyWebhook)) googleApis.POST("/youtube/webhook", context.PatchCtx(google.GetNotifyWebhook)) } + + twitchApis := r.Group("/twitch") + { + twitchApis.GET("/login", context.PatchCtx(twitch.OAuthLogin)) + twitchApis.GET("/oauth", context.PatchCtx(twitch.OAuthProc)) + } } diff --git a/router/twitch/twitch.go b/router/twitch/twitch.go new file mode 100644 index 0000000..5a90eed --- /dev/null +++ b/router/twitch/twitch.go @@ -0,0 +1,103 @@ +package twitch + +import ( + "net/url" + "strings" + + "git.trj.tw/golang/mtfosbot/model" + twitchapi "git.trj.tw/golang/mtfosbot/module/apis/twitch" + "git.trj.tw/golang/mtfosbot/module/config" + "git.trj.tw/golang/mtfosbot/module/context" + "github.com/gin-gonic/contrib/sessions" +) + +// OAuthLogin - +func OAuthLogin(c *context.Context) { + session := sessions.Default(c.Context) + conf := config.GetConf() + redirectTo := strings.TrimRight(conf.URL, "/") + redirectTo += "/twitch/oauth" + qs := url.Values{} + qs.Add("client_id", conf.Twitch.ClientID) + qs.Add("redirect_uri", redirectTo) + qs.Add("response_type", "code") + qs.Add("scope", "user:read:email") + + toURL, ok := c.GetQuery("tourl") + if ok && len(toURL) > 0 { + session.Set("backUrl", toURL) + session.Save() + } + + u, err := url.Parse(redirectTo) + if err != nil { + c.ServerError(nil) + return + } + finalURL, err := u.Parse(qs.Encode()) + if err != nil { + c.ServerError(nil) + return + } + c.Redirect(301, finalURL.String()) +} + +// OAuthProc - +func OAuthProc(c *context.Context) { + code, ok := c.GetQuery("code") + if !ok || len(code) == 0 { + c.DataFormat(nil) + return + } + + tokenData, err := twitchapi.GetTokenData(code) + if err != nil { + c.DataFormat(nil) + return + } + + session := sessions.Default(c.Context) + + userData := twitchapi.GetUserDataByToken(tokenData.AccessToken) + if userData == nil { + c.ServerError(nil) + return + } + + session.Set("token", tokenData) + session.Set("user", userData) + session.Set("loginType", "twitch") + + chData, err := model.GetTwitchChannelWithID(userData.ID) + if err != nil { + c.ServerError(nil) + return + } + if chData == nil { + chData = &model.TwitchChannel{ + ID: userData.ID, + Name: userData.DisplayName, + } + err = chData.Add() + if err != nil { + c.ServerError(nil) + return + } + } else { + if userData.DisplayName != chData.Name { + chData.UpdateName(userData.DisplayName) + } + } + + conf := config.GetConf() + goURL := strings.TrimRight(conf.URL, "/") + "/web" + tourl := session.Get("backUrl") + if tourl != nil { + if str, ok := tourl.(string); ok { + goURL = str + session.Delete("backUrl") + } + } + session.Save() + c.Redirect(301, goURL) +}