1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-01 17:38:37 +00:00

Move internal packages to an internal folder

For reference: https://go.dev/doc/go1.4#internalpackages
This commit is contained in:
Frédéric Guillot 2023-08-10 19:46:45 -07:00
parent c234903255
commit 168a870c02
433 changed files with 1121 additions and 1123 deletions

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"miniflux.app/v2/internal/errors"
)
// APIKeyForm represents the API Key form.
type APIKeyForm struct {
Description string
}
// Validate makes sure the form values are valid.
func (a APIKeyForm) Validate() error {
if a.Description == "" {
return errors.NewLocalizedError("error.fields_mandatory")
}
return nil
}
// NewAPIKeyForm returns a new APIKeyForm.
func NewAPIKeyForm(r *http.Request) *APIKeyForm {
return &APIKeyForm{
Description: r.FormValue("description"),
}
}

33
internal/ui/form/auth.go Normal file
View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"miniflux.app/v2/internal/errors"
)
// AuthForm represents the authentication form.
type AuthForm struct {
Username string
Password string
}
// Validate makes sure the form values are valid.
func (a AuthForm) Validate() error {
if a.Username == "" || a.Password == "" {
return errors.NewLocalizedError("error.fields_mandatory")
}
return nil
}
// NewAuthForm returns a new AuthForm.
func NewAuthForm(r *http.Request) *AuthForm {
return &AuthForm{
Username: r.FormValue("username"),
Password: r.FormValue("password"),
}
}

View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
)
// CategoryForm represents a feed form in the UI
type CategoryForm struct {
Title string
HideGlobally string
}
// NewCategoryForm returns a new CategoryForm.
func NewCategoryForm(r *http.Request) *CategoryForm {
return &CategoryForm{
Title: r.FormValue("title"),
HideGlobally: r.FormValue("hide_globally"),
}
}

93
internal/ui/form/feed.go Normal file
View file

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"strconv"
"miniflux.app/v2/internal/model"
)
// FeedForm represents a feed form in the UI
type FeedForm struct {
FeedURL string
SiteURL string
Title string
ScraperRules string
RewriteRules string
BlocklistRules string
KeeplistRules string
UrlRewriteRules string
Crawler bool
UserAgent string
Cookie string
CategoryID int64
Username string
Password string
IgnoreHTTPCache bool
AllowSelfSignedCertificates bool
FetchViaProxy bool
Disabled bool
NoMediaPlayer bool
HideGlobally bool
CategoryHidden bool // Category has "hide_globally"
}
// Merge updates the fields of the given feed.
func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
feed.Category.ID = f.CategoryID
feed.Title = f.Title
feed.SiteURL = f.SiteURL
feed.FeedURL = f.FeedURL
feed.ScraperRules = f.ScraperRules
feed.RewriteRules = f.RewriteRules
feed.BlocklistRules = f.BlocklistRules
feed.KeeplistRules = f.KeeplistRules
feed.UrlRewriteRules = f.UrlRewriteRules
feed.Crawler = f.Crawler
feed.UserAgent = f.UserAgent
feed.Cookie = f.Cookie
feed.ParsingErrorCount = 0
feed.ParsingErrorMsg = ""
feed.Username = f.Username
feed.Password = f.Password
feed.IgnoreHTTPCache = f.IgnoreHTTPCache
feed.AllowSelfSignedCertificates = f.AllowSelfSignedCertificates
feed.FetchViaProxy = f.FetchViaProxy
feed.Disabled = f.Disabled
feed.NoMediaPlayer = f.NoMediaPlayer
feed.HideGlobally = f.HideGlobally
return feed
}
// NewFeedForm parses the HTTP request and returns a FeedForm
func NewFeedForm(r *http.Request) *FeedForm {
categoryID, err := strconv.Atoi(r.FormValue("category_id"))
if err != nil {
categoryID = 0
}
return &FeedForm{
FeedURL: r.FormValue("feed_url"),
SiteURL: r.FormValue("site_url"),
Title: r.FormValue("title"),
ScraperRules: r.FormValue("scraper_rules"),
UserAgent: r.FormValue("user_agent"),
Cookie: r.FormValue("cookie"),
RewriteRules: r.FormValue("rewrite_rules"),
BlocklistRules: r.FormValue("blocklist_rules"),
KeeplistRules: r.FormValue("keeplist_rules"),
UrlRewriteRules: r.FormValue("urlrewrite_rules"),
Crawler: r.FormValue("crawler") == "1",
CategoryID: int64(categoryID),
Username: r.FormValue("feed_username"),
Password: r.FormValue("feed_password"),
IgnoreHTTPCache: r.FormValue("ignore_http_cache") == "1",
AllowSelfSignedCertificates: r.FormValue("allow_self_signed_certificates") == "1",
FetchViaProxy: r.FormValue("fetch_via_proxy") == "1",
Disabled: r.FormValue("disabled") == "1",
NoMediaPlayer: r.FormValue("no_media_player") == "1",
HideGlobally: r.FormValue("hide_globally") == "1",
}
}

