diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index e1c1c14e9..d3fc3478b 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -5823,6 +5823,53 @@ paths: summary: View domain allow with the given ID. tags: - admin + put: + consumes: + - multipart/form-data + operationId: domainAllowUpdate + parameters: + - description: The id of the domain allow. + in: path + name: id + required: true + type: string + - description: Obfuscate the name of the domain when serving it publicly. Eg., `example.org` becomes something like `ex***e.org`. + in: formData + name: obfuscate + type: boolean + - description: Public comment about this domain allow. This will be displayed alongside the domain allow if you choose to share allows. + in: formData + name: public_comment + type: string + - description: Private comment about this domain allow. Will only be shown to other admins, so this is a useful way of internally keeping track of why a certain domain ended up allowed. + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: The updated domain allow. + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request + "401": + description: unauthorized + "403": + description: forbidden + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error + security: + - OAuth2 Bearer: + - admin:write:domain_allows + summary: Update a single domain allow. + tags: + - admin /api/v1/admin/domain_blocks: get: operationId: domainBlocksGet @@ -5990,6 +6037,53 @@ paths: summary: View domain block with the given ID. tags: - admin + put: + consumes: + - multipart/form-data + operationId: domainBlockUpdate + parameters: + - description: The id of the domain block. + in: path + name: id + required: true + type: string + - description: Obfuscate the name of the domain when serving it publicly. Eg., `example.org` becomes something like `ex***e.org`. + in: formData + name: obfuscate + type: boolean + - description: Public comment about this domain block. This will be displayed alongside the domain block if you choose to share blocks. + in: formData + name: public_comment + type: string + - description: Private comment about this domain block. Will only be shown to other admins, so this is a useful way of internally keeping track of why a certain domain ended up blocked. + in: formData + name: private_comment + type: string + produces: + - application/json + responses: + "200": + description: The updated domain block. + schema: + $ref: '#/definitions/domainPermission' + "400": + description: bad request + "401": + description: unauthorized + "403": + description: forbidden + "404": + description: not found + "406": + description: not acceptable + "500": + description: internal server error + security: + - OAuth2 Bearer: + - admin:write:domain_blocks + summary: Update a single domain block. + tags: + - admin /api/v1/admin/domain_keys_expire: post: consumes: diff --git a/internal/api/model/domain.go b/internal/api/model/domain.go index 9c868cfc0..dbb4a974c 100644 --- a/internal/api/model/domain.go +++ b/internal/api/model/domain.go @@ -34,7 +34,7 @@ type Domain struct { SilencedAt string `json:"silenced_at,omitempty"` // If the domain is blocked, what's the publicly-stated reason for the block. // example: they smell - PublicComment string `form:"public_comment" json:"public_comment,omitempty"` + PublicComment *string `form:"public_comment" json:"public_comment,omitempty"` } // DomainPermission represents a permission applied to one domain (explicit block/allow). @@ -48,10 +48,10 @@ type DomainPermission struct { ID string `json:"id,omitempty"` // Obfuscate the domain name when serving this domain permission entry publicly. // example: false - Obfuscate bool `json:"obfuscate,omitempty"` + Obfuscate *bool `json:"obfuscate,omitempty"` // Private comment for this permission entry, visible to this instance's admins only. // example: they are poopoo - PrivateComment string `json:"private_comment,omitempty"` + PrivateComment *string `json:"private_comment,omitempty"` // If applicable, the ID of the subscription that caused this domain permission entry to be created. // example: 01FBW25TF5J67JW3HFHZCSD23K SubscriptionID string `json:"subscription_id,omitempty"` diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go index c2906819b..14eb6942e 100644 --- a/internal/processing/admin/domainpermission.go +++ b/internal/processing/admin/domainpermission.go @@ -29,6 +29,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // DomainPermissionCreate creates an instance-level permission @@ -197,14 +198,14 @@ func (p *Processor) DomainPermissionsImport( } defer file.Close() - // Parse file as slice of domain blocks. - domainPerms := make([]*apimodel.DomainPermission, 0) - if err := json.NewDecoder(file).Decode(&domainPerms); err != nil { + // Parse file as slice of domain permissions. + apiDomainPerms := make([]*apimodel.DomainPermission, 0) + if err := json.NewDecoder(file).Decode(&apiDomainPerms); err != nil { err = gtserror.Newf("error parsing attachment as domain permissions: %w", err) return nil, gtserror.NewErrorBadRequest(err, err.Error()) } - count := len(domainPerms) + count := len(apiDomainPerms) if count == 0 { err = gtserror.New("error importing domain permissions: 0 entries provided") return nil, gtserror.NewErrorBadRequest(err, err.Error()) @@ -214,52 +215,97 @@ func (p *Processor) DomainPermissionsImport( // between successes and errors so that the caller can // try failed imports again if desired. multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count) - - for _, domainPerm := range domainPerms { - var ( - domain = domainPerm.Domain.Domain - obfuscate = domainPerm.Obfuscate - publicComment = domainPerm.PublicComment - privateComment = domainPerm.PrivateComment - subscriptionID = "" // No sub ID for imports. - errWithCode gtserror.WithCode + for _, apiDomainPerm := range apiDomainPerms { + multiStatusEntries = append( + multiStatusEntries, + p.importOrUpdateDomainPerm( + ctx, + permissionType, + account, + apiDomainPerm, + ), ) - - domainPerm, _, errWithCode = p.DomainPermissionCreate( - ctx, - permissionType, - account, - domain, - obfuscate, - publicComment, - privateComment, - subscriptionID, - ) - - var entry *apimodel.MultiStatusEntry - - if errWithCode != nil { - entry = &apimodel.MultiStatusEntry{ - // Use the failed domain entry as the resource value. - Resource: domain, - Message: errWithCode.Safe(), - Status: errWithCode.Code(), - } - } else { - entry = &apimodel.MultiStatusEntry{ - // Use successfully created API model domain block as the resource value. - Resource: domainPerm, - Message: http.StatusText(http.StatusOK), - Status: http.StatusOK, - } - } - - multiStatusEntries = append(multiStatusEntries, *entry) } return apimodel.NewMultiStatus(multiStatusEntries), nil } +func (p *Processor) importOrUpdateDomainPerm( + ctx context.Context, + permType gtsmodel.DomainPermissionType, + account *gtsmodel.Account, + apiDomainPerm *apimodel.DomainPermission, +) apimodel.MultiStatusEntry { + var ( + domain = apiDomainPerm.Domain.Domain + obfuscate = apiDomainPerm.Obfuscate + publicComment = apiDomainPerm.PublicComment + privateComment = apiDomainPerm.PrivateComment + subscriptionID = "" // No sub ID for imports. + ) + + // Check if this domain + // perm already exists. + var ( + domainPerm gtsmodel.DomainPermission + err error + ) + if permType == gtsmodel.DomainPermissionBlock { + domainPerm, err = p.state.DB.GetDomainBlock(ctx, domain) + } else { + domainPerm, err = p.state.DB.GetDomainAllow(ctx, domain) + } + + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // Real db error. + return apimodel.MultiStatusEntry{ + Resource: domain, + Message: "db error checking for existence of domain permission", + Status: http.StatusInternalServerError, + } + } + + var errWithCode gtserror.WithCode + if domainPerm != nil { + // Permission already exists, update it. + apiDomainPerm, errWithCode = p.DomainPermissionUpdate( + ctx, + permType, + domainPerm.GetID(), + obfuscate, + publicComment, + privateComment, + nil, + ) + } else { + // Permission didn't exist yet, create it. + apiDomainPerm, _, errWithCode = p.DomainPermissionCreate( + ctx, + permType, + account, + domain, + util.PtrOrZero(obfuscate), + util.PtrOrZero(publicComment), + util.PtrOrZero(privateComment), + subscriptionID, + ) + } + + if errWithCode != nil { + return apimodel.MultiStatusEntry{ + Resource: domain, + Message: errWithCode.Safe(), + Status: errWithCode.Code(), + } + } + + return apimodel.MultiStatusEntry{ + Resource: apiDomainPerm, + Message: http.StatusText(http.StatusOK), + Status: http.StatusOK, + } +} + // DomainPermissionsGet returns all existing domain // permissions of the requested type. If export is // true, the format will be suitable for writing out diff --git a/internal/processing/instance.go b/internal/processing/instance.go index 4cbbb742a..3848c4e87 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -108,7 +108,7 @@ func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, domains = append(domains, &apimodel.Domain{ Domain: d, SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt), - PublicComment: domainBlock.PublicComment, + PublicComment: &domainBlock.PublicComment, }) } } diff --git a/internal/subscriptions/domainperms.go b/internal/subscriptions/domainperms.go index 8ac758a67..6bc4e81fe 100644 --- a/internal/subscriptions/domainperms.go +++ b/internal/subscriptions/domainperms.go @@ -742,8 +742,8 @@ func permsFromJSON( } // Set remaining fields. - perm.SetPublicComment(apiPerm.PublicComment) - perm.SetObfuscate(&apiPerm.Obfuscate) + perm.SetPublicComment(util.PtrOrZero(apiPerm.PublicComment)) + perm.SetObfuscate(util.Ptr(util.PtrOrZero(apiPerm.Obfuscate))) // We're done. perms = append(perms, perm) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index b0f5d12fa..62a1ebc1e 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -2182,7 +2182,7 @@ func (c *Converter) DomainPermToAPIDomainPerm( domainPerm := &apimodel.DomainPermission{ Domain: apimodel.Domain{ Domain: domain, - PublicComment: d.GetPublicComment(), + PublicComment: util.Ptr(d.GetPublicComment()), }, } @@ -2193,8 +2193,8 @@ func (c *Converter) DomainPermToAPIDomainPerm( } domainPerm.ID = d.GetID() - domainPerm.Obfuscate = util.PtrOrZero(d.GetObfuscate()) - domainPerm.PrivateComment = d.GetPrivateComment() + domainPerm.Obfuscate = d.GetObfuscate() + domainPerm.PrivateComment = util.Ptr(d.GetPrivateComment()) domainPerm.SubscriptionID = d.GetSubscriptionID() domainPerm.CreatedBy = d.GetCreatedByAccountID() if createdAt := d.GetCreatedAt(); !createdAt.IsZero() { diff --git a/web/source/settings/lib/query/admin/domain-permissions/import.ts b/web/source/settings/lib/query/admin/domain-permissions/import.ts index cbcf44964..a83448a1f 100644 --- a/web/source/settings/lib/query/admin/domain-permissions/import.ts +++ b/web/source/settings/lib/query/admin/domain-permissions/import.ts @@ -40,39 +40,19 @@ function importEntriesProcessor(formData: ImportDomainPermsParams): (_entry: Dom // Override each obfuscate entry if necessary. if (formData.obfuscate !== undefined) { - const obfuscateEntry = (entry: DomainPerm) => { + processingFuncs.push((entry: DomainPerm) => { entry.obfuscate = formData.obfuscate; - }; - processingFuncs.push(obfuscateEntry); + }); } - // Check whether we need to append or replace - // private_comment and public_comment. + // Check whether we need to replace + // private_comment and/or public_comment. ["private_comment","public_comment"].forEach((commentType) => { - let text = formData.commentType?.trim(); - if (!text) { - return; - } - - switch(formData[`${commentType}_behavior`]) { - case "append": - const appendComment = (entry: DomainPerm) => { - if (entry.commentType == undefined) { - entry.commentType = text; - } else { - entry.commentType = [entry.commentType, text].join("\n"); - } - }; - - processingFuncs.push(appendComment); - break; - case "replace": - const replaceComment = (entry: DomainPerm) => { - entry.commentType = text; - }; - - processingFuncs.push(replaceComment); - break; + if (formData[`replace_${commentType}`]) { + const text = formData[commentType]?.trim(); + processingFuncs.push((entry: DomainPerm) => { + entry[commentType] = text; + }); } }); diff --git a/web/source/settings/lib/query/admin/domain-permissions/update.ts b/web/source/settings/lib/query/admin/domain-permissions/update.ts index cb657d09c..396c30d6e 100644 --- a/web/source/settings/lib/query/admin/domain-permissions/update.ts +++ b/web/source/settings/lib/query/admin/domain-permissions/update.ts @@ -57,9 +57,9 @@ const extended = gtsApi.injectEndpoints({ }), updateDomainBlock: build.mutation({ - query: (formData) => ({ + query: ({ id, ...formData}) => ({ method: "PUT", - url: `/api/v1/admin/domain_blocks/${formData.id}`, + url: `/api/v1/admin/domain_blocks/${id}`, asForm: true, body: formData, discardEmpty: false @@ -72,9 +72,9 @@ const extended = gtsApi.injectEndpoints({ }), updateDomainAllow: build.mutation({ - query: (formData) => ({ + query: ({ id, ...formData}) => ({ method: "PUT", - url: `/api/v1/admin/domain_allows/${formData.id}`, + url: `/api/v1/admin/domain_allows/${id}`, asForm: true, body: formData, discardEmpty: false diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts index c4560d79b..27c4b56c9 100644 --- a/web/source/settings/lib/types/domain-permission.ts +++ b/web/source/settings/lib/types/domain-permission.ts @@ -46,8 +46,8 @@ export interface DomainPerm { valid?: boolean; checked?: boolean; commentType?: string; - private_comment_behavior?: "append" | "replace"; - public_comment_behavior?: "append" | "replace"; + replace_private_comment?: boolean; + replace_public_comment?: boolean; } /** @@ -65,8 +65,8 @@ const domainPermStripOnImport: Set = new Set([ "valid", "checked", "commentType", - "private_comment_behavior", - "public_comment_behavior", + "replace_private_comment", + "replace_public_comment", ]); /** diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 6169cab78..c05072043 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -618,6 +618,15 @@ span.form-info { } } +section > div.domain-block, +section > div.domain-allow { + height: 100%; + + > a { + margin-top: auto; + } +} + .domain-permissions-list { p { margin-top: 0; @@ -976,32 +985,26 @@ button.tab-button { .domain-perm-import-list { .checkbox-list-wrapper { - overflow-x: auto; display: grid; gap: 1rem; } .checkbox-list { + overflow-x: auto; .header { + align-items: center; input[type="checkbox"] { - align-self: start; height: 1.5rem; } } .entry { - gap: 0; - width: 100%; - grid-template-columns: auto minmax(25ch, 2fr) minmax(40ch, 1fr); - grid-template-rows: auto 1fr; - - input[type="checkbox"] { - margin-right: 1rem; - } + grid-template-columns: auto max(50%, 14rem) 1fr; + column-gap: 1rem; + align-items: center; .domain-input { - margin-right: 0.5rem; display: grid; grid-template-columns: 1fr $fa-fw; gap: 0.5rem; @@ -1020,13 +1023,21 @@ button.tab-button { } p { - align-self: center; margin: 0; - grid-column: 4; - grid-row: 1 / span 2; } } } + + .set-comment-checkbox { + display: flex; + flex-direction: column; + gap: 0.25rem; + + padding: 0.5rem 1rem 1rem 1rem; + width: 100%; + border: 0.1rem solid var(--gray1); + border-radius: 0.1rem; + } } .import-export { diff --git a/web/source/settings/views/moderation/domain-permissions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/detail.tsx index 1486cdddf..e8ef487e3 100644 --- a/web/source/settings/views/moderation/domain-permissions/detail.tsx +++ b/web/source/settings/views/moderation/domain-permissions/detail.tsx @@ -52,6 +52,7 @@ import { PermType } from "../../../lib/types/perm"; import { useCapitalize } from "../../../lib/util"; import { formDomainValidator } from "../../../lib/util/formvalidators"; import UsernameLozenge from "../../../components/username-lozenge"; +import { FormSubmitEvent } from "../../../lib/form/types"; export default function DomainPermView() { const baseUrl = useBaseUrl(); @@ -161,7 +162,7 @@ function DomainPermDetails({ @@ -207,8 +208,8 @@ function CreateOrUpdateDomainPerm({ validator: formDomainValidator, }), obfuscate: useBoolInput("obfuscate", { source: perm }), - commentPrivate: useTextInput("private_comment", { source: perm }), - commentPublic: useTextInput("public_comment", { source: perm }) + privateComment: useTextInput("private_comment", { source: perm }), + publicComment: useTextInput("public_comment", { source: perm }) }; // Check which perm type we're meant to be handling @@ -248,12 +249,23 @@ function CreateOrUpdateDomainPerm({ // permType, and whether we're creating or updating. const [submit, submitResult] = useFormSubmit( form, - [ - createOrUpdateTrigger, - createOrUpdateResult, - ], + [ createOrUpdateTrigger, createOrUpdateResult ], { - changedOnly: isExistingPerm + changedOnly: isExistingPerm, + // If we're updating an existing perm, + // insert the perm ID into the mutation + // data before submitting. Otherwise just + // return the mutationData unmodified. + customizeMutationArgs: (mutationData) => { + if (isExistingPerm) { + return { + id: perm?.id, + ...mutationData, + }; + } else { + return mutationData; + } + }, }, ); @@ -261,7 +273,7 @@ function CreateOrUpdateDomainPerm({ const permTypeUpper = useCapitalize(permType); const [location, setLocation] = useLocation(); - function verifyUrlThenSubmit(e) { + function onSubmit(e: FormSubmitEvent) { // Adding a new domain permissions happens on a url like // "/settings/admin/domain-permissions/:permType/domain.com", // but if domain input changes, that doesn't match anymore @@ -277,7 +289,7 @@ function CreateOrUpdateDomainPerm({ } return ( -
+ { !isExistingPerm &&