From 0bf52b3fac206bf4b157a440fd028b02fb842cf8 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Sun, 6 Apr 2025 10:48:15 -0400 Subject: [PATCH] queue: add types for ingest -> indexer --- queue/message.go | 13 +++++++++ queue/sender.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ queue/sender_test.go | 39 +++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 queue/message.go create mode 100644 queue/sender.go create mode 100644 queue/sender_test.go diff --git a/queue/message.go b/queue/message.go new file mode 100644 index 0000000..39f43e6 --- /dev/null +++ b/queue/message.go @@ -0,0 +1,13 @@ +package queue + +// Message is the shape of messages handed from ingest to indexers. +type Message struct { + // ID is the message ID. + ID string `json:"id"` + // Channel is an identifier for the chat channel where the message was sent. + Channel string `json:"ch"` + // Sender is an obfuscated identifier for the sending user. + Sender sender `json:"u"` + // Text is the message content. + Text string `json:"t"` +} diff --git a/queue/sender.go b/queue/sender.go new file mode 100644 index 0000000..7aed4a7 --- /dev/null +++ b/queue/sender.go @@ -0,0 +1,64 @@ +package queue + +import ( + "crypto/sha3" + "encoding/base64" + "fmt" + "io" + + "github.com/go-json-experiment/json/jsontext" +) + +// sender is an obfuscated sender ID. +// +// It acts approximately as a quotient type: since it is unexported, the only +// way to obtain values of type sender without unusual effort is to use the +// constructors provided by this package. +// This helps to prevent misuse that exposes actual user IDs. +type sender [28]byte + +// Sender constructs an obfuscated sender ID. +// The salt must be shared by all ingesters. +// While the salt may be of any length, a length between 16 and 64 bytes is suggested. +func Sender(salt []byte, channel, user string) sender { + b := make([]byte, 128) + b = append(b, channel...) + b = append(b, 0) + b = append(b, salt...) + b = append(b, 0) + b = append(b, user...) + return sender(sha3.Sum224(b)) +} + +func (s sender) String() string { + b := base64.RawStdEncoding.AppendEncode(make([]byte, 0, 38), s[:]) + return string(b) +} + +func (s sender) MarshalJSONTo(e *jsontext.Encoder) error { + return e.WriteToken(jsontext.String(s.String())) +} + +func (s *sender) UnmarshalJSONFrom(d *jsontext.Decoder) error { + t, err := d.ReadToken() + switch err { + case nil: // do nothing + case io.EOF: + return io.ErrUnexpectedEOF + default: + return err + } + e := t.String() + if t.Kind() != '"' { + return fmt.Errorf("invalid token for sender %q", e) + } + if len(e) != 38 { + return fmt.Errorf("invalid string for sender %q", e) + } + b, err := base64.RawStdEncoding.AppendDecode(make([]byte, 0, 28), []byte(e)) + if err != nil { + return err + } + *s = sender(b) + return nil +} diff --git a/queue/sender_test.go b/queue/sender_test.go new file mode 100644 index 0000000..300a50c --- /dev/null +++ b/queue/sender_test.go @@ -0,0 +1,39 @@ +package queue + +import ( + "testing" + + "github.com/go-json-experiment/json" +) + +func TestSenderUnique(t *testing.T) { + // Salt, channel, and user should all produce differences in the sender ID. + base := Sender([]byte("nijika"), "kessoku", "bocchi") + salt := Sender([]byte("kita"), "kessoku", "bocchi") + channel := Sender([]byte("nijika"), "sickhack", "bocchi") + user := Sender([]byte("nijika"), "kessoku", "ryō") + m := map[sender]struct{}{ + base: {}, + salt: {}, + channel: {}, + user: {}, + } + if len(m) != 4 { + t.Errorf("collision:\nbase: %v\nsalt: %v\nchan: %v\nuser: %v", base, salt, channel, user) + } +} + +func TestSenderRoundTrip(t *testing.T) { + want := Sender([]byte("nijika"), "kessoku", "bocchi") + b, err := json.Marshal(want) + if err != nil { + t.Errorf("encode failed: %v", err) + } + var got sender + if err := json.Unmarshal(b, &got); err != nil { + t.Errorf("decode failed: %v", err) + } + if want != got { + t.Errorf("round-trip failed:\nwant %v\ngot %v", want, got) + } +}