View file

@ -0,0 +1,175 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"miniflux.app/v2/internal/model"
)
// IntegrationForm represents user integration settings form.
type IntegrationForm struct {
PinboardEnabled bool
PinboardToken string
PinboardTags string
PinboardMarkAsUnread bool
InstapaperEnabled bool
InstapaperUsername string
InstapaperPassword string
FeverEnabled bool
FeverUsername string
FeverPassword string
GoogleReaderEnabled bool
GoogleReaderUsername string
GoogleReaderPassword string
WallabagEnabled bool
WallabagOnlyURL bool
WallabagURL string
WallabagClientID string
WallabagClientSecret string
WallabagUsername string
WallabagPassword string
NotionEnabled bool
NotionPageID string
NotionToken string
NunuxKeeperEnabled bool
NunuxKeeperURL string
NunuxKeeperAPIKey string
EspialEnabled bool
EspialURL string
EspialAPIKey string
EspialTags string
ReadwiseEnabled bool
ReadwiseAPIKey string
PocketEnabled bool
PocketAccessToken string
PocketConsumerKey string
TelegramBotEnabled bool
TelegramBotToken string
TelegramBotChatID string
LinkdingEnabled bool
LinkdingURL string
LinkdingAPIKey string
LinkdingTags string
LinkdingMarkAsUnread bool
MatrixBotEnabled bool
MatrixBotUser string
MatrixBotPassword string
MatrixBotURL string
MatrixBotChatID string
AppriseEnabled bool
AppriseURL string
AppriseServicesURL string
}
// Merge copy form values to the model.
func (i IntegrationForm) Merge(integration *model.Integration) {
integration.PinboardEnabled = i.PinboardEnabled
integration.PinboardToken = i.PinboardToken
integration.PinboardTags = i.PinboardTags
integration.PinboardMarkAsUnread = i.PinboardMarkAsUnread
integration.InstapaperEnabled = i.InstapaperEnabled
integration.InstapaperUsername = i.InstapaperUsername
integration.InstapaperPassword = i.InstapaperPassword
integration.FeverEnabled = i.FeverEnabled
integration.FeverUsername = i.FeverUsername
integration.GoogleReaderEnabled = i.GoogleReaderEnabled
integration.GoogleReaderUsername = i.GoogleReaderUsername
integration.WallabagEnabled = i.WallabagEnabled
integration.WallabagOnlyURL = i.WallabagOnlyURL
integration.WallabagURL = i.WallabagURL
integration.WallabagClientID = i.WallabagClientID
integration.WallabagClientSecret = i.WallabagClientSecret
integration.WallabagUsername = i.WallabagUsername
integration.WallabagPassword = i.WallabagPassword
integration.NotionEnabled = i.NotionEnabled
integration.NotionPageID = i.NotionPageID
integration.NotionToken = i.NotionToken
integration.NunuxKeeperEnabled = i.NunuxKeeperEnabled
integration.NunuxKeeperURL = i.NunuxKeeperURL
integration.NunuxKeeperAPIKey = i.NunuxKeeperAPIKey
integration.EspialEnabled = i.EspialEnabled
integration.EspialURL = i.EspialURL
integration.EspialAPIKey = i.EspialAPIKey
integration.EspialTags = i.EspialTags
integration.ReadwiseEnabled = i.ReadwiseEnabled
integration.ReadwiseAPIKey = i.ReadwiseAPIKey
integration.PocketEnabled = i.PocketEnabled
integration.PocketAccessToken = i.PocketAccessToken
integration.PocketConsumerKey = i.PocketConsumerKey
integration.TelegramBotEnabled = i.TelegramBotEnabled
integration.TelegramBotToken = i.TelegramBotToken
integration.TelegramBotChatID = i.TelegramBotChatID
integration.LinkdingEnabled = i.LinkdingEnabled
integration.LinkdingURL = i.LinkdingURL
integration.LinkdingAPIKey = i.LinkdingAPIKey
integration.LinkdingTags = i.LinkdingTags
integration.LinkdingMarkAsUnread = i.LinkdingMarkAsUnread
integration.MatrixBotEnabled = i.MatrixBotEnabled
integration.MatrixBotUser = i.MatrixBotUser
integration.MatrixBotPassword = i.MatrixBotPassword
integration.MatrixBotURL = i.MatrixBotURL
integration.MatrixBotChatID = i.MatrixBotChatID
integration.AppriseEnabled = i.AppriseEnabled
integration.AppriseServicesURL = i.AppriseServicesURL
integration.AppriseURL = i.AppriseURL
}
// NewIntegrationForm returns a new IntegrationForm.
func NewIntegrationForm(r *http.Request) *IntegrationForm {
return &IntegrationForm{
PinboardEnabled: r.FormValue("pinboard_enabled") == "1",
PinboardToken: r.FormValue("pinboard_token"),
PinboardTags: r.FormValue("pinboard_tags"),
PinboardMarkAsUnread: r.FormValue("pinboard_mark_as_unread") == "1",
InstapaperEnabled: r.FormValue("instapaper_enabled") == "1",
InstapaperUsername: r.FormValue("instapaper_username"),
InstapaperPassword: r.FormValue("instapaper_password"),
FeverEnabled: r.FormValue("fever_enabled") == "1",
FeverUsername: r.FormValue("fever_username"),
FeverPassword: r.FormValue("fever_password"),
GoogleReaderEnabled: r.FormValue("googlereader_enabled") == "1",
GoogleReaderUsername: r.FormValue("googlereader_username"),
GoogleReaderPassword: r.FormValue("googlereader_password"),
WallabagEnabled: r.FormValue("wallabag_enabled") == "1",
WallabagOnlyURL: r.FormValue("wallabag_only_url") == "1",
WallabagURL: r.FormValue("wallabag_url"),
WallabagClientID: r.FormValue("wallabag_client_id"),
WallabagClientSecret: r.FormValue("wallabag_client_secret"),
WallabagUsername: r.FormValue("wallabag_username"),
WallabagPassword: r.FormValue("wallabag_password"),
NotionEnabled: r.FormValue("notion_enabled") == "1",
NotionPageID: r.FormValue("notion_page_id"),
NotionToken: r.FormValue("notion_token"),
NunuxKeeperEnabled: r.FormValue("nunux_keeper_enabled") == "1",
NunuxKeeperURL: r.FormValue("nunux_keeper_url"),
NunuxKeeperAPIKey: r.FormValue("nunux_keeper_api_key"),
EspialEnabled: r.FormValue("espial_enabled") == "1",
EspialURL: r.FormValue("espial_url"),
EspialAPIKey: r.FormValue("espial_api_key"),
EspialTags: r.FormValue("espial_tags"),
ReadwiseEnabled: r.FormValue("readwise_enabled") == "1",
ReadwiseAPIKey: r.FormValue("readwise_api_key"),
PocketEnabled: r.FormValue("pocket_enabled") == "1",
PocketAccessToken: r.FormValue("pocket_access_token"),
PocketConsumerKey: r.FormValue("pocket_consumer_key"),
TelegramBotEnabled: r.FormValue("telegram_bot_enabled") == "1",
TelegramBotToken: r.FormValue("telegram_bot_token"),
TelegramBotChatID: r.FormValue("telegram_bot_chat_id"),
LinkdingEnabled: r.FormValue("linkding_enabled") == "1",
LinkdingURL: r.FormValue("linkding_url"),
LinkdingAPIKey: r.FormValue("linkding_api_key"),
LinkdingTags: r.FormValue("linkding_tags"),
LinkdingMarkAsUnread: r.FormValue("linkding_mark_as_unread") == "1",
MatrixBotEnabled: r.FormValue("matrix_bot_enabled") == "1",
MatrixBotUser: r.FormValue("matrix_bot_user"),
MatrixBotPassword: r.FormValue("matrix_bot_password"),
MatrixBotURL: r.FormValue("matrix_bot_url"),
MatrixBotChatID: r.FormValue("matrix_bot_chat_id"),
AppriseEnabled: r.FormValue("apprise_enabled") == "1",
AppriseURL: r.FormValue("apprise_url"),
AppriseServicesURL: r.FormValue("apprise_services_url"),
}
}

