feat(oauth,mastodon): mastodon instace selection and list fetching
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
TWITTER_CLIENT_ID=**********************************
|
TWITTER_CLIENT_ID=**********************************
|
||||||
TWITTER_CLIENT_SECRET=**************************************************
|
TWITTER_CLIENT_SECRET=**************************************************
|
||||||
|
INSTANCES_SOCIAL_TOKEN=********************************************************************************************************************************
|
||||||
|
|||||||
97
components/datalists_options.templ
Normal file
97
components/datalists_options.templ
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"net/url"
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"slices"
|
||||||
|
"extrovert/internals"
|
||||||
|
"fmt"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchInstanceList(limit int) ([]Instance, error) {
|
||||||
|
|
||||||
|
u, err := url.ParseRequestURI("https://instances.social/api/1.0/instances/list")
|
||||||
|
if err != nil {
|
||||||
|
return []Instance{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Query().Add("min_version", "0.1.0")
|
||||||
|
u.Query().Add("sort_by", "active_users")
|
||||||
|
u.Query().Add("count", fmt.Sprintf("%v", limit))
|
||||||
|
u.Query().Add("prohibted_content", "nudity_nocw")
|
||||||
|
u.Query().Add("prohibted_content", "pornography_nocw")
|
||||||
|
u.Query().Add("prohibted_content", "illegalContentLinks")
|
||||||
|
u.Query().Add("prohibted_content", "spam")
|
||||||
|
u.Query().Add("prohibted_content", "advertising")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), bytes.NewReader([]byte("")))
|
||||||
|
if err != nil {
|
||||||
|
return []Instance{}, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+os.Getenv("INSTANCES_SOCIAL_TOKEN"))
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return []Instance{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return []Instance{}, err
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
|
return []Instance{}, errors.New(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var list struct {
|
||||||
|
Instances []Instance `json:"instances"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &list)
|
||||||
|
|
||||||
|
return list.Instances, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var INSTANCES = []Instance{
|
||||||
|
/*
|
||||||
|
These servers are not endorsed, curated or affiliated by Capytal in any way, shape
|
||||||
|
or form. All the instances where got from the top 15 listed in https://joinmastodon.org/servers,
|
||||||
|
at July 8th, 2024 (2024-07-08).
|
||||||
|
*/
|
||||||
|
{Name: "mastodon.social"},
|
||||||
|
{Name: "mstdn.social"},
|
||||||
|
{Name: "mas.to"},
|
||||||
|
{Name: "social.vivaldi.net"},
|
||||||
|
{Name: "mastodonapp.uk"},
|
||||||
|
{Name: "universeodon.com"},
|
||||||
|
{Name: "c.im"},
|
||||||
|
{Name: "mstdn.party"},
|
||||||
|
{Name: "toot.community"},
|
||||||
|
{Name: "ohai.social"},
|
||||||
|
{Name: "mstdn.business"},
|
||||||
|
{Name: "ieji.de"},
|
||||||
|
{Name: "toot.io"},
|
||||||
|
{Name: "masto.nu"},
|
||||||
|
{Name: "mstdn.plus"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstanceList(limit int) []Instance {
|
||||||
|
i, err := fetchInstanceList(limit)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WARN: Unable to fetch Mastodon instance datalist due to:\n%s\n\nFall backing into static list.", err.Error())
|
||||||
|
return INSTANCES
|
||||||
|
}
|
||||||
|
return internals.RemoveDuplicates(slices.Concat(INSTANCES, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
templ InstancesOptions(limit int) {
|
||||||
|
for _, v := range getInstanceList(limit) {
|
||||||
|
<option value={ v.Name }></option>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,3 +21,32 @@ var loginUrl = fmt.Sprintf("https://x.com/i/oauth2/authorize"+
|
|||||||
templ LoginTwitter() {
|
templ LoginTwitter() {
|
||||||
<a href={ templ.SafeURL(loginUrl) } rel="">Login on Twitter</a>
|
<a href={ templ.SafeURL(loginUrl) } rel="">Login on Twitter</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ LoginMastodon() {
|
||||||
|
<button popovertargetaction="show" popovertarget="mastodon-login">Login on Mastodon</button>
|
||||||
|
<div id="mastodon-login" popover>
|
||||||
|
<dialog open>
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<button
|
||||||
|
popovertargetaction="hide"
|
||||||
|
popovertarget="mastodon-login"
|
||||||
|
aria-label="Close"
|
||||||
|
rel="prev"
|
||||||
|
></button>
|
||||||
|
<label for="instance-url">Choose a instance</label>
|
||||||
|
</header>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="instance-url"
|
||||||
|
placeholder="Instance Url"
|
||||||
|
aria-label="Instance Url"
|
||||||
|
list="instance-suggestions"
|
||||||
|
/>
|
||||||
|
<datalist id="instance-suggestions">
|
||||||
|
@InstancesOptions(20)
|
||||||
|
</datalist>
|
||||||
|
</article>
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func RemoveDuplicates[T comparable](slice []T) []T {
|
||||||
|
keys := make(map[T]bool)
|
||||||
|
list := []T{}
|
||||||
|
for _, entry := range slice {
|
||||||
|
if _, value := keys[entry]; !value {
|
||||||
|
keys[entry] = true
|
||||||
|
list = append(list, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
func GetCookie(name string, w http.ResponseWriter, r *http.Request) *http.Cookie {
|
func GetCookie(name string, w http.ResponseWriter, r *http.Request) *http.Cookie {
|
||||||
name = fmt.Sprintf("__Host-%s-%s-%s", APP_NAME, APP_VERSION, name)
|
name = fmt.Sprintf("__Host-%s-%s-%s", APP_NAME, APP_VERSION, name)
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ templ IndexPage() {
|
|||||||
<main
|
<main
|
||||||
style="height:15rem"
|
style="height:15rem"
|
||||||
>
|
>
|
||||||
<form
|
<aside
|
||||||
style="height:100%;display:flex;gap:2rem;"
|
style="height:100%;display:flex;gap:2rem;"
|
||||||
>
|
>
|
||||||
<fieldset style="display:flex;flex-direction:column;gap:1rem;width:15rem;">
|
<div style="display:flex;flex-direction:column;gap:1rem;width:15rem;">
|
||||||
@components.LoginTwitter()
|
@components.LoginTwitter()
|
||||||
<button>Login on Mastodon</button>
|
@components.LoginMastodon()
|
||||||
</fieldset>
|
</div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<textarea
|
<textarea
|
||||||
style="height:100%;resize:none;"
|
style="height:100%;resize:none;"
|
||||||
@@ -36,7 +36,7 @@ templ IndexPage() {
|
|||||||
aria-label="Post input"
|
aria-label="Post input"
|
||||||
></textarea>
|
></textarea>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
@components.Warning("In Development") {
|
@components.Warning("In Development") {
|
||||||
|
|||||||
98
routes/mastodon_login.templ
Normal file
98
routes/mastodon_login.templ
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"fmt"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"extrovert/layouts"
|
||||||
|
"extrovert/internals"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MastodonTokenResponse struct {
|
||||||
|
Type string `json:"token_type"`
|
||||||
|
Token string `json:"access_token"`
|
||||||
|
Expires int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MastodonLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
error := internals.HttpErrorHelper(w)
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
if code == "" {
|
||||||
|
error(
|
||||||
|
"Bad request",
|
||||||
|
errors.New("Missing \"code\" parameter"),
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tReq := fmt.Sprintf("https://api.twitter.com/2/oauth2/token"+
|
||||||
|
"?grant_type=authorization_code"+
|
||||||
|
"&client_id=%s"+
|
||||||
|
"&code_verifier=challenge"+
|
||||||
|
"&code=%s"+
|
||||||
|
"&challenge_method=plain"+
|
||||||
|
"&redirect_uri=http://localhost:7331/api/oauth/twitter",
|
||||||
|
os.Getenv("TWITTER_CLIENT_ID"),
|
||||||
|
code,
|
||||||
|
)
|
||||||
|
|
||||||
|
t, err := http.Post(tReq, "application/x-www-form-urlencoded", bytes.NewReader([]byte("")))
|
||||||
|
if error("Error trying to request token from twitter", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(t.Body)
|
||||||
|
if error("Error trying to read response body from twitter", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
} else if t.StatusCode < 200 || t.StatusCode > 299 {
|
||||||
|
error(
|
||||||
|
"Error trying to request token from twitter, returned non-200 code",
|
||||||
|
errors.New(fmt.Sprintf("Code: %v, Return value: %s", t.StatusCode, string(b))),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var res TwitterTokenResponse
|
||||||
|
err = json.Unmarshal(b, &res)
|
||||||
|
if error("Error trying to parse response body from twitter", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print(res)
|
||||||
|
|
||||||
|
c := internals.GetCookie("twitter-data", w, r)
|
||||||
|
c.Value = res.Token
|
||||||
|
http.SetCookie(w, c)
|
||||||
|
|
||||||
|
MastodonLogin().Render(context.Background(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ MastodonLogin() {
|
||||||
|
@layouts.Page("Project Extrovert") {
|
||||||
|
<dialog open>
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<p>Logged into Twitter!</p>
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
Your account was succefully connected with project-extrovert!
|
||||||
|
Click "Ok" to return to the index page.
|
||||||
|
</p>
|
||||||
|
<footer>
|
||||||
|
<a href="/index.html">
|
||||||
|
<button>Ok</button>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</dialog>
|
||||||
|
@IndexPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user