feat(refractor): use misskey userinfo key as scope

...and UI tweaks 🎨
This commit is contained in:
Nya Candy 2023-01-29 14:23:49 +08:00
parent 6aa0db6d0e
commit a99045a034
No known key found for this signature in database
GPG Key ID: 8B1BE5E86F2E66AE
14 changed files with 208 additions and 252 deletions

View File

@ -3,6 +3,6 @@ package consts
const ( const (
REDIS_KEY_LOGIN_SESSION = "misso:login:%s" // Username, token as value REDIS_KEY_LOGIN_SESSION = "misso:login:%s" // Username, token as value
REDIS_KEY_CONSENT_CSRF = "misso:consent:%s" // Random string, consent challenge as value REDIS_KEY_CONSENT_CSRF = "misso:consent:%s" // Random string, consent challenge as value
REDIS_KEY_SHARE_ACCESS_TOKEN = "misso:share:at:%s" // Subject, access token as value REDIS_KEY_USER_ACCESS_TOKEN = "misso:user:token:%s" // Subject, access token as value
REDIS_KEY_SHARE_USER_INFO = "misso:share:ui:%s" // Subject, user info as value REDIS_KEY_USER_INFO = "misso:user:info:%s" // Subject, user info as value
) )

View File

@ -8,5 +8,5 @@ const (
TIME_LOGIN_REMEMBER = 10 * time.Minute TIME_LOGIN_REMEMBER = 10 * time.Minute
TIME_CONSENT_REMEMBER = 0 // Forever TIME_CONSENT_REMEMBER = 0 // Forever
TIME_USERINFO_CACHE = 10 * time.Minute TIME_USERINFO_CACHE = 1 * time.Hour
) )

View File

@ -56,8 +56,8 @@ func ConsentCheck(ctx *gin.Context) {
// Generate CSRF token // Generate CSRF token
global.Logger.Debugf("Generating CSRF token...") global.Logger.Debugf("Generating CSRF token...")
csrf := utils.RandString(32) csrf := utils.RandString(32)
sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, csrf) sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, oauth2challenge)
err := global.Redis.Set(context.Background(), sessKey, oauth2challenge, consts.TIME_REQUEST_VALID).Err() err := global.Redis.Set(context.Background(), sessKey, csrf, consts.TIME_REQUEST_VALID).Err()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to save csrf into redis with error: %v", err) global.Logger.Errorf("Failed to save csrf into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -69,7 +69,7 @@ func ConsentCheck(ctx *gin.Context) {
// Retrieve context // Retrieve context
global.Logger.Debugf("Retrieving context...") global.Logger.Debugf("Retrieving context...")
userinfoCtx, err := utils.GetUserinfo(*consentReq.Subject) userinfo, err := utils.GetUserinfo(*consentReq.Subject)
if err != nil { if err != nil {
global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err) global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -81,9 +81,10 @@ func ConsentCheck(ctx *gin.Context) {
// Show the consent UI // Show the consent UI
global.Logger.Debugf("Rendering consent UI...") global.Logger.Debugf("Rendering consent UI...")
templateFields := gin.H{ templateFields := gin.H{
"user": *userinfoCtx, "user": *userinfo,
"challenge": oauth2challenge, "challenge": oauth2challenge,
"csrf": csrf, "csrf": csrf,
"scopes": consentReq.RequestedScope,
} }
if consentReq.Client.LogoUri != nil && *consentReq.Client.LogoUri != "" { if consentReq.Client.LogoUri != nil && *consentReq.Client.LogoUri != "" {
@ -94,6 +95,12 @@ func ConsentCheck(ctx *gin.Context) {
} else { } else {
templateFields["clientName"] = *consentReq.Client.ClientId templateFields["clientName"] = *consentReq.Client.ClientId
} }
if consentReq.Client.PolicyUri != nil && *consentReq.Client.PolicyUri != "" {
templateFields["clientPolicy"] = *consentReq.Client.PolicyUri
}
if consentReq.Client.TosUri != nil && *consentReq.Client.TosUri != "" {
templateFields["clientTos"] = *consentReq.Client.TosUri
}
ctx.HTML(http.StatusOK, "consent.tmpl", templateFields) ctx.HTML(http.StatusOK, "consent.tmpl", templateFields)
global.Logger.Debugf("User should now see Consent UI.") global.Logger.Debugf("User should now see Consent UI.")

View File

@ -13,6 +13,7 @@ import (
type ConsentConfirmRequest struct { type ConsentConfirmRequest struct {
CSRF string `form:"_csrf"` CSRF string `form:"_csrf"`
Challenge string `form:"challenge"`
Remember bool `form:"remember"` Remember bool `form:"remember"`
Action string `form:"action"` Action string `form:"action"`
} }
@ -32,16 +33,23 @@ func ConsentConfirm(ctx *gin.Context) {
// Validate CSRF // Validate CSRF
global.Logger.Debugf("Validating CSRF...") global.Logger.Debugf("Validating CSRF...")
sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, req.CSRF) sessKey := fmt.Sprintf(consts.REDIS_KEY_CONSENT_CSRF, req.Challenge)
oauth2challenge, err := global.Redis.Get(context.Background(), sessKey).Result() csrfSession, err := global.Redis.Get(context.Background(), sessKey).Result()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to get csrf from redis with error: %v", err) global.Logger.Errorf("Failed to get csrf from redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to get csrf", "error": "Failed to get csrf",
}) })
return return
} else if csrfSession != req.CSRF {
ctx.HTML(http.StatusForbidden, "error.tmpl", gin.H{
"error": "CSRF not match",
})
return
} }
oauth2challenge := req.Challenge
// Delete used challenge // Delete used challenge
global.Redis.Del(context.Background(), sessKey) global.Redis.Del(context.Background(), sessKey)