View file

@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"strconv"
"miniflux.app/v2/internal/errors"
"miniflux.app/v2/internal/model"
)
// SettingsForm represents the settings form.
type SettingsForm struct {
Username string
Password string
Confirmation string
Theme string
Language string
Timezone string
EntryDirection string
EntryOrder string
EntriesPerPage int
KeyboardShortcuts bool
ShowReadingTime bool
CustomCSS string
EntrySwipe bool
GestureNav string
DisplayMode string
DefaultReadingSpeed int
CJKReadingSpeed int
DefaultHomePage string
CategoriesSortingOrder string
MarkReadOnView bool
}
// Merge updates the fields of the given user.
func (s *SettingsForm) Merge(user *model.User) *model.User {
user.Username = s.Username
user.Theme = s.Theme
user.Language = s.Language
user.Timezone = s.Timezone
user.EntryDirection = s.EntryDirection
user.EntryOrder = s.EntryOrder
user.EntriesPerPage = s.EntriesPerPage
user.KeyboardShortcuts = s.KeyboardShortcuts
user.ShowReadingTime = s.ShowReadingTime
user.Stylesheet = s.CustomCSS
user.EntrySwipe = s.EntrySwipe
user.GestureNav = s.GestureNav
user.DisplayMode = s.DisplayMode
user.CJKReadingSpeed = s.CJKReadingSpeed
user.DefaultReadingSpeed = s.DefaultReadingSpeed
user.DefaultHomePage = s.DefaultHomePage
user.CategoriesSortingOrder = s.CategoriesSortingOrder
user.MarkReadOnView = s.MarkReadOnView
if s.Password != "" {
user.Password = s.Password
}
return user
}
// Validate makes sure the form values are valid.
func (s *SettingsForm) Validate() error {
if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" || s.DisplayMode == "" || s.DefaultHomePage == "" {
return errors.NewLocalizedError("error.settings_mandatory_fields")
}
if s.CJKReadingSpeed <= 0 || s.DefaultReadingSpeed <= 0 {
return errors.NewLocalizedError("error.settings_reading_speed_is_positive")
}
if s.Confirmation == "" {
// Firefox insists on auto-completing the password field.
// If the confirmation field is blank, the user probably
// didn't intend to change their password.
s.Password = ""
} else if s.Password != "" {
if s.Password != s.Confirmation {
return errors.NewLocalizedError("error.different_passwords")
}
}
return nil
}
// NewSettingsForm returns a new SettingsForm.
func NewSettingsForm(r *http.Request) *SettingsForm {
entriesPerPage, err := strconv.ParseInt(r.FormValue("entries_per_page"), 10, 0)
if err != nil {
entriesPerPage = 0
}
defaultReadingSpeed, err := strconv.ParseInt(r.FormValue("default_reading_speed"), 10, 0)
if err != nil {
defaultReadingSpeed = 0
}
cjkReadingSpeed, err := strconv.ParseInt(r.FormValue("cjk_reading_speed"), 10, 0)
if err != nil {
cjkReadingSpeed = 0
}
return &SettingsForm{
Username: r.FormValue("username"),
Password: r.FormValue("password"),
Confirmation: r.FormValue("confirmation"),
Theme: r.FormValue("theme"),
Language: r.FormValue("language"),
Timezone: r.FormValue("timezone"),
EntryDirection: r.FormValue("entry_direction"),
EntryOrder: r.FormValue("entry_order"),
EntriesPerPage: int(entriesPerPage),
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
ShowReadingTime: r.FormValue("show_reading_time") == "1",
CustomCSS: r.FormValue("custom_css"),
EntrySwipe: r.FormValue("entry_swipe") == "1",
GestureNav: r.FormValue("gesture_nav"),
DisplayMode: r.FormValue("display_mode"),
DefaultReadingSpeed: int(defaultReadingSpeed),
CJKReadingSpeed: int(cjkReadingSpeed),
DefaultHomePage: r.FormValue("default_home_page"),
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
}
}

