diff --git a/models/organization/team.go b/models/organization/team.go index ff33b799853..b419aa32252 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -117,8 +117,8 @@ func (t *Team) CanNonMemberReadMeta(ctx context.Context, org, doer *user_model.U } } -func NormalizeTeamVisibility(s string) structs.VisibleType { - if vt, ok := structs.VisibilityModes[s]; ok { +func NormalizeTeamVisibility[T ~string](v T) structs.VisibleType { + if vt, ok := structs.VisibilityModes[structs.VisibilityString(v)]; ok { return vt } return structs.VisibleTypePrivate diff --git a/modules/setting/service.go b/modules/setting/service.go index b578ce4d2e6..ad5c73556e9 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -25,12 +25,9 @@ const ( // Service settings var Service = struct { - DefaultUserVisibility string - DefaultUserVisibilityMode structs.VisibleType - AllowedUserVisibilityModes []string - AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` - DefaultOrgVisibility string - DefaultOrgVisibilityMode structs.VisibleType + DefaultUserVisibilityMode structs.VisibleType `ini:"-"` + AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` + DefaultOrgVisibilityMode structs.VisibleType `ini:"-"` ActiveCodeLives int ResetPwdCodeLives int RegisterEmailConfirm bool @@ -217,36 +214,35 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) - modes := sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",") - if len(modes) != 0 { - Service.AllowedUserVisibilityModes = []string{} - Service.AllowedUserVisibilityModesSlice = []bool{false, false, false} - for _, sMode := range modes { - if tp, ok := structs.VisibilityModes[sMode]; ok { // remove unsupported modes - Service.AllowedUserVisibilityModes = append(Service.AllowedUserVisibilityModes, sMode) - Service.AllowedUserVisibilityModesSlice[tp] = true - } else { - log.Warn("ALLOWED_USER_VISIBILITY_MODES %s is unsupported", sMode) - } + + Service.AllowedUserVisibilityModesSlice = AllowedVisibility{false, false, false} + var allowedUserVisibilityModes []structs.VisibilityString + for _, allowedMode := range sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",") { + if tp, ok := structs.VisibilityModes[structs.VisibilityString(allowedMode)]; ok { // remove unsupported modes + allowedUserVisibilityModes = append(allowedUserVisibilityModes, structs.VisibilityString(allowedMode)) + Service.AllowedUserVisibilityModesSlice[tp] = true + } else { + log.Warn("ALLOWED_USER_VISIBILITY_MODES %s is unsupported", allowedMode) } } - - if len(Service.AllowedUserVisibilityModes) == 0 { - Service.AllowedUserVisibilityModes = []string{"public", "limited", "private"} - Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} + if len(allowedUserVisibilityModes) == 0 { + allowedUserVisibilityModes = []structs.VisibilityString{structs.VisibilityStringPublic, structs.VisibilityStringLimited, structs.VisibilityStringPrivate} + Service.AllowedUserVisibilityModesSlice = AllowedVisibility{true, true, true} } - Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").String() - if Service.DefaultUserVisibility == "" { - Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0] - } else if !Service.AllowedUserVisibilityModesSlice[structs.VisibilityModes[Service.DefaultUserVisibility]] { - log.Warn("DEFAULT_USER_VISIBILITY %s is wrong or not in ALLOWED_USER_VISIBILITY_MODES, using first allowed", Service.DefaultUserVisibility) - Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0] + defaultUserVisibility := structs.VisibilityString(sec.Key("DEFAULT_USER_VISIBILITY").String()) + if defaultUserVisibility == "" { + defaultUserVisibility = allowedUserVisibilityModes[0] + } else if vt, ok := structs.VisibilityModes[defaultUserVisibility]; !ok || !Service.AllowedUserVisibilityModesSlice[vt] { + log.Warn("DEFAULT_USER_VISIBILITY %s is wrong or not in ALLOWED_USER_VISIBILITY_MODES, using first allowed", defaultUserVisibility) + defaultUserVisibility = allowedUserVisibilityModes[0] } - Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] - Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) - Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] + Service.DefaultUserVisibilityMode = structs.VisibilityModes[defaultUserVisibility] + + defaultOrgVisibility := structs.VisibilityString(sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.VisibilityModeKeys())) + Service.DefaultOrgVisibilityMode = structs.VisibilityModes[defaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() + Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https") Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",") diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go index 4358f0db679..1527d4f263b 100644 --- a/modules/setting/service_test.go +++ b/modules/setting/service_test.go @@ -48,80 +48,83 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b func TestLoadServiceVisibilityModes(t *testing.T) { defer test.MockVariableValue(&Service)() - kases := map[string]func(){ + visibleTypeSlice := func(s ...structs.VisibilityString) (ret []structs.VisibleType) { + for _, v := range s { + ret = append(ret, structs.VisibilityModes[v]) + } + return ret + } + testCases := map[string]func(){ ` [service] DEFAULT_USER_VISIBILITY = public ALLOWED_USER_VISIBILITY_MODES = public,limited,private `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = public `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = limited `: func() { - assert.Equal(t, "limited", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] ALLOWED_USER_VISIBILITY_MODES = public,limited,private `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = public ALLOWED_USER_VISIBILITY_MODES = limited,private `: func() { - assert.Equal(t, "limited", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = my_type ALLOWED_USER_VISIBILITY_MODES = limited,private `: func() { - assert.Equal(t, "limited", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) + }, + ` +[service] +DEFAULT_USER_VISIBILITY = my_type +`: func() { + assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = public ALLOWED_USER_VISIBILITY_MODES = public, limit, privated `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, } - for kase, fun := range kases { - t.Run(kase, func(t *testing.T) { - cfg, err := NewConfigProviderFromData(kase) + for tc, fn := range testCases { + t.Run(tc, func(t *testing.T) { + Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} + Service.DefaultUserVisibilityMode = structs.VisibleTypePublic + cfg, err := NewConfigProviderFromData(tc) assert.NoError(t, err) loadServiceFrom(cfg) - fun() - // reset - Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} - Service.AllowedUserVisibilityModes = []string{} - Service.DefaultUserVisibility = "" - Service.DefaultUserVisibilityMode = structs.VisibleTypePublic + fn() }) } } diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index aa4f7550054..4fd10aa5805 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -29,7 +29,7 @@ type CreateUserOption struct { // Whether the user has restricted access privileges Restricted *bool `json:"restricted"` // User visibility level: public, limited, or private - Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility VisibilityString `json:"visibility" binding:"In(,public,limited,private)"` // For explicitly setting the user creation timestamp. Useful when users are // migrated from other systems. When omitted, the user's creation timestamp @@ -78,5 +78,5 @@ type EditUserOption struct { // Whether the user has restricted access privileges Restricted *bool `json:"restricted"` // User visibility level: public, limited, or private - Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility VisibilityString `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/org.go b/modules/structs/org.go index b93f68b6009..162a47904ff 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -22,7 +22,7 @@ type Organization struct { // The location of the organization Location string `json:"location"` // The visibility level of the organization (public, limited, private) - Visibility UserVisibility `json:"visibility"` + Visibility VisibilityString `json:"visibility"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` // username of the organization @@ -60,7 +60,7 @@ type CreateOrgOption struct { // The location of the organization Location string `json:"location" binding:"MaxSize(50)"` // possible values are `public` (default), `limited` or `private` - Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility VisibilityString `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } @@ -78,7 +78,7 @@ type EditOrgOption struct { // The location of the organization Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` - Visibility *UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility *VisibilityString `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } diff --git a/modules/structs/user.go b/modules/structs/user.go index 852d2c4bdd4..1c1fc68f8b8 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -51,7 +51,7 @@ type User struct { // the user's description Description string `json:"description"` // User visibility level option: public, limited, private - Visibility UserVisibility `json:"visibility"` + Visibility VisibilityString `json:"visibility"` // user counts Followers int `json:"followers_count"` diff --git a/modules/structs/visible_type.go b/modules/structs/visible_type.go index f537884963d..5031809e74e 100644 --- a/modules/structs/visible_type.go +++ b/modules/structs/visible_type.go @@ -18,10 +18,10 @@ const ( ) // VisibilityModes is a map of Visibility types -var VisibilityModes = map[string]VisibleType{ - "public": VisibleTypePublic, - "limited": VisibleTypeLimited, - "private": VisibleTypePrivate, +var VisibilityModes = map[VisibilityString]VisibleType{ + VisibilityStringPublic: VisibleTypePublic, + VisibilityStringLimited: VisibleTypeLimited, + VisibilityStringPrivate: VisibleTypePrivate, } // IsPublic returns true if VisibleType is public @@ -43,27 +43,27 @@ func (vt VisibleType) IsPrivate() bool { func (vt VisibleType) String() string { for k, v := range VisibilityModes { if vt == v { - return k + return string(k) } } return "" } -// ExtractKeysFromMapString provides a slice of keys from map -func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) { - for k := range in { - keys = append(keys, k) +// VisibilityModeKeys returns the visibility mode names (public, limited, private) +func VisibilityModeKeys() (keys []string) { + for k := range VisibilityModes { + keys = append(keys, string(k)) } return keys } -// UserVisibility defines the visibility level of a user or organization as -// rendered in API payloads. The DB representation is VisibleType (int). -// swagger:enum UserVisibility -type UserVisibility string +// VisibilityString defines the visibility level of a user/organization/team as +// rendered in API and config payloads. The DB representation is VisibleType (int). +// swagger:enum VisibilityString +type VisibilityString string const ( - UserVisibilityPublic UserVisibility = "public" - UserVisibilityLimited UserVisibility = "limited" - UserVisibilityPrivate UserVisibility = "private" + VisibilityStringPublic VisibilityString = "public" + VisibilityStringLimited VisibilityString = "limited" + VisibilityStringPrivate VisibilityString = "private" ) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 055933bf3c2..587d166caee 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3306,7 +3306,6 @@ "admin.config.default_enable_timetracking": "Enable Time Tracking by Default", "admin.config.default_allow_only_contributors_to_track_time": "Let Only Contributors Track Time", "admin.config.no_reply_address": "Hidden Email Domain", - "admin.config.default_visibility_organization": "Default visibility for new Organizations", "admin.config.default_enable_dependencies": "Enable Issue Dependencies by Default", "admin.config.webhook_config": "Webhook Configuration", "admin.config.queue_length": "Queue Length", diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 4713c7a1c78..2e1262589d5 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) { visibility := api.VisibleTypePublic if form.Visibility != "" { - visibility = api.VisibilityModes[string(form.Visibility)] + visibility = api.VisibilityModes[form.Visibility] } org := &organization.Organization{ diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 9441fe0c18e..ec0d5eb3ced 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -121,7 +121,7 @@ func CreateUser(ctx *context.APIContext) { } if form.Visibility != "" { - visibility := api.VisibilityModes[string(form.Visibility)] + visibility := api.VisibilityModes[form.Visibility] overwriteDefault.Visibility = &visibility } @@ -237,7 +237,7 @@ func EditUser(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), IsActive: optional.FromPtr(form.Active), IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), - Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)), + Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), @@ -469,7 +469,7 @@ func SearchUsers(ctx *context.APIContext) { var visible []api.VisibleType visibilityParam := ctx.FormString("visibility") - if visibility, ok := api.VisibilityModes[visibilityParam]; ok { + if visibility, ok := api.VisibilityModes[api.VisibilityString(visibilityParam)]; ok { visible = []api.VisibleType{visibility} } else if visibilityParam != "" { ctx.APIError(http.StatusUnprocessableEntity, "invalid visibility") diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index c4f513d0bbe..196b1c0fc92 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -269,7 +269,7 @@ func Create(ctx *context.APIContext) { visibility := api.VisibleTypePublic if form.Visibility != "" { - visibility = api.VisibilityModes[string(form.Visibility)] + visibility = api.VisibilityModes[form.Visibility] } org := &organization.Organization{ @@ -413,7 +413,7 @@ func Edit(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), Website: optional.FromPtr(form.Website), Location: optional.FromPtr(form.Location), - Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())), + Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 5d64a9164aa..f57b997a152 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -223,7 +223,7 @@ func CreateTeam(ctx *context.APIContext) { IncludesAllRepositories: form.IncludesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, AccessMode: teamPermission, - Visibility: organization.NormalizeTeamVisibility(string(form.Visibility)), + Visibility: organization.NormalizeTeamVisibility(form.Visibility), } if team.AccessMode < perm.AccessModeAdmin { @@ -302,7 +302,7 @@ func EditTeam(ctx *context.APIContext) { } if form.Visibility != nil && !team.IsOwnerTeam() { - team.Visibility = organization.NormalizeTeamVisibility(string(*form.Visibility)) + team.Visibility = organization.NormalizeTeamVisibility(*form.Visibility) } isAuthChanged := false diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 2dafe88cc50..9e656350d2c 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -235,9 +235,10 @@ func SettingsRenamePost(ctx *context.Context) { // SettingsChangeVisibilityPost response for change organization visibility func SettingsChangeVisibilityPost(ctx *context.Context) { - visibility, ok := structs.VisibilityModes[ctx.FormString("visibility")] + visibilityStr := ctx.FormString("visibility") + visibility, ok := structs.VisibilityModes[structs.VisibilityString(visibilityStr)] if !ok { - ctx.Flash.Error(ctx.Tr("invalid_data", visibility)) + ctx.Flash.Error(ctx.Tr("invalid_data", visibilityStr)) ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(ctx.Org.Organization.Name) + "/settings") return } diff --git a/services/convert/convert.go b/services/convert/convert.go index 140af9ffa34..3122cd931fd 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -864,7 +864,7 @@ func ToOrganization(ctx context.Context, org *organization.Organization) *api.Or Description: org.Description, Website: org.Website, Location: org.Location, - Visibility: api.UserVisibility(org.Visibility.String()), + Visibility: api.VisibilityString(org.Visibility.String()), RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess, } } diff --git a/services/convert/user.go b/services/convert/user.go index 525eb46818a..d9ed124f04b 100644 --- a/services/convert/user.go +++ b/services/convert/user.go @@ -65,7 +65,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap StarredRepos: user.NumStars, } - result.Visibility = api.UserVisibility(user.Visibility.String()) + result.Visibility = api.VisibilityString(user.Visibility.String()) // hide primary email if API caller is anonymous or user keep email private if signed && (!user.KeepEmailPrivate || authed) { diff --git a/services/convert/user_test.go b/services/convert/user_test.go index 3ef7609f042..c42fc1455f4 100644 --- a/services/convert/user_test.go +++ b/services/convert/user_test.go @@ -29,11 +29,11 @@ func TestUser_ToUser(t *testing.T) { apiUser = toUser(t.Context(), user1, false, false) assert.False(t, apiUser.IsAdmin) - assert.Equal(t, api.UserVisibilityPublic, apiUser.Visibility) + assert.Equal(t, api.VisibilityStringPublic, apiUser.Visibility) user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}) apiUser = toUser(t.Context(), user31, true, true) assert.False(t, apiUser.IsAdmin) - assert.Equal(t, api.UserVisibilityPrivate, apiUser.Visibility) + assert.Equal(t, api.VisibilityStringPrivate, apiUser.Visibility) } diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index ae37950fb51..14fbf448fab 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -155,8 +155,6 @@