View File

@ -9,7 +9,6 @@ import (
"misso/consts" "misso/consts"
"misso/global" "misso/global"
"misso/misskey" "misso/misskey"
"misso/utils"
"net/http" "net/http"
"time" "time"
) )
@ -62,9 +61,10 @@ func MisskeyAuthCallback(ctx *gin.Context) {
return return
} }
userid := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance) // Save user key
userIdentifier := fmt.Sprintf("%s@%s", usermeta.User.Username, config.Config.Misskey.Instance)
sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, userid) sessAccessTokenKey := fmt.Sprintf(consts.REDIS_KEY_USER_ACCESS_TOKEN, userIdentifier)
err = global.Redis.Set(context.Background(), sessAccessTokenKey, usermeta.AccessToken, 0).Err() err = global.Redis.Set(context.Background(), sessAccessTokenKey, usermeta.AccessToken, 0).Err()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to save session access token into redis with error: %v", err) global.Logger.Errorf("Failed to save session access token into redis with error: %v", err)
@ -74,21 +74,11 @@ func MisskeyAuthCallback(ctx *gin.Context) {
return return
} }
// Save context into redis
err = utils.SaveUserinfo(userid, &usermeta.User)
if err != nil {
global.Logger.Errorf("Failed to save session user info into redis with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
"error": "Failed to save userinfo",
})
return
}
global.Logger.Debugf("User accepted the request, reporting back to hydra...") global.Logger.Debugf("User accepted the request, reporting back to hydra...")
remember := true remember := true
rememberFor := int64(consts.TIME_LOGIN_REMEMBER / time.Second) rememberFor := int64(consts.TIME_LOGIN_REMEMBER / time.Second)
acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{ acceptReq, _, err := global.Hydra.Admin.OAuth2Api.AcceptOAuth2LoginRequest(context.Background()).LoginChallenge(oauth2challenge).AcceptOAuth2LoginRequest(client.AcceptOAuth2LoginRequest{
Subject: userid, Subject: userIdentifier,
Remember: &remember, Remember: &remember,
RememberFor: &rememberFor, RememberFor: &rememberFor,
}).Execute() }).Execute()

View File