View file

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"testing"
)
func TestValid(t *testing.T) {
settings := &SettingsForm{
Username: "user",
Password: "hunter2",
Confirmation: "hunter2",
Theme: "default",
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 50,
DisplayMode: "standalone",
GestureNav: "tap",
DefaultReadingSpeed: 35,
CJKReadingSpeed: 25,
DefaultHomePage: "unread",
}
err := settings.Validate()
if err != nil {
t.Error(err)
}
}
func TestConfirmationEmpty(t *testing.T) {
settings := &SettingsForm{
Username: "user",
Password: "hunter2",
Confirmation: "",
Theme: "default",
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 50,
DisplayMode: "standalone",
GestureNav: "tap",
DefaultReadingSpeed: 35,
CJKReadingSpeed: 25,
DefaultHomePage: "unread",
}
err := settings.Validate()
if err != nil {
t.Error(err)
}
if settings.Password != "" {
t.Error("Password should have been cleared")
}
}
func TestConfirmationIncorrect(t *testing.T) {
settings := &SettingsForm{
Username: "user",
Password: "hunter2",
Confirmation: "unter2",
Theme: "default",
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 50,
DisplayMode: "standalone",
GestureNav: "tap",
DefaultReadingSpeed: 35,
CJKReadingSpeed: 25,
DefaultHomePage: "unread",
}
err := settings.Validate()
if err == nil {
t.Error("Validate should return an error")
}
}

