Received: from mail.netlandish.com (mail.netlandish.com [174.136.98.166]) by code.netlandish.com (Postfix) with ESMTP id BAD69E1 for <~netlandish/links-dev@lists.code.netlandish.com>; Sun, 17 Aug 2025 15:02:31 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.222.46; helo=mail-ua1-f46.google.com; envelope-from=peter@netlandish.com; receiver= Authentication-Results: mail.netlandish.com; dkim=pass (1024-bit key; unprotected) header.d=netlandish.com header.i=@netlandish.com header.b=azA/qI5a Received: from mail-ua1-f46.google.com (mail-ua1-f46.google.com [209.85.222.46]) by mail.netlandish.com (Postfix) with ESMTP id 496C11D643F for <~netlandish/links-dev@lists.code.netlandish.com>; Sun, 17 Aug 2025 15:03:24 +0000 (UTC) Received: by mail-ua1-f46.google.com with SMTP id a1e0cc1a2514c-890190bee8bso2101383241.2 for <~netlandish/links-dev@lists.code.netlandish.com>; Sun, 17 Aug 2025 08:03:23 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=netlandish.com; s=google; t=1755443003; x=1756047803; darn=lists.code.netlandish.com; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=o6XRdR11lmG4h+LsO8jsa/6JzEim2727523Tdr1BcLU=; b=azA/qI5a4kXch9TTnRLT71iA8g5FW8UJyLgLMqO/EuM7d+A0lDsVsKDdXcfOO4nEkX 7NARr6OH5oYuXozOUUJgGZl4PWcOsR8rC0DOlUIQoEwNhIA4eF5c9Qo6vciMannyBKWR MYtwPPhCGc1STN0JC+vt8+GGpPFneXnUETPdY= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1755443003; x=1756047803; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=o6XRdR11lmG4h+LsO8jsa/6JzEim2727523Tdr1BcLU=; b=AuJ+XGOeDw/1zVPMAMtzykDwHrm0fyKiQIHjd8GTviiBMvixdcYs5KFVJD9R48D8cW JttMGMeRrJ+ugvUVSeqzKwDTgXyRmoBU3h/iLdEpiWkMKqkC5A5ZeycZZI/Myyw1nbd0 BfUhn0bjvidB1d1HjOx2DyRxlabwouP9LMkLVa7ZX2FrVjOMn95TZ07TtvtmlEgGLopv 7fH9yYIBxgv00IGfdXjw7P0rqVB2fuztMidW6ioJGui0TGKVmOQUhVAu5fRw2l8F+EUp QKPuseNgM1lHUtT97il2bm1FMPK0RQHiNP7Dyj0eD051V8J0Q2en+YvZEbmV/tl9EXFK lFoA== X-Gm-Message-State: AOJu0YwC+T883qonCGOWERKjoZUFBSJTLsTs8qz/f2IAfWA64CxzsQVe xiXUFHFFJ/3tiiJwXWN4cMGAp2ncr4Z5BKfYcFdEFJL8G7eACfqjQDgaKLNCCTYp6MWvX+de17D yzOwrJss= X-Gm-Gg: ASbGnctJUyy+o0eS0Dyz9201aFlX40vbln2ZaX/JUkKl1LYyUVEzd/wNTRVNcMj5AeN 8owAhnKemeIvNb/cpfatmNiZc4REmM2RQvUkrYhUQEo6pWBl64P+25D8xsIMYiDF+f0S806mhRl s4jLBrMOYT+6Hrtd1Y2tlZOtHAsNbuPxKXIfnNqi9/1CCslrMFQ1FoLeNi9xgkHcGM+Qehx3ycH R/T47cpp1DSBjMsm5EyIHfV6eZBNqN9hvEI9i8XDYL1FWVf/CEMCAkLnY0ypQmzaExchhFTHZ55 6J4QyqG1tOGiNReHq0zBqTRHyfrZt+13nj9OvqlfhAZI3Jd/Em9n86zVgEa9ubeTakm+6VbVJpx lVGPb9KB64LZ51rjjVCaAl8xk X-Google-Smtp-Source: AGHT+IFgXbm80lUjy+Kr9y2lmDkJK/OQuprbm0Vt4m5bp7q0lwaMUNA/6ByFy+9oEZbpqClh3Q2L2A== X-Received: by 2002:a05:6102:4411:b0:4e6:a33d:9925 with SMTP id ada2fe7eead31-514c8b678a0mr2236611137.5.1755443003018; Sun, 17 Aug 2025 08:03:23 -0700 (PDT) Received: from localhost ([2803:2d60:1602:61cd:c4dd:3e82:e783:1551]) by smtp.gmail.com with UTF8SMTPSA id ada2fe7eead31-5127f226b46sm1395373137.12.2025.08.17.08.03.21 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 17 Aug 2025 08:03:22 -0700 (PDT) From: Peter Sanchez To: ~netlandish/links-dev@lists.code.netlandish.com Cc: Peter Sanchez Subject: [PATCH links] Add tags delete and rename to Pinboard API bridge. Should now have complete coverage for relevant operations. Date: Sun, 17 Aug 2025 09:03:12 -0600 Message-ID: <20250817150319.24975-1-peter@netlandish.com> X-Mailer: git-send-email 2.49.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Changelog-added: Added ability to rename and delete tags via the Pinboard API bridge. --- pinboard/input.go | 11 ++++ pinboard/routes.go | 97 ++++++++++++++++++++++++++++++- pinboard/routes_test.go | 124 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 226 insertions(+), 6 deletions(-) diff --git a/pinboard/input.go b/pinboard/input.go index 1a72e47..baac2a6 100644 --- a/pinboard/input.go +++ b/pinboard/input.go @@ -44,4 +44,15 @@ type AllPostInput struct { Fromdt string `query:"fromdt"` Todt string `query:"todt"` Meta string `query:"meta"` +} + +// DeleteTagInput represents the input for /v1/tags/delete +type DeleteTagInput struct { + Tag string `query:"tag" validate:"required"` +} + +// RenameTagInput represents the input for /v1/tags/rename +type RenameTagInput struct { + Old string `query:"old" validate:"required"` + New string `query:"new" validate:"required"` } \ No newline at end of file diff --git a/pinboard/routes.go b/pinboard/routes.go index 5393056..19c838a 100644 --- a/pinboard/routes.go +++ b/pinboard/routes.go @@ -847,10 +847,103 @@ func (s *Service) TagsGet(c echo.Context) error { // TagsRename handles /v1/tags/rename func (s *Service) TagsRename(c echo.Context) error { - return formatError(c, "Tag renaming is unsupported") + var input RenameTagInput + if err := c.Bind(&input); err != nil { + return formatError(c, "Invalid input parameters") + } + + if err := c.Validate(input); err != nil { + return formatError(c, err.Error()) + } + + org, err := s.getUserOrg(c) + if err != nil { + return formatError(c, "Failed to get organization") + } + + type GraphQLResponse struct { + RenameTag struct { + Success bool `json:"success"` + Message string `json:"message"` + } `json:"renameTag"` + } + + var result GraphQLResponse + op := gqlclient.NewOperation( + `mutation RenameTag($orgSlug: String!, $service: DomainService!, $tag: String!, $newTag: String!) { + renameTag(input: {orgSlug: $orgSlug, service: $service, tag: $tag, newTag: $newTag}) { + success + message + } + }`) + + op.Var("orgSlug", org.Slug) + op.Var("service", "LINKS") + op.Var("tag", input.Old) + op.Var("newTag", input.New) + + err = links.Execute(c.Request().Context(), op, &result) + if err != nil { + if graphError, ok := err.(*gqlclient.Error); ok { + return formatError(c, graphError.Error()) + } + return formatError(c, err.Error()) + } + + if !result.RenameTag.Success { + return formatError(c, "Failed to rename tag") + } + + return formatSuccess(c) } // TagsDelete handles /v1/tags/delete func (s *Service) TagsDelete(c echo.Context) error { - return formatError(c, "Tag deletion is unsupported") + var input DeleteTagInput + if err := c.Bind(&input); err != nil { + return formatError(c, "Invalid input parameters") + } + + if err := c.Validate(input); err != nil { + return formatError(c, err.Error()) + } + + org, err := s.getUserOrg(c) + if err != nil { + return formatError(c, "Failed to get organization") + } + + type GraphQLResponse struct { + DeleteTag struct { + Success bool `json:"success"` + ObjectID string `json:"objectId"` + } `json:"deleteTag"` + } + + var result GraphQLResponse + op := gqlclient.NewOperation( + `mutation DeleteTag($orgSlug: String!, $service: DomainService!, $tag: String!) { + deleteTag(input: {orgSlug: $orgSlug, service: $service, tag: $tag}) { + success + objectId + } + }`) + + op.Var("orgSlug", org.Slug) + op.Var("service", "LINKS") + op.Var("tag", input.Tag) + + err = links.Execute(c.Request().Context(), op, &result) + if err != nil { + if graphError, ok := err.(*gqlclient.Error); ok { + return formatError(c, graphError.Error()) + } + return formatError(c, err.Error()) + } + + if !result.DeleteTag.Success { + return formatError(c, "Failed to delete tag") + } + + return formatSuccess(c) } diff --git a/pinboard/routes_test.go b/pinboard/routes_test.go index 07bea5b..44831df 100644 --- a/pinboard/routes_test.go +++ b/pinboard/routes_test.go @@ -1453,7 +1453,20 @@ func TestTagEndpoints(t *testing.T) { c.False(strings.Contains(body, "