package main import ( "bytes" "database/sql" "fmt" "log" "net/http" "slices" "strconv" "strings" "time" "humungus.tedunangst.com/r/webs/junk" "humungus.tedunangst.com/r/webs/login" ) type MastoApp struct { Name string `db:"clientname"` RedirectURI string `db:"redirecturis"` ClientID string `db:"clientid"` ClientSecret string `db:"clientsecret"` VapidKey string `db:"vapidkey"` AuthToken string `db:"authtoken"` Scopes string `db:"scopes"` } func showoauthlogin(rw http.ResponseWriter, r *http.Request) { templinfo := make(map[string]interface{}) templinfo = getInfo(r) templinfo["ClientID"] = r.URL.Query().Get("client_id") templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri") if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil { elog.Println(err) } } // https://docs.joinmastodon.org/methods/apps/#create func apiapps(rw http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(rw, "invalid input", http.StatusUnprocessableEntity) elog.Println(err) return } clientName := r.Form.Get("client_name") redirectURI := r.Form.Get("redirect_uris") scopes := r.Form.Get("scopes") website := r.Form.Get("website") clientID := tokengen() clientSecret := tokengen() vapidKey := tokengen() _, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "") if err != nil { elog.Printf("error saving masto app: %v", err) http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity) return } j := junk.New() j["id"] = "19" j["website"] = website j["name"] = clientName j["redirect_uri"] = redirectURI j["client_id"] = clientID j["client_secret"] = clientSecret j["vapid_key"] = vapidKey fmt.Println(j.ToString()) goodjunk(rw, j) } // https://docs.joinmastodon.org/methods/oauth/#authorize func oauthorize(rw http.ResponseWriter, r *http.Request) { clientID := r.FormValue("client_id") redirectURI := r.FormValue("redirect_uri") if !checkClientID(clientID) { elog.Println("oauth: no such client:", clientID) rw.WriteHeader(http.StatusUnauthorized) return } var nrw NotResponseWriter login.LoginFunc(&nrw, r) _, err := stmtSaveMastoAppToken.Exec(nrw.auth) if err != nil { elog.Println("oauth: failed to save masto app token", err) rw.WriteHeader(http.StatusInternalServerError) return } uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth) log.Println("redirecting to", uri) rw.Header().Set("Content-Type", "") rw.Header().Set("Location", uri) rw.WriteHeader(302) } // https://docs.joinmastodon.org/methods/oauth/#token func oauthtoken(rw http.ResponseWriter, r *http.Request) { grantType := r.FormValue("grant_type") code := r.FormValue("code") clientID := r.FormValue("client_id") clientSecret := r.FormValue("client_secret") redirectURI := r.FormValue("redirect_uri") gotScope := r.FormValue("scope") if !checkClient(clientID, clientSecret) { elog.Println("oauth: no such client:", clientID) rw.WriteHeader(http.StatusBadRequest) return } app := MastoApp{} row := stmtGetMastoApp.QueryRowx(clientID) err := row.StructScan(&app) if err == sql.ErrNoRows { elog.Printf("oauth: invalid client: %s\n", clientID) rw.WriteHeader(http.StatusBadRequest) return } log.Printf("%#v", app) possibleScopes := strings.Split(app.Scopes, " ") requestedScopes := strings.Split(gotScope, " ") for _, scope := range requestedScopes { if !slices.Contains(possibleScopes, scope) { elog.Printf("oauth: invalid scope: %s", scope) rw.WriteHeader(http.StatusBadRequest) return } } if app.Scopes != gotScope { elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes) rw.WriteHeader(http.StatusBadRequest) return } if app.RedirectURI != redirectURI { elog.Println("oauth: incorrect redirect URI") rw.WriteHeader(http.StatusBadRequest) return } if grantType == "authorization_code" { // idk if this is ok? should code be reset? if app.AuthToken == code { accessToken := tokengen() _, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken) if err != nil { elog.Println("oauth: failed to save masto access token", err) rw.WriteHeader(http.StatusInternalServerError) return } j := junk.New() j["access_token"] = accessToken j["token_type"] = "Bearer" j["scope"] = app.Scopes j["created_at"] = time.Now().UTC().Unix() goodjunk(rw, j) } } else { // gaslight the client elog.Println("oauth: bad grant_type: must be authorization_code") rw.WriteHeader(http.StatusBadRequest) return } } // https://docs.joinmastodon.org/methods/instance/#v2 func instance(rw http.ResponseWriter, r *http.Request) { j := junk.New() var servername string if err := getconfig("servername", &servername); err != nil { http.Error(rw, "getting servername", http.StatusInternalServerError) return } j["uri"] = servername j["title"] = "honk" j["description"] = "federated honk conveyance" j["version"] = "develop" thumbnail := junk.New() thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername) j["thumbnail"] = thumbnail j["languages"] = []string{"en"} config := junk.New() a := junk.New() a["max_featured_tags"] = 10 config["accounts"] = a s := junk.New() s["max_characters"] = 5000 s["max_media_attachments"] = 1 s["characters_reserved_per_url"] = 23 config["statuses"] = s m := junk.New() m["supported_mime_types"] = []string{ "image/jpeg", "image/png", "image/gif", "image/heic", "image/heif", "image/webp", "image/avif", "video/webm", "video/mp4", "video/quicktime", "video/ogg", "audio/wave", "audio/wav", "audio/x-wav", "audio/x-pn-wave", "audio/vnd.wave", "audio/ogg", "audio/vorbis", "audio/mpeg", "audio/mp3", "audio/webm", "audio/flac", "audio/aac", "audio/m4a", "audio/x-m4a", "audio/mp4", "audio/3gpp", "video/x-ms-asf", } m["image_size_limit"] = 10485760 m["image_matrix_limit"] = 16777216 m["video_size_limit"] = 41943040 m["video_frame_rate_limit"] = 60 m["video_matrix_limit"] = 2304000 j["media_attachments"] = m goodjunk(rw, j) return } // /// The ID of the account. // public let id: String // /// The username of the account. // public let username: String // /// Equals username for local users, includes @domain for remote ones. // public let acct: String // /// The account's display name. // public let displayName: String // /// Biography of user. // public let note: String // /// URL of the user's profile page (can be remote). // public let url: String // /// URL to the avatar image. // public let avatar: String // /// URL to the avatar static image // public let avatarStatic: String // /// URL to the header image. // public let header: String // /// URL to the header static image // public let headerStatic: String // /// Boolean for when the account cannot be followed without waiting for approval first. // public let locked: Bool // /// The time the account was created. // public let createdAt: String? // /// The number of followers for the account. // public var followersCount: Int // /// The number of accounts the given account is following. // public var followingCount: Int // /// The number of statuses the account has made. // public let statusesCount: Int // /// An array of Emoji. // public let emojis: [Emoji] // // public let fields: [HashType] // // /// A 3-5 word description of the account // /// Optional, as this is a Mammoth addition // public let summary: String? // // public let bot: Bool // public let lastStatusAt: String? // public let discoverable: Bool? // // public let source: AccountSource? // https://docs.joinmastodon.org/methods/accounts/#verify_credentials func verifycreds(rw http.ResponseWriter, r *http.Request) { user, ok := somenumberedusers.Get(UserID(1)) if !ok { elog.Fatalf("masto: no user number 1???") } j := junk.New() j["id"] = fmt.Sprintf("%d", user.ID) j["username"] = user.Name j["created_at"] = "" j["acct"] = user.Name j["display_name"] = user.Options.CustomDisplay j["url"] = user.URL j["note"] = user.About j["avatar"] = user.Options.Avatar j["avatar_static"] = user.Options.Avatar j["header"] = user.Options.Banner j["header_static"] = user.Options.Banner j["followers_count"] = 0 j["following_count"] = 0 j["statuses_count"] = getlocalhonkcount() j["locked"] = false j["bot"] = false j["last_status_at"] = "" j["discoverable"] = false j["source"] = nil j["emojis"] = []junk.Junk{} j["fields"] = []junk.Junk{} log.Println(j.ToString()) goodjunk(rw, j) } // https://docs.joinmastodon.org/methods/timelines/#home func hometimeline(rw http.ResponseWriter, r *http.Request) { limit := r.URL.Query().Get("limit") minid := r.URL.Query().Get("min_id") maxid := r.URL.Query().Get("max_id") dlog.Println("limit", limit, "minid", minid, "maxid", maxid) var ( minidInt int maxidInt int limitInt int err error ) list := []junk.Junk{} me, ok := somenumberedusers.Get(UserID(1)) if !ok { elog.Fatalf("masto: no user number 1???") } if limit == "" { limit = "5" } limitInt, _ = strconv.Atoi(limit) if minid != "" { minidInt, err = strconv.Atoi(minid) if err != nil { elog.Printf("masto: invalid min_id: %s: %s", minid, err) http.Error(rw, "invalid min_id", http.StatusBadRequest) return } } if maxid != "" { maxidInt, err = strconv.Atoi(maxid) if err != nil { elog.Printf("masto: invalid max_id: %s: %s", minid, err) http.Error(rw, "invalid max_id", http.StatusBadRequest) return } } honks := getlimitedhonksforuser(me.ID, int64(minidInt), int64(maxidInt), limitInt) reverbolate(me.ID, honks) for _, h := range honks { hj := honktomasto(h) if hj == nil { elog.Printf("masto: failed to convert honk %d to masto", h.ID) http.Error(rw, "failed to convert honk", http.StatusInternalServerError) } list = append(list, hj) } buf := bytes.Buffer{} err = listjunk(&buf, list) if err != nil { elog.Println(err) http.Error(rw, "error encoding json", http.StatusInternalServerError) } rw.Header().Set("Content-Type", "application/json; charset=utf-8") rw.WriteHeader(http.StatusOK) rw.Write(buf.Bytes()) }