summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--default.nix1
-rwxr-xr-xtools/bin/__dispatch.sh3
l---------tools/bin/blog_cli1
-rw-r--r--tools/blog_cli/README.md41
-rw-r--r--tools/blog_cli/default.nix8
-rw-r--r--tools/blog_cli/deps.nix111
-rw-r--r--tools/blog_cli/main.go185
7 files changed, 350 insertions, 0 deletions
diff --git a/default.nix b/default.nix
index e6e19d83aa00..e9288179c986 100644
--- a/default.nix
+++ b/default.nix
@@ -11,6 +11,7 @@ let
     # Local projects should be added here:
     tazjin = {
       blog = import ./services/tazblog { inherit pkgs; };
+      blog_cli = pkgs.callPackage ./tools/blog_cli {};
       gemma = import ./services/gemma { inherit pkgs; };
     };
 
diff --git a/tools/bin/__dispatch.sh b/tools/bin/__dispatch.sh
index bb61c4776969..7af832c65f00 100755
--- a/tools/bin/__dispatch.sh
+++ b/tools/bin/__dispatch.sh
@@ -22,6 +22,9 @@ case "${TARGET_TOOL}" in
   kontemplate)
     attr="kontemplate"
     ;;
+  blog_cli)
+    attr="tazjin.blog_cli"
+    ;;
   *)
     echo "The tool '${TARGET_TOOL}' is currently not installed in this repository."
     exit 1
