Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
7fc7b19d
Commit
7fc7b19d
authored
Dec 08, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: deprecate user setting legacy api
parent
b2d898dc
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
54 additions
and
494 deletions
+54
-494
memo.go
api/v1/memo.go
+5
-18
user.go
api/v1/user.go
+6
-29
user_setting.go
api/v1/user_setting.go
+0
-174
v1.go
api/v1/v1.go
+0
-1
user_service.go
api/v2/user_service.go
+1
-1
copydb.go
cmd/copydb.go
+1
-5
telegram.go
server/integration/telegram.go
+5
-10
cache.go
store/cache.go
+0
-4
user_setting.go
store/db/mysql/user_setting.go
+2
-48
user_setting.go
store/db/postgres/user_setting.go
+2
-69
00__webhook.sql
store/db/sqlite/migration/prod/0.18/00__webhook.sql
+12
-0
01__user_setting.sql
store/db/sqlite/migration/prod/0.18/01__user_setting.sql
+4
-0
user_setting.go
store/db/sqlite/user_setting.go
+2
-61
driver.go
store/driver.go
+2
-4
user_setting.go
store/user_setting.go
+5
-58
user_setting_test.go
test/store/user_setting_test.go
+7
-12
No files found.
api/v1/memo.go
View file @
7fc7b19d
...
@@ -265,20 +265,15 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
...
@@ -265,20 +265,15 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
}
}
if
createMemoRequest
.
Visibility
==
""
{
if
createMemoRequest
.
Visibility
==
""
{
userMemoVisibilitySetting
,
err
:=
s
.
Store
.
GetUserSetting
(
ctx
,
&
store
.
FindUserSetting
{
userMemoVisibilitySetting
,
err
:=
s
.
Store
.
GetUserSetting
V1
(
ctx
,
&
store
.
FindUserSetting
{
UserID
:
&
userID
,
UserID
:
&
userID
,
Key
:
UserSettingMemoVisibilityKey
.
String
()
,
Key
:
storepb
.
UserSettingKey_USER_SETTING_MEMO_VISIBILITY
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find user setting"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find user setting"
)
.
SetInternal
(
err
)
}
}
if
userMemoVisibilitySetting
!=
nil
{
if
userMemoVisibilitySetting
!=
nil
{
memoVisibility
:=
Private
createMemoRequest
.
Visibility
=
Visibility
(
userMemoVisibilitySetting
.
GetMemoVisibility
())
err
:=
json
.
Unmarshal
([]
byte
(
userMemoVisibilitySetting
.
Value
),
&
memoVisibility
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to unmarshal user setting value"
)
.
SetInternal
(
err
)
}
createMemoRequest
.
Visibility
=
memoVisibility
}
else
{
}
else
{
// Private is the default memo visibility.
// Private is the default memo visibility.
createMemoRequest
.
Visibility
=
Private
createMemoRequest
.
Visibility
=
Private
...
@@ -394,20 +389,12 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
...
@@ -394,20 +389,12 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
// Send notification to telegram if memo is not private.
// Send notification to telegram if memo is not private.
if
memoResponse
.
Visibility
!=
Private
{
if
memoResponse
.
Visibility
!=
Private
{
// fetch all telegram UserID
// fetch all telegram UserID
userSettings
,
err
:=
s
.
Store
.
ListUserSettings
(
ctx
,
&
store
.
FindUserSetting
{
Key
:
UserSettingTelegramUserIDKey
.
String
()
})
userSettings
,
err
:=
s
.
Store
.
ListUserSettings
V1
(
ctx
,
&
store
.
FindUserSetting
{
Key
:
storepb
.
UserSettingKey_USER_SETTING_TELEGRAM_USER_ID
})
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to ListUserSettings"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to ListUserSettings"
)
.
SetInternal
(
err
)
}
}
for
_
,
userSetting
:=
range
userSettings
{
for
_
,
userSetting
:=
range
userSettings
{
// parse telegram UserID setting value into a int64
tgUserID
,
err
:=
strconv
.
ParseInt
(
userSetting
.
GetTelegramUserId
(),
10
,
64
)
var
tgUserIDStr
string
err
:=
json
.
Unmarshal
([]
byte
(
userSetting
.
Value
),
&
tgUserIDStr
)
if
err
!=
nil
{
log
.
Error
(
"failed to parse Telegram UserID"
,
zap
.
Error
(
err
))
continue
}
tgUserID
,
err
:=
strconv
.
ParseInt
(
tgUserIDStr
,
10
,
64
)
if
err
!=
nil
{
if
err
!=
nil
{
log
.
Error
(
"failed to parse Telegram UserID"
,
zap
.
Error
(
err
))
log
.
Error
(
"failed to parse Telegram UserID"
,
zap
.
Error
(
err
))
continue
continue
...
...
api/v1/user.go
View file @
7fc7b19d
...
@@ -41,13 +41,12 @@ type User struct {
...
@@ -41,13 +41,12 @@ type User struct {
UpdatedTs
int64
`json:"updatedTs"`
UpdatedTs
int64
`json:"updatedTs"`
// Domain specific fields
// Domain specific fields
Username
string
`json:"username"`
Username
string
`json:"username"`
Role
Role
`json:"role"`
Role
Role
`json:"role"`
Email
string
`json:"email"`
Email
string
`json:"email"`
Nickname
string
`json:"nickname"`
Nickname
string
`json:"nickname"`
PasswordHash
string
`json:"-"`
PasswordHash
string
`json:"-"`
AvatarURL
string
`json:"avatarUrl"`
AvatarURL
string
`json:"avatarUrl"`
UserSettingList
[]
*
UserSetting
`json:"userSettingList"`
}
}
type
CreateUserRequest
struct
{
type
CreateUserRequest
struct
{
...
@@ -212,18 +211,7 @@ func (s *APIV1Service) GetCurrentUser(c echo.Context) error {
...
@@ -212,18 +211,7 @@ func (s *APIV1Service) GetCurrentUser(c echo.Context) error {
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Missing auth session"
)
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Missing auth session"
)
}
}
list
,
err
:=
s
.
Store
.
ListUserSettings
(
ctx
,
&
store
.
FindUserSetting
{
UserID
:
&
userID
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find userSettingList"
)
.
SetInternal
(
err
)
}
userSettingList
:=
[]
*
UserSetting
{}
for
_
,
userSetting
:=
range
list
{
userSettingList
=
append
(
userSettingList
,
convertUserSettingFromStore
(
userSetting
))
}
userMessage
:=
convertUserFromStore
(
user
)
userMessage
:=
convertUserFromStore
(
user
)
userMessage
.
UserSettingList
=
userSettingList
return
c
.
JSON
(
http
.
StatusOK
,
userMessage
)
return
c
.
JSON
(
http
.
StatusOK
,
userMessage
)
}
}
...
@@ -415,18 +403,7 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error {
...
@@ -415,18 +403,7 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error {
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to patch user"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to patch user"
)
.
SetInternal
(
err
)
}
}
list
,
err
:=
s
.
Store
.
ListUserSettings
(
ctx
,
&
store
.
FindUserSetting
{
UserID
:
&
userID
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find userSettingList"
)
.
SetInternal
(
err
)
}
userSettingList
:=
[]
*
UserSetting
{}
for
_
,
userSetting
:=
range
list
{
userSettingList
=
append
(
userSettingList
,
convertUserSettingFromStore
(
userSetting
))
}
userMessage
:=
convertUserFromStore
(
user
)
userMessage
:=
convertUserFromStore
(
user
)
userMessage
.
UserSettingList
=
userSettingList
return
c
.
JSON
(
http
.
StatusOK
,
userMessage
)
return
c
.
JSON
(
http
.
StatusOK
,
userMessage
)
}
}
...
...
api/v1/user_setting.go
deleted
100644 → 0
View file @
b2d898dc
package
v1
import
(
"encoding/json"
"net/http"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"github.com/usememos/memos/store"
)
type
UserSettingKey
string
const
(
// UserSettingLocaleKey is the key type for user locale.
UserSettingLocaleKey
UserSettingKey
=
"locale"
// UserSettingAppearanceKey is the key type for user appearance.
UserSettingAppearanceKey
UserSettingKey
=
"appearance"
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
UserSettingMemoVisibilityKey
UserSettingKey
=
"memo-visibility"
// UserSettingTelegramUserIDKey is the key type for telegram UserID of memos user.
UserSettingTelegramUserIDKey
UserSettingKey
=
"telegram-user-id"
)
// String returns the string format of UserSettingKey type.
func
(
key
UserSettingKey
)
String
()
string
{
switch
key
{
case
UserSettingLocaleKey
:
return
"locale"
case
UserSettingAppearanceKey
:
return
"appearance"
case
UserSettingMemoVisibilityKey
:
return
"memo-visibility"
case
UserSettingTelegramUserIDKey
:
return
"telegram-user-id"
}
return
""
}
var
(
UserSettingLocaleValue
=
[]
string
{
"ar"
,
"de"
,
"en"
,
"es"
,
"fr"
,
"hi"
,
"hr"
,
"it"
,
"ja"
,
"ko"
,
"nl"
,
"pl"
,
"pt-BR"
,
"ru"
,
"sl"
,
"sv"
,
"tr"
,
"uk"
,
"vi"
,
"zh-Hans"
,
"zh-Hant"
,
}
UserSettingAppearanceValue
=
[]
string
{
"system"
,
"light"
,
"dark"
}
UserSettingMemoVisibilityValue
=
[]
Visibility
{
Private
,
Protected
,
Public
}
)
type
UserSetting
struct
{
UserID
int32
`json:"userId"`
Key
UserSettingKey
`json:"key"`
Value
string
`json:"value"`
}
type
UpsertUserSettingRequest
struct
{
UserID
int32
`json:"-"`
Key
UserSettingKey
`json:"key"`
Value
string
`json:"value"`
}
func
(
s
*
APIV1Service
)
registerUserSettingRoutes
(
g
*
echo
.
Group
)
{
g
.
POST
(
"/user/setting"
,
s
.
UpsertUserSetting
)
}
// UpsertUserSetting godoc
//
// @Summary Upsert user setting
// @Tags user-setting
// @Accept json
// @Produce json
// @Param body body UpsertUserSettingRequest true "Request object."
// @Success 200 {object} store.UserSetting "Created user setting"
// @Failure 400 {object} nil "Malformatted post user setting upsert request | Invalid user setting format"
// @Failure 401 {object} nil "Missing auth session"
// @Failure 500 {object} nil "Failed to upsert user setting"
// @Router /api/v1/user/setting [POST]
func
(
s
*
APIV1Service
)
UpsertUserSetting
(
c
echo
.
Context
)
error
{
ctx
:=
c
.
Request
()
.
Context
()
userID
,
ok
:=
c
.
Get
(
userIDContextKey
)
.
(
int32
)
if
!
ok
{
return
echo
.
NewHTTPError
(
http
.
StatusUnauthorized
,
"Missing auth session"
)
}
userSettingUpsert
:=
&
UpsertUserSettingRequest
{}
if
err
:=
json
.
NewDecoder
(
c
.
Request
()
.
Body
)
.
Decode
(
userSettingUpsert
);
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"Malformatted post user setting upsert request"
)
.
SetInternal
(
err
)
}
if
err
:=
userSettingUpsert
.
Validate
();
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusBadRequest
,
"Invalid user setting format"
)
.
SetInternal
(
err
)
}
userSettingUpsert
.
UserID
=
userID
userSetting
,
err
:=
s
.
Store
.
UpsertUserSetting
(
ctx
,
&
store
.
UserSetting
{
UserID
:
userID
,
Key
:
userSettingUpsert
.
Key
.
String
(),
Value
:
userSettingUpsert
.
Value
,
})
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to upsert user setting"
)
.
SetInternal
(
err
)
}
userSettingMessage
:=
convertUserSettingFromStore
(
userSetting
)
return
c
.
JSON
(
http
.
StatusOK
,
userSettingMessage
)
}
func
(
upsert
UpsertUserSettingRequest
)
Validate
()
error
{
if
upsert
.
Key
==
UserSettingLocaleKey
{
localeValue
:=
"en"
err
:=
json
.
Unmarshal
([]
byte
(
upsert
.
Value
),
&
localeValue
)
if
err
!=
nil
{
return
errors
.
New
(
"failed to unmarshal user setting locale value"
)
}
if
!
slices
.
Contains
(
UserSettingLocaleValue
,
localeValue
)
{
return
errors
.
New
(
"invalid user setting locale value"
)
}
}
else
if
upsert
.
Key
==
UserSettingAppearanceKey
{
appearanceValue
:=
"system"
err
:=
json
.
Unmarshal
([]
byte
(
upsert
.
Value
),
&
appearanceValue
)
if
err
!=
nil
{
return
errors
.
New
(
"failed to unmarshal user setting appearance value"
)
}
if
!
slices
.
Contains
(
UserSettingAppearanceValue
,
appearanceValue
)
{
return
errors
.
New
(
"invalid user setting appearance value"
)
}
}
else
if
upsert
.
Key
==
UserSettingMemoVisibilityKey
{
memoVisibilityValue
:=
Private
err
:=
json
.
Unmarshal
([]
byte
(
upsert
.
Value
),
&
memoVisibilityValue
)
if
err
!=
nil
{
return
errors
.
New
(
"failed to unmarshal user setting memo visibility value"
)
}
if
!
slices
.
Contains
(
UserSettingMemoVisibilityValue
,
memoVisibilityValue
)
{
return
errors
.
New
(
"invalid user setting memo visibility value"
)
}
}
else
if
upsert
.
Key
==
UserSettingTelegramUserIDKey
{
var
key
string
err
:=
json
.
Unmarshal
([]
byte
(
upsert
.
Value
),
&
key
)
if
err
!=
nil
{
return
errors
.
New
(
"invalid user setting telegram user id value"
)
}
}
else
{
return
errors
.
New
(
"invalid user setting key"
)
}
return
nil
}
func
convertUserSettingFromStore
(
userSetting
*
store
.
UserSetting
)
*
UserSetting
{
return
&
UserSetting
{
UserID
:
userSetting
.
UserID
,
Key
:
UserSettingKey
(
userSetting
.
Key
),
Value
:
userSetting
.
Value
,
}
}
api/v1/v1.go
View file @
7fc7b19d
...
@@ -72,7 +72,6 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
...
@@ -72,7 +72,6 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
s
.
registerAuthRoutes
(
apiV1Group
)
s
.
registerAuthRoutes
(
apiV1Group
)
s
.
registerIdentityProviderRoutes
(
apiV1Group
)
s
.
registerIdentityProviderRoutes
(
apiV1Group
)
s
.
registerUserRoutes
(
apiV1Group
)
s
.
registerUserRoutes
(
apiV1Group
)
s
.
registerUserSettingRoutes
(
apiV1Group
)
s
.
registerTagRoutes
(
apiV1Group
)
s
.
registerTagRoutes
(
apiV1Group
)
s
.
registerStorageRoutes
(
apiV1Group
)
s
.
registerStorageRoutes
(
apiV1Group
)
s
.
registerResourceRoutes
(
apiV1Group
)
s
.
registerResourceRoutes
(
apiV1Group
)
...
...
api/v2/user_service.go
View file @
7fc7b19d
...
@@ -201,7 +201,7 @@ func (s *APIV2Service) GetUserSetting(ctx context.Context, _ *apiv2pb.GetUserSet
...
@@ -201,7 +201,7 @@ func (s *APIV2Service) GetUserSetting(ctx context.Context, _ *apiv2pb.GetUserSet
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get current user: %v"
,
err
)
}
}
userSettings
,
err
:=
s
.
Store
.
ListUserSettingsV1
(
ctx
,
&
store
.
FindUserSetting
V1
{
userSettings
,
err
:=
s
.
Store
.
ListUserSettingsV1
(
ctx
,
&
store
.
FindUserSetting
{
UserID
:
&
user
.
ID
,
UserID
:
&
user
.
ID
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
...
...
cmd/copydb.go
View file @
7fc7b19d
...
@@ -375,11 +375,7 @@ func copyUserSettings(ctx context.Context, fromDriver, toDriver store.Driver) er
...
@@ -375,11 +375,7 @@ func copyUserSettings(ctx context.Context, fromDriver, toDriver store.Driver) er
fmt
.
Printf
(
"
\t
Total %d records
\n
"
,
len
(
list
))
fmt
.
Printf
(
"
\t
Total %d records
\n
"
,
len
(
list
))
for
_
,
item
:=
range
list
{
for
_
,
item
:=
range
list
{
_
,
err
:=
toDriver
.
UpsertUserSetting
(
ctx
,
&
store
.
UserSetting
{
_
,
err
:=
toDriver
.
UpsertUserSetting
(
ctx
,
item
)
Key
:
item
.
Key
,
Value
:
item
.
Value
,
UserID
:
item
.
UserID
,
})
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
...
...
server/integration/telegram.go
View file @
7fc7b19d
...
@@ -3,7 +3,6 @@ package integration
...
@@ -3,7 +3,6 @@ package integration
import
(
import
(
"bytes"
"bytes"
"context"
"context"
"encoding/json"
"fmt"
"fmt"
"strconv"
"strconv"
"unicode/utf16"
"unicode/utf16"
...
@@ -12,6 +11,7 @@ import (
...
@@ -12,6 +11,7 @@ import (
apiv1
"github.com/usememos/memos/api/v1"
apiv1
"github.com/usememos/memos/api/v1"
"github.com/usememos/memos/plugin/telegram"
"github.com/usememos/memos/plugin/telegram"
storepb
"github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
...
@@ -39,20 +39,15 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
...
@@ -39,20 +39,15 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
}
}
var
creatorID
int32
var
creatorID
int32
userSettingList
,
err
:=
t
.
store
.
ListUserSettings
(
ctx
,
&
store
.
FindUserSetting
{
userSettingList
,
err
:=
t
.
store
.
ListUserSettings
V1
(
ctx
,
&
store
.
FindUserSetting
{
Key
:
apiv1
.
UserSettingTelegramUserIDKey
.
String
()
,
Key
:
storepb
.
UserSettingKey_USER_SETTING_TELEGRAM_USER_ID
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"Failed to find userSettingList"
)
return
errors
.
Wrap
(
err
,
"Failed to find userSettingList"
)
}
}
for
_
,
userSetting
:=
range
userSettingList
{
for
_
,
userSetting
:=
range
userSettingList
{
var
value
string
if
userSetting
.
GetTelegramUserId
()
==
strconv
.
FormatInt
(
message
.
From
.
ID
,
10
)
{
if
err
:=
json
.
Unmarshal
([]
byte
(
userSetting
.
Value
),
&
value
);
err
!=
nil
{
creatorID
=
userSetting
.
UserId
continue
}
if
value
==
strconv
.
FormatInt
(
message
.
From
.
ID
,
10
)
{
creatorID
=
userSetting
.
UserID
}
}
}
}
...
...
store/cache.go
View file @
7fc7b19d
...
@@ -4,10 +4,6 @@ import (
...
@@ -4,10 +4,6 @@ import (
"fmt"
"fmt"
)
)
func
getUserSettingCacheKey
(
userID
int32
,
key
string
)
string
{
return
fmt
.
Sprintf
(
"%d-%s"
,
userID
,
key
)
}
func
getUserSettingV1CacheKey
(
userID
int32
,
key
string
)
string
{
func
getUserSettingV1CacheKey
(
userID
int32
,
key
string
)
string
{
return
fmt
.
Sprintf
(
"%d-%s-v1"
,
userID
,
key
)
return
fmt
.
Sprintf
(
"%d-%s-v1"
,
userID
,
key
)
}
}
store/db/mysql/user_setting.go
View file @
7fc7b19d
...
@@ -12,53 +12,7 @@ import (
...
@@ -12,53 +12,7 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
d
*
DB
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
store
.
UserSetting
)
(
*
store
.
UserSetting
,
error
)
{
func
(
d
*
DB
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
stmt
:=
"INSERT INTO `user_setting` (`user_id`, `key`, `value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?"
if
_
,
err
:=
d
.
db
.
ExecContext
(
ctx
,
stmt
,
upsert
.
UserID
,
upsert
.
Key
,
upsert
.
Value
,
upsert
.
Value
);
err
!=
nil
{
return
nil
,
err
}
return
upsert
,
nil
}
func
(
d
*
DB
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
store
.
FindUserSetting
)
([]
*
store
.
UserSetting
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
Key
;
v
!=
""
{
where
,
args
=
append
(
where
,
"`key` = ?"
),
append
(
args
,
v
)
}
if
v
:=
find
.
UserID
;
v
!=
nil
{
where
,
args
=
append
(
where
,
"`user_id` = ?"
),
append
(
args
,
*
find
.
UserID
)
}
query
:=
"SELECT `user_id`, `key`, `value` FROM `user_setting` WHERE "
+
strings
.
Join
(
where
,
" AND "
)
rows
,
err
:=
d
.
db
.
QueryContext
(
ctx
,
query
,
args
...
)
if
err
!=
nil
{
return
nil
,
err
}
defer
rows
.
Close
()
userSettingList
:=
make
([]
*
store
.
UserSetting
,
0
)
for
rows
.
Next
()
{
var
userSetting
store
.
UserSetting
if
err
:=
rows
.
Scan
(
&
userSetting
.
UserID
,
&
userSetting
.
Key
,
&
userSetting
.
Value
,
);
err
!=
nil
{
return
nil
,
err
}
userSettingList
=
append
(
userSettingList
,
&
userSetting
)
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
userSettingList
,
nil
}
func
(
d
*
DB
)
UpsertUserSettingV1
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
stmt
:=
"INSERT INTO `user_setting` (`user_id`, `key`, `value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?"
stmt
:=
"INSERT INTO `user_setting` (`user_id`, `key`, `value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?"
var
valueString
string
var
valueString
string
if
upsert
.
Key
==
storepb
.
UserSettingKey_USER_SETTING_ACCESS_TOKENS
{
if
upsert
.
Key
==
storepb
.
UserSettingKey_USER_SETTING_ACCESS_TOKENS
{
...
@@ -86,7 +40,7 @@ func (d *DB) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSettin
...
@@ -86,7 +40,7 @@ func (d *DB) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSettin
return
upsert
,
nil
return
upsert
,
nil
}
}
func
(
d
*
DB
)
ListUserSettings
V1
(
ctx
context
.
Context
,
find
*
store
.
FindUserSettingV1
)
([]
*
storepb
.
UserSetting
,
error
)
{
func
(
d
*
DB
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
store
.
FindUserSetting
)
([]
*
storepb
.
UserSetting
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
Key
;
v
!=
storepb
.
UserSettingKey_USER_SETTING_KEY_UNSPECIFIED
{
if
v
:=
find
.
Key
;
v
!=
storepb
.
UserSettingKey_USER_SETTING_KEY_UNSPECIFIED
{
...
...
store/db/postgres/user_setting.go
View file @
7fc7b19d
...
@@ -13,74 +13,7 @@ import (
...
@@ -13,74 +13,7 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
d
*
DB
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
store
.
UserSetting
)
(
*
store
.
UserSetting
,
error
)
{
func
(
d
*
DB
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
// Construct the query using Squirrel
query
,
args
,
err
:=
squirrel
.
Insert
(
"user_setting"
)
.
Columns
(
"user_id"
,
"key"
,
"value"
)
.
Values
(
upsert
.
UserID
,
upsert
.
Key
,
upsert
.
Value
)
.
PlaceholderFormat
(
squirrel
.
Dollar
)
.
// no need to specify ON CONFLICT clause, as the primary key is (user_id, key)
ToSql
()
if
err
!=
nil
{
return
nil
,
err
}
// Execute the query
if
_
,
err
:=
d
.
db
.
ExecContext
(
ctx
,
query
,
args
...
);
err
!=
nil
{
return
nil
,
err
}
return
upsert
,
nil
}
func
(
d
*
DB
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
store
.
FindUserSetting
)
([]
*
store
.
UserSetting
,
error
)
{
// Start building the query
qb
:=
squirrel
.
Select
(
"user_id"
,
"key"
,
"value"
)
.
From
(
"user_setting"
)
.
Where
(
"1 = 1"
)
.
PlaceholderFormat
(
squirrel
.
Dollar
)
// Add conditions based on the provided find parameters
if
v
:=
find
.
Key
;
v
!=
""
{
qb
=
qb
.
Where
(
squirrel
.
Eq
{
"key"
:
v
})
}
if
v
:=
find
.
UserID
;
v
!=
nil
{
qb
=
qb
.
Where
(
squirrel
.
Eq
{
"user_id"
:
*
v
})
}
// Finalize the query
query
,
args
,
err
:=
qb
.
ToSql
()
if
err
!=
nil
{
return
nil
,
err
}
// Execute the query
rows
,
err
:=
d
.
db
.
QueryContext
(
ctx
,
query
,
args
...
)
if
err
!=
nil
{
return
nil
,
err
}
defer
rows
.
Close
()
// Process the rows
userSettingList
:=
make
([]
*
store
.
UserSetting
,
0
)
for
rows
.
Next
()
{
var
userSetting
store
.
UserSetting
if
err
:=
rows
.
Scan
(
&
userSetting
.
UserID
,
&
userSetting
.
Key
,
&
userSetting
.
Value
,
);
err
!=
nil
{
return
nil
,
err
}
userSettingList
=
append
(
userSettingList
,
&
userSetting
)
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
userSettingList
,
nil
}
func
(
d
*
DB
)
UpsertUserSettingV1
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
var
valueString
string
var
valueString
string
if
upsert
.
Key
==
storepb
.
UserSettingKey_USER_SETTING_ACCESS_TOKENS
{
if
upsert
.
Key
==
storepb
.
UserSettingKey_USER_SETTING_ACCESS_TOKENS
{
valueBytes
,
err
:=
protojson
.
Marshal
(
upsert
.
GetAccessTokens
())
valueBytes
,
err
:=
protojson
.
Marshal
(
upsert
.
GetAccessTokens
())
...
@@ -120,7 +53,7 @@ func (d *DB) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSettin
...
@@ -120,7 +53,7 @@ func (d *DB) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSettin
return
upsert
,
nil
return
upsert
,
nil
}
}
func
(
d
*
DB
)
ListUserSettings
V1
(
ctx
context
.
Context
,
find
*
store
.
FindUserSettingV1
)
([]
*
storepb
.
UserSetting
,
error
)
{
func
(
d
*
DB
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
store
.
FindUserSetting
)
([]
*
storepb
.
UserSetting
,
error
)
{
// Start building the query using Squirrel
// Start building the query using Squirrel
qb
:=
squirrel
.
Select
(
"user_id"
,
"key"
,
"value"
)
.
From
(
"user_setting"
)
.
PlaceholderFormat
(
squirrel
.
Dollar
)
qb
:=
squirrel
.
Select
(
"user_id"
,
"key"
,
"value"
)
.
From
(
"user_setting"
)
.
PlaceholderFormat
(
squirrel
.
Dollar
)
...
...
store/db/sqlite/migration/prod/0.18/00__webhook.sql
0 → 100644
View file @
7fc7b19d
-- webhook
CREATE
TABLE
webhook
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT
,
created_ts
BIGINT
NOT
NULL
DEFAULT
(
strftime
(
'%s'
,
'now'
)),
updated_ts
BIGINT
NOT
NULL
DEFAULT
(
strftime
(
'%s'
,
'now'
)),
row_status
TEXT
NOT
NULL
CHECK
(
row_status
IN
(
'NORMAL'
,
'ARCHIVED'
))
DEFAULT
'NORMAL'
,
creator_id
INTEGER
NOT
NULL
,
name
TEXT
NOT
NULL
,
url
TEXT
NOT
NULL
);
CREATE
INDEX
idx_webhook_creator_id
ON
webhook
(
creator_id
);
store/db/sqlite/migration/prod/0.18/01__user_setting.sql
0 → 100644
View file @
7fc7b19d
UPDATE
user_setting
SET
key
=
'USER_SETTING_LOCALE'
WHERE
key
=
'locale'
;
UPDATE
user_setting
SET
key
=
'USER_SETTING_APPEARANCE'
WHERE
key
=
'appearance'
;
UPDATE
user_setting
SET
key
=
'USER_SETTING_MEMO_VISIBILITY'
WHERE
key
=
'memo-visibility'
;
UPDATE
user_setting
SET
key
=
'USER_SETTING_TELEGRAM_USER_ID'
WHERE
key
=
'telegram-user-id'
;
store/db/sqlite/user_setting.go
View file @
7fc7b19d
...
@@ -12,66 +12,7 @@ import (
...
@@ -12,66 +12,7 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
d
*
DB
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
store
.
UserSetting
)
(
*
store
.
UserSetting
,
error
)
{
func
(
d
*
DB
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
stmt
:=
`
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET value = EXCLUDED.value
`
if
_
,
err
:=
d
.
db
.
ExecContext
(
ctx
,
stmt
,
upsert
.
UserID
,
upsert
.
Key
,
upsert
.
Value
);
err
!=
nil
{
return
nil
,
err
}
return
upsert
,
nil
}
func
(
d
*
DB
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
store
.
FindUserSetting
)
([]
*
store
.
UserSetting
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
Key
;
v
!=
""
{
where
,
args
=
append
(
where
,
"key = ?"
),
append
(
args
,
v
)
}
if
v
:=
find
.
UserID
;
v
!=
nil
{
where
,
args
=
append
(
where
,
"user_id = ?"
),
append
(
args
,
*
find
.
UserID
)
}
query
:=
`
SELECT
user_id,
key,
value
FROM user_setting
WHERE `
+
strings
.
Join
(
where
,
" AND "
)
rows
,
err
:=
d
.
db
.
QueryContext
(
ctx
,
query
,
args
...
)
if
err
!=
nil
{
return
nil
,
err
}
defer
rows
.
Close
()
userSettingList
:=
make
([]
*
store
.
UserSetting
,
0
)
for
rows
.
Next
()
{
var
userSetting
store
.
UserSetting
if
err
:=
rows
.
Scan
(
&
userSetting
.
UserID
,
&
userSetting
.
Key
,
&
userSetting
.
Value
,
);
err
!=
nil
{
return
nil
,
err
}
userSettingList
=
append
(
userSettingList
,
&
userSetting
)
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
userSettingList
,
nil
}
func
(
d
*
DB
)
UpsertUserSettingV1
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
stmt
:=
`
stmt
:=
`
INSERT INTO user_setting (
INSERT INTO user_setting (
user_id, key, value
user_id, key, value
...
@@ -106,7 +47,7 @@ func (d *DB) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSettin
...
@@ -106,7 +47,7 @@ func (d *DB) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSettin
return
upsert
,
nil
return
upsert
,
nil
}
}
func
(
d
*
DB
)
ListUserSettings
V1
(
ctx
context
.
Context
,
find
*
store
.
FindUserSettingV1
)
([]
*
storepb
.
UserSetting
,
error
)
{
func
(
d
*
DB
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
store
.
FindUserSetting
)
([]
*
storepb
.
UserSetting
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
Key
;
v
!=
storepb
.
UserSettingKey_USER_SETTING_KEY_UNSPECIFIED
{
if
v
:=
find
.
Key
;
v
!=
storepb
.
UserSettingKey_USER_SETTING_KEY_UNSPECIFIED
{
...
...
store/driver.go
View file @
7fc7b19d
...
@@ -62,10 +62,8 @@ type Driver interface {
...
@@ -62,10 +62,8 @@ type Driver interface {
DeleteUser
(
ctx
context
.
Context
,
delete
*
DeleteUser
)
error
DeleteUser
(
ctx
context
.
Context
,
delete
*
DeleteUser
)
error
// UserSetting model related methods.
// UserSetting model related methods.
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
UserSetting
)
(
*
UserSetting
,
error
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
FindUserSetting
)
([]
*
UserSetting
,
error
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
FindUserSetting
)
([]
*
storepb
.
UserSetting
,
error
)
UpsertUserSettingV1
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
ListUserSettingsV1
(
ctx
context
.
Context
,
find
*
FindUserSettingV1
)
([]
*
storepb
.
UserSetting
,
error
)
// IdentityProvider model related methods.
// IdentityProvider model related methods.
CreateIdentityProvider
(
ctx
context
.
Context
,
create
*
IdentityProvider
)
(
*
IdentityProvider
,
error
)
CreateIdentityProvider
(
ctx
context
.
Context
,
create
*
IdentityProvider
)
(
*
IdentityProvider
,
error
)
...
...
store/user_setting.go
View file @
7fc7b19d
...
@@ -6,66 +6,13 @@ import (
...
@@ -6,66 +6,13 @@ import (
storepb
"github.com/usememos/memos/proto/gen/store"
storepb
"github.com/usememos/memos/proto/gen/store"
)
)
type
UserSetting
struct
{
UserID
int32
Key
string
Value
string
}
type
FindUserSetting
struct
{
type
FindUserSetting
struct
{
UserID
*
int32
Key
string
}
func
(
s
*
Store
)
UpsertUserSetting
(
ctx
context
.
Context
,
upsert
*
UserSetting
)
(
*
UserSetting
,
error
)
{
userSetting
,
err
:=
s
.
driver
.
UpsertUserSetting
(
ctx
,
upsert
)
if
err
!=
nil
{
return
nil
,
err
}
s
.
userSettingCache
.
Store
(
getUserSettingCacheKey
(
userSetting
.
UserID
,
userSetting
.
Key
),
userSetting
)
return
userSetting
,
nil
}
func
(
s
*
Store
)
ListUserSettings
(
ctx
context
.
Context
,
find
*
FindUserSetting
)
([]
*
UserSetting
,
error
)
{
userSettingList
,
err
:=
s
.
driver
.
ListUserSettings
(
ctx
,
find
)
if
err
!=
nil
{
return
nil
,
err
}
for
_
,
userSetting
:=
range
userSettingList
{
s
.
userSettingCache
.
Store
(
getUserSettingCacheKey
(
userSetting
.
UserID
,
userSetting
.
Key
),
userSetting
)
}
return
userSettingList
,
nil
}
func
(
s
*
Store
)
GetUserSetting
(
ctx
context
.
Context
,
find
*
FindUserSetting
)
(
*
UserSetting
,
error
)
{
if
find
.
UserID
!=
nil
{
if
cache
,
ok
:=
s
.
userSettingCache
.
Load
(
getUserSettingCacheKey
(
*
find
.
UserID
,
find
.
Key
));
ok
{
return
cache
.
(
*
UserSetting
),
nil
}
}
list
,
err
:=
s
.
ListUserSettings
(
ctx
,
find
)
if
err
!=
nil
{
return
nil
,
err
}
if
len
(
list
)
==
0
{
return
nil
,
nil
}
userSetting
:=
list
[
0
]
return
userSetting
,
nil
}
type
FindUserSettingV1
struct
{
UserID
*
int32
UserID
*
int32
Key
storepb
.
UserSettingKey
Key
storepb
.
UserSettingKey
}
}
func
(
s
*
Store
)
UpsertUserSettingV1
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
func
(
s
*
Store
)
UpsertUserSettingV1
(
ctx
context
.
Context
,
upsert
*
storepb
.
UserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
userSettingMessage
,
err
:=
s
.
driver
.
UpsertUserSetting
V1
(
ctx
,
upsert
)
userSettingMessage
,
err
:=
s
.
driver
.
UpsertUserSetting
(
ctx
,
upsert
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
...
@@ -74,8 +21,8 @@ func (s *Store) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSet
...
@@ -74,8 +21,8 @@ func (s *Store) UpsertUserSettingV1(ctx context.Context, upsert *storepb.UserSet
return
userSettingMessage
,
nil
return
userSettingMessage
,
nil
}
}
func
(
s
*
Store
)
ListUserSettingsV1
(
ctx
context
.
Context
,
find
*
FindUserSetting
V1
)
([]
*
storepb
.
UserSetting
,
error
)
{
func
(
s
*
Store
)
ListUserSettingsV1
(
ctx
context
.
Context
,
find
*
FindUserSetting
)
([]
*
storepb
.
UserSetting
,
error
)
{
userSettingList
,
err
:=
s
.
driver
.
ListUserSettings
V1
(
ctx
,
find
)
userSettingList
,
err
:=
s
.
driver
.
ListUserSettings
(
ctx
,
find
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
...
@@ -86,7 +33,7 @@ func (s *Store) ListUserSettingsV1(ctx context.Context, find *FindUserSettingV1)
...
@@ -86,7 +33,7 @@ func (s *Store) ListUserSettingsV1(ctx context.Context, find *FindUserSettingV1)
return
userSettingList
,
nil
return
userSettingList
,
nil
}
}
func
(
s
*
Store
)
GetUserSettingV1
(
ctx
context
.
Context
,
find
*
FindUserSetting
V1
)
(
*
storepb
.
UserSetting
,
error
)
{
func
(
s
*
Store
)
GetUserSettingV1
(
ctx
context
.
Context
,
find
*
FindUserSetting
)
(
*
storepb
.
UserSetting
,
error
)
{
if
find
.
UserID
!=
nil
{
if
find
.
UserID
!=
nil
{
if
cache
,
ok
:=
s
.
userSettingCache
.
Load
(
getUserSettingV1CacheKey
(
*
find
.
UserID
,
find
.
Key
.
String
()));
ok
{
if
cache
,
ok
:=
s
.
userSettingCache
.
Load
(
getUserSettingV1CacheKey
(
*
find
.
UserID
,
find
.
Key
.
String
()));
ok
{
return
cache
.
(
*
storepb
.
UserSetting
),
nil
return
cache
.
(
*
storepb
.
UserSetting
),
nil
...
@@ -109,7 +56,7 @@ func (s *Store) GetUserSettingV1(ctx context.Context, find *FindUserSettingV1) (
...
@@ -109,7 +56,7 @@ func (s *Store) GetUserSettingV1(ctx context.Context, find *FindUserSettingV1) (
// GetUserAccessTokens returns the access tokens of the user.
// GetUserAccessTokens returns the access tokens of the user.
func
(
s
*
Store
)
GetUserAccessTokens
(
ctx
context
.
Context
,
userID
int32
)
([]
*
storepb
.
AccessTokensUserSetting_AccessToken
,
error
)
{
func
(
s
*
Store
)
GetUserAccessTokens
(
ctx
context
.
Context
,
userID
int32
)
([]
*
storepb
.
AccessTokensUserSetting_AccessToken
,
error
)
{
userSetting
,
err
:=
s
.
GetUserSettingV1
(
ctx
,
&
FindUserSetting
V1
{
userSetting
,
err
:=
s
.
GetUserSettingV1
(
ctx
,
&
FindUserSetting
{
UserID
:
&
userID
,
UserID
:
&
userID
,
Key
:
storepb
.
UserSettingKey_USER_SETTING_ACCESS_TOKENS
,
Key
:
storepb
.
UserSettingKey_USER_SETTING_ACCESS_TOKENS
,
})
})
...
...
test/store/user_setting_test.go
View file @
7fc7b19d
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
storepb
"github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
...
@@ -14,19 +15,13 @@ func TestUserSettingStore(t *testing.T) {
...
@@ -14,19 +15,13 @@ func TestUserSettingStore(t *testing.T) {
ts
:=
NewTestingStore
(
ctx
,
t
)
ts
:=
NewTestingStore
(
ctx
,
t
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
UpsertUserSetting
(
ctx
,
&
store
.
UserSetting
{
_
,
err
=
ts
.
UpsertUserSetting
V1
(
ctx
,
&
storepb
.
UserSetting
{
UserI
D
:
user
.
ID
,
UserI
d
:
user
.
ID
,
Key
:
"test_key"
,
Key
:
storepb
.
UserSettingKey_USER_SETTING_LOCALE
,
Value
:
"test_value"
,
Value
:
&
storepb
.
UserSetting_Locale
{
Locale
:
"en"
}
,
})
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
UpsertUserSetting
(
ctx
,
&
store
.
UserSetting
{
list
,
err
:=
ts
.
ListUserSettingsV1
(
ctx
,
&
store
.
FindUserSetting
{})
UserID
:
user
.
ID
,
Key
:
"locale"
,
Value
:
"zh"
,
})
require
.
NoError
(
t
,
err
)
list
,
err
:=
ts
.
ListUserSettings
(
ctx
,
&
store
.
FindUserSetting
{})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
2
,
len
(
list
))
require
.
Equal
(
t
,
1
,
len
(
list
))
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment