feat: multi-feature update
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const hhhlMisskeyHost = "https://dc.hhhl.cc"
|
||||
|
||||
// HHHLAuthorize adapts Misskey MiAuth to the OAuth authorization endpoint shape.
|
||||
func HHHLAuthorize(c *gin.Context) {
|
||||
redirectURI := strings.TrimSpace(c.Query("redirect_uri"))
|
||||
if redirectURI == "" {
|
||||
c.String(http.StatusBadRequest, "missing redirect_uri")
|
||||
return
|
||||
}
|
||||
|
||||
state := c.Query("state")
|
||||
sessionID := uuid.NewString()
|
||||
callbackURL := fmt.Sprintf(
|
||||
"%s/api/hhhl/callback?r=%s&s=%s&sid=%s",
|
||||
strings.TrimRight(system_setting.ServerAddress, "/"),
|
||||
url.QueryEscape(redirectURI),
|
||||
url.QueryEscape(state),
|
||||
url.QueryEscape(sessionID),
|
||||
)
|
||||
|
||||
miAuthURL := fmt.Sprintf(
|
||||
"%s/miauth/%s?name=NewAPI%%E7%%99%%BB%%E5%%BD%%95&callback=%s&permission=read:account",
|
||||
hhhlMisskeyHost,
|
||||
url.PathEscape(sessionID),
|
||||
url.QueryEscape(callbackURL),
|
||||
)
|
||||
c.Redirect(http.StatusFound, miAuthURL)
|
||||
}
|
||||
|
||||
// HHHLCallback returns the MiAuth session id as an OAuth authorization code.
|
||||
// Wrap in pkce.{base64json} format so the generic OAuth provider forwards it correctly.
|
||||
func HHHLCallback(c *gin.Context) {
|
||||
redirectURI := strings.TrimSpace(c.Query("r"))
|
||||
sessionID := strings.TrimSpace(c.Query("sid"))
|
||||
if redirectURI == "" || sessionID == "" {
|
||||
c.String(http.StatusBadRequest, "invalid callback")
|
||||
return
|
||||
}
|
||||
|
||||
targetURL, err := url.Parse(redirectURI)
|
||||
if err != nil || targetURL.Scheme == "" || targetURL.Host == "" {
|
||||
c.String(http.StatusBadRequest, "invalid redirect_uri")
|
||||
return
|
||||
}
|
||||
codePayload, _ := common.Marshal(map[string]string{"token": sessionID})
|
||||
code := "pkce." + base64.RawURLEncoding.EncodeToString(codePayload)
|
||||
query := targetURL.Query()
|
||||
query.Set("code", code)
|
||||
query.Set("state", c.Query("s"))
|
||||
targetURL.RawQuery = query.Encode()
|
||||
c.Redirect(http.StatusFound, targetURL.String())
|
||||
}
|
||||
|
||||
// HHHLToken exchanges a MiAuth session id for a Misskey access token.
|
||||
func HHHLToken(c *gin.Context) {
|
||||
code := strings.TrimSpace(c.Query("code"))
|
||||
if code == "" {
|
||||
if err := c.Request.ParseForm(); err == nil {
|
||||
code = strings.TrimSpace(c.Request.Form.Get("code"))
|
||||
}
|
||||
}
|
||||
if code == "" {
|
||||
var payload struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&payload); err == nil {
|
||||
code = strings.TrimSpace(payload.Code)
|
||||
}
|
||||
}
|
||||
if code == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "Missing code"})
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := code
|
||||
if strings.HasPrefix(code, "pkce.") {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(code[5:])
|
||||
if err == nil {
|
||||
var pkceData struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if jsonErr := common.Unmarshal(decoded, &pkceData); jsonErr == nil && pkceData.Token != "" {
|
||||
sessionID = pkceData.Token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body, err := common.Marshal(gin.H{})
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequestWithContext(
|
||||
c.Request.Context(),
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("%s/api/miauth/%s/check", hhhlMisskeyHost, url.PathEscape(sessionID)),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
|
||||
client := http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "MiAuth check request failed"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var tokenData struct {
|
||||
OK bool `json:"ok"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := common.Unmarshal(respBody, &tokenData); err != nil || !tokenData.OK || tokenData.Token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "Failed to validate MiAuth session"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"access_token": tokenData.Token, "token_type": "Bearer"})
|
||||
}
|
||||
|
||||
// HHHLUserInfo adapts Misskey /api/i to an OIDC-like userinfo response.
|
||||
func HHHLUserInfo(c *gin.Context) {
|
||||
token := strings.TrimSpace(c.Query("access_token"))
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer "))
|
||||
}
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_request", "error_description": "Missing token"})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := common.Marshal(gin.H{"i": token})
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, hhhlMisskeyHost+"/api/i", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
|
||||
client := http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token", "error_description": "Failed to fetch user info"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var userData struct {
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := common.Unmarshal(respBody, &userData); err != nil || userData.Id == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
if userData.Name == "" {
|
||||
userData.Name = userData.Username
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"sub": userData.Id,
|
||||
"preferred_username": userData.Username,
|
||||
"name": userData.Name,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user