diff --git a/tools/bin/blog_cli b/tools/bin/blog_cli
new file mode 120000
index 000000000000..8390ec9c9652
--- /dev/null
+++ b/tools/bin/blog_cli
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/blog_cli/README.md b/tools/blog_cli/README.md
new file mode 100644
index 000000000000..7afa0fe9207a
--- /dev/null
+++ b/tools/blog_cli/README.md
@@ -0,0 +1,41 @@
+tazblog CLI
+===========
+
+My blog stores its content in DNS, spread out over three types of `TXT` entries:
+
+* `TXT _posts.blog.tazj.in.`: A sorted list of posts, serialised as a JSON list of
+  strings (e.g. `["1486830338", "1476807384"]`)
+
+* `TXT _chunks.$postID.blog.tazj.in`: JSON chunks containing the blog post text
+
+* `TXT _meta.$postID.blog.tazj.in`: JSON blob with blog post metadata
+
+All JSON blobs are base64-encoded.
+
+This CLI tool helps to update those records.
+
+Each blog post data is a series of JSON-encoded structures which follow one of
+these formats:
+
+```
+struct metadata {
+    chunks: int
+    title: string
+    date: date
+}
+```
+
+Where `chunks` describes the number of chunks following this format:
+
+```
+struct chunk {
+    c: int
+    t: string
+}
+```
+
+Writing a blog post to DNS means taking its text and metadata, chunking it up
+and writing the chunks.
+
+Reading a blog post means retrieving all data, reading the metadata and then
+assembling the chunks in order.
diff --git a/tools/blog_cli/default.nix b/tools/blog_cli/default.nix
new file mode 100644
index 000000000000..c755d273a2b0
--- /dev/null
+++ b/tools/blog_cli/default.nix
@@ -0,0 +1,8 @@
+{ buildGoPackage }:
+
+buildGoPackage {
+  name = "blog_cli";
+  goPackagePath = "github.com/tazjin/personal/blog_cli";
+  src = ./.;
+  goDeps = ./deps.nix;
+}
diff --git a/tools/blog_cli/deps.nix b/tools/blog_cli/deps.nix
new file mode 100644
index 000000000000..4d75fcfdd835
--- /dev/null
+++ b/tools/blog_cli/deps.nix
@@ -0,0 +1,111 @@
+# This file was generated by https://github.com/kamilchm/go2nix v1.3.0
+[
+  {
+    goPackagePath = "cloud.google.com/go";
+    fetch = {
+      type = "git";
+      url = "https://code.googlesource.com/gocloud";
+      rev = "76e973f7c1e722b4859698ace0daed4e7eccdc60";
+      sha256 = "0m3ncaz0br67zmzsi76wwk6c00rqr8r7kiqgirnkcd1iwlql0qdr";
+    };
+  }
+  {
+    goPackagePath = "github.com/golang/protobuf";
+    fetch = {
+      type = "git";
+      url = "https://github.com/golang/protobuf";
+      rev = "4c88cc3f1a34ffade77b79abc53335d1e511f25b";
+      sha256 = "0chbdc4q55z7myiwnbvhryc5ihf6cxh8p4w3c1imy2gyzjn9sf4r";
+    };
+  }
+  {
+    goPackagePath = "github.com/googleapis/gax-go";
+    fetch = {
+      type = "git";
+      url = "https://github.com/googleapis/gax-go";
+      rev = "bd5b16380fd03dc758d11cef74ba2e3bc8b0e8c2";
+      sha256 = "1lxawwngv6miaqd25s3ba0didfzylbwisd2nz7r4gmbmin6jsjrx";
+    };
+  }
+  {
+    goPackagePath = "github.com/hashicorp/golang-lru";
+    fetch = {
+      type = "git";
+      url = "https://github.com/hashicorp/golang-lru";
+      rev = "7f827b33c0f158ec5dfbba01bb0b14a4541fd81d";
+      sha256 = "1p2igd58xkm8yaj2c2wxiplkf2hj6kxwrg6ss7mx61s5rd71v5xb";
+    };
+  }
+  {
+    goPackagePath = "go.opencensus.io";
+    fetch = {
+      type = "git";
+      url = "https://github.com/census-instrumentation/opencensus-go";
+      rev = "b4a14686f0a98096416fe1b4cb848e384fb2b22b";
+      sha256 = "1aidyp301v5ngwsnnc8v1s09vvbsnch1jc4vd615f7qv77r9s7dn";
+    };
+  }
+  {
+    goPackagePath = "golang.org/x/net";
+    fetch = {
+      type = "git";
+      url = "https://go.googlesource.com/net";
+      rev = "74dc4d7220e7acc4e100824340f3e66577424772";
+      sha256 = "0563yswwqknxx2gsvl0qikn0lmwalilbng8i12iw4d3v40n23s0l";
+    };
+  }
+  {
+    goPackagePath = "golang.org/x/oauth2";
+    fetch = {
+      type = "git";
+      url = "https://go.googlesource.com/oauth2";
+      rev = "0f29369cfe4552d0e4bcddc57cc75f4d7e672a33";
+      sha256 = "06jwpvx0x2gjn2y959drbcir5kd7vg87k0r1216abk6rrdzzrzi2";
+    };
+  }
+  {
+    goPackagePath = "golang.org/x/sys";
+    fetch = {
+      type = "git";
+      url = "https://go.googlesource.com/sys";
+      rev = "fde4db37ae7ad8191b03d30d27f258b5291ae4e3";
+      sha256 = "16k4w4pzziq1kln18k5fg01qgk4hpzb5xsm7175kaky6d6gwyhg3";
+    };
+  }
+  {
+    goPackagePath = "golang.org/x/text";
+    fetch = {
+      type = "git";
+      url = "https://go.googlesource.com/text";
+      rev = "342b2e1fbaa52c93f31447ad2c6abc048c63e475";
+      sha256 = "0flv9idw0jm5nm8lx25xqanbkqgfiym6619w575p7nrdh0riqwqh";
+    };
+  }
+  {
+    goPackagePath = "google.golang.org/api";
+    fetch = {
+      type = "git";
+      url = "https://code.googlesource.com/google-api-go-client";
+      rev = "573115b31dcba90b19c25508412495d63f86e804";
+      sha256 = "0vs0783azc1ja5l2ijs9d2szysrs4gpg8blvpkjbslyr07ca0dha";
+    };
+  }
+  {
+    goPackagePath = "google.golang.org/genproto";
+    fetch = {
+      type = "git";
+      url = "https://github.com/google/go-genproto";
+      rev = "55e96fffbd486c27fc0d5b4468c497d0de3f2727";
+      sha256 = "1nnz3rb3ppj9dvalyf7xz6cpndlvi4ra5wm5hlv34nn498zpqc70";
+    };
+  }
+  {
+    goPackagePath = "google.golang.org/grpc";
+    fetch = {
+      type = "git";
+      url = "https://github.com/grpc/grpc-go";
+      rev = "451cf373a706e089ca34abd9ea14cbc20b34c1fc";
+      sha256 = "11sza0g00v8xlmm3sah51l93m2993g4lw89df76z9d9lyk094dl8";
+    };
+  }
+]
diff --git a/tools/blog_cli/main.go b/tools/blog_cli/main.go
new file mode 100644
index 000000000000..f376f256ea20
--- /dev/null
+++ b/tools/blog_cli/main.go
@@ -0,0 +1,185 @@
+// The tazblog CLI implements updating my blog records in DNS, see the
+// README in this folder for details.
+//
+// The post input format is a file with the title on one line,
+// followed by the date on a line, followed by an empty line, followed
+// by the post text.
+package main
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"time"
+
+	"google.golang.org/api/dns/v1"
+)
+
+var (
+	project = flag.String("project", "tazjins-infrastructure", "Target GCP project")
+	zone    = flag.String("zone", "blog-tazj-in", "Target Cloud DNS zone")
+	title   = flag.String("title", "", "Title of the blog post")
+	infile  = flag.String("text", "", "Text file containing the blog post")
+	id      = flag.String("id", "", "Post ID - will be generated if unset")
+)
+
+// Number of runes to include in a single chunk. If any chunks exceed
+// the limit of what can be encoded, the chunk size is reduced and we
+// try again.
+var chunkSize = 200
+
+type metadata struct {
+	Chunks int       `json:"chunks"`
+	Title  string    `json:"title"`
+	Date   time.Time `json:"date"`
+}
+
+type chunk struct {
+	Chunk int    `json:"c"`
+	Text  string `json:"t"`
+}
+
+type post struct {
+	ID     string
+	Meta   metadata
+	Chunks []string
+}
+
+func (p *post) writeToDNS() error {
+	metaRecord := dns.ResourceRecordSet{
+		Name: fmt.Sprintf("_meta.%s.blog.tazj.in.", p.ID),
+		Type: "TXT",
+		Ttl:  1200,
+		Rrdatas: []string{
+			encodeJSON(p.Meta),
+		},
+	}
+
+	chunkRecord := dns.ResourceRecordSet{
+		Name:    fmt.Sprintf("_chunks.%s.blog.tazj.in.", p.ID),
+		Type:    "TXT",
+		Ttl:     1200,
+		Rrdatas: p.Chunks,
+	}
+
+	ctx := context.Background()
+	dnsSvc, err := dns.NewService(ctx)
+	if err != nil {
+		return err
+	}
+
+	change := dns.Change{
+		Additions: []*dns.ResourceRecordSet{&metaRecord, &chunkRecord},
+	}
+
+	_, err = dnsSvc.Changes.Create(*project, *zone, &change).Do()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Encode given value as JSON and base64-encode it.
+func encodeJSON(v interface{}) string {
+	outer, _ := json.Marshal(v)
+	return base64.RawStdEncoding.EncodeToString(outer)
+}
+
+// Encode a chunk and check whether it is too large
+func encodeChunk(c chunk) (string, bool) {
+	tooLarge := false
+
+	j := encodeJSON(c)
+
+	if len(j) >= 255 {
+		tooLarge = true
+	}
+
+	return j, tooLarge
+}
+
+func createPost(id, title, text string, date time.Time) post {
+	runes := []rune(text)
+	n := 0
+	tooLarge := false
+
+	var chunks []string
+
+	for chunkSize < len(runes) {
+		n++
+
+		c, l := encodeChunk(chunk{
+			Chunk: n,
+			Text:  string(runes[0:chunkSize:chunkSize]),
+		})
+
+		tooLarge = tooLarge || l
+		chunks = append(chunks, c)
+		runes = runes[chunkSize:]
+	}
+
+	if len(runes) > 0 {
+		n++
+
+		c, l := encodeChunk(chunk{
+			Chunk: n,
+			Text:  string(runes),
+		})
+
+		tooLarge = tooLarge || l
+		chunks = append(chunks, c)
+	}
+
+	if tooLarge {
+		log.Println("Too large at chunk size", chunkSize)
+		chunkSize -= 5
+		return createPost(id, title, text, date)
+	}
+
+	return post{
+		ID: id,
+		Meta: metadata{
+			Chunks: n,
+			Title:  title,
+			Date:   date,
+		},
+		Chunks: chunks,
+	}
+}
+
+func main() {
+	flag.Parse()
+
+	if *title == "" {
+		log.Fatalln("Post title must be set (-title)")
+	}
+
+	if *infile == "" {
+		log.Fatalln("Post text file must be set (-text)")
+	}
+
+	if *id == "" {
+		log.Fatalln("Post ID must be set (-id)")
+	}
+
+	t, err := ioutil.ReadFile(*infile)
+	if err != nil {
+		log.Fatalln("Failed to read post:", err)
+	}
+
+	post := createPost(*id, *title, string(t), time.Now())
+
+	log.Println("Writing post to DNS ...")
+	err = post.writeToDNS()
+
+	if err != nil {
+		log.Fatalln("Failed to write post:", err)
+	}
+
+	log.Println("Successfully wrote entries")
+}