1. add youtube command
2. add google webhook
3. add twitch oauth route
4. add api checkSession middleware
This commit is contained in:
Jay 2018-09-16 23:39:15 +08:00
parent 3318352135
commit e8db6fdab5
12 changed files with 487 additions and 13 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
config.yml
.vscode
.idea

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View File

@ -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 -

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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"
}

View File

@ -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()

View File

@ -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))
}
}

103
router/twitch/twitch.go Normal file
View File

@ -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)
}