moira_db/moira_db.go
2026-07-06 22:08:53 +02:00

536 lines
14 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"crypto/rand"
"encoding/base64"
"github.com/tidwall/gjson"
"context" // <- TODO: what's this??
"golang.org/x/oauth2"
"github.com/gorilla/sessions"
"github.com/gorilla/securecookie"
//"path/filepath"
//"regexp"
//"strings"
//"maps"
//"slices"
)
var templates *template.Template = template.New("")
var client = &http.Client{Timeout: 2*time.Second}
var session_db = sessions.NewCookieStore([]byte(os.Getenv("SESSIONS_SECRET")))
type UserInfo struct{
ProviderCode, // DC--Discord, GG--Google, GH--GitHub, LC--local database
ExtID,
Username,
Fullname,
Email,
Locale string
CreatedTS,
UpdatedTS int64 // UNIX timestamps
}
type User struct{
IntID string
UserInfos map[string]UserInfo // the string is the ProviderCode
}
var db_user = make(map[string]User) // the string is the IntID
var oauth2_dc_config = &oauth2.Config{
ClientID: os.Getenv("MOIRA_OAUTH2_DC_CLI_ID"),
ClientSecret: os.Getenv("MOIRA_OAUTH2_DC_CLI_SECRET"),
Scopes: []string{"identify", "email"},
RedirectURL: "https://moira.makemore.cloud/oauth2/ext/dc/clbk/",
Endpoint: oauth2.Endpoint{
AuthURL: "https://discord.com/oauth2/authorize",
TokenURL: "https://discord.com/api/oauth2/token",
},
}
func oauth2_dc_genRandState() (_ string, err error){
ret := make([]byte, 24)
_, err = rand.Read(ret)
if err != nil{ return }
return base64.RawURLEncoding.EncodeToString(ret), nil
}
func oauth2_dc_genLoginURL() (_ string, _ *http.Cookie, err error){
state, err := oauth2_dc_genRandState()
if err != nil{ return }
ret_cookie := &http.Cookie{
Name: "moira_db_oauth_state",
Value: state,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode, // TODO: what's lax??
MaxAge: 600,
}
return oauth2_dc_config.AuthCodeURL(state), ret_cookie, nil
}
func oauth2_dc_exchangeReceivedCode(req *http.Request) (_ *http.Client, err error){
cookie, err := req.Cookie("moira_db_oauth_state")
if err != nil{ return }
if cookie.Value != req.URL.Query().Get("state"){
err = errors.New("oauth2_dc: csrf test failed")
return
}
token, err := oauth2_dc_config.Exchange(context.Background(), req.URL.Query().Get("code"))
if err != nil{ return }
return oauth2_dc_config.Client(context.Background(), token), nil
}
func oauth2_fetch_dcUserInfo(in_client *http.Client) (ret []byte, err error){
req, err := http.NewRequest("GET", "https://discord.com/api/users/@me", nil)
if err != nil{ return }
//req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MoiraBot/1.0)")
resp,err := in_client.Do(req)
if err != nil{ return }
defer resp.Body.Close()
resp_text, err := io.ReadAll(resp.Body)
if err != nil{ return }
//fmt.Printf("%+v\n", string(resp_text))
return resp_text, nil
}
func oauth2_dcUserInfo_to_UserInfo(resp_text []byte) (ret UserInfo, err error){
//fmt.Printf("%s\n", string(resp_text))
// TODO: you might want to check if the data fields are not empty
ts_now := time.Now().Unix()
return UserInfo{
ProviderCode: "DC",
ExtID: gjson.GetBytes(resp_text, "id").String(),
Username: gjson.GetBytes(resp_text, "username").String(),
Fullname: gjson.GetBytes(resp_text, "global_name").String(),
Email: gjson.GetBytes(resp_text, "email").String(),
Locale: gjson.GetBytes(resp_text, "locale").String(),
CreatedTS: ts_now,
UpdatedTS: ts_now,
}, nil
}
func oauth2_hdlr_login(resp http.ResponseWriter, req *http.Request){
url, cookie, err := oauth2_dc_genLoginURL()
if err != nil {
http.Error(resp, err.Error(), http.StatusInternalServerError)
}
http.SetCookie(resp, cookie)
http.Redirect(resp, req, url, http.StatusFound)
}
func oauth2_hdlr_clbk(resp http.ResponseWriter, req *http.Request){
client, err := oauth2_dc_exchangeReceivedCode(req)
if err != nil { return } // TODO: should the error be reported somehow?
resp_text, err := oauth2_fetch_dcUserInfo(client)
if err != nil { return } // TODO: should the error be reported somehow?
new_userInfo, err := oauth2_dcUserInfo_to_UserInfo(resp_text)
if err != nil { return } // TODO: should the error be reported somehow?
session, err := session_db.Get(req, "moira_db_session_main")
if err != nil{
fmt.Println(err.Error())
return
}
user_found_in_db := false
for key,val := range db_user{
if val_val,ok:=val.UserInfos[new_userInfo.ProviderCode]; ok{
if val_val.ExtID == new_userInfo.ExtID {
// found the user in the DB!
session.Values["user"] = key
user_found_in_db = true
break
}
}
}
if(!user_found_in_db){
var new_user_intID string
new_user_intID_found := false
for range 100{
new_user_intID = base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(16))
if _,ok:=db_user[new_user_intID]; !ok{
new_user_intID_found = true
break
}
}
if(!new_user_intID_found){
fmt.Println("Whoops! 100 tries and no luck, take care of 'em:")
t_json, _ := json.MarshalIndent(new_userInfo, "", " ")
fmt.Printf("%+v\n", string(t_json))
return
}
db_user[new_user_intID] = User{
IntID: new_user_intID,
UserInfos: map[string]UserInfo{new_userInfo.ProviderCode: new_userInfo},
}
session.Values["user"] = new_user_intID
}
err = session.Save(req, resp)
if err != nil{
fmt.Println(err.Error())
return
}
http.Redirect(resp, req, "/", http.StatusFound)
}
func sessions_is_loggedIn(resp http.ResponseWriter, req *http.Request) (ret bool, err error){
session, err := session_db.Get(req, "moira_db_session_main")
if err != nil{
fmt.Println(err.Error())
return
}
if session.IsNew{
err = session.Save(req, resp)
if err != nil{
fmt.Println(err.Error())
return
}
fmt.Println("new session")
return false, nil
}
val := session.Values["user"]
if val == "" || val == nil{
fmt.Println("empty user")
return false, nil
}
//if val not in user_db: false,nil
fmt.Println(val)
return true, nil
}
type Film struct{
Title_orig,
Title_EN,
Title_PL,
Desc,
Img,
Source string
Year int
}
var db_film = make(map[string]any)
func findURL_in_msg(in_msg string) (_ string,err error){
i := strings.Index(in_msg, "http")
if(i == -1){
err = errors.New("No URL found in: " + in_msg)
return
}
j := strings.Index(in_msg[i:], " ")
if(j == -1){
j = len(in_msg)-i
}
return in_msg[i:i+j], nil
}
func resolve_redirs(in_url string) (string,error){
resp, err := client.Get(in_url)
if(err != nil){
return "", err
}
defer resp.Body.Close()
return resp.Request.URL.String(), nil
}
func fetch_filmInfo(in_url string) (_ Film, err error){
parsedUrl, err := url.Parse(in_url)
if err != nil{ return }
parsedUrl.RawQuery = ""
var in_url_api string
switch(parsedUrl.Hostname()){
case "www.filmweb.pl":
in_url_api = fmt.Sprintf(
"https://www.filmweb.pl/api/v1/film/%s/preview",
in_url[strings.LastIndex(in_url, "-")+1:],
)
case "www.imdb.com":
t_parsedUrl, _ := url.Parse(in_url)
t_parsedUrl.RawQuery = ""
t_parsedUrl.Host = "api.imdbapi.dev"
in_url_api = strings.Replace(t_parsedUrl.String(), "/title/", "/titles/", 1)
if in_url_api[len(in_url_api)-1] == '/'{
in_url_api = in_url_api[:len(in_url_api)-1]
}
default:
err = errors.New("[0] Film database unsupported " + parsedUrl.Hostname())
return
}
fmt.Println("in_url_api:", in_url_api);
req,err := http.NewRequest("GET", in_url_api, nil)
if err != nil{ return }
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MoiraBot/1.0)")
resp,err := client.Do(req)
if err != nil{ return }
defer resp.Body.Close()
resp_text, err := io.ReadAll(resp.Body)
if err != nil{ return }
//fmt.Printf("%+v\n", string(resp_text))
titles := make(map[string]string)
switch(parsedUrl.Hostname()){
case "www.filmweb.pl":
if(len(gjson.GetBytes(resp_text, "internationalTitle").String())>0){
titles[gjson.GetBytes(resp_text, "internationalTitle.lang").String()] = gjson.GetBytes(resp_text, "internationalTitle.title").String()
titles["_int"] = gjson.GetBytes(resp_text, "internationalTitle.title").String()
}
if(len(gjson.GetBytes(resp_text, "originalTitle").String())>0){
titles[gjson.GetBytes(resp_text, "originalTitle.lang").String()] = gjson.GetBytes(resp_text, "originalTitle.title").String()
titles["_orig"] = gjson.GetBytes(resp_text, "originalTitle.title").String()
}
if(len(gjson.GetBytes(resp_text, "title").String())>0){
titles[gjson.GetBytes(resp_text, "title.lang").String()] = gjson.GetBytes(resp_text, "title.title").String()
}
return Film{
Title_orig: (func()string{
if val,ok:=titles["_orig"]; ok{
return val
}else{
return ""
}
})(),
Title_EN: (func()string{
if val,ok:=titles["en"]; ok{
return val
}else{
return ""
}
})(),
Title_PL: (func()string{
if val,ok:=titles["pl"]; ok{
return val
}else{
return ""
}
})(),
Desc: gjson.GetBytes(resp_text, "plotOrDescriptionSynopsis").String(),
Year: int(gjson.GetBytes(resp_text,"year").Int()),
Img: "https://fwcdn.pl/fpo"+gjson.GetBytes(resp_text, "poster.path").String(),
Source: in_url,
}, err
case "www.imdb.com":
if(len(gjson.GetBytes(resp_text, "primaryTitle").String())>0){
titles["en"] = gjson.GetBytes(resp_text, "primaryTitle").String()
titles["_int"] = gjson.GetBytes(resp_text, "primaryTitle").String()
}
if(len(gjson.GetBytes(resp_text, "originalTitle").String())>0){
if(len(gjson.GetBytes(resp_text, "originCountries.0.code").String())>0){
titles[strings.ToLower(gjson.GetBytes(resp_text, "originCountries.0.code").String())] = gjson.GetBytes(resp_text, "originalTitle").String()
}
titles["_orig"] = gjson.GetBytes(resp_text, "originalTitle").String()
}else{
if val,ok:=titles["_int"]; ok{
titles["_orig"] = val
}
}
return Film{
Title_orig: (func()string{
if val,ok:=titles["_orig"]; ok{
return val
}else{
return ""
}
})(),
Title_EN: (func()string{
if val,ok:=titles["en"]; ok{
return val
}else{
return ""
}
})(),
Title_PL: (func()string{
if val,ok:=titles["pl"]; ok{
return val
}else{
return ""
}
})(),
Desc: gjson.GetBytes(resp_text, "plot").String(),
Year: int(gjson.GetBytes(resp_text,"startYear").Int()),
Img: gjson.GetBytes(resp_text, "primaryImage.url").String(),
Source: in_url,
}, err
default:
err = errors.New("[1] Film database unsupported " + parsedUrl.Hostname())
return
}
}
func hdlr_root(template_renders map[string]struct{Template string;Context *map[string]any}) http.HandlerFunc{
fSrv := http.FileServer(http.Dir("./website/static"))
return func (resp http.ResponseWriter, req *http.Request){
if fileName, ok := template_renders[req.URL.Path]; ok{
fmt.Println("[srv] template", req.URL.Path, fileName.Template)
is_loggedIn, _ := sessions_is_loggedIn(resp, req)
fmt.Println(">>LoggedIn?:", is_loggedIn)
url_query := req.URL.Query()
url_query_id := url_query.Get("id")
var context any
if(url_query_id != ""){
context = (*fileName.Context)[url_query_id]
}else{
context = nil
}
err := templates.ExecuteTemplate(resp, fileName.Template, context)
if(err != nil){
http.Error(resp, err.Error(), http.StatusInternalServerError)
}
}else{
fmt.Println("[srv] static", req.URL.Path)
fSrv.ServeHTTP(resp, req)
}
}
}
func templates_register(template_renders map[string]struct{Template string;Context *map[string]any}){
for _,i_file := range template_renders{
text, err := os.ReadFile("./website/templates/" + i_file.Template)
if(err != nil){
panic(err)
}
templates.New(i_file.Template).Parse(string(text))
}
}
func hdlr_film_parseMsg(resp http.ResponseWriter, req *http.Request){
fmt.Println("[srv] util", req.URL)
url_query := req.URL.Query()
t_url, _ := findURL_in_msg(url_query.Get("text"))
fmt.Println(t_url)
t_url_noRed, _ := resolve_redirs(t_url)
fmt.Println(t_url_noRed)
t_film, _ := fetch_filmInfo(t_url_noRed)
db_film[fmt.Sprintf("%d",len(db_film))] = t_film
t_json, _ := json.MarshalIndent(db_film, "", " ")
fmt.Printf("%+v\n", string(t_json))
fmt.Println("/film/view/?id="+fmt.Sprintf("%d",len(db_film)-1))
http.Redirect(resp, req, "/film/view/?id="+fmt.Sprintf("%d",len(db_film)-1), http.StatusFound)
}
func main(){
fmt.Println("CLIENT_ID:", oauth2_dc_config.ClientID);
fmt.Println("CLIENT_SECRET:", oauth2_dc_config.ClientSecret);
fmt.Println("SESSIONS_SECRET:", os.Getenv("SESSIONS_SECRET"));
//t_film, _ := fetch_filmInfo("https://www.filmweb.pl/film/Spirited+Away%3A+W+krainie+Bog%C3%B3w-2001-33288")
//t_film, _ := fetch_filmInfo("https://www.filmweb.pl/film/Pulp+Fiction-1994-1039")
//t_film, _ := fetch_filmInfo("https://www.filmweb.pl/film/Kl%C4%85twa+Doliny+W%C4%99%C5%BCy-1987-6785")
//t_film, _ := fetch_filmInfo("https://www.imdb.com/title/tt0245429/?ref_=nv_sr_srsg_0_tt_8_nm_0_in_0_q_spirited")
//t_film, _ := fetch_filmInfo("https://www.imdb.com/title/tt0110912/")
//t_film, _ := fetch_filmInfo("https://www.imdb.com/title/tt8814476/")
//t_url_noRed, _ := resolve_redirs("https://www.imdb.com/title/tt0095456/?ref_=nv_sr_srsg_0_tt_1_nm_0_in_0_q_kl%C4%85twa%20doliny%20w%C4%99%C5%BC")
//t_url_noRed, _ := resolve_redirs("https://share.google/EsMXhLYol3eQTKcQH")
//t_url, _ := findURL_in_msg("https://share.google/EsMXhLYol3eQTKcQH")
//t_url, _ := findURL_in_msg("abcabc https://share.google/EsMXhLYol3eQTKcQH abcabc")
//t_url, _ := findURL_in_msg("abcabchttps://share.google/EsMXhLYol3eQTKcQH abcabc")
//t_url, _ := findURL_in_msg("abcabchttps://share.google/EsMXhLYol3eQTKcQH")
//t_url, _ := findURL_in_msg("Fantastyczny Pan Lis (2009) - Filmweb https://share.google/EsMXhLYol3eQTKcQH")
//fmt.Println(t_url)
//t_url_noRed, _ := resolve_redirs(t_url)
//fmt.Println(t_url_noRed)
//t_film, _ := fetch_filmInfo(t_url_noRed)
//t_json, _ := json.MarshalIndent(t_film, "", " ")
//fmt.Printf("%+v\n", string(t_json))
template_renders := map[string]struct{Template string;Context *map[string]any} {
"/": {"index.html", nil},
"/film/view/": {"film/view/index.html", &db_film},
"/film/edit/": {"index.html", nil},
//"/list/view/": {"index.html", nil},
//"/list/edit/": {"index.html", nil},
//"/group/view/": {"index.html", nil},
//"/group/edit/": {"index.html", nil},
}
templates_register(template_renders)
http.HandleFunc("/", hdlr_root(template_renders))
http.HandleFunc("/film/parse_msg/", hdlr_film_parseMsg)
http.HandleFunc("/oauth2/ext/dc/login/", oauth2_hdlr_login)
http.HandleFunc("/oauth2/ext/dc/clbk/", oauth2_hdlr_clbk)
//fmt.Println("Welcome to moira_db!");
log.Fatal(http.ListenAndServe(":8099", nil))
}