View file

@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"strconv"
"miniflux.app/v2/internal/errors"
"miniflux.app/v2/internal/validator"
)
// SubscriptionForm represents the subscription form.
type SubscriptionForm struct {
URL string
CategoryID int64
Crawler bool
FetchViaProxy bool
AllowSelfSignedCertificates bool
UserAgent string
Cookie string
Username string
Password string
ScraperRules string
RewriteRules string
BlocklistRules string
KeeplistRules string
UrlRewriteRules string
}
// Validate makes sure the form values are valid.
func (s *SubscriptionForm) Validate() error {
if s.URL == "" || s.CategoryID == 0 {
return errors.NewLocalizedError("error.feed_mandatory_fields")
}
if !validator.IsValidURL(s.URL) {
return errors.NewLocalizedError("error.invalid_feed_url")
}
if !validator.IsValidRegex(s.BlocklistRules) {
return errors.NewLocalizedError("error.feed_invalid_blocklist_rule")
}
if !validator.IsValidRegex(s.KeeplistRules) {
return errors.NewLocalizedError("error.feed_invalid_keeplist_rule")
}
if !validator.IsValidRegex(s.UrlRewriteRules) {
return errors.NewLocalizedError("error.feed_invalid_urlrewrite_rule")
}
return nil
}
// NewSubscriptionForm returns a new SubscriptionForm.
func NewSubscriptionForm(r *http.Request) *SubscriptionForm {
categoryID, err := strconv.Atoi(r.FormValue("category_id"))
if err != nil {
categoryID = 0
}
return &SubscriptionForm{
URL: r.FormValue("url"),
CategoryID: int64(categoryID),
Crawler: r.FormValue("crawler") == "1",
AllowSelfSignedCertificates: r.FormValue("allow_self_signed_certificates") == "1",
FetchViaProxy: r.FormValue("fetch_via_proxy") == "1",
UserAgent: r.FormValue("user_agent"),
Cookie: r.FormValue("cookie"),
Username: r.FormValue("feed_username"),
Password: r.FormValue("feed_password"),
ScraperRules: r.FormValue("scraper_rules"),
RewriteRules: r.FormValue("rewrite_rules"),
BlocklistRules: r.FormValue("blocklist_rules"),
KeeplistRules: r.FormValue("keeplist_rules"),
UrlRewriteRules: r.FormValue("urlrewrite_rules"),
}
}

73
internal/ui/form/user.go Normal file
View file

@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package form // import "miniflux.app/v2/internal/ui/form"
import (
"net/http"
"miniflux.app/v2/internal/errors"
"miniflux.app/v2/internal/model"
)
// UserForm represents the user form.
type UserForm struct {
Username string
Password string
Confirmation string
IsAdmin bool
}
// ValidateCreation validates user creation.
func (u UserForm) ValidateCreation() error {
if u.Username == "" || u.Password == "" || u.Confirmation == "" {
return errors.NewLocalizedError("error.fields_mandatory")
}
if u.Password != u.Confirmation {
return errors.NewLocalizedError("error.different_passwords")
}
return nil
}
// ValidateModification validates user modification.
func (u UserForm) ValidateModification() error {
if u.Username == "" {
return errors.NewLocalizedError("error.user_mandatory_fields")
}
if u.Password != "" {
if u.Password != u.Confirmation {
return errors.NewLocalizedError("error.different_passwords")
}
if len(u.Password) < 6 {
return errors.NewLocalizedError("error.password_min_length")
}
}
return nil
}
// Merge updates the fields of the given user.
func (u UserForm) Merge(user *model.User) *model.User {
user.Username = u.Username
user.IsAdmin = u.IsAdmin
if u.Password != "" {
user.Password = u.Password
}
return user
}
// NewUserForm returns a new UserForm.
func NewUserForm(r *http.Request) *UserForm {
return &UserForm{
Username: r.FormValue("username"),
Password: r.FormValue("password"),
Confirmation: r.FormValue("confirmation"),
IsAdmin: r.FormValue("is_admin") == "1",
}
}