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" //"path/filepath" //"regexp" //"strings" //"maps" //"slices" ) var templates *template.Template = template.New("") var client = &http.Client{Timeout: 2*time.Second} type UserInfo struct{ ProviderCode, // DC--Discord, GG--Google, GH--GitHub, LC--local database ExtID, Username, Fullname, Email, Locale string CreatedTS, UpdatedTS int64 // UNIX timestamps } 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: "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("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? t_json, _ := json.MarshalIndent(new_userInfo, "", " ") fmt.Printf("%+v\n", string(t_json)) } 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) 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); //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)) }