Compare commits
6 Commits
5f35823033
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01bd2eb9b | ||
|
|
a6d932a560 | ||
|
|
81e8bca787 | ||
|
|
5c6e93c7e4 | ||
|
|
9770ef9f21 | ||
|
|
0083d42e06 |
@@ -1,7 +1,13 @@
|
||||
CLIENT_ID= # Twitch Client ID
|
||||
# Required
|
||||
CLIENT_SECRET= # Twitch Client Secret
|
||||
# Required
|
||||
REDIR_URI= # Twitch OAuth Redirect URI
|
||||
# Required
|
||||
|
||||
WS_AUTHORIZATION= # the authorization code all websocket clients will need to connect
|
||||
# Required
|
||||
# TODO this is not the final version, this will not be a permanent config option
|
||||
|
||||
SQLITE_DB= # SQlite DB location
|
||||
# Default: ./db.sqlite
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"zomo.dev/largehadroncollider/ttv"
|
||||
)
|
||||
|
||||
func (server *ApiServer) loadEndpoints() {
|
||||
@@ -18,9 +20,9 @@ func (server *ApiServer) loadEndpoints() {
|
||||
serverInfo := ServerInfo{
|
||||
ClientID: server.conf.ClientID,
|
||||
AuthParams: TwitchAuthParams{
|
||||
ClientID: server.conf.ClientID,
|
||||
ForceVerify: false,
|
||||
RedirectURI: server.conf.RedirectURI,
|
||||
ClientID: server.conf.ClientID,
|
||||
ForceVerify: false,
|
||||
RedirectURI: server.conf.RedirectURI,
|
||||
ResponseType: "code",
|
||||
Scope: []string{
|
||||
"bits:read",
|
||||
@@ -51,46 +53,70 @@ func (server *ApiServer) loadEndpoints() {
|
||||
|
||||
server.engine.GET("/auth", func(c *gin.Context) {
|
||||
q := c.Request.URL.Query()
|
||||
if resp := loadAuthQueryOk(q); resp != nil {
|
||||
if reqBody := loadAuthQueryOk(q); reqBody != nil {
|
||||
// ok
|
||||
// TODO check state (need state system)
|
||||
// TODO POST https://id.twitch.tv/oauth2/token - returns TwitchAuthTokenResp
|
||||
// convert expiresIn to time.Time (minus like 15 minutes as a buffer period)
|
||||
// UpdateUserAuth()
|
||||
// TODO return twitch ok (or err if can't POST)
|
||||
} else if resp := loadAuthQueryErr(q); resp != nil {
|
||||
|
||||
// TODO check state value (need state system)
|
||||
|
||||
authResp, err := server.twitch.Auth.DoAuth(server.conf, reqBody.Code)
|
||||
if err != nil {
|
||||
log.Println("Error Authenticating:")
|
||||
log.Printf(" Parsed Request Body: %+v", reqBody)
|
||||
log.Printf(" Authorization Error: %v", err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = server.twitch.Auth.UpdateUserDetails(server.db, authResp.AccessToken, authResp.RefreshToken, authResp.ExpiresIn)
|
||||
if err != nil {
|
||||
log.Println("Error Updating User Details:")
|
||||
log.Printf(" Parsed Request Body: %+v", reqBody)
|
||||
log.Printf(" Authorization Response: %+v", authResp)
|
||||
log.Printf(" Error: %v", err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO return twitch ok
|
||||
c.Status(http.StatusOK)
|
||||
} else if reqBody := loadAuthQueryErr(q); reqBody != nil {
|
||||
// err from twitch
|
||||
// TODO check state (need state system)
|
||||
// TODO return twitch err
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
} else {
|
||||
// err in params
|
||||
// TODO return param err
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
// TODO auth response from twitch
|
||||
// parse args as TwitchAuthRespOk or TwitchAuthRespErr
|
||||
// verify state with db and client id with config
|
||||
c.JSON(http.StatusOK, serverInfo)
|
||||
server.engine.Any("/ws", func(c *gin.Context) {
|
||||
err := server.ws.Handle(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
log.Printf("Error handing websocket connection from %s: %v", c.RemoteIP(), err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
AuthParams TwitchAuthParams `json:"auth_params"`
|
||||
}
|
||||
|
||||
type TwitchAuthParams struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ForceVerify bool `json:"force_verify"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
ResponseType string `json:"response_type"`
|
||||
Scope []string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
ClientID string `json:"client_id"`
|
||||
ForceVerify bool `json:"force_verify"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
ResponseType string `json:"response_type"`
|
||||
Scope []string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func loadAuthQueryOk(query url.Values) *TwitchAuthRespOk {
|
||||
func loadAuthQueryOk(query url.Values) *ttv.TwitchAuthRespOk {
|
||||
if query.Has("code") && query.Has("scope") && query.Has("state") {
|
||||
return &TwitchAuthRespOk{
|
||||
return &ttv.TwitchAuthRespOk{
|
||||
Code: query.Get("code"),
|
||||
Scope: query.Get("scope"),
|
||||
State: query.Get("state"),
|
||||
@@ -99,9 +125,9 @@ func loadAuthQueryOk(query url.Values) *TwitchAuthRespOk {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadAuthQueryErr(query url.Values) *TwitchAuthRespErr {
|
||||
func loadAuthQueryErr(query url.Values) *ttv.TwitchAuthRespErr {
|
||||
if query.Has("error") && query.Has("error_description") && query.Has("state") {
|
||||
return &TwitchAuthRespErr{
|
||||
return &ttv.TwitchAuthRespErr{
|
||||
Err: query.Get("error"),
|
||||
ErrDesc: query.Get("error_description"),
|
||||
State: query.Get("state"),
|
||||
@@ -109,23 +135,3 @@ func loadAuthQueryErr(query url.Values) *TwitchAuthRespErr {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TwitchAuthRespOk struct {
|
||||
Code string `json:"code"`
|
||||
Scope string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type TwitchAuthRespErr struct {
|
||||
Err string `json:"error"`
|
||||
ErrDesc string `json:"error_description"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type TwitchAuthTokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope []string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
14
api/main.go
14
api/main.go
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"zomo.dev/largehadroncollider/api/ws"
|
||||
"zomo.dev/largehadroncollider/db"
|
||||
"zomo.dev/largehadroncollider/ttv"
|
||||
"zomo.dev/largehadroncollider/util"
|
||||
@@ -9,18 +10,23 @@ import (
|
||||
|
||||
func InitApiServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*ApiServer, error) {
|
||||
engine := gin.Default()
|
||||
wsServer, err := ws.InitWSServer(conf, dbConn, twitchConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiServer := &ApiServer{ engine, conf, dbConn, twitchConn }
|
||||
apiServer := &ApiServer{engine, wsServer, conf, dbConn, twitchConn}
|
||||
|
||||
apiServer.loadEndpoints()
|
||||
|
||||
|
||||
return apiServer, nil
|
||||
}
|
||||
|
||||
type ApiServer struct {
|
||||
engine *gin.Engine
|
||||
conf *util.Config
|
||||
db *db.DBConn
|
||||
ws *ws.WSServer
|
||||
conf *util.Config
|
||||
db *db.DBConn
|
||||
twitch *ttv.TwitchConn
|
||||
}
|
||||
|
||||
|
||||
234
api/ws/commands.go
Normal file
234
api/ws/commands.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Command string
|
||||
Args []string
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
cmd *Command
|
||||
r *strings.Reader
|
||||
arg string
|
||||
args []string
|
||||
}
|
||||
|
||||
type parserQuote rune
|
||||
const (
|
||||
parserQuoteSingle parserQuote = '\''
|
||||
parserQuoteDouble parserQuote = '"'
|
||||
)
|
||||
|
||||
type parserBracket rune
|
||||
const (
|
||||
parserBracketCurly parserBracket = '{'
|
||||
parserBracketSquare parserBracket = '['
|
||||
parserBracketTriangle parserBracket = '<'
|
||||
)
|
||||
var parserClosingBracket = map[parserBracket]rune{
|
||||
parserBracketCurly: '}',
|
||||
parserBracketSquare: ']',
|
||||
parserBracketTriangle: '>',
|
||||
}
|
||||
|
||||
func parseCommand(msg string) (*Command, error) {
|
||||
cmd := &Command{}
|
||||
r := strings.NewReader(msg)
|
||||
parser := &parser{cmd: cmd, r: r}
|
||||
return cmd, parser.parse()
|
||||
}
|
||||
|
||||
func (p *parser) parse() error {
|
||||
err := p.stateBase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.args = append(p.args, p.arg)
|
||||
|
||||
filteredArgs := []string{}
|
||||
for _, arg := range p.args {
|
||||
if arg != "" {
|
||||
filteredArgs = append(filteredArgs, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.args) >= 1 {
|
||||
p.cmd.Command = p.args[0]
|
||||
}
|
||||
|
||||
if len(p.args) >= 2 {
|
||||
p.cmd.Args = p.args[1:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) consume() (rune, bool, error) {
|
||||
b := make([]byte, 1)
|
||||
_, err := p.r.Read(b)
|
||||
if err == io.EOF {
|
||||
return 0, true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return rune(b[0]), false, nil
|
||||
}
|
||||
|
||||
func (p *parser) push(r rune) {
|
||||
p.arg += string(r)
|
||||
}
|
||||
|
||||
func (p *parser) flush() {
|
||||
p.args = append(p.args, p.arg)
|
||||
p.arg = ""
|
||||
}
|
||||
|
||||
func (p *parser) stateBase() error {
|
||||
for {
|
||||
ch, eof, err := p.consume()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if eof {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '\\':
|
||||
err := p.stateEscape()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case rune(parserQuoteDouble): fallthrough
|
||||
case rune(parserQuoteSingle):
|
||||
err := p.stateQuote(parserQuote(ch))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// special case, if the arg starts with a { then parse it with rough object mode
|
||||
case rune(parserBracketCurly): fallthrough
|
||||
case rune(parserBracketSquare): fallthrough
|
||||
case rune(parserBracketTriangle):
|
||||
if p.arg != "" {
|
||||
p.push(ch)
|
||||
} else {
|
||||
err := p.stateBracket(parserBracket(ch))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case '\n': fallthrough
|
||||
case '\t': fallthrough
|
||||
case ' ':
|
||||
p.flush()
|
||||
|
||||
default:
|
||||
p.push(ch)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) stateQuote(quote parserQuote) error {
|
||||
for {
|
||||
ch, eof, err := p.consume()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if eof {
|
||||
return errors.New("missing closing quote")
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '\\':
|
||||
err := p.stateEscape()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case rune(quote):
|
||||
return nil
|
||||
|
||||
default:
|
||||
p.push(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) stateBracket(bracket parserBracket) error {
|
||||
p.push(rune(bracket))
|
||||
for {
|
||||
ch, eof, err := p.consume()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if eof {
|
||||
return errors.New("missing closing bracket")
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case '\\':
|
||||
err := p.stateEscape()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case rune(parserQuoteDouble): fallthrough
|
||||
case rune(parserQuoteSingle):
|
||||
p.push(ch)
|
||||
err := p.stateQuote(parserQuote(ch))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.push(ch)
|
||||
|
||||
case rune(parserBracketCurly): fallthrough
|
||||
case rune(parserBracketSquare): fallthrough
|
||||
case rune(parserBracketTriangle):
|
||||
err := p.stateBracket(parserBracket(ch))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case parserClosingBracket[bracket]:
|
||||
p.push(ch)
|
||||
return nil
|
||||
|
||||
default:
|
||||
p.push(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) stateEscape() error {
|
||||
ch, eof, err := p.consume()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if eof {
|
||||
return errors.New("invalid escape sequence before end of file")
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case 'n':
|
||||
p.arg += "\n"
|
||||
|
||||
case 't':
|
||||
p.arg += "\t"
|
||||
|
||||
default:
|
||||
p.push(ch)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
api/ws/main.go
119
api/ws/main.go
@@ -1,17 +1,128 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"zomo.dev/largehadroncollider/db"
|
||||
"zomo.dev/largehadroncollider/ttv"
|
||||
"zomo.dev/largehadroncollider/util"
|
||||
)
|
||||
|
||||
func InitWSServer(dbConn db.DBConn, twitchConn ttv.TwitchConn) (WSServer, error) {
|
||||
return WSServer{}, nil
|
||||
func InitWSServer(conf *util.Config, dbConn *db.DBConn, twitchConn *ttv.TwitchConn) (*WSServer, error) {
|
||||
return &WSServer{conf, dbConn, twitchConn}, nil
|
||||
}
|
||||
|
||||
type WSServer struct {
|
||||
conf *util.Config
|
||||
db *db.DBConn
|
||||
twitch *ttv.TwitchConn
|
||||
}
|
||||
|
||||
func (wsServer *WSServer) Listen() {
|
||||
// start web server itself
|
||||
type WSConn struct {
|
||||
conf *util.Config
|
||||
c net.Conn
|
||||
events []string
|
||||
authrorized bool
|
||||
}
|
||||
|
||||
func (wsServer *WSServer) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||
conn, _, _, err := ws.UpgradeHTTP(r, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wsconn := &WSConn{}
|
||||
go wsconn.handleThread(wsServer.conf, conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *WSConn) handleThread(conf *util.Config, c net.Conn) {
|
||||
conn.conf = conf
|
||||
conn.c = c
|
||||
defer c.Close()
|
||||
|
||||
for {
|
||||
msg, _, err := wsutil.ReadClientData(conn.c)
|
||||
if err != nil {
|
||||
// TODO handle error better, print more conn info
|
||||
log.Printf("Error reading data from ws connection %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd, err := parseCommand(string(msg))
|
||||
if err != nil {
|
||||
log.Printf("Error parsing command data from ws connection %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("%+v", cmd)
|
||||
|
||||
err = conn.runCommand(cmd)
|
||||
|
||||
if err != nil {
|
||||
_, err := fmt.Fprintf(conn.c, "error running command: %s: %v", cmd.Command, err)
|
||||
if err != nil {
|
||||
// TODO better print
|
||||
log.Printf("ERROR: unable to send error to client: %v", err)
|
||||
log.Printf("ERROR: client details: %+v", conn)
|
||||
log.Printf("ERROR: command details: %+v", cmd)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *WSConn) runCommand(cmd *Command) error {
|
||||
switch strings.ToLower(cmd.Command) {
|
||||
case "ping":
|
||||
return conn.runCommandPing(cmd.Args)
|
||||
case "authorization":
|
||||
return conn.runCommandAuthorization(cmd.Args)
|
||||
|
||||
case "events-add":
|
||||
return conn.runCommandEventsAdd(cmd.Args)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid command: %s", cmd.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *WSConn) runCommandPing(_ []string) error {
|
||||
_, err := fmt.Fprint(conn.c, "pong")
|
||||
return err
|
||||
}
|
||||
|
||||
func (conn *WSConn) runCommandAuthorization(args []string) error {
|
||||
if conn.authrorized {
|
||||
return errors.New("connection already authorized")
|
||||
}
|
||||
if len(args) < 1 {
|
||||
return errors.New("no authorization key given")
|
||||
}
|
||||
|
||||
key := args[0]
|
||||
if key != conn.conf.WSAuthorization {
|
||||
return errors.New("invalid authorization key")
|
||||
}
|
||||
|
||||
conn.authrorized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *WSConn) runCommandEventsAdd(args []string) error {
|
||||
if !conn.authrorized {
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
// TODO subscribe
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package db_cold
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const EXPIRATION_BUFFER = 5 * time.Minute
|
||||
|
||||
type UserAuth struct {
|
||||
gorm.Model
|
||||
UserID string `gorm:"primarykey"`
|
||||
@@ -31,12 +35,12 @@ func (db *DBColdConn) GetAllUserAuth() ([]UserAuth, error) {
|
||||
}
|
||||
|
||||
// add or update user auth, based on ID
|
||||
func (db *DBColdConn) UpdateUserAuth(userID, userName, userLogin, accessToken, refreshToken string, tokenExpires time.Time) error {
|
||||
func (db *DBColdConn) updateUserAuthTime(userID, userName, userLogin, accessToken, refreshToken string, tokenExpires time.Time) error {
|
||||
userAuth := UserAuth{
|
||||
UserID: userID,
|
||||
UserName: userName,
|
||||
UserLogin: userLogin,
|
||||
AccessToken: accessToken,
|
||||
UserID: userID,
|
||||
UserName: userName,
|
||||
UserLogin: userLogin,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenExpires: tokenExpires,
|
||||
}
|
||||
@@ -62,3 +66,36 @@ func (db *DBColdConn) UpdateUserAuth(userID, userName, userLogin, accessToken, r
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DBColdConn) updateUserAuthDuration(userID, userName, userLogin, accessToken, refreshToken string, tokenExpires time.Duration) error {
|
||||
return db.updateUserAuthTime(userID, userName, userLogin, accessToken, refreshToken, time.Now().Add(tokenExpires-EXPIRATION_BUFFER))
|
||||
}
|
||||
|
||||
func (db *DBColdConn) updateUserAuthInt(userID, userName, userLogin, accessToken, refreshToken string, tokenExpires int) error {
|
||||
return db.updateUserAuthDuration(userID, userName, userLogin, accessToken, refreshToken, time.Duration(tokenExpires)*time.Second)
|
||||
}
|
||||
|
||||
/** tokenExpires: time.Time | time.Duration | int */
|
||||
func (db *DBColdConn) UpdateUserAuth(userID, userName, userLogin, accessToken, refreshToken string, tokenExpires any) error {
|
||||
/*
|
||||
expires can be:
|
||||
- time.Time
|
||||
- passed directly
|
||||
- time.Duration
|
||||
- converted to time.Time from now
|
||||
- int
|
||||
- converted to time.Duration in seconds then time.Time from now
|
||||
*/
|
||||
|
||||
switch exp := tokenExpires.(type) {
|
||||
case time.Time:
|
||||
return db.updateUserAuthTime(userID, userName, userLogin, accessToken, refreshToken, exp)
|
||||
case time.Duration:
|
||||
return db.updateUserAuthDuration(userID, userName, userLogin, accessToken, refreshToken, exp)
|
||||
case int:
|
||||
return db.updateUserAuthInt(userID, userName, userLogin, accessToken, refreshToken, exp)
|
||||
default:
|
||||
log.Panicf("invalid type passed to any field in TwitchAuth.updateUserDetails(): %s, value: %v", reflect.TypeOf(tokenExpires), tokenExpires)
|
||||
panic("we've already panicked")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ func InitDBColdConn(conf *util.Config) (*DBColdConn, error) {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
cold := &DBColdConn{ db, ctx }
|
||||
|
||||
cold := &DBColdConn{db, ctx}
|
||||
cold.initUserAuth()
|
||||
|
||||
return cold, nil
|
||||
@@ -25,5 +25,5 @@ func InitDBColdConn(conf *util.Config) (*DBColdConn, error) {
|
||||
|
||||
type DBColdConn struct {
|
||||
Gorm *gorm.DB
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ func InitDBConn(conf *util.Config) (*DBConn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DBConn{ hot, cold }, nil
|
||||
return &DBConn{hot, cold}, nil
|
||||
}
|
||||
|
||||
type DBConn struct {
|
||||
Hot *db_hot.DBHotConn
|
||||
Hot *db_hot.DBHotConn
|
||||
Cold *db_cold.DBColdConn
|
||||
}
|
||||
|
||||
15
test/main.go
Normal file
15
test/main.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
|
||||
|
||||
func main() {
|
||||
// cmd, err := parseCommand("abc \\\"def ghi\"\" [abc][] {\"abc\":'def'}")
|
||||
// if err != nil {
|
||||
// fmt.Printf("ERROR: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
// fmt.Printf("cmd: %s\n", cmd.Command)
|
||||
// for _, arg := range cmd.Args {
|
||||
// fmt.Printf("arg: %s\n", arg)
|
||||
// }
|
||||
}
|
||||
129
ttv/auth.go
129
ttv/auth.go
@@ -2,83 +2,116 @@ package ttv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/adeithe/go-twitch/api"
|
||||
"zomo.dev/largehadroncollider/db"
|
||||
"zomo.dev/largehadroncollider/db/db_cold"
|
||||
"zomo.dev/largehadroncollider/util"
|
||||
)
|
||||
|
||||
// sign in to twitch with each saved tokens
|
||||
const TWITCH_AUTH_URL = "https://id.twitch.tv/oauth2/token"
|
||||
|
||||
func initAuth(conf *util.Config, dbConn *db.DBConn) (*TwitchAuth, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
tokens, err := dbConn.Cold.GetAllUserAuth()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := api.New(conf.ClientID)
|
||||
twitchAuth := &TwitchAuth{ctx, client}
|
||||
|
||||
accounts, err := testTokens(ctx, client, tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// run once synchronously then start looping in a thread
|
||||
twitchAuth.updateDetailsRefreshTokens(conf, dbConn)
|
||||
go twitchAuth.loopUpdateDetailsRefreshTokens(conf, dbConn)
|
||||
|
||||
for _, account := range accounts {
|
||||
err := dbConn.Cold.UpdateUserAuth(account.UserID, account.UserName, account.UserLogin, account.AccessToken, account.RefreshToken, account.TokenExpires)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &TwitchAuth{ ctx, client, accounts }, nil
|
||||
return twitchAuth, nil
|
||||
}
|
||||
|
||||
type TwitchAuth struct {
|
||||
Ctx context.Context
|
||||
Ctx context.Context
|
||||
Client *api.Client
|
||||
Accounts []db_cold.UserAuth
|
||||
}
|
||||
|
||||
func testTokens(ctx context.Context, client *api.Client, tokens []db_cold.UserAuth) ([]db_cold.UserAuth, error) {
|
||||
accounts := make([]db_cold.UserAuth, 0)
|
||||
for _, token := range tokens {
|
||||
account, err := testToken(ctx, client, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
return accounts, nil
|
||||
type TwitchAuthRespOk struct {
|
||||
Code string `json:"code"`
|
||||
Scope string `json:"scope"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func testToken(ctx context.Context, client *api.Client, token db_cold.UserAuth) (db_cold.UserAuth, error) {
|
||||
// TODO check refresh time, refresh token if needed
|
||||
type TwitchAuthRespErr struct {
|
||||
Err string `json:"error"`
|
||||
ErrDesc string `json:"error_description"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
users, err := client.Users.List().Do(ctx, api.WithBearerToken(token.AccessToken))
|
||||
type TwitchAuthTokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope []string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
func (twitch *TwitchAuth) doAuth(formData url.Values) (TwitchAuthTokenResp, error) {
|
||||
resp, err := http.PostForm(TWITCH_AUTH_URL, formData)
|
||||
if err != nil {
|
||||
return db_cold.UserAuth{}, err
|
||||
return TwitchAuthTokenResp{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return TwitchAuthTokenResp{}, err
|
||||
}
|
||||
|
||||
var authResp TwitchAuthTokenResp
|
||||
err = json.Unmarshal(bodyBytes, &authResp)
|
||||
if err != nil {
|
||||
return TwitchAuthTokenResp{}, err
|
||||
}
|
||||
|
||||
return authResp, nil
|
||||
}
|
||||
|
||||
func (twitch *TwitchAuth) DoAuth(conf *util.Config, code string) (TwitchAuthTokenResp, error) {
|
||||
formData := url.Values{
|
||||
"client_id": {conf.ClientID},
|
||||
"client_secret": {conf.ClientSecret},
|
||||
"redirect_uri": {conf.RedirectURI},
|
||||
"code": {code},
|
||||
"grant_type": {"authorization_code"},
|
||||
}
|
||||
|
||||
return twitch.doAuth(formData)
|
||||
}
|
||||
|
||||
func (twitch *TwitchAuth) DoRefresh(conf *util.Config, refreshToken string) (TwitchAuthTokenResp, error) {
|
||||
formData := url.Values{
|
||||
"client_id": {conf.ClientID},
|
||||
"client_secret": {conf.ClientSecret},
|
||||
"refresh_token": {refreshToken},
|
||||
"grant_type": {"refresh_token"},
|
||||
}
|
||||
|
||||
return twitch.doAuth(formData)
|
||||
}
|
||||
|
||||
func (twitch *TwitchAuth) GetTokenUser(accessToken string) (api.User, error) {
|
||||
return getTokenUser(twitch.Ctx, twitch.Client, accessToken)
|
||||
}
|
||||
|
||||
func getTokenUser(ctx context.Context, client *api.Client, accessToken string) (api.User, error) {
|
||||
users, err := client.Users.List().Do(ctx, api.WithBearerToken(accessToken))
|
||||
if err != nil {
|
||||
return api.User{}, err
|
||||
}
|
||||
|
||||
usersData := users.Data
|
||||
|
||||
if len(usersData) <= 0 {
|
||||
return db_cold.UserAuth{}, errors.New("user data returned an empty array")
|
||||
return api.User{}, errors.New("user data returned an empty array")
|
||||
}
|
||||
|
||||
// from twitch
|
||||
mainUser := usersData[0]
|
||||
token.UserLogin = mainUser.UserLogin
|
||||
token.UserName = mainUser.UserName
|
||||
token.UserEmail = mainUser.Email
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func refreshToken(token db_cold.UserAuth) (db_cold.UserAuth, error) {
|
||||
// TODO get new access token using refresh token
|
||||
// TODO this should be called regularly, as needed based on Expires
|
||||
return usersData[0], nil
|
||||
}
|
||||
|
||||
69
ttv/expiration.go
Normal file
69
ttv/expiration.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package ttv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"zomo.dev/largehadroncollider/db"
|
||||
"zomo.dev/largehadroncollider/util"
|
||||
)
|
||||
|
||||
const REFRESH_INTERVAL = 5 * time.Minute
|
||||
|
||||
// token expiration thread
|
||||
|
||||
func (twitch *TwitchAuth) loopUpdateDetailsRefreshTokens(conf *util.Config, dbConn *db.DBConn) {
|
||||
for {
|
||||
// sleep until next interval
|
||||
now := time.Now()
|
||||
nextTime := now.Truncate(REFRESH_INTERVAL).Add(REFRESH_INTERVAL)
|
||||
time.Sleep(nextTime.Sub(now))
|
||||
|
||||
// check tokens
|
||||
err := twitch.updateDetailsRefreshTokens(conf, dbConn)
|
||||
if err != nil {
|
||||
log.Printf("Error Updating Tokens: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (twitch *TwitchAuth) updateDetailsRefreshTokens(conf *util.Config, dbConn *db.DBConn) error {
|
||||
now := time.Now().Add(REFRESH_INTERVAL)
|
||||
|
||||
tokens, err := dbConn.Cold.GetAllUserAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
if token.TokenExpires.Before(now) {
|
||||
// refresh and update details
|
||||
authResp, err := twitch.DoRefresh(conf, token.RefreshToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = twitch.UpdateUserDetails(dbConn, authResp.AccessToken, authResp.RefreshToken, authResp.ExpiresIn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// only update details
|
||||
err = twitch.UpdateUserDetails(dbConn, token.AccessToken, token.RefreshToken, token.TokenExpires)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/** tokenExpires: time.Time | time.Duration | int */
|
||||
func (twitch *TwitchAuth) UpdateUserDetails(dbConn *db.DBConn, accessToken, refreshToken string, tokenExpires any) error {
|
||||
user, err := twitch.GetTokenUser(accessToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbConn.Cold.UpdateUserAuth(user.UserID, user.UserName, user.UserLogin, accessToken, refreshToken, tokenExpires)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ func InitTwitchConn(conf *util.Config, dbConn *db.DBConn) (*TwitchConn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TwitchConn{ auth }, nil
|
||||
return &TwitchConn{auth}, nil
|
||||
}
|
||||
|
||||
type TwitchConn struct {
|
||||
|
||||
24
util/conf.go
24
util/conf.go
@@ -10,22 +10,24 @@ import (
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
config := Config{}
|
||||
|
||||
config.def()
|
||||
|
||||
err := config.loadEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// other sources?
|
||||
|
||||
config.def()
|
||||
config.verify()
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
SQliteDB string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURI string
|
||||
SQliteDB string
|
||||
WSAuthorization string
|
||||
}
|
||||
|
||||
func (c *Config) def() {
|
||||
@@ -41,12 +43,18 @@ func (c *Config) loadEnv() error {
|
||||
if str, found := os.LookupEnv("CLIENT_ID"); found {
|
||||
c.ClientID = strings.TrimSpace(str)
|
||||
}
|
||||
if str, found := os.LookupEnv("CLIENT_SECRET"); found {
|
||||
c.ClientSecret = strings.TrimSpace(str)
|
||||
}
|
||||
if str, found := os.LookupEnv("REDIR_URI"); found {
|
||||
c.RedirectURI = strings.TrimSpace(str)
|
||||
}
|
||||
if str, found := os.LookupEnv("SQLITE_DB"); found {
|
||||
c.SQliteDB = strings.TrimSpace(str)
|
||||
}
|
||||
if str, found := os.LookupEnv("WS_AUTHORIZATION"); found {
|
||||
c.WSAuthorization = strings.TrimSpace(str)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -55,9 +63,15 @@ func (c *Config) verify() error {
|
||||
if c.ClientID == "" {
|
||||
return errors.New("unable to load a configured Client ID")
|
||||
}
|
||||
if c.ClientSecret == "" {
|
||||
return errors.New("unable to load a configured Client Secret")
|
||||
}
|
||||
if c.RedirectURI == "" {
|
||||
return errors.New("unable to load a configured Redirect URI")
|
||||
}
|
||||
if c.WSAuthorization == "" {
|
||||
return errors.New("unable to load a configured WS Authorization code")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user