@ -4,17 +4,11 @@ import (
"context" "context"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"misso/global" "misso/global"
"misso/types"
"misso/utils" "misso/utils"
"net/http" "net/http"
"strings" "strings"
) )
type UserinfoResponse struct {
types.MisskeyUser
EMail string `json:"email"`
}
func UserInfo(ctx *gin.Context) { func UserInfo(ctx *gin.Context) {
// Get token from header // Get token from header
accessToken := strings.Replace(ctx.GetHeader("Authorization"), "Bearer ", "", 1) accessToken := strings.Replace(ctx.GetHeader("Authorization"), "Bearer ", "", 1)
@ -45,7 +39,7 @@ func UserInfo(ctx *gin.Context) {
// Return user info // Return user info
global.Logger.Debugf("Retrieving context...") global.Logger.Debugf("Retrieving context...")
userinfoCtx, err := utils.GetUserinfo(*tokenInfo.Sub) userinfo, err := utils.GetUserinfo(*tokenInfo.Sub)
if err != nil { if err != nil {
global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err) global.Logger.Errorf("Failed to retrieve userinfo with error: %v", err)
ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{ ctx.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
@ -54,9 +48,19 @@ func UserInfo(ctx *gin.Context) {
return return
} }
ctx.JSON(http.StatusOK, UserinfoResponse{ userinfoRes := gin.H{} // map[string]interface{}
MisskeyUser: *userinfoCtx,
EMail: *tokenInfo.Sub, // Get scopes
}) if tokenInfo.Scope != nil && *tokenInfo.Scope != "" {
// Has scopes
scopes := strings.Split(*tokenInfo.Scope, " ")
for _, s := range scopes {
if value, ok := (*userinfo)[s]; ok {
userinfoRes[s] = value
}
}
}
ctx.JSON(http.StatusOK, userinfoRes)
} }

View File

@ -12,7 +12,7 @@ type AuthSessionUserkey_Request struct {
type AuthSessionUserkey_Response struct { type AuthSessionUserkey_Response struct {
AccessToken string `json:"accessToken"` AccessToken string `json:"accessToken"`
User types.MisskeyUser `json:"user"` User types.MisskeyUserBase `json:"user"`
} }
func GetUserkey(token string) (*AuthSessionUserkey_Response, error) { func GetUserkey(token string) (*AuthSessionUserkey_Response, error) {

View File

@ -1,11 +1,58 @@
{{ define "consent.tmpl" }} {{ define "consent.tmpl" }}
<html> <html lang="zh">
<head> <head>
<title>授权确认</title> <title>授权确认</title>
{{ template "head.tmpl" }} {{ template "head.tmpl" }}
<style> <style>
ul.scopes,
ul.app-terms {
list-style: none;
padding-left: 0;
}
ul.scopes {
text-align: left;
width: 100%;
}
ul.scopes > li {
padding: 6px 12px;
margin: 6px 0;
border-radius: 6px;
background-color: #282c34;
}
ul.scopes > li > * {
cursor: pointer;
}
ul.app-terms {
display: inline-flex;
gap: 0;
}
ul.app-terms > li {
padding: 0 6px;
margin: 0;
}
ul.app-terms > li:not(:last-child) {
border-right: 1px solid white;
}
ul.app-terms > li > a {
color: #62b6e7;
}
.consent-notice {
width: 100%;
border-radius: 6px;
background-color: #282c34;
}
p.remember { p.remember {
display: flex; display: flex;
justify-content: center;
} }
p.remember, p.remember > * { p.remember, p.remember > * {
cursor: pointer; cursor: pointer;
@ -59,7 +106,7 @@
background: #15803d; background: #15803d;
} }
#app-name, #user-name { .app-name, .user-name {
color: #62b6e7; color: #62b6e7;
} }
</style> </style>
@ -67,25 +114,65 @@
<body> <body>
<form id="main" action="/consent" method="POST"> <form id="main" action="/consent" method="POST">
<input type="hidden" name="_csrf" value="{{ .csrf }}" /> <input type="hidden" name="_csrf" value="{{ .csrf }}" />
<input type="hidden" name="challenge" value="{{ .challenge }}" />
<div class="logo">
{{ if .logo }} {{ if .logo }}
<img src="{{ .logo }}" alt="{{ .clientName }}" width="120" height="120" /> <img src="{{ .logo }}" alt="{{ .clientName }}" width="120" height="120" />
{{ else }} {{ else }}
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48"><title>c-warning</title><g><path fill="#EFD358" d="M24,1C11.31787,1,1,11.31787,1,24s10.31787,23,23,23s23-10.31787,23-23S36.68213,1,24,1z"></path> <path fill="#FFFFFF" d="M24,28c0.55225,0,1-0.44775,1-1V14c0-0.55225-0.44775-1-1-1s-1,0.44775-1,1v13 C23,27.55225,23.44775,28,24,28z"></path> <circle fill="#FFFFFF" cx="24" cy="33" r="2"></circle></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48">
<g>
<circle fill="#EFD358" cx="24" cy="24" r="24"></circle>
<path fill="#FFFFFF" d="M24,28c0.55225,0,1-0.44775,1-1V14c0-0.55225-0.44775-1-1-1s-1,0.44775-1,1v13 C23,27.55225,23.44775,28,24,28z"></path>
<circle fill="#FFFFFF" cx="24" cy="33" r="2"></circle>
</g>
</svg>
{{ end }} {{ end }}
</div>
<div>
<p> <p>
应用程序 应用程序
<span id="app-name">{{ .clientName }}</span> <span class="app-name">{{ .clientName }}</span>
正请求读取 正请求
<span id="user-name">{{ .user.Name }}</span> <br />
的信息 读取
<span class="user-name">{{ .user.name }}</span>
的这些信息:
</p> </p>
<ul class="scopes">
{{ $user := .user }}
{{ range $scope := .scopes }}
<li>
<details>
<summary>{{ $scope }}</summary>
<pre>{{ index $user $scope }}</pre>
</details>
</li>
{{ end }}
</ul>
{{ if or .clientPolicy .clientTos }}
<ul class="app-terms">
{{ if .clientPolicy }}
<li><a href="{{ .clientPolicy }}" target="_blank" referrerpolicy="no-referrer">使用政策</a></li>
{{ end }}
{{ if .clientTos }}
<li><a href="{{ .clientTos }}" target="_blank" referrerpolicy="no-referrer">服务条款</a></li>
{{ end }}
</ul>
{{ end }}
</div>
<div class="consent-notice">
<p>是否接受该请求?</p>
<p class="remember"> <p class="remember">
<input type="checkbox" id="remember" name="remember" value="true" /> <input type="checkbox" id="remember" name="remember" value="true" />
<label for="remember">记住我的选择</label> <label for="remember">记住我的选择</label>
</p> </p>
</div>
<p class="buttons"> <p class="buttons">
<button type="submit" name="action" value="reject" class="reject">拒绝</button> <button type="submit" name="action" value="reject" class="reject">拒绝</button>

View File

@ -1,14 +1,30 @@
{{ define "error.tmpl" }} {{ define "error.tmpl" }}
<html> <html lang="zh">
<head> <head>
<title>出错了</title> <title>出错了</title>
{{ template "head.tmpl" }} {{ template "head.tmpl" }}
<style>
.error {
width: 100%;
padding: 20px 0;
border-radius: 6px;
background-color: #282c34;
user-select: all;
}
</style>
</head> </head>
<body> <body>
<div id="main"> <div id="main">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48"><title>c-remove</title><g><path fill="#E86C60" d="M24,47C11.31787,47,1,36.68262,1,24S11.31787,1,24,1s23,10.31738,23,23S36.68213,47,24,47z"></path> <path fill="#FFFFFF" d="M25.41406,24l7.29297-7.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406s-1.02344-0.39062-1.41406,0 L24,22.58594l-7.29297-7.29297c-0.39062-0.39062-1.02344-0.39062-1.41406,0s-0.39062,1.02344,0,1.41406L22.58594,24 l-7.29297,7.29297c-0.39062,0.39062-0.39062,1.02344,0,1.41406C15.48828,32.90234,15.74414,33,16,33 s0.51172-0.09766,0.70703-0.29297L24,25.41406l7.29297,7.29297C31.48828,32.90234,31.74414,33,32,33 s0.51172-0.09766,0.70703-0.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406L25.41406,24z"></path></g></svg> <div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48">
<g>
<circle fill="#E86C60" cx="24" cy="24" r="24"></circle>
<path fill="#FFFFFF" d="M25.41406,24l7.29297-7.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406s-1.02344-0.39062-1.41406,0 L24,22.58594l-7.29297-7.29297c-0.39062-0.39062-1.02344-0.39062-1.41406,0s-0.39062,1.02344,0,1.41406L22.58594,24 l-7.29297,7.29297c-0.39062,0.39062-0.39062,1.02344,0,1.41406C15.48828,32.90234,15.74414,33,16,33 s0.51172-0.09766,0.70703-0.29297L24,25.41406l7.29297,7.29297C31.48828,32.90234,31.74414,33,32,33 s0.51172-0.09766,0.70703-0.29297c0.39062-0.39062,0.39062-1.02344,0-1.41406L25.41406,24z"></path>
</g>
</svg>
</div>
<h1>发生了一些错误</h1> <h1>发生了一些错误</h1>
<p>{{ .error }}</p> <p class="error">{{ .error }}</p>
</div> </div>
</body> </body>
</html> </html>

View File

@ -4,7 +4,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #282c34; background-color: #282c34;
} }
#main { #main {
@ -13,12 +13,23 @@
text-align: center; text-align: center;
align-items: center; align-items: center;
color: white; color: white;
padding: 40px; padding: 80px 40px 40px 40px;
background-color: #21252b; background-color: #21252b;
border-radius: 12px; border-radius: 12px;
margin: 0 40px; margin: 0 40px;
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
word-break: break-word; word-break: break-word;
position: relative;
}
.logo {
position: absolute;
border: 10px solid #282c34;
top: -70px;
}
.logo, .logo > * {
border-radius: 120px;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
{{ define "index.tmpl" }} {{ define "index.tmpl" }}
<html> <html lang="zh">
<head> <head>
<title>MiSSO</title> <title>MiSSO</title>
{{ template "head.tmpl" }} {{ template "head.tmpl" }}
@ -24,7 +24,15 @@
</head> </head>
<body> <body>
<div id="main"> <div id="main">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48"><title>access-key</title><rect data-element="frame" x="0" y="0" width="48" height="48" rx="48" ry="48" stroke="none" fill="#62b6e7"></rect><g transform="translate(9.600000000000001 9.600000000000001) scale(0.6)"><path d="M38,23a1,1,0,0,1-.707-.293l-6-6a1,1,0,0,1,0-1.414l8-8a1,1,0,0,1,1.414,0l6,6a1,1,0,0,1,0,1.414l-2,2a1,1,0,0,1-1.414,0L41,14.414,38.414,17l2.293,2.293a1,1,0,0,1,0,1.414l-2,2A1,1,0,0,1,38,23Z" fill="#eba40a"></path><path d="M44.061,3.939a1.5,1.5,0,0,0-2.122,0L17.923,27.956a10.027,10.027,0,1,0,2.121,2.121L44.061,6.061A1.5,1.5,0,0,0,44.061,3.939ZM12,43a7,7,0,1,1,4.914-11.978c.011.012.014.027.025.039s.027.014.039.025A6.995,6.995,0,0,1,12,43Z" fill="#ffd764"></path></g></svg> <div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 48 48">
<circle fill="#62b6e7" cx="24" cy="24" r="24"></circle>
<g transform="translate(9.600000000000001 9.600000000000001) scale(0.6)">
<path d="M38,23a1,1,0,0,1-.707-.293l-6-6a1,1,0,0,1,0-1.414l8-8a1,1,0,0,1,1.414,0l6,6a1,1,0,0,1,0,1.414l-2,2a1,1,0,0,1-1.414,0L41,14.414,38.414,17l2.293,2.293a1,1,0,0,1,0,1.414l-2,2A1,1,0,0,1,38,23Z" fill="#eba40a"></path>
<path d="M44.061,3.939a1.5,1.5,0,0,0-2.122,0L17.923,27.956a10.027,10.027,0,1,0,2.121,2.121L44.061,6.061A1.5,1.5,0,0,0,44.061,3.939ZM12,43a7,7,0,1,1,4.914-11.978c.011.012.014.027.025.039s.027.014.039.025A6.995,6.995,0,0,1,12,43Z" fill="#ffd764"></path>
</g>
</svg>
</div>
<h1>欢迎来到 MiSSO</h1> <h1>欢迎来到 MiSSO</h1>
<p> <p>
直接访问这里好像不太对哦 直接访问这里好像不太对哦

View File

@ -1,186 +1,8 @@
package types package types
import "time" type MisskeyUserBase struct {
type MisskeyUser struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"` Username string `json:"username"`
Host interface{} `json:"host"` // Ignore other fields
AvatarUrl string `json:"avatarUrl"`
AvatarBlurhash string `json:"avatarBlurhash"`
IsBot bool `json:"isBot"`
IsCat bool `json:"isCat"`
OnlineStatus string `json:"onlineStatus"`
Url interface{} `json:"url"`
Uri interface{} `json:"uri"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastFetchedAt interface{} `json:"lastFetchedAt"`
BannerUrl string `json:"bannerUrl"`
BannerBlurhash string `json:"bannerBlurhash"`
IsLocked bool `json:"isLocked"`
IsSilenced bool `json:"isSilenced"`
IsSuspended bool `json:"isSuspended"`
Description string `json:"description"`
Location string `json:"location"`
Birthday string `json:"birthday"`
Lang string `json:"lang"`
Fields []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"fields"`
FollowersCount int `json:"followersCount"`
FollowingCount int `json:"followingCount"`
NotesCount int `json:"notesCount"`
PinnedNoteIds []string `json:"pinnedNoteIds"`
PinnedNotes []struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UserId string `json:"userId"`
User struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Host interface{} `json:"host"`
AvatarUrl string `json:"avatarUrl"`
AvatarBlurhash string `json:"avatarBlurhash"`
IsBot bool `json:"isBot"`
IsCat bool `json:"isCat"`
OnlineStatus string `json:"onlineStatus"`
} `json:"user"`
Text string `json:"text"`
Cw *string `json:"cw"`
Visibility string `json:"visibility"`
LocalOnly bool `json:"localOnly"`
RenoteCount int `json:"renoteCount"`
RepliesCount int `json:"repliesCount"`
Reactions map[string]int `json:"reactions"`
FileIds []string `json:"fileIds"`
Files []struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Type string `json:"type"`
Md5 string `json:"md5"`
Size int `json:"size"`
IsSensitive bool `json:"isSensitive"`
Blurhash string `json:"blurhash"`
Properties struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"properties"`
Url string `json:"url"`
ThumbnailUrl string `json:"thumbnailUrl"`
Comment interface{} `json:"comment"`
FolderId interface{} `json:"folderId"`
Folder interface{} `json:"folder"`
UserId interface{} `json:"userId"`
User interface{} `json:"user"`
} `json:"files"`
ReplyId interface{} `json:"replyId"`
RenoteId interface{} `json:"renoteId"`
MyReaction string `json:"myReaction,omitempty"`
} `json:"pinnedNotes"`
PinnedPageId string `json:"pinnedPageId"`
PinnedPage struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
UserId string `json:"userId"`
User struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Host interface{} `json:"host"`
AvatarUrl string `json:"avatarUrl"`
AvatarBlurhash string `json:"avatarBlurhash"`
IsBot bool `json:"isBot"`
IsCat bool `json:"isCat"`
OnlineStatus string `json:"onlineStatus"`
} `json:"user"`
Content []struct {
Id string `json:"id"`
Text string `json:"text,omitempty"`
Type string `json:"type"`
Title string `json:"title,omitempty"`
} `json:"content"`
Variables []interface{} `json:"variables"`
Title string `json:"title"`
Name string `json:"name"`
Summary string `json:"summary"`
HideTitleWhenPinned bool `json:"hideTitleWhenPinned"`
AlignCenter bool `json:"alignCenter"`
Font string `json:"font"`
Script string `json:"script"`
EyeCatchingImageId string `json:"eyeCatchingImageId"`
EyeCatchingImage struct {
Id string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Type string `json:"type"`
Md5 string `json:"md5"`
Size int `json:"size"`
IsSensitive bool `json:"isSensitive"`
Blurhash string `json:"blurhash"`
Properties struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"properties"`
Url string `json:"url"`
ThumbnailUrl string `json:"thumbnailUrl"`
Comment interface{} `json:"comment"`
FolderId interface{} `json:"folderId"`
Folder interface{} `json:"folder"`
UserId interface{} `json:"userId"`
User interface{} `json:"user"`
} `json:"eyeCatchingImage"`
AttachedFiles []interface{} `json:"attachedFiles"`
LikedCount int `json:"likedCount"`
IsLiked bool `json:"isLiked"`
} `json:"pinnedPage"`
PublicReactions bool `json:"publicReactions"`
FfVisibility string `json:"ffVisibility"`
TwoFactorEnabled bool `json:"twoFactorEnabled"`
UsePasswordLessLogin bool `json:"usePasswordLessLogin"`
SecurityKeys bool `json:"securityKeys"`
Roles []struct {
Id string `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
IsModerator bool `json:"isModerator"`
IsAdministrator bool `json:"isAdministrator"`
} `json:"roles"`
AvatarId string `json:"avatarId"`
BannerId string `json:"bannerId"`
IsModerator bool `json:"isModerator"`
IsAdmin bool `json:"isAdmin"`
InjectFeaturedNote bool `json:"injectFeaturedNote"`
ReceiveAnnouncementEmail bool `json:"receiveAnnouncementEmail"`
AlwaysMarkNsfw bool `json:"alwaysMarkNsfw"`
AutoSensitive bool `json:"autoSensitive"`
CarefulBot bool `json:"carefulBot"`
AutoAcceptFollowed bool `json:"autoAcceptFollowed"`
NoCrawle bool `json:"noCrawle"`
IsExplorable bool `json:"isExplorable"`
IsDeleted bool `json:"isDeleted"`
HideOnlineStatus bool `json:"hideOnlineStatus"`
HasUnreadSpecifiedNotes bool `json:"hasUnreadSpecifiedNotes"`
HasUnreadMentions bool `json:"hasUnreadMentions"`
HasUnreadAnnouncement bool `json:"hasUnreadAnnouncement"`
HasUnreadAntenna bool `json:"hasUnreadAntenna"`
HasUnreadChannel bool `json:"hasUnreadChannel"`
HasUnreadMessagingMessage bool `json:"hasUnreadMessagingMessage"`
HasUnreadNotification bool `json:"hasUnreadNotification"`
HasPendingReceivedFollowRequest bool `json:"hasPendingReceivedFollowRequest"`
Integrations struct {
} `json:"integrations"`
MutedWords [][]string `json:"mutedWords"`
MutedInstances []string `json:"mutedInstances"`
MutingNotificationTypes []string `json:"mutingNotificationTypes"`
EmailNotificationTypes []string `json:"emailNotificationTypes"`
ShowTimelineReplies bool `json:"showTimelineReplies"`
} }
// TODO: Find a better way to split necessary fields and additional fields type MisskeyUser = map[string]interface{} // Just raw json map

View File

@ -13,7 +13,7 @@ import (
func GetUserinfo(subject string) (*types.MisskeyUser, error) { func GetUserinfo(subject string) (*types.MisskeyUser, error) {
// Check cache key // Check cache key
global.Logger.Debugf("Checking userinfo cache...") global.Logger.Debugf("Checking userinfo cache...")
userinfoCacheKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_USER_INFO, subject) userinfoCacheKey := fmt.Sprintf(consts.REDIS_KEY_USER_INFO, subject)
exist, err := global.Redis.Exists(context.Background(), userinfoCacheKey).Result() exist, err := global.Redis.Exists(context.Background(), userinfoCacheKey).Result()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to check userinfo exist status with error: %v", err) global.Logger.Errorf("Failed to check userinfo exist status with error: %v", err)
@ -38,7 +38,7 @@ func GetUserinfo(subject string) (*types.MisskeyUser, error) {
// Fallback to get info directly, we need user's access token. // Fallback to get info directly, we need user's access token.
global.Logger.Debugf("No cached userinfo found (or valid), trying to get latest response.") global.Logger.Debugf("No cached userinfo found (or valid), trying to get latest response.")
accessTokenCacheKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_ACCESS_TOKEN, subject) accessTokenCacheKey := fmt.Sprintf(consts.REDIS_KEY_USER_ACCESS_TOKEN, subject)
accessToken, err := global.Redis.Get(context.Background(), accessTokenCacheKey).Result() accessToken, err := global.Redis.Get(context.Background(), accessTokenCacheKey).Result()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to get user access token with error: %v", err) global.Logger.Errorf("Failed to get user access token with error: %v", err)
@ -52,6 +52,9 @@ func GetUserinfo(subject string) (*types.MisskeyUser, error) {
return nil, err return nil, err
} }
// Append subject as email to userinfo
(*userinfo)["email"] = subject
// Save userinfo into redis // Save userinfo into redis
_ = SaveUserinfo(subject, userinfo) // Ignore errors _ = SaveUserinfo(subject, userinfo) // Ignore errors

View File

@ -15,7 +15,7 @@ func SaveUserinfo(subject string, userinfo *types.MisskeyUser) error {
global.Logger.Errorf("Failed to parse accept context with error: %v", err) global.Logger.Errorf("Failed to parse accept context with error: %v", err)
return err return err
} }
sessUserInfoKey := fmt.Sprintf(consts.REDIS_KEY_SHARE_USER_INFO, subject) sessUserInfoKey := fmt.Sprintf(consts.REDIS_KEY_USER_INFO, subject)
err = global.Redis.Set(context.Background(), sessUserInfoKey, userinfoBytes, consts.TIME_USERINFO_CACHE).Err() err = global.Redis.Set(context.Background(), sessUserInfoKey, userinfoBytes, consts.TIME_USERINFO_CACHE).Err()
if err != nil { if err != nil {
global.Logger.Errorf("Failed to save session user info into redis with error: %v", err) global.Logger.Errorf("Failed to save session user info into redis with error: %v", err)