diff --git a/README.md b/README.md index 6a02388..d8a8bf3 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,7 @@ the application. | `POST` | `/fmsg/:id/send` | Send a message | | `POST` | `/fmsg/:id/read` | Mark a message as read | | `POST` | `/fmsg/:id/add-to` | Add recipients | +| `POST` | `/fmsg/:id/react` | Set or clear an emoji reaction (FMSG-005) | | `GET` | `/fmsg/:id/data` | Download message data | | `GET` | `/fmsg/:id/thread` | Render direct ancestry as plain text | | `GET` | `/fmsg/:id/thread/messages` | Load direct ancestry as structured JSON | @@ -424,7 +425,10 @@ event types can be added without breaking clients: | `type` | `data` | Sent when | | ---------- | ------ | --------- | -| `new_msg` | A message object, same shape as an item in the `GET /fmsg` list response (includes `id`). | A new message arrives for the authenticated user. | +| `new_msg` | A message object, same shape as an item in the `GET /fmsg` list response (includes `id`). | A new message arrives for the authenticated user. Not sent for reactions. | +| `delivered` | The refreshed message object. | A recipient's delivery state changed on a message the user sent. | +| `recipients_added` | The refreshed message object. | An add-to batch was recorded on a message the user participates in. | +| `reaction` | The refreshed **subject** message object, with its `reactions` up to date. | A reaction (FMSG-005) on a message the user participates in arrives. The reaction message itself is not pushed as `new_msg` and triggers no Web Push. | A client only ever receives events for messages it is a participant on. The server sends periodic WebSocket pings; clients should respond with pongs (most @@ -493,6 +497,7 @@ Add-to recipients are not part of this body — they are added later via `POST / | `size` | `int` | yes | Data size in bytes | | `important` | `bool` | no | Mark message as important | | `no_reply` | `bool` | no | Indicate replies will be discarded | +| `terminal` | `bool` | no | Mark the message terminal: a leaf nobody can reply to or add recipients to (fmsg Specification v0.6.0 flag bit 6, enforced by every host) | | `data` | `string` | no | Message body content | **Response:** `201 Created` with `{"id": }`. @@ -501,8 +506,9 @@ Add-to recipients are not part of this body — they are added later via `POST / | Status | Condition | | ------ | --------- | -| `400` | Missing/invalid fields, empty `to`, or `topic` set together with `pid` | +| `400` | Missing/invalid fields, empty `to`, `topic` set together with `pid`, or `pid` not found | | `403` | `from` does not match authenticated user | +| `409` | `pid` references a terminal message, which cannot be replied to | ### GET `/fmsg/:id` @@ -519,6 +525,7 @@ Retrieves a single message by ID. The authenticated identity must be a participa "important": false, "no_reply": false, "deflate": false, + "terminal": false, "pid": null, "from": "@alice@example.com", "to": ["@bob@example.com"], @@ -544,10 +551,28 @@ Retrieves a single message by ID. The authenticated identity must be a participa "short_text": "hello world", "read": false, "time_read": null, - "attachments": [] + "attachments": [], + "reaction": null, + "reactions": [ + { "emoji": "👍", "from": ["@bob@example.com", "@carol@example.com"] } + ] } ``` +`terminal` is the fmsg _terminal_ flag: the message is a leaf of its thread +and no host will accept a reply to it or an add-to on it. + +`reaction` and `reactions` implement +[FMSG-005 Reactions](https://github.com/markmnl/fmsg/blob/main/standards/fmsg-005-reactions.md). +A reaction is an ordinary message recognised by its shape — a reply with +`no_reply` and `terminal` set, type `text/plain;charset=UTF-8`, no attachments, +and a body that is a single emoji (or empty to clear). `reaction` is the emoji +such a message carries (`""` for a clearing reaction) and `null` for every +other message, so clients can hide reaction messages from message lists. +`reactions` lists the effective reactions on this message: each participant's +latest reaction, grouped by emoji in order of first reaction, with the +reactors of each. It is `[]` when there are none. + `add_to` is an array of add-to batches, one per `POST /fmsg/:id/add-to` call. Each batch has a stable `batch_id` (unique within the database and referenced with the message ID), records who added the recipients (`add_to_from`), the @@ -590,9 +615,10 @@ Updates a draft message. Only the owner (`from`) may update, and the message mus | Status | Condition | | ------ | --------- | -| `400` | Invalid fields, or `topic` set together with `pid` | +| `400` | Invalid fields, `topic` set together with `pid`, or `pid` not found | | `403` | Not the owner, or message already sent | | `404` | Message not found | +| `409` | `pid` references a terminal message, which cannot be replied to | ### DELETE `/fmsg/:id` @@ -680,6 +706,41 @@ New addresses must be distinct among themselves (case-insensitive). | `400` | Empty `add_to` or duplicate addresses | | `403` | Authenticated user is not an existing participant (sender or `to` recipient) | | `404` | Message not found | +| `409` | Message is terminal; recipients cannot be added to it | + +### POST `/fmsg/:id/react` + +Sets or clears the authenticated identity's reaction on a message, per +[FMSG-005 Reactions](https://github.com/markmnl/fmsg/blob/main/standards/fmsg-005-reactions.md). +The identity must be a participant of the message — the sender or a recipient +in `to` or `add_to` — and the message must be sent and not terminal. + +The reaction is sent immediately as a reaction message: a reply to `:id` with +`no_reply` and `terminal` set, type `text/plain;charset=UTF-8`, body the emoji, +addressed to every other participant of the message. Delivery then proceeds +exactly as for any sent message. A reaction message cannot be replied to, +reacted to, or have recipients added. + +**Request body (JSON):** + +| Field | Type | Required | Description | +| ------- | -------- | -------- | ----------- | +| `emoji` | `string` | no | A single emoji (a Unicode `RGI_Emoji` sequence, or any single well-formed emoji sequence). `""` or `null` clears the caller's reaction. | + +Setting the same reaction the caller already has is idempotent: nothing is +sent and the existing reaction message is returned with `200 OK`. Clearing when +the caller has no reaction returns `200 OK` with `null` values. + +**Response:** `201 Created` with the reaction message's `{"id": , "time": }`. + +**Errors:** + +| Status | Condition | +| ------ | --------- | +| `400` | `emoji` is not a single emoji | +| `403` | Authenticated identity is not a participant | +| `404` | Message not found | +| `409` | Message is a draft, is terminal, has no other participants, or a remote recipient host cannot accept the reaction because it does not hold the message | ### GET `/fmsg/:id/data` diff --git a/cmd/fmsg-webapi/main.go b/cmd/fmsg-webapi/main.go index a614c18..48512ca 100644 --- a/cmd/fmsg-webapi/main.go +++ b/cmd/fmsg-webapi/main.go @@ -192,6 +192,7 @@ func main() { fmsg.POST("/:id/send", msgHandler.Send) fmsg.POST("/:id/read", msgHandler.MarkRead) fmsg.POST("/:id/add-to", msgHandler.AddRecipients) + fmsg.POST("/:id/react", msgHandler.React) fmsg.GET("/:id/data", msgHandler.DownloadData) fmsg.GET("/:id/thread", msgHandler.ThreadText) fmsg.GET("/:id/thread/messages", msgHandler.ThreadMessages) diff --git a/internal/emoji/emoji.go b/internal/emoji/emoji.go new file mode 100644 index 0000000..78110cf --- /dev/null +++ b/internal/emoji/emoji.go @@ -0,0 +1,124 @@ +// Package emoji recognises single emoji for FMSG-005 reactions. +// +// A reaction's data must be exactly one emoji. [IsRGI] tests membership of the +// Unicode RGI_Emoji set (UTS #51), the set senders are required to use. +// [IsPossible] accepts the wider UTS #51 possible_emoji grammar, restricted to +// sequences that present as emoji, so a reaction using a sequence newer than +// the compiled tables, or a minimally-qualified one, is still recognised as a +// reaction rather than demoted to an ordinary message. +// +// Run `go run ./internal/emoji/gen` from the repository root to refresh the +// tables from unicode.org. +package emoji + +import ( + "unicode" + "unicode/utf8" +) + +const ( + zwj = 0x200D + vs16 = 0xFE0F + keycap = 0x20E3 + tagBegin = 0xE0020 + tagEnd = 0xE007E + tagTerm = 0xE007F + riFirst = 0x1F1E6 + riLast = 0x1F1FF + blackFlag = 0x1F3F4 + maxReaction = 64 // bytes; FMSG-005 size bound + maxSequenceR = 24 // runes; generous bound for ZWJ sequences +) + +// IsRGI reports whether s is exactly one fully-qualified RGI emoji sequence. +func IsRGI(s string) bool { + _, ok := rgi[s] + return ok +} + +// IsPossible reports whether s is exactly one emoji under the UTS #51 +// possible_emoji grammar, requiring emoji presentation: a text-default +// character such as a digit counts only with U+FE0F, a keycap or a modifier. +// +// possible_emoji := flag_sequence | zwj_element (ZWJ zwj_element)* +// flag_sequence := RI RI +// zwj_element := Emoji emoji_modification? +// emoji_modification := Emoji_Modifier | FE0F 20E3? | tag_modifier +// tag_modifier := [E0020-E007E]+ E007F +func IsPossible(s string) bool { + if s == "" || len(s) > maxReaction || !utf8.ValidString(s) { + return false + } + runes := []rune(s) + if len(runes) > maxSequenceR { + return false + } + if len(runes) == 2 && isRI(runes[0]) && isRI(runes[1]) { + return true + } + i := 0 + for { + n, ok := zwjElement(runes[i:]) + if !ok { + return false + } + i += n + if i == len(runes) { + return true + } + if runes[i] != zwj { + return false + } + i++ + if i == len(runes) { + return false + } + } +} + +// IsSingle reports whether s is one emoji by either test. +func IsSingle(s string) bool { + return IsRGI(s) || IsPossible(s) +} + +func isRI(r rune) bool { return r >= riFirst && r <= riLast } + +// zwjElement consumes one zwj_element from the front of runes, returning its +// length. The element must present as emoji. +func zwjElement(runes []rune) (int, bool) { + if len(runes) == 0 || !unicode.Is(propEmoji, runes[0]) || isRI(runes[0]) { + return 0, false + } + base := runes[0] + presents := unicode.Is(propEmojiPresentation, base) + i := 1 + if i < len(runes) { + switch r := runes[i]; { + case unicode.Is(propEmojiModifier, r): + if !unicode.Is(propEmojiModifierBase, base) { + return 0, false + } + i++ + presents = true + case r == vs16: + i++ + presents = true + if i < len(runes) && runes[i] == keycap { + i++ + } + case r >= tagBegin && r <= tagEnd: + if base != blackFlag { + return 0, false + } + for i < len(runes) && runes[i] >= tagBegin && runes[i] <= tagEnd { + i++ + } + if i >= len(runes) || runes[i] != tagTerm { + return 0, false + } + i++ + presents = true + } + } + return i, presents +} diff --git a/internal/emoji/emoji_test.go b/internal/emoji/emoji_test.go new file mode 100644 index 0000000..e4b8950 --- /dev/null +++ b/internal/emoji/emoji_test.go @@ -0,0 +1,78 @@ +package emoji + +import "testing" + +func TestIsRGI(t *testing.T) { + for _, s := range []string{ + "👍", + "❤️", + "👍🏽", + "🇳🇿", + "👨‍👩‍👧", + "#️⃣", + "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + } { + if !IsRGI(s) { + t.Errorf("IsRGI(%q) = false, want true", s) + } + } + for _, s := range []string{ + "", + "a", + "👍👍", + "👍 ", + "❤", // unqualified: U+2764 without VS16 + "1", + } { + if IsRGI(s) { + t.Errorf("IsRGI(%q) = true, want false", s) + } + } +} + +func TestIsPossible(t *testing.T) { + for _, s := range []string{ + "👍", + "❤️", + "👍🏽", + "🇳🇿", + "👨‍👩‍👧", + "#️⃣", + "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "🧑‍🦯", // any well-formed ZWJ sequence + } { + if !IsPossible(s) { + t.Errorf("IsPossible(%q) = false, want true", s) + } + } + for _, s := range []string{ + "", + "a", + "1", // text-default without VS16 + "❤", // text-default without VS16 + "👍👍", // two emoji + "👍 ", // trailing space + "👍‍", // dangling ZWJ + "‍👍", // leading ZWJ + "🇳", // lone regional indicator + "🇳🇿🇳", // three regional indicators + "a🏽", // modifier on a non-emoji + "👍🏽🏽", // double modifier + "🏳\U000E007F", // tag on a non-black-flag base + "\xff", // invalid UTF-8 + } { + if IsPossible(s) { + t.Errorf("IsPossible(%q) = true, want false", s) + } + } +} + +func TestIsSingleRejectsOversize(t *testing.T) { + s := "" + for len(s) <= maxReaction { + s += "‍👍" + } + if IsSingle(s) { + t.Errorf("IsSingle accepted %d-byte sequence", len(s)) + } +} diff --git a/internal/emoji/gen/main.go b/internal/emoji/gen/main.go new file mode 100644 index 0000000..dfd3494 --- /dev/null +++ b/internal/emoji/gen/main.go @@ -0,0 +1,211 @@ +// Command gen regenerates tables.go for package emoji from the Unicode emoji +// data files. Run from the repository root: +// +// go run ./internal/emoji/gen [emoji-test.txt emoji-data.txt] +// +// Without arguments the latest files are downloaded from unicode.org. +package main + +import ( + "bufio" + "bytes" + "fmt" + "go/format" + "io" + "net/http" + "os" + "sort" + "strconv" + "strings" +) + +const ( + testURL = "https://www.unicode.org/Public/emoji/latest/emoji-test.txt" + dataURL = "https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt" +) + +func open(arg, url string) (io.ReadCloser, error) { + if arg != "" { + return os.Open(arg) + } + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("GET %s: %s", url, resp.Status) + } + return resp.Body, nil +} + +type span struct{ lo, hi rune } + +func main() { + var testArg, dataArg string + if len(os.Args) == 3 { + testArg, dataArg = os.Args[1], os.Args[2] + } else if len(os.Args) != 1 { + fmt.Fprintln(os.Stderr, "usage: gen [emoji-test.txt emoji-data.txt]") + os.Exit(2) + } + + test, err := open(testArg, testURL) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer test.Close() + rgi, version := parseTest(test) + + data, err := open(dataArg, dataURL) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer data.Close() + props := parseData(data) + + var buf bytes.Buffer + w := &buf + + fmt.Fprintf(w, "// Code generated by internal/emoji/gen from Unicode Emoji %s; DO NOT EDIT.\n\n", version) + fmt.Fprintln(w, "package emoji") + fmt.Fprintln(w) + fmt.Fprintln(w, `import "unicode"`) + fmt.Fprintln(w) + fmt.Fprintf(w, "// Version is the Unicode Emoji version the tables were generated from.\nconst Version = %q\n\n", version) + fmt.Fprintf(w, "// rgi is the RGI_Emoji set (UTS #51): every fully-qualified emoji sequence.\nvar rgi = map[string]struct{}{\n") + for _, s := range rgi { + fmt.Fprintf(w, "\t%q: {},\n", s) + } + fmt.Fprintln(w, "}") + for _, name := range []string{"Emoji", "Emoji_Presentation", "Emoji_Modifier", "Emoji_Modifier_Base", "Emoji_Component"} { + writeTable(w, name, props[name]) + } + + src, err := format.Source(buf.Bytes()) + if err != nil { + fmt.Fprintln(os.Stderr, "format generated source:", err) + os.Exit(1) + } + if err := os.WriteFile("internal/emoji/tables.go", src, 0o644); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// parseTest returns the fully-qualified sequences and the file's version. +func parseTest(r io.Reader) ([]string, string) { + var seqs []string + version := "unknown" + sc := bufio.NewScanner(r) + for sc.Scan() { + line := sc.Text() + if strings.HasPrefix(line, "# Version:") { + version = strings.TrimSpace(strings.TrimPrefix(line, "# Version:")) + continue + } + if line == "" || strings.HasPrefix(line, "#") { + continue + } + // ; # E + semi := strings.IndexByte(line, ';') + hash := strings.IndexByte(line, '#') + if semi < 0 || hash < 0 { + continue + } + status := strings.TrimSpace(line[semi+1 : hash]) + if status != "fully-qualified" { + continue + } + var b strings.Builder + for _, cp := range strings.Fields(line[:semi]) { + n, err := strconv.ParseUint(cp, 16, 32) + if err != nil { + fmt.Fprintf(os.Stderr, "bad code point %q\n", cp) + os.Exit(1) + } + b.WriteRune(rune(n)) + } + seqs = append(seqs, b.String()) + } + sort.Strings(seqs) + return seqs, version +} + +// parseData returns the code point spans of each emoji property. +func parseData(r io.Reader) map[string][]span { + props := make(map[string][]span) + sc := bufio.NewScanner(r) + for sc.Scan() { + line := sc.Text() + if hash := strings.IndexByte(line, '#'); hash >= 0 { + line = line[:hash] + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Split(line, ";") + if len(parts) != 2 { + continue + } + rng, prop := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + lo, hi := rng, rng + if dots := strings.Index(rng, ".."); dots >= 0 { + lo, hi = rng[:dots], rng[dots+2:] + } + l, err1 := strconv.ParseUint(lo, 16, 32) + h, err2 := strconv.ParseUint(hi, 16, 32) + if err1 != nil || err2 != nil { + fmt.Fprintf(os.Stderr, "bad range %q\n", rng) + os.Exit(1) + } + props[prop] = append(props[prop], span{rune(l), rune(h)}) + } + for name := range props { + s := props[name] + sort.Slice(s, func(i, j int) bool { return s[i].lo < s[j].lo }) + // merge adjacent spans + merged := s[:0] + for _, sp := range s { + if n := len(merged); n > 0 && merged[n-1].hi+1 >= sp.lo { + if sp.hi > merged[n-1].hi { + merged[n-1].hi = sp.hi + } + continue + } + merged = append(merged, sp) + } + props[name] = merged + } + return props +} + +func writeTable(w io.Writer, name string, spans []span) { + goName := "prop" + strings.ReplaceAll(name, "_", "") + fmt.Fprintf(w, "\n// %s is the %s property.\nvar %s = &unicode.RangeTable{\n", goName, name, goName) + var r16, r32 []span + for _, sp := range spans { + if sp.hi < 0x10000 { + r16 = append(r16, sp) + } else if sp.lo >= 0x10000 { + r32 = append(r32, sp) + } else { + r16 = append(r16, span{sp.lo, 0xFFFF}) + r32 = append(r32, span{0x10000, sp.hi}) + } + } + fmt.Fprintln(w, "\tR16: []unicode.Range16{") + for _, sp := range r16 { + fmt.Fprintf(w, "\t\t{0x%04X, 0x%04X, 1},\n", sp.lo, sp.hi) + } + fmt.Fprintln(w, "\t},") + fmt.Fprintln(w, "\tR32: []unicode.Range32{") + for _, sp := range r32 { + fmt.Fprintf(w, "\t\t{0x%X, 0x%X, 1},\n", sp.lo, sp.hi) + } + fmt.Fprintln(w, "\t},") + fmt.Fprintln(w, "}") +} diff --git a/internal/emoji/tables.go b/internal/emoji/tables.go new file mode 100644 index 0000000..3371088 --- /dev/null +++ b/internal/emoji/tables.go @@ -0,0 +1,4278 @@ +// Code generated by internal/emoji/gen from Unicode Emoji 17.0; DO NOT EDIT. + +package emoji + +import "unicode" + +// Version is the Unicode Emoji version the tables were generated from. +const Version = "17.0" + +// rgi is the RGI_Emoji set (UTS #51): every fully-qualified emoji sequence. +var rgi = map[string]struct{}{ + "#️⃣": {}, + "*️⃣": {}, + "0️⃣": {}, + "1️⃣": {}, + "2️⃣": {}, + "3️⃣": {}, + "4️⃣": {}, + "5️⃣": {}, + "6️⃣": {}, + "7️⃣": {}, + "8️⃣": {}, + "9️⃣": {}, + "©️": {}, + "®️": {}, + "‼️": {}, + "⁉️": {}, + "™️": {}, + "ℹ️": {}, + "↔️": {}, + "↕️": {}, + "↖️": {}, + "↗️": {}, + "↘️": {}, + "↙️": {}, + "↩️": {}, + "↪️": {}, + "⌚": {}, + "⌛": {}, + "⌨️": {}, + "⏏️": {}, + "⏩": {}, + "⏪": {}, + "⏫": {}, + "⏬": {}, + "⏭️": {}, + "⏮️": {}, + "⏯️": {}, + "⏰": {}, + "⏱️": {}, + "⏲️": {}, + "⏳": {}, + "⏸️": {}, + "⏹️": {}, + "⏺️": {}, + "Ⓜ️": {}, + "▪️": {}, + "▫️": {}, + "▶️": {}, + "◀️": {}, + "◻️": {}, + "◼️": {}, + "◽": {}, + "◾": {}, + "☀️": {}, + "☁️": {}, + "☂️": {}, + "☃️": {}, + "☄️": {}, + "☎️": {}, + "☑️": {}, + "☔": {}, + "☕": {}, + "☘️": {}, + "☝️": {}, + "☝🏻": {}, + "☝🏼": {}, + "☝🏽": {}, + "☝🏾": {}, + "☝🏿": {}, + "☠️": {}, + "☢️": {}, + "☣️": {}, + "☦️": {}, + "☪️": {}, + "☮️": {}, + "☯️": {}, + "☸️": {}, + "☹️": {}, + "☺️": {}, + "♀️": {}, + "♂️": {}, + "♈": {}, + "♉": {}, + "♊": {}, + "♋": {}, + "♌": {}, + "♍": {}, + "♎": {}, + "♏": {}, + "♐": {}, + "♑": {}, + "♒": {}, + "♓": {}, + "♟️": {}, + "♠️": {}, + "♣️": {}, + "♥️": {}, + "♦️": {}, + "♨️": {}, + "♻️": {}, + "♾️": {}, + "♿": {}, + "⚒️": {}, + "⚓": {}, + "⚔️": {}, + "⚕️": {}, + "⚖️": {}, + "⚗️": {}, + "⚙️": {}, + "⚛️": {}, + "⚜️": {}, + "⚠️": {}, + "⚡": {}, + "⚧️": {}, + "⚪": {}, + "⚫": {}, + "⚰️": {}, + "⚱️": {}, + "⚽": {}, + "⚾": {}, + "⛄": {}, + "⛅": {}, + "⛈️": {}, + "⛎": {}, + "⛏️": {}, + "⛑️": {}, + "⛓️": {}, + "⛓️\u200d💥": {}, + "⛔": {}, + "⛩️": {}, + "⛪": {}, + "⛰️": {}, + "⛱️": {}, + "⛲": {}, + "⛳": {}, + "⛴️": {}, + "⛵": {}, + "⛷️": {}, + "⛸️": {}, + "⛹️": {}, + "⛹️\u200d♀️": {}, + "⛹️\u200d♂️": {}, + "⛹🏻": {}, + "⛹🏻\u200d♀️": {}, + "⛹🏻\u200d♂️": {}, + "⛹🏼": {}, + "⛹🏼\u200d♀️": {}, + "⛹🏼\u200d♂️": {}, + "⛹🏽": {}, + "⛹🏽\u200d♀️": {}, + "⛹🏽\u200d♂️": {}, + "⛹🏾": {}, + "⛹🏾\u200d♀️": {}, + "⛹🏾\u200d♂️": {}, + "⛹🏿": {}, + "⛹🏿\u200d♀️": {}, + "⛹🏿\u200d♂️": {}, + "⛺": {}, + "⛽": {}, + "✂️": {}, + "✅": {}, + "✈️": {}, + "✉️": {}, + "✊": {}, + "✊🏻": {}, + "✊🏼": {}, + "✊🏽": {}, + "✊🏾": {}, + "✊🏿": {}, + "✋": {}, + "✋🏻": {}, + "✋🏼": {}, + "✋🏽": {}, + "✋🏾": {}, + "✋🏿": {}, + "✌️": {}, + "✌🏻": {}, + "✌🏼": {}, + "✌🏽": {}, + "✌🏾": {}, + "✌🏿": {}, + "✍️": {}, + "✍🏻": {}, + "✍🏼": {}, + "✍🏽": {}, + "✍🏾": {}, + "✍🏿": {}, + "✏️": {}, + "✒️": {}, + "✔️": {}, + "✖️": {}, + "✝️": {}, + "✡️": {}, + "✨": {}, + "✳️": {}, + "✴️": {}, + "❄️": {}, + "❇️": {}, + "❌": {}, + "❎": {}, + "❓": {}, + "❔": {}, + "❕": {}, + "❗": {}, + "❣️": {}, + "❤️": {}, + "❤️\u200d🔥": {}, + "❤️\u200d🩹": {}, + "➕": {}, + "➖": {}, + "➗": {}, + "➡️": {}, + "➰": {}, + "➿": {}, + "⤴️": {}, + "⤵️": {}, + "⬅️": {}, + "⬆️": {}, + "⬇️": {}, + "⬛": {}, + "⬜": {}, + "⭐": {}, + "⭕": {}, + "〰️": {}, + "〽️": {}, + "㊗️": {}, + "㊙️": {}, + "🀄": {}, + "🃏": {}, + "🅰️": {}, + "🅱️": {}, + "🅾️": {}, + "🅿️": {}, + "🆎": {}, + "🆑": {}, + "🆒": {}, + "🆓": {}, + "🆔": {}, + "🆕": {}, + "🆖": {}, + "🆗": {}, + "🆘": {}, + "🆙": {}, + "🆚": {}, + "🇦🇨": {}, + "🇦🇩": {}, + "🇦🇪": {}, + "🇦🇫": {}, + "🇦🇬": {}, + "🇦🇮": {}, + "🇦🇱": {}, + "🇦🇲": {}, + "🇦🇴": {}, + "🇦🇶": {}, + "🇦🇷": {}, + "🇦🇸": {}, + "🇦🇹": {}, + "🇦🇺": {}, + "🇦🇼": {}, + "🇦🇽": {}, + "🇦🇿": {}, + "🇧🇦": {}, + "🇧🇧": {}, + "🇧🇩": {}, + "🇧🇪": {}, + "🇧🇫": {}, + "🇧🇬": {}, + "🇧🇭": {}, + "🇧🇮": {}, + "🇧🇯": {}, + "🇧🇱": {}, + "🇧🇲": {}, + "🇧🇳": {}, + "🇧🇴": {}, + "🇧🇶": {}, + "🇧🇷": {}, + "🇧🇸": {}, + "🇧🇹": {}, + "🇧🇻": {}, + "🇧🇼": {}, + "🇧🇾": {}, + "🇧🇿": {}, + "🇨🇦": {}, + "🇨🇨": {}, + "🇨🇩": {}, + "🇨🇫": {}, + "🇨🇬": {}, + "🇨🇭": {}, + "🇨🇮": {}, + "🇨🇰": {}, + "🇨🇱": {}, + "🇨🇲": {}, + "🇨🇳": {}, + "🇨🇴": {}, + "🇨🇵": {}, + "🇨🇶": {}, + "🇨🇷": {}, + "🇨🇺": {}, + "🇨🇻": {}, + "🇨🇼": {}, + "🇨🇽": {}, + "🇨🇾": {}, + "🇨🇿": {}, + "🇩🇪": {}, + "🇩🇬": {}, + "🇩🇯": {}, + "🇩🇰": {}, + "🇩🇲": {}, + "🇩🇴": {}, + "🇩🇿": {}, + "🇪🇦": {}, + "🇪🇨": {}, + "🇪🇪": {}, + "🇪🇬": {}, + "🇪🇭": {}, + "🇪🇷": {}, + "🇪🇸": {}, + "🇪🇹": {}, + "🇪🇺": {}, + "🇫🇮": {}, + "🇫🇯": {}, + "🇫🇰": {}, + "🇫🇲": {}, + "🇫🇴": {}, + "🇫🇷": {}, + "🇬🇦": {}, + "🇬🇧": {}, + "🇬🇩": {}, + "🇬🇪": {}, + "🇬🇫": {}, + "🇬🇬": {}, + "🇬🇭": {}, + "🇬🇮": {}, + "🇬🇱": {}, + "🇬🇲": {}, + "🇬🇳": {}, + "🇬🇵": {}, + "🇬🇶": {}, + "🇬🇷": {}, + "🇬🇸": {}, + "🇬🇹": {}, + "🇬🇺": {}, + "🇬🇼": {}, + "🇬🇾": {}, + "🇭🇰": {}, + "🇭🇲": {}, + "🇭🇳": {}, + "🇭🇷": {}, + "🇭🇹": {}, + "🇭🇺": {}, + "🇮🇨": {}, + "🇮🇩": {}, + "🇮🇪": {}, + "🇮🇱": {}, + "🇮🇲": {}, + "🇮🇳": {}, + "🇮🇴": {}, + "🇮🇶": {}, + "🇮🇷": {}, + "🇮🇸": {}, + "🇮🇹": {}, + "🇯🇪": {}, + "🇯🇲": {}, + "🇯🇴": {}, + "🇯🇵": {}, + "🇰🇪": {}, + "🇰🇬": {}, + "🇰🇭": {}, + "🇰🇮": {}, + "🇰🇲": {}, + "🇰🇳": {}, + "🇰🇵": {}, + "🇰🇷": {}, + "🇰🇼": {}, + "🇰🇾": {}, + "🇰🇿": {}, + "🇱🇦": {}, + "🇱🇧": {}, + "🇱🇨": {}, + "🇱🇮": {}, + "🇱🇰": {}, + "🇱🇷": {}, + "🇱🇸": {}, + "🇱🇹": {}, + "🇱🇺": {}, + "🇱🇻": {}, + "🇱🇾": {}, + "🇲🇦": {}, + "🇲🇨": {}, + "🇲🇩": {}, + "🇲🇪": {}, + "🇲🇫": {}, + "🇲🇬": {}, + "🇲🇭": {}, + "🇲🇰": {}, + "🇲🇱": {}, + "🇲🇲": {}, + "🇲🇳": {}, + "🇲🇴": {}, + "🇲🇵": {}, + "🇲🇶": {}, + "🇲🇷": {}, + "🇲🇸": {}, + "🇲🇹": {}, + "🇲🇺": {}, + "🇲🇻": {}, + "🇲🇼": {}, + "🇲🇽": {}, + "🇲🇾": {}, + "🇲🇿": {}, + "🇳🇦": {}, + "🇳🇨": {}, + "🇳🇪": {}, + "🇳🇫": {}, + "🇳🇬": {}, + "🇳🇮": {}, + "🇳🇱": {}, + "🇳🇴": {}, + "🇳🇵": {}, + "🇳🇷": {}, + "🇳🇺": {}, + "🇳🇿": {}, + "🇴🇲": {}, + "🇵🇦": {}, + "🇵🇪": {}, + "🇵🇫": {}, + "🇵🇬": {}, + "🇵🇭": {}, + "🇵🇰": {}, + "🇵🇱": {}, + "🇵🇲": {}, + "🇵🇳": {}, + "🇵🇷": {}, + "🇵🇸": {}, + "🇵🇹": {}, + "🇵🇼": {}, + "🇵🇾": {}, + "🇶🇦": {}, + "🇷🇪": {}, + "🇷🇴": {}, + "🇷🇸": {}, + "🇷🇺": {}, + "🇷🇼": {}, + "🇸🇦": {}, + "🇸🇧": {}, + "🇸🇨": {}, + "🇸🇩": {}, + "🇸🇪": {}, + "🇸🇬": {}, + "🇸🇭": {}, + "🇸🇮": {}, + "🇸🇯": {}, + "🇸🇰": {}, + "🇸🇱": {}, + "🇸🇲": {}, + "🇸🇳": {}, + "🇸🇴": {}, + "🇸🇷": {}, + "🇸🇸": {}, + "🇸🇹": {}, + "🇸🇻": {}, + "🇸🇽": {}, + "🇸🇾": {}, + "🇸🇿": {}, + "🇹🇦": {}, + "🇹🇨": {}, + "🇹🇩": {}, + "🇹🇫": {}, + "🇹🇬": {}, + "🇹🇭": {}, + "🇹🇯": {}, + "🇹🇰": {}, + "🇹🇱": {}, + "🇹🇲": {}, + "🇹🇳": {}, + "🇹🇴": {}, + "🇹🇷": {}, + "🇹🇹": {}, + "🇹🇻": {}, + "🇹🇼": {}, + "🇹🇿": {}, + "🇺🇦": {}, + "🇺🇬": {}, + "🇺🇲": {}, + "🇺🇳": {}, + "🇺🇸": {}, + "🇺🇾": {}, + "🇺🇿": {}, + "🇻🇦": {}, + "🇻🇨": {}, + "🇻🇪": {}, + "🇻🇬": {}, + "🇻🇮": {}, + "🇻🇳": {}, + "🇻🇺": {}, + "🇼🇫": {}, + "🇼🇸": {}, + "🇽🇰": {}, + "🇾🇪": {}, + "🇾🇹": {}, + "🇿🇦": {}, + "🇿🇲": {}, + "🇿🇼": {}, + "🈁": {}, + "🈂️": {}, + "🈚": {}, + "🈯": {}, + "🈲": {}, + "🈳": {}, + "🈴": {}, + "🈵": {}, + "🈶": {}, + "🈷️": {}, + "🈸": {}, + "🈹": {}, + "🈺": {}, + "🉐": {}, + "🉑": {}, + "🌀": {}, + "🌁": {}, + "🌂": {}, + "🌃": {}, + "🌄": {}, + "🌅": {}, + "🌆": {}, + "🌇": {}, + "🌈": {}, + "🌉": {}, + "🌊": {}, + "🌋": {}, + "🌌": {}, + "🌍": {}, + "🌎": {}, + "🌏": {}, + "🌐": {}, + "🌑": {}, + "🌒": {}, + "🌓": {}, + "🌔": {}, + "🌕": {}, + "🌖": {}, + "🌗": {}, + "🌘": {}, + "🌙": {}, + "🌚": {}, + "🌛": {}, + "🌜": {}, + "🌝": {}, + "🌞": {}, + "🌟": {}, + "🌠": {}, + "🌡️": {}, + "🌤️": {}, + "🌥️": {}, + "🌦️": {}, + "🌧️": {}, + "🌨️": {}, + "🌩️": {}, + "🌪️": {}, + "🌫️": {}, + "🌬️": {}, + "🌭": {}, + "🌮": {}, + "🌯": {}, + "🌰": {}, + "🌱": {}, + "🌲": {}, + "🌳": {}, + "🌴": {}, + "🌵": {}, + "🌶️": {}, + "🌷": {}, + "🌸": {}, + "🌹": {}, + "🌺": {}, + "🌻": {}, + "🌼": {}, + "🌽": {}, + "🌾": {}, + "🌿": {}, + "🍀": {}, + "🍁": {}, + "🍂": {}, + "🍃": {}, + "🍄": {}, + "🍄\u200d🟫": {}, + "🍅": {}, + "🍆": {}, + "🍇": {}, + "🍈": {}, + "🍉": {}, + "🍊": {}, + "🍋": {}, + "🍋\u200d🟩": {}, + "🍌": {}, + "🍍": {}, + "🍎": {}, + "🍏": {}, + "🍐": {}, + "🍑": {}, + "🍒": {}, + "🍓": {}, + "🍔": {}, + "🍕": {}, + "🍖": {}, + "🍗": {}, + "🍘": {}, + "🍙": {}, + "🍚": {}, + "🍛": {}, + "🍜": {}, + "🍝": {}, + "🍞": {}, + "🍟": {}, + "🍠": {}, + "🍡": {}, + "🍢": {}, + "🍣": {}, + "🍤": {}, + "🍥": {}, + "🍦": {}, + "🍧": {}, + "🍨": {}, + "🍩": {}, + "🍪": {}, + "🍫": {}, + "🍬": {}, + "🍭": {}, + "🍮": {}, + "🍯": {}, + "🍰": {}, + "🍱": {}, + "🍲": {}, + "🍳": {}, + "🍴": {}, + "🍵": {}, + "🍶": {}, + "🍷": {}, + "🍸": {}, + "🍹": {}, + "🍺": {}, + "🍻": {}, + "🍼": {}, + "🍽️": {}, + "🍾": {}, + "🍿": {}, + "🎀": {}, + "🎁": {}, + "🎂": {}, + "🎃": {}, + "🎄": {}, + "🎅": {}, + "🎅🏻": {}, + "🎅🏼": {}, + "🎅🏽": {}, + "🎅🏾": {}, + "🎅🏿": {}, + "🎆": {}, + "🎇": {}, + "🎈": {}, + "🎉": {}, + "🎊": {}, + "🎋": {}, + "🎌": {}, + "🎍": {}, + "🎎": {}, + "🎏": {}, + "🎐": {}, + "🎑": {}, + "🎒": {}, + "🎓": {}, + "🎖️": {}, + "🎗️": {}, + "🎙️": {}, + "🎚️": {}, + "🎛️": {}, + "🎞️": {}, + "🎟️": {}, + "🎠": {}, + "🎡": {}, + "🎢": {}, + "🎣": {}, + "🎤": {}, + "🎥": {}, + "🎦": {}, + "🎧": {}, + "🎨": {}, + "🎩": {}, + "🎪": {}, + "🎫": {}, + "🎬": {}, + "🎭": {}, + "🎮": {}, + "🎯": {}, + "🎰": {}, + "🎱": {}, + "🎲": {}, + "🎳": {}, + "🎴": {}, + "🎵": {}, + "🎶": {}, + "🎷": {}, + "🎸": {}, + "🎹": {}, + "🎺": {}, + "🎻": {}, + "🎼": {}, + "🎽": {}, + "🎾": {}, + "🎿": {}, + "🏀": {}, + "🏁": {}, + "🏂": {}, + "🏂🏻": {}, + "🏂🏼": {}, + "🏂🏽": {}, + "🏂🏾": {}, + "🏂🏿": {}, + "🏃": {}, + "🏃\u200d♀️": {}, + "🏃\u200d♀️\u200d➡️": {}, + "🏃\u200d♂️": {}, + "🏃\u200d♂️\u200d➡️": {}, + "🏃\u200d➡️": {}, + "🏃🏻": {}, + "🏃🏻\u200d♀️": {}, + "🏃🏻\u200d♀️\u200d➡️": {}, + "🏃🏻\u200d♂️": {}, + "🏃🏻\u200d♂️\u200d➡️": {}, + "🏃🏻\u200d➡️": {}, + "🏃🏼": {}, + "🏃🏼\u200d♀️": {}, + "🏃🏼\u200d♀️\u200d➡️": {}, + "🏃🏼\u200d♂️": {}, + "🏃🏼\u200d♂️\u200d➡️": {}, + "🏃🏼\u200d➡️": {}, + "🏃🏽": {}, + "🏃🏽\u200d♀️": {}, + "🏃🏽\u200d♀️\u200d➡️": {}, + "🏃🏽\u200d♂️": {}, + "🏃🏽\u200d♂️\u200d➡️": {}, + "🏃🏽\u200d➡️": {}, + "🏃🏾": {}, + "🏃🏾\u200d♀️": {}, + "🏃🏾\u200d♀️\u200d➡️": {}, + "🏃🏾\u200d♂️": {}, + "🏃🏾\u200d♂️\u200d➡️": {}, + "🏃🏾\u200d➡️": {}, + "🏃🏿": {}, + "🏃🏿\u200d♀️": {}, + "🏃🏿\u200d♀️\u200d➡️": {}, + "🏃🏿\u200d♂️": {}, + "🏃🏿\u200d♂️\u200d➡️": {}, + "🏃🏿\u200d➡️": {}, + "🏄": {}, + "🏄\u200d♀️": {}, + "🏄\u200d♂️": {}, + "🏄🏻": {}, + "🏄🏻\u200d♀️": {}, + "🏄🏻\u200d♂️": {}, + "🏄🏼": {}, + "🏄🏼\u200d♀️": {}, + "🏄🏼\u200d♂️": {}, + "🏄🏽": {}, + "🏄🏽\u200d♀️": {}, + "🏄🏽\u200d♂️": {}, + "🏄🏾": {}, + "🏄🏾\u200d♀️": {}, + "🏄🏾\u200d♂️": {}, + "🏄🏿": {}, + "🏄🏿\u200d♀️": {}, + "🏄🏿\u200d♂️": {}, + "🏅": {}, + "🏆": {}, + "🏇": {}, + "🏇🏻": {}, + "🏇🏼": {}, + "🏇🏽": {}, + "🏇🏾": {}, + "🏇🏿": {}, + "🏈": {}, + "🏉": {}, + "🏊": {}, + "🏊\u200d♀️": {}, + "🏊\u200d♂️": {}, + "🏊🏻": {}, + "🏊🏻\u200d♀️": {}, + "🏊🏻\u200d♂️": {}, + "🏊🏼": {}, + "🏊🏼\u200d♀️": {}, + "🏊🏼\u200d♂️": {}, + "🏊🏽": {}, + "🏊🏽\u200d♀️": {}, + "🏊🏽\u200d♂️": {}, + "🏊🏾": {}, + "🏊🏾\u200d♀️": {}, + "🏊🏾\u200d♂️": {}, + "🏊🏿": {}, + "🏊🏿\u200d♀️": {}, + "🏊🏿\u200d♂️": {}, + "🏋️": {}, + "🏋️\u200d♀️": {}, + "🏋️\u200d♂️": {}, + "🏋🏻": {}, + "🏋🏻\u200d♀️": {}, + "🏋🏻\u200d♂️": {}, + "🏋🏼": {}, + "🏋🏼\u200d♀️": {}, + "🏋🏼\u200d♂️": {}, + "🏋🏽": {}, + "🏋🏽\u200d♀️": {}, + "🏋🏽\u200d♂️": {}, + "🏋🏾": {}, + "🏋🏾\u200d♀️": {}, + "🏋🏾\u200d♂️": {}, + "🏋🏿": {}, + "🏋🏿\u200d♀️": {}, + "🏋🏿\u200d♂️": {}, + "🏌️": {}, + "🏌️\u200d♀️": {}, + "🏌️\u200d♂️": {}, + "🏌🏻": {}, + "🏌🏻\u200d♀️": {}, + "🏌🏻\u200d♂️": {}, + "🏌🏼": {}, + "🏌🏼\u200d♀️": {}, + "🏌🏼\u200d♂️": {}, + "🏌🏽": {}, + "🏌🏽\u200d♀️": {}, + "🏌🏽\u200d♂️": {}, + "🏌🏾": {}, + "🏌🏾\u200d♀️": {}, + "🏌🏾\u200d♂️": {}, + "🏌🏿": {}, + "🏌🏿\u200d♀️": {}, + "🏌🏿\u200d♂️": {}, + "🏍️": {}, + "🏎️": {}, + "🏏": {}, + "🏐": {}, + "🏑": {}, + "🏒": {}, + "🏓": {}, + "🏔️": {}, + "🏕️": {}, + "🏖️": {}, + "🏗️": {}, + "🏘️": {}, + "🏙️": {}, + "🏚️": {}, + "🏛️": {}, + "🏜️": {}, + "🏝️": {}, + "🏞️": {}, + "🏟️": {}, + "🏠": {}, + "🏡": {}, + "🏢": {}, + "🏣": {}, + "🏤": {}, + "🏥": {}, + "🏦": {}, + "🏧": {}, + "🏨": {}, + "🏩": {}, + "🏪": {}, + "🏫": {}, + "🏬": {}, + "🏭": {}, + "🏮": {}, + "🏯": {}, + "🏰": {}, + "🏳️": {}, + "🏳️\u200d⚧️": {}, + "🏳️\u200d🌈": {}, + "🏴": {}, + "🏴\u200d☠️": {}, + "🏴\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f": {}, + "🏴\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f": {}, + "🏴\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f": {}, + "🏵️": {}, + "🏷️": {}, + "🏸": {}, + "🏹": {}, + "🏺": {}, + "🐀": {}, + "🐁": {}, + "🐂": {}, + "🐃": {}, + "🐄": {}, + "🐅": {}, + "🐆": {}, + "🐇": {}, + "🐈": {}, + "🐈\u200d⬛": {}, + "🐉": {}, + "🐊": {}, + "🐋": {}, + "🐌": {}, + "🐍": {}, + "🐎": {}, + "🐏": {}, + "🐐": {}, + "🐑": {}, + "🐒": {}, + "🐓": {}, + "🐔": {}, + "🐕": {}, + "🐕\u200d🦺": {}, + "🐖": {}, + "🐗": {}, + "🐘": {}, + "🐙": {}, + "🐚": {}, + "🐛": {}, + "🐜": {}, + "🐝": {}, + "🐞": {}, + "🐟": {}, + "🐠": {}, + "🐡": {}, + "🐢": {}, + "🐣": {}, + "🐤": {}, + "🐥": {}, + "🐦": {}, + "🐦\u200d⬛": {}, + "🐦\u200d🔥": {}, + "🐧": {}, + "🐨": {}, + "🐩": {}, + "🐪": {}, + "🐫": {}, + "🐬": {}, + "🐭": {}, + "🐮": {}, + "🐯": {}, + "🐰": {}, + "🐱": {}, + "🐲": {}, + "🐳": {}, + "🐴": {}, + "🐵": {}, + "🐶": {}, + "🐷": {}, + "🐸": {}, + "🐹": {}, + "🐺": {}, + "🐻": {}, + "🐻\u200d❄️": {}, + "🐼": {}, + "🐽": {}, + "🐾": {}, + "🐿️": {}, + "👀": {}, + "👁️": {}, + "👁️\u200d🗨️": {}, + "👂": {}, + "👂🏻": {}, + "👂🏼": {}, + "👂🏽": {}, + "👂🏾": {}, + "👂🏿": {}, + "👃": {}, + "👃🏻": {}, + "👃🏼": {}, + "👃🏽": {}, + "👃🏾": {}, + "👃🏿": {}, + "👄": {}, + "👅": {}, + "👆": {}, + "👆🏻": {}, + "👆🏼": {}, + "👆🏽": {}, + "👆🏾": {}, + "👆🏿": {}, + "👇": {}, + "👇🏻": {}, + "👇🏼": {}, + "👇🏽": {}, + "👇🏾": {}, + "👇🏿": {}, + "👈": {}, + "👈🏻": {}, + "👈🏼": {}, + "👈🏽": {}, + "👈🏾": {}, + "👈🏿": {}, + "👉": {}, + "👉🏻": {}, + "👉🏼": {}, + "👉🏽": {}, + "👉🏾": {}, + "👉🏿": {}, + "👊": {}, + "👊🏻": {}, + "👊🏼": {}, + "👊🏽": {}, + "👊🏾": {}, + "👊🏿": {}, + "👋": {}, + "👋🏻": {}, + "👋🏼": {}, + "👋🏽": {}, + "👋🏾": {}, + "👋🏿": {}, + "👌": {}, + "👌🏻": {}, + "👌🏼": {}, + "👌🏽": {}, + "👌🏾": {}, + "👌🏿": {}, + "👍": {}, + "👍🏻": {}, + "👍🏼": {}, + "👍🏽": {}, + "👍🏾": {}, + "👍🏿": {}, + "👎": {}, + "👎🏻": {}, + "👎🏼": {}, + "👎🏽": {}, + "👎🏾": {}, + "👎🏿": {}, + "👏": {}, + "👏🏻": {}, + "👏🏼": {}, + "👏🏽": {}, + "👏🏾": {}, + "👏🏿": {}, + "👐": {}, + "👐🏻": {}, + "👐🏼": {}, + "👐🏽": {}, + "👐🏾": {}, + "👐🏿": {}, + "👑": {}, + "👒": {}, + "👓": {}, + "👔": {}, + "👕": {}, + "👖": {}, + "👗": {}, + "👘": {}, + "👙": {}, + "👚": {}, + "👛": {}, + "👜": {}, + "👝": {}, + "👞": {}, + "👟": {}, + "👠": {}, + "👡": {}, + "👢": {}, + "👣": {}, + "👤": {}, + "👥": {}, + "👦": {}, + "👦🏻": {}, + "👦🏼": {}, + "👦🏽": {}, + "👦🏾": {}, + "👦🏿": {}, + "👧": {}, + "👧🏻": {}, + "👧🏼": {}, + "👧🏽": {}, + "👧🏾": {}, + "👧🏿": {}, + "👨": {}, + "👨\u200d⚕️": {}, + "👨\u200d⚖️": {}, + "👨\u200d✈️": {}, + "👨\u200d❤️\u200d👨": {}, + "👨\u200d❤️\u200d💋\u200d👨": {}, + "👨\u200d🌾": {}, + "👨\u200d🍳": {}, + "👨\u200d🍼": {}, + "👨\u200d🎓": {}, + "👨\u200d🎤": {}, + "👨\u200d🎨": {}, + "👨\u200d🏫": {}, + "👨\u200d🏭": {}, + "👨\u200d👦": {}, + "👨\u200d👦\u200d👦": {}, + "👨\u200d👧": {}, + "👨\u200d👧\u200d👦": {}, + "👨\u200d👧\u200d👧": {}, + "👨\u200d👨\u200d👦": {}, + "👨\u200d👨\u200d👦\u200d👦": {}, + "👨\u200d👨\u200d👧": {}, + "👨\u200d👨\u200d👧\u200d👦": {}, + "👨\u200d👨\u200d👧\u200d👧": {}, + "👨\u200d👩\u200d👦": {}, + "👨\u200d👩\u200d👦\u200d👦": {}, + "👨\u200d👩\u200d👧": {}, + "👨\u200d👩\u200d👧\u200d👦": {}, + "👨\u200d👩\u200d👧\u200d👧": {}, + "👨\u200d💻": {}, + "👨\u200d💼": {}, + "👨\u200d🔧": {}, + "👨\u200d🔬": {}, + "👨\u200d🚀": {}, + "👨\u200d🚒": {}, + "👨\u200d🦯": {}, + "👨\u200d🦯\u200d➡️": {}, + "👨\u200d🦰": {}, + "👨\u200d🦱": {}, + "👨\u200d🦲": {}, + "👨\u200d🦳": {}, + "👨\u200d🦼": {}, + "👨\u200d🦼\u200d➡️": {}, + "👨\u200d🦽": {}, + "👨\u200d🦽\u200d➡️": {}, + "👨🏻": {}, + "👨🏻\u200d⚕️": {}, + "👨🏻\u200d⚖️": {}, + "👨🏻\u200d✈️": {}, + "👨🏻\u200d❤️\u200d👨🏻": {}, + "👨🏻\u200d❤️\u200d👨🏼": {}, + "👨🏻\u200d❤️\u200d👨🏽": {}, + "👨🏻\u200d❤️\u200d👨🏾": {}, + "👨🏻\u200d❤️\u200d👨🏿": {}, + "👨🏻\u200d❤️\u200d💋\u200d👨🏻": {}, + "👨🏻\u200d❤️\u200d💋\u200d👨🏼": {}, + "👨🏻\u200d❤️\u200d💋\u200d👨🏽": {}, + "👨🏻\u200d❤️\u200d💋\u200d👨🏾": {}, + "👨🏻\u200d❤️\u200d💋\u200d👨🏿": {}, + "👨🏻\u200d🌾": {}, + "👨🏻\u200d🍳": {}, + "👨🏻\u200d🍼": {}, + "👨🏻\u200d🎓": {}, + "👨🏻\u200d🎤": {}, + "👨🏻\u200d🎨": {}, + "👨🏻\u200d🏫": {}, + "👨🏻\u200d🏭": {}, + "👨🏻\u200d🐰\u200d👨🏼": {}, + "👨🏻\u200d🐰\u200d👨🏽": {}, + "👨🏻\u200d🐰\u200d👨🏾": {}, + "👨🏻\u200d🐰\u200d👨🏿": {}, + "👨🏻\u200d💻": {}, + "👨🏻\u200d💼": {}, + "👨🏻\u200d🔧": {}, + "👨🏻\u200d🔬": {}, + "👨🏻\u200d🚀": {}, + "👨🏻\u200d🚒": {}, + "👨🏻\u200d🤝\u200d👨🏼": {}, + "👨🏻\u200d🤝\u200d👨🏽": {}, + "👨🏻\u200d🤝\u200d👨🏾": {}, + "👨🏻\u200d🤝\u200d👨🏿": {}, + "👨🏻\u200d🦯": {}, + "👨🏻\u200d🦯\u200d➡️": {}, + "👨🏻\u200d🦰": {}, + "👨🏻\u200d🦱": {}, + "👨🏻\u200d🦲": {}, + "👨🏻\u200d🦳": {}, + "👨🏻\u200d🦼": {}, + "👨🏻\u200d🦼\u200d➡️": {}, + "👨🏻\u200d🦽": {}, + "👨🏻\u200d🦽\u200d➡️": {}, + "👨🏻\u200d\U0001faef\u200d👨🏼": {}, + "👨🏻\u200d\U0001faef\u200d👨🏽": {}, + "👨🏻\u200d\U0001faef\u200d👨🏾": {}, + "👨🏻\u200d\U0001faef\u200d👨🏿": {}, + "👨🏼": {}, + "👨🏼\u200d⚕️": {}, + "👨🏼\u200d⚖️": {}, + "👨🏼\u200d✈️": {}, + "👨🏼\u200d❤️\u200d👨🏻": {}, + "👨🏼\u200d❤️\u200d👨🏼": {}, + "👨🏼\u200d❤️\u200d👨🏽": {}, + "👨🏼\u200d❤️\u200d👨🏾": {}, + "👨🏼\u200d❤️\u200d👨🏿": {}, + "👨🏼\u200d❤️\u200d💋\u200d👨🏻": {}, + "👨🏼\u200d❤️\u200d💋\u200d👨🏼": {}, + "👨🏼\u200d❤️\u200d💋\u200d👨🏽": {}, + "👨🏼\u200d❤️\u200d💋\u200d👨🏾": {}, + "👨🏼\u200d❤️\u200d💋\u200d👨🏿": {}, + "👨🏼\u200d🌾": {}, + "👨🏼\u200d🍳": {}, + "👨🏼\u200d🍼": {}, + "👨🏼\u200d🎓": {}, + "👨🏼\u200d🎤": {}, + "👨🏼\u200d🎨": {}, + "👨🏼\u200d🏫": {}, + "👨🏼\u200d🏭": {}, + "👨🏼\u200d🐰\u200d👨🏻": {}, + "👨🏼\u200d🐰\u200d👨🏽": {}, + "👨🏼\u200d🐰\u200d👨🏾": {}, + "👨🏼\u200d🐰\u200d👨🏿": {}, + "👨🏼\u200d💻": {}, + "👨🏼\u200d💼": {}, + "👨🏼\u200d🔧": {}, + "👨🏼\u200d🔬": {}, + "👨🏼\u200d🚀": {}, + "👨🏼\u200d🚒": {}, + "👨🏼\u200d🤝\u200d👨🏻": {}, + "👨🏼\u200d🤝\u200d👨🏽": {}, + "👨🏼\u200d🤝\u200d👨🏾": {}, + "👨🏼\u200d🤝\u200d👨🏿": {}, + "👨🏼\u200d🦯": {}, + "👨🏼\u200d🦯\u200d➡️": {}, + "👨🏼\u200d🦰": {}, + "👨🏼\u200d🦱": {}, + "👨🏼\u200d🦲": {}, + "👨🏼\u200d🦳": {}, + "👨🏼\u200d🦼": {}, + "👨🏼\u200d🦼\u200d➡️": {}, + "👨🏼\u200d🦽": {}, + "👨🏼\u200d🦽\u200d➡️": {}, + "👨🏼\u200d\U0001faef\u200d👨🏻": {}, + "👨🏼\u200d\U0001faef\u200d👨🏽": {}, + "👨🏼\u200d\U0001faef\u200d👨🏾": {}, + "👨🏼\u200d\U0001faef\u200d👨🏿": {}, + "👨🏽": {}, + "👨🏽\u200d⚕️": {}, + "👨🏽\u200d⚖️": {}, + "👨🏽\u200d✈️": {}, + "👨🏽\u200d❤️\u200d👨🏻": {}, + "👨🏽\u200d❤️\u200d👨🏼": {}, + "👨🏽\u200d❤️\u200d👨🏽": {}, + "👨🏽\u200d❤️\u200d👨🏾": {}, + "👨🏽\u200d❤️\u200d👨🏿": {}, + "👨🏽\u200d❤️\u200d💋\u200d👨🏻": {}, + "👨🏽\u200d❤️\u200d💋\u200d👨🏼": {}, + "👨🏽\u200d❤️\u200d💋\u200d👨🏽": {}, + "👨🏽\u200d❤️\u200d💋\u200d👨🏾": {}, + "👨🏽\u200d❤️\u200d💋\u200d👨🏿": {}, + "👨🏽\u200d🌾": {}, + "👨🏽\u200d🍳": {}, + "👨🏽\u200d🍼": {}, + "👨🏽\u200d🎓": {}, + "👨🏽\u200d🎤": {}, + "👨🏽\u200d🎨": {}, + "👨🏽\u200d🏫": {}, + "👨🏽\u200d🏭": {}, + "👨🏽\u200d🐰\u200d👨🏻": {}, + "👨🏽\u200d🐰\u200d👨🏼": {}, + "👨🏽\u200d🐰\u200d👨🏾": {}, + "👨🏽\u200d🐰\u200d👨🏿": {}, + "👨🏽\u200d💻": {}, + "👨🏽\u200d💼": {}, + "👨🏽\u200d🔧": {}, + "👨🏽\u200d🔬": {}, + "👨🏽\u200d🚀": {}, + "👨🏽\u200d🚒": {}, + "👨🏽\u200d🤝\u200d👨🏻": {}, + "👨🏽\u200d🤝\u200d👨🏼": {}, + "👨🏽\u200d🤝\u200d👨🏾": {}, + "👨🏽\u200d🤝\u200d👨🏿": {}, + "👨🏽\u200d🦯": {}, + "👨🏽\u200d🦯\u200d➡️": {}, + "👨🏽\u200d🦰": {}, + "👨🏽\u200d🦱": {}, + "👨🏽\u200d🦲": {}, + "👨🏽\u200d🦳": {}, + "👨🏽\u200d🦼": {}, + "👨🏽\u200d🦼\u200d➡️": {}, + "👨🏽\u200d🦽": {}, + "👨🏽\u200d🦽\u200d➡️": {}, + "👨🏽\u200d\U0001faef\u200d👨🏻": {}, + "👨🏽\u200d\U0001faef\u200d👨🏼": {}, + "👨🏽\u200d\U0001faef\u200d👨🏾": {}, + "👨🏽\u200d\U0001faef\u200d👨🏿": {}, + "👨🏾": {}, + "👨🏾\u200d⚕️": {}, + "👨🏾\u200d⚖️": {}, + "👨🏾\u200d✈️": {}, + "👨🏾\u200d❤️\u200d👨🏻": {}, + "👨🏾\u200d❤️\u200d👨🏼": {}, + "👨🏾\u200d❤️\u200d👨🏽": {}, + "👨🏾\u200d❤️\u200d👨🏾": {}, + "👨🏾\u200d❤️\u200d👨🏿": {}, + "👨🏾\u200d❤️\u200d💋\u200d👨🏻": {}, + "👨🏾\u200d❤️\u200d💋\u200d👨🏼": {}, + "👨🏾\u200d❤️\u200d💋\u200d👨🏽": {}, + "👨🏾\u200d❤️\u200d💋\u200d👨🏾": {}, + "👨🏾\u200d❤️\u200d💋\u200d👨🏿": {}, + "👨🏾\u200d🌾": {}, + "👨🏾\u200d🍳": {}, + "👨🏾\u200d🍼": {}, + "👨🏾\u200d🎓": {}, + "👨🏾\u200d🎤": {}, + "👨🏾\u200d🎨": {}, + "👨🏾\u200d🏫": {}, + "👨🏾\u200d🏭": {}, + "👨🏾\u200d🐰\u200d👨🏻": {}, + "👨🏾\u200d🐰\u200d👨🏼": {}, + "👨🏾\u200d🐰\u200d👨🏽": {}, + "👨🏾\u200d🐰\u200d👨🏿": {}, + "👨🏾\u200d💻": {}, + "👨🏾\u200d💼": {}, + "👨🏾\u200d🔧": {}, + "👨🏾\u200d🔬": {}, + "👨🏾\u200d🚀": {}, + "👨🏾\u200d🚒": {}, + "👨🏾\u200d🤝\u200d👨🏻": {}, + "👨🏾\u200d🤝\u200d👨🏼": {}, + "👨🏾\u200d🤝\u200d👨🏽": {}, + "👨🏾\u200d🤝\u200d👨🏿": {}, + "👨🏾\u200d🦯": {}, + "👨🏾\u200d🦯\u200d➡️": {}, + "👨🏾\u200d🦰": {}, + "👨🏾\u200d🦱": {}, + "👨🏾\u200d🦲": {}, + "👨🏾\u200d🦳": {}, + "👨🏾\u200d🦼": {}, + "👨🏾\u200d🦼\u200d➡️": {}, + "👨🏾\u200d🦽": {}, + "👨🏾\u200d🦽\u200d➡️": {}, + "👨🏾\u200d\U0001faef\u200d👨🏻": {}, + "👨🏾\u200d\U0001faef\u200d👨🏼": {}, + "👨🏾\u200d\U0001faef\u200d👨🏽": {}, + "👨🏾\u200d\U0001faef\u200d👨🏿": {}, + "👨🏿": {}, + "👨🏿\u200d⚕️": {}, + "👨🏿\u200d⚖️": {}, + "👨🏿\u200d✈️": {}, + "👨🏿\u200d❤️\u200d👨🏻": {}, + "👨🏿\u200d❤️\u200d👨🏼": {}, + "👨🏿\u200d❤️\u200d👨🏽": {}, + "👨🏿\u200d❤️\u200d👨🏾": {}, + "👨🏿\u200d❤️\u200d👨🏿": {}, + "👨🏿\u200d❤️\u200d💋\u200d👨🏻": {}, + "👨🏿\u200d❤️\u200d💋\u200d👨🏼": {}, + "👨🏿\u200d❤️\u200d💋\u200d👨🏽": {}, + "👨🏿\u200d❤️\u200d💋\u200d👨🏾": {}, + "👨🏿\u200d❤️\u200d💋\u200d👨🏿": {}, + "👨🏿\u200d🌾": {}, + "👨🏿\u200d🍳": {}, + "👨🏿\u200d🍼": {}, + "👨🏿\u200d🎓": {}, + "👨🏿\u200d🎤": {}, + "👨🏿\u200d🎨": {}, + "👨🏿\u200d🏫": {}, + "👨🏿\u200d🏭": {}, + "👨🏿\u200d🐰\u200d👨🏻": {}, + "👨🏿\u200d🐰\u200d👨🏼": {}, + "👨🏿\u200d🐰\u200d👨🏽": {}, + "👨🏿\u200d🐰\u200d👨🏾": {}, + "👨🏿\u200d💻": {}, + "👨🏿\u200d💼": {}, + "👨🏿\u200d🔧": {}, + "👨🏿\u200d🔬": {}, + "👨🏿\u200d🚀": {}, + "👨🏿\u200d🚒": {}, + "👨🏿\u200d🤝\u200d👨🏻": {}, + "👨🏿\u200d🤝\u200d👨🏼": {}, + "👨🏿\u200d🤝\u200d👨🏽": {}, + "👨🏿\u200d🤝\u200d👨🏾": {}, + "👨🏿\u200d🦯": {}, + "👨🏿\u200d🦯\u200d➡️": {}, + "👨🏿\u200d🦰": {}, + "👨🏿\u200d🦱": {}, + "👨🏿\u200d🦲": {}, + "👨🏿\u200d🦳": {}, + "👨🏿\u200d🦼": {}, + "👨🏿\u200d🦼\u200d➡️": {}, + "👨🏿\u200d🦽": {}, + "👨🏿\u200d🦽\u200d➡️": {}, + "👨🏿\u200d\U0001faef\u200d👨🏻": {}, + "👨🏿\u200d\U0001faef\u200d👨🏼": {}, + "👨🏿\u200d\U0001faef\u200d👨🏽": {}, + "👨🏿\u200d\U0001faef\u200d👨🏾": {}, + "👩": {}, + "👩\u200d⚕️": {}, + "👩\u200d⚖️": {}, + "👩\u200d✈️": {}, + "👩\u200d❤️\u200d👨": {}, + "👩\u200d❤️\u200d👩": {}, + "👩\u200d❤️\u200d💋\u200d👨": {}, + "👩\u200d❤️\u200d💋\u200d👩": {}, + "👩\u200d🌾": {}, + "👩\u200d🍳": {}, + "👩\u200d🍼": {}, + "👩\u200d🎓": {}, + "👩\u200d🎤": {}, + "👩\u200d🎨": {}, + "👩\u200d🏫": {}, + "👩\u200d🏭": {}, + "👩\u200d👦": {}, + "👩\u200d👦\u200d👦": {}, + "👩\u200d👧": {}, + "👩\u200d👧\u200d👦": {}, + "👩\u200d👧\u200d👧": {}, + "👩\u200d👩\u200d👦": {}, + "👩\u200d👩\u200d👦\u200d👦": {}, + "👩\u200d👩\u200d👧": {}, + "👩\u200d👩\u200d👧\u200d👦": {}, + "👩\u200d👩\u200d👧\u200d👧": {}, + "👩\u200d💻": {}, + "👩\u200d💼": {}, + "👩\u200d🔧": {}, + "👩\u200d🔬": {}, + "👩\u200d🚀": {}, + "👩\u200d🚒": {}, + "👩\u200d🦯": {}, + "👩\u200d🦯\u200d➡️": {}, + "👩\u200d🦰": {}, + "👩\u200d🦱": {}, + "👩\u200d🦲": {}, + "👩\u200d🦳": {}, + "👩\u200d🦼": {}, + "👩\u200d🦼\u200d➡️": {}, + "👩\u200d🦽": {}, + "👩\u200d🦽\u200d➡️": {}, + "👩🏻": {}, + "👩🏻\u200d⚕️": {}, + "👩🏻\u200d⚖️": {}, + "👩🏻\u200d✈️": {}, + "👩🏻\u200d❤️\u200d👨🏻": {}, + "👩🏻\u200d❤️\u200d👨🏼": {}, + "👩🏻\u200d❤️\u200d👨🏽": {}, + "👩🏻\u200d❤️\u200d👨🏾": {}, + "👩🏻\u200d❤️\u200d👨🏿": {}, + "👩🏻\u200d❤️\u200d👩🏻": {}, + "👩🏻\u200d❤️\u200d👩🏼": {}, + "👩🏻\u200d❤️\u200d👩🏽": {}, + "👩🏻\u200d❤️\u200d👩🏾": {}, + "👩🏻\u200d❤️\u200d👩🏿": {}, + "👩🏻\u200d❤️\u200d💋\u200d👨🏻": {}, + "👩🏻\u200d❤️\u200d💋\u200d👨🏼": {}, + "👩🏻\u200d❤️\u200d💋\u200d👨🏽": {}, + "👩🏻\u200d❤️\u200d💋\u200d👨🏾": {}, + "👩🏻\u200d❤️\u200d💋\u200d👨🏿": {}, + "👩🏻\u200d❤️\u200d💋\u200d👩🏻": {}, + "👩🏻\u200d❤️\u200d💋\u200d👩🏼": {}, + "👩🏻\u200d❤️\u200d💋\u200d👩🏽": {}, + "👩🏻\u200d❤️\u200d💋\u200d👩🏾": {}, + "👩🏻\u200d❤️\u200d💋\u200d👩🏿": {}, + "👩🏻\u200d🌾": {}, + "👩🏻\u200d🍳": {}, + "👩🏻\u200d🍼": {}, + "👩🏻\u200d🎓": {}, + "👩🏻\u200d🎤": {}, + "👩🏻\u200d🎨": {}, + "👩🏻\u200d🏫": {}, + "👩🏻\u200d🏭": {}, + "👩🏻\u200d🐰\u200d👩🏼": {}, + "👩🏻\u200d🐰\u200d👩🏽": {}, + "👩🏻\u200d🐰\u200d👩🏾": {}, + "👩🏻\u200d🐰\u200d👩🏿": {}, + "👩🏻\u200d💻": {}, + "👩🏻\u200d💼": {}, + "👩🏻\u200d🔧": {}, + "👩🏻\u200d🔬": {}, + "👩🏻\u200d🚀": {}, + "👩🏻\u200d🚒": {}, + "👩🏻\u200d🤝\u200d👨🏼": {}, + "👩🏻\u200d🤝\u200d👨🏽": {}, + "👩🏻\u200d🤝\u200d👨🏾": {}, + "👩🏻\u200d🤝\u200d👨🏿": {}, + "👩🏻\u200d🤝\u200d👩🏼": {}, + "👩🏻\u200d🤝\u200d👩🏽": {}, + "👩🏻\u200d🤝\u200d👩🏾": {}, + "👩🏻\u200d🤝\u200d👩🏿": {}, + "👩🏻\u200d🦯": {}, + "👩🏻\u200d🦯\u200d➡️": {}, + "👩🏻\u200d🦰": {}, + "👩🏻\u200d🦱": {}, + "👩🏻\u200d🦲": {}, + "👩🏻\u200d🦳": {}, + "👩🏻\u200d🦼": {}, + "👩🏻\u200d🦼\u200d➡️": {}, + "👩🏻\u200d🦽": {}, + "👩🏻\u200d🦽\u200d➡️": {}, + "👩🏻\u200d\U0001faef\u200d👩🏼": {}, + "👩🏻\u200d\U0001faef\u200d👩🏽": {}, + "👩🏻\u200d\U0001faef\u200d👩🏾": {}, + "👩🏻\u200d\U0001faef\u200d👩🏿": {}, + "👩🏼": {}, + "👩🏼\u200d⚕️": {}, + "👩🏼\u200d⚖️": {}, + "👩🏼\u200d✈️": {}, + "👩🏼\u200d❤️\u200d👨🏻": {}, + "👩🏼\u200d❤️\u200d👨🏼": {}, + "👩🏼\u200d❤️\u200d👨🏽": {}, + "👩🏼\u200d❤️\u200d👨🏾": {}, + "👩🏼\u200d❤️\u200d👨🏿": {}, + "👩🏼\u200d❤️\u200d👩🏻": {}, + "👩🏼\u200d❤️\u200d👩🏼": {}, + "👩🏼\u200d❤️\u200d👩🏽": {}, + "👩🏼\u200d❤️\u200d👩🏾": {}, + "👩🏼\u200d❤️\u200d👩🏿": {}, + "👩🏼\u200d❤️\u200d💋\u200d👨🏻": {}, + "👩🏼\u200d❤️\u200d💋\u200d👨🏼": {}, + "👩🏼\u200d❤️\u200d💋\u200d👨🏽": {}, + "👩🏼\u200d❤️\u200d💋\u200d👨🏾": {}, + "👩🏼\u200d❤️\u200d💋\u200d👨🏿": {}, + "👩🏼\u200d❤️\u200d💋\u200d👩🏻": {}, + "👩🏼\u200d❤️\u200d💋\u200d👩🏼": {}, + "👩🏼\u200d❤️\u200d💋\u200d👩🏽": {}, + "👩🏼\u200d❤️\u200d💋\u200d👩🏾": {}, + "👩🏼\u200d❤️\u200d💋\u200d👩🏿": {}, + "👩🏼\u200d🌾": {}, + "👩🏼\u200d🍳": {}, + "👩🏼\u200d🍼": {}, + "👩🏼\u200d🎓": {}, + "👩🏼\u200d🎤": {}, + "👩🏼\u200d🎨": {}, + "👩🏼\u200d🏫": {}, + "👩🏼\u200d🏭": {}, + "👩🏼\u200d🐰\u200d👩🏻": {}, + "👩🏼\u200d🐰\u200d👩🏽": {}, + "👩🏼\u200d🐰\u200d👩🏾": {}, + "👩🏼\u200d🐰\u200d👩🏿": {}, + "👩🏼\u200d💻": {}, + "👩🏼\u200d💼": {}, + "👩🏼\u200d🔧": {}, + "👩🏼\u200d🔬": {}, + "👩🏼\u200d🚀": {}, + "👩🏼\u200d🚒": {}, + "👩🏼\u200d🤝\u200d👨🏻": {}, + "👩🏼\u200d🤝\u200d👨🏽": {}, + "👩🏼\u200d🤝\u200d👨🏾": {}, + "👩🏼\u200d🤝\u200d👨🏿": {}, + "👩🏼\u200d🤝\u200d👩🏻": {}, + "👩🏼\u200d🤝\u200d👩🏽": {}, + "👩🏼\u200d🤝\u200d👩🏾": {}, + "👩🏼\u200d🤝\u200d👩🏿": {}, + "👩🏼\u200d🦯": {}, + "👩🏼\u200d🦯\u200d➡️": {}, + "👩🏼\u200d🦰": {}, + "👩🏼\u200d🦱": {}, + "👩🏼\u200d🦲": {}, + "👩🏼\u200d🦳": {}, + "👩🏼\u200d🦼": {}, + "👩🏼\u200d🦼\u200d➡️": {}, + "👩🏼\u200d🦽": {}, + "👩🏼\u200d🦽\u200d➡️": {}, + "👩🏼\u200d\U0001faef\u200d👩🏻": {}, + "👩🏼\u200d\U0001faef\u200d👩🏽": {}, + "👩🏼\u200d\U0001faef\u200d👩🏾": {}, + "👩🏼\u200d\U0001faef\u200d👩🏿": {}, + "👩🏽": {}, + "👩🏽\u200d⚕️": {}, + "👩🏽\u200d⚖️": {}, + "👩🏽\u200d✈️": {}, + "👩🏽\u200d❤️\u200d👨🏻": {}, + "👩🏽\u200d❤️\u200d👨🏼": {}, + "👩🏽\u200d❤️\u200d👨🏽": {}, + "👩🏽\u200d❤️\u200d👨🏾": {}, + "👩🏽\u200d❤️\u200d👨🏿": {}, + "👩🏽\u200d❤️\u200d👩🏻": {}, + "👩🏽\u200d❤️\u200d👩🏼": {}, + "👩🏽\u200d❤️\u200d👩🏽": {}, + "👩🏽\u200d❤️\u200d👩🏾": {}, + "👩🏽\u200d❤️\u200d👩🏿": {}, + "👩🏽\u200d❤️\u200d💋\u200d👨🏻": {}, + "👩🏽\u200d❤️\u200d💋\u200d👨🏼": {}, + "👩🏽\u200d❤️\u200d💋\u200d👨🏽": {}, + "👩🏽\u200d❤️\u200d💋\u200d👨🏾": {}, + "👩🏽\u200d❤️\u200d💋\u200d👨🏿": {}, + "👩🏽\u200d❤️\u200d💋\u200d👩🏻": {}, + "👩🏽\u200d❤️\u200d💋\u200d👩🏼": {}, + "👩🏽\u200d❤️\u200d💋\u200d👩🏽": {}, + "👩🏽\u200d❤️\u200d💋\u200d👩🏾": {}, + "👩🏽\u200d❤️\u200d💋\u200d👩🏿": {}, + "👩🏽\u200d🌾": {}, + "👩🏽\u200d🍳": {}, + "👩🏽\u200d🍼": {}, + "👩🏽\u200d🎓": {}, + "👩🏽\u200d🎤": {}, + "👩🏽\u200d🎨": {}, + "👩🏽\u200d🏫": {}, + "👩🏽\u200d🏭": {}, + "👩🏽\u200d🐰\u200d👩🏻": {}, + "👩🏽\u200d🐰\u200d👩🏼": {}, + "👩🏽\u200d🐰\u200d👩🏾": {}, + "👩🏽\u200d🐰\u200d👩🏿": {}, + "👩🏽\u200d💻": {}, + "👩🏽\u200d💼": {}, + "👩🏽\u200d🔧": {}, + "👩🏽\u200d🔬": {}, + "👩🏽\u200d🚀": {}, + "👩🏽\u200d🚒": {}, + "👩🏽\u200d🤝\u200d👨🏻": {}, + "👩🏽\u200d🤝\u200d👨🏼": {}, + "👩🏽\u200d🤝\u200d👨🏾": {}, + "👩🏽\u200d🤝\u200d👨🏿": {}, + "👩🏽\u200d🤝\u200d👩🏻": {}, + "👩🏽\u200d🤝\u200d👩🏼": {}, + "👩🏽\u200d🤝\u200d👩🏾": {}, + "👩🏽\u200d🤝\u200d👩🏿": {}, + "👩🏽\u200d🦯": {}, + "👩🏽\u200d🦯\u200d➡️": {}, + "👩🏽\u200d🦰": {}, + "👩🏽\u200d🦱": {}, + "👩🏽\u200d🦲": {}, + "👩🏽\u200d🦳": {}, + "👩🏽\u200d🦼": {}, + "👩🏽\u200d🦼\u200d➡️": {}, + "👩🏽\u200d🦽": {}, + "👩🏽\u200d🦽\u200d➡️": {}, + "👩🏽\u200d\U0001faef\u200d👩🏻": {}, + "👩🏽\u200d\U0001faef\u200d👩🏼": {}, + "👩🏽\u200d\U0001faef\u200d👩🏾": {}, + "👩🏽\u200d\U0001faef\u200d👩🏿": {}, + "👩🏾": {}, + "👩🏾\u200d⚕️": {}, + "👩🏾\u200d⚖️": {}, + "👩🏾\u200d✈️": {}, + "👩🏾\u200d❤️\u200d👨🏻": {}, + "👩🏾\u200d❤️\u200d👨🏼": {}, + "👩🏾\u200d❤️\u200d👨🏽": {}, + "👩🏾\u200d❤️\u200d👨🏾": {}, + "👩🏾\u200d❤️\u200d👨🏿": {}, + "👩🏾\u200d❤️\u200d👩🏻": {}, + "👩🏾\u200d❤️\u200d👩🏼": {}, + "👩🏾\u200d❤️\u200d👩🏽": {}, + "👩🏾\u200d❤️\u200d👩🏾": {}, + "👩🏾\u200d❤️\u200d👩🏿": {}, + "👩🏾\u200d❤️\u200d💋\u200d👨🏻": {}, + "👩🏾\u200d❤️\u200d💋\u200d👨🏼": {}, + "👩🏾\u200d❤️\u200d💋\u200d👨🏽": {}, + "👩🏾\u200d❤️\u200d💋\u200d👨🏾": {}, + "👩🏾\u200d❤️\u200d💋\u200d👨🏿": {}, + "👩🏾\u200d❤️\u200d💋\u200d👩🏻": {}, + "👩🏾\u200d❤️\u200d💋\u200d👩🏼": {}, + "👩🏾\u200d❤️\u200d💋\u200d👩🏽": {}, + "👩🏾\u200d❤️\u200d💋\u200d👩🏾": {}, + "👩🏾\u200d❤️\u200d💋\u200d👩🏿": {}, + "👩🏾\u200d🌾": {}, + "👩🏾\u200d🍳": {}, + "👩🏾\u200d🍼": {}, + "👩🏾\u200d🎓": {}, + "👩🏾\u200d🎤": {}, + "👩🏾\u200d🎨": {}, + "👩🏾\u200d🏫": {}, + "👩🏾\u200d🏭": {}, + "👩🏾\u200d🐰\u200d👩🏻": {}, + "👩🏾\u200d🐰\u200d👩🏼": {}, + "👩🏾\u200d🐰\u200d👩🏽": {}, + "👩🏾\u200d🐰\u200d👩🏿": {}, + "👩🏾\u200d💻": {}, + "👩🏾\u200d💼": {}, + "👩🏾\u200d🔧": {}, + "👩🏾\u200d🔬": {}, + "👩🏾\u200d🚀": {}, + "👩🏾\u200d🚒": {}, + "👩🏾\u200d🤝\u200d👨🏻": {}, + "👩🏾\u200d🤝\u200d👨🏼": {}, + "👩🏾\u200d🤝\u200d👨🏽": {}, + "👩🏾\u200d🤝\u200d👨🏿": {}, + "👩🏾\u200d🤝\u200d👩🏻": {}, + "👩🏾\u200d🤝\u200d👩🏼": {}, + "👩🏾\u200d🤝\u200d👩🏽": {}, + "👩🏾\u200d🤝\u200d👩🏿": {}, + "👩🏾\u200d🦯": {}, + "👩🏾\u200d🦯\u200d➡️": {}, + "👩🏾\u200d🦰": {}, + "👩🏾\u200d🦱": {}, + "👩🏾\u200d🦲": {}, + "👩🏾\u200d🦳": {}, + "👩🏾\u200d🦼": {}, + "👩🏾\u200d🦼\u200d➡️": {}, + "👩🏾\u200d🦽": {}, + "👩🏾\u200d🦽\u200d➡️": {}, + "👩🏾\u200d\U0001faef\u200d👩🏻": {}, + "👩🏾\u200d\U0001faef\u200d👩🏼": {}, + "👩🏾\u200d\U0001faef\u200d👩🏽": {}, + "👩🏾\u200d\U0001faef\u200d👩🏿": {}, + "👩🏿": {}, + "👩🏿\u200d⚕️": {}, + "👩🏿\u200d⚖️": {}, + "👩🏿\u200d✈️": {}, + "👩🏿\u200d❤️\u200d👨🏻": {}, + "👩🏿\u200d❤️\u200d👨🏼": {}, + "👩🏿\u200d❤️\u200d👨🏽": {}, + "👩🏿\u200d❤️\u200d👨🏾": {}, + "👩🏿\u200d❤️\u200d👨🏿": {}, + "👩🏿\u200d❤️\u200d👩🏻": {}, + "👩🏿\u200d❤️\u200d👩🏼": {}, + "👩🏿\u200d❤️\u200d👩🏽": {}, + "👩🏿\u200d❤️\u200d👩🏾": {}, + "👩🏿\u200d❤️\u200d👩🏿": {}, + "👩🏿\u200d❤️\u200d💋\u200d👨🏻": {}, + "👩🏿\u200d❤️\u200d💋\u200d👨🏼": {}, + "👩🏿\u200d❤️\u200d💋\u200d👨🏽": {}, + "👩🏿\u200d❤️\u200d💋\u200d👨🏾": {}, + "👩🏿\u200d❤️\u200d💋\u200d👨🏿": {}, + "👩🏿\u200d❤️\u200d💋\u200d👩🏻": {}, + "👩🏿\u200d❤️\u200d💋\u200d👩🏼": {}, + "👩🏿\u200d❤️\u200d💋\u200d👩🏽": {}, + "👩🏿\u200d❤️\u200d💋\u200d👩🏾": {}, + "👩🏿\u200d❤️\u200d💋\u200d👩🏿": {}, + "👩🏿\u200d🌾": {}, + "👩🏿\u200d🍳": {}, + "👩🏿\u200d🍼": {}, + "👩🏿\u200d🎓": {}, + "👩🏿\u200d🎤": {}, + "👩🏿\u200d🎨": {}, + "👩🏿\u200d🏫": {}, + "👩🏿\u200d🏭": {}, + "👩🏿\u200d🐰\u200d👩🏻": {}, + "👩🏿\u200d🐰\u200d👩🏼": {}, + "👩🏿\u200d🐰\u200d👩🏽": {}, + "👩🏿\u200d🐰\u200d👩🏾": {}, + "👩🏿\u200d💻": {}, + "👩🏿\u200d💼": {}, + "👩🏿\u200d🔧": {}, + "👩🏿\u200d🔬": {}, + "👩🏿\u200d🚀": {}, + "👩🏿\u200d🚒": {}, + "👩🏿\u200d🤝\u200d👨🏻": {}, + "👩🏿\u200d🤝\u200d👨🏼": {}, + "👩🏿\u200d🤝\u200d👨🏽": {}, + "👩🏿\u200d🤝\u200d👨🏾": {}, + "👩🏿\u200d🤝\u200d👩🏻": {}, + "👩🏿\u200d🤝\u200d👩🏼": {}, + "👩🏿\u200d🤝\u200d👩🏽": {}, + "👩🏿\u200d🤝\u200d👩🏾": {}, + "👩🏿\u200d🦯": {}, + "👩🏿\u200d🦯\u200d➡️": {}, + "👩🏿\u200d🦰": {}, + "👩🏿\u200d🦱": {}, + "👩🏿\u200d🦲": {}, + "👩🏿\u200d🦳": {}, + "👩🏿\u200d🦼": {}, + "👩🏿\u200d🦼\u200d➡️": {}, + "👩🏿\u200d🦽": {}, + "👩🏿\u200d🦽\u200d➡️": {}, + "👩🏿\u200d\U0001faef\u200d👩🏻": {}, + "👩🏿\u200d\U0001faef\u200d👩🏼": {}, + "👩🏿\u200d\U0001faef\u200d👩🏽": {}, + "👩🏿\u200d\U0001faef\u200d👩🏾": {}, + "👪": {}, + "👫": {}, + "👫🏻": {}, + "👫🏼": {}, + "👫🏽": {}, + "👫🏾": {}, + "👫🏿": {}, + "👬": {}, + "👬🏻": {}, + "👬🏼": {}, + "👬🏽": {}, + "👬🏾": {}, + "👬🏿": {}, + "👭": {}, + "👭🏻": {}, + "👭🏼": {}, + "👭🏽": {}, + "👭🏾": {}, + "👭🏿": {}, + "👮": {}, + "👮\u200d♀️": {}, + "👮\u200d♂️": {}, + "👮🏻": {}, + "👮🏻\u200d♀️": {}, + "👮🏻\u200d♂️": {}, + "👮🏼": {}, + "👮🏼\u200d♀️": {}, + "👮🏼\u200d♂️": {}, + "👮🏽": {}, + "👮🏽\u200d♀️": {}, + "👮🏽\u200d♂️": {}, + "👮🏾": {}, + "👮🏾\u200d♀️": {}, + "👮🏾\u200d♂️": {}, + "👮🏿": {}, + "👮🏿\u200d♀️": {}, + "👮🏿\u200d♂️": {}, + "👯": {}, + "👯\u200d♀️": {}, + "👯\u200d♂️": {}, + "👯🏻": {}, + "👯🏻\u200d♀️": {}, + "👯🏻\u200d♂️": {}, + "👯🏼": {}, + "👯🏼\u200d♀️": {}, + "👯🏼\u200d♂️": {}, + "👯🏽": {}, + "👯🏽\u200d♀️": {}, + "👯🏽\u200d♂️": {}, + "👯🏾": {}, + "👯🏾\u200d♀️": {}, + "👯🏾\u200d♂️": {}, + "👯🏿": {}, + "👯🏿\u200d♀️": {}, + "👯🏿\u200d♂️": {}, + "👰": {}, + "👰\u200d♀️": {}, + "👰\u200d♂️": {}, + "👰🏻": {}, + "👰🏻\u200d♀️": {}, + "👰🏻\u200d♂️": {}, + "👰🏼": {}, + "👰🏼\u200d♀️": {}, + "👰🏼\u200d♂️": {}, + "👰🏽": {}, + "👰🏽\u200d♀️": {}, + "👰🏽\u200d♂️": {}, + "👰🏾": {}, + "👰🏾\u200d♀️": {}, + "👰🏾\u200d♂️": {}, + "👰🏿": {}, + "👰🏿\u200d♀️": {}, + "👰🏿\u200d♂️": {}, + "👱": {}, + "👱\u200d♀️": {}, + "👱\u200d♂️": {}, + "👱🏻": {}, + "👱🏻\u200d♀️": {}, + "👱🏻\u200d♂️": {}, + "👱🏼": {}, + "👱🏼\u200d♀️": {}, + "👱🏼\u200d♂️": {}, + "👱🏽": {}, + "👱🏽\u200d♀️": {}, + "👱🏽\u200d♂️": {}, + "👱🏾": {}, + "👱🏾\u200d♀️": {}, + "👱🏾\u200d♂️": {}, + "👱🏿": {}, + "👱🏿\u200d♀️": {}, + "👱🏿\u200d♂️": {}, + "👲": {}, + "👲🏻": {}, + "👲🏼": {}, + "👲🏽": {}, + "👲🏾": {}, + "👲🏿": {}, + "👳": {}, + "👳\u200d♀️": {}, + "👳\u200d♂️": {}, + "👳🏻": {}, + "👳🏻\u200d♀️": {}, + "👳🏻\u200d♂️": {}, + "👳🏼": {}, + "👳🏼\u200d♀️": {}, + "👳🏼\u200d♂️": {}, + "👳🏽": {}, + "👳🏽\u200d♀️": {}, + "👳🏽\u200d♂️": {}, + "👳🏾": {}, + "👳🏾\u200d♀️": {}, + "👳🏾\u200d♂️": {}, + "👳🏿": {}, + "👳🏿\u200d♀️": {}, + "👳🏿\u200d♂️": {}, + "👴": {}, + "👴🏻": {}, + "👴🏼": {}, + "👴🏽": {}, + "👴🏾": {}, + "👴🏿": {}, + "👵": {}, + "👵🏻": {}, + "👵🏼": {}, + "👵🏽": {}, + "👵🏾": {}, + "👵🏿": {}, + "👶": {}, + "👶🏻": {}, + "👶🏼": {}, + "👶🏽": {}, + "👶🏾": {}, + "👶🏿": {}, + "👷": {}, + "👷\u200d♀️": {}, + "👷\u200d♂️": {}, + "👷🏻": {}, + "👷🏻\u200d♀️": {}, + "👷🏻\u200d♂️": {}, + "👷🏼": {}, + "👷🏼\u200d♀️": {}, + "👷🏼\u200d♂️": {}, + "👷🏽": {}, + "👷🏽\u200d♀️": {}, + "👷🏽\u200d♂️": {}, + "👷🏾": {}, + "👷🏾\u200d♀️": {}, + "👷🏾\u200d♂️": {}, + "👷🏿": {}, + "👷🏿\u200d♀️": {}, + "👷🏿\u200d♂️": {}, + "👸": {}, + "👸🏻": {}, + "👸🏼": {}, + "👸🏽": {}, + "👸🏾": {}, + "👸🏿": {}, + "👹": {}, + "👺": {}, + "👻": {}, + "👼": {}, + "👼🏻": {}, + "👼🏼": {}, + "👼🏽": {}, + "👼🏾": {}, + "👼🏿": {}, + "👽": {}, + "👾": {}, + "👿": {}, + "💀": {}, + "💁": {}, + "💁\u200d♀️": {}, + "💁\u200d♂️": {}, + "💁🏻": {}, + "💁🏻\u200d♀️": {}, + "💁🏻\u200d♂️": {}, + "💁🏼": {}, + "💁🏼\u200d♀️": {}, + "💁🏼\u200d♂️": {}, + "💁🏽": {}, + "💁🏽\u200d♀️": {}, + "💁🏽\u200d♂️": {}, + "💁🏾": {}, + "💁🏾\u200d♀️": {}, + "💁🏾\u200d♂️": {}, + "💁🏿": {}, + "💁🏿\u200d♀️": {}, + "💁🏿\u200d♂️": {}, + "💂": {}, + "💂\u200d♀️": {}, + "💂\u200d♂️": {}, + "💂🏻": {}, + "💂🏻\u200d♀️": {}, + "💂🏻\u200d♂️": {}, + "💂🏼": {}, + "💂🏼\u200d♀️": {}, + "💂🏼\u200d♂️": {}, + "💂🏽": {}, + "💂🏽\u200d♀️": {}, + "💂🏽\u200d♂️": {}, + "💂🏾": {}, + "💂🏾\u200d♀️": {}, + "💂🏾\u200d♂️": {}, + "💂🏿": {}, + "💂🏿\u200d♀️": {}, + "💂🏿\u200d♂️": {}, + "💃": {}, + "💃🏻": {}, + "💃🏼": {}, + "💃🏽": {}, + "💃🏾": {}, + "💃🏿": {}, + "💄": {}, + "💅": {}, + "💅🏻": {}, + "💅🏼": {}, + "💅🏽": {}, + "💅🏾": {}, + "💅🏿": {}, + "💆": {}, + "💆\u200d♀️": {}, + "💆\u200d♂️": {}, + "💆🏻": {}, + "💆🏻\u200d♀️": {}, + "💆🏻\u200d♂️": {}, + "💆🏼": {}, + "💆🏼\u200d♀️": {}, + "💆🏼\u200d♂️": {}, + "💆🏽": {}, + "💆🏽\u200d♀️": {}, + "💆🏽\u200d♂️": {}, + "💆🏾": {}, + "💆🏾\u200d♀️": {}, + "💆🏾\u200d♂️": {}, + "💆🏿": {}, + "💆🏿\u200d♀️": {}, + "💆🏿\u200d♂️": {}, + "💇": {}, + "💇\u200d♀️": {}, + "💇\u200d♂️": {}, + "💇🏻": {}, + "💇🏻\u200d♀️": {}, + "💇🏻\u200d♂️": {}, + "💇🏼": {}, + "💇🏼\u200d♀️": {}, + "💇🏼\u200d♂️": {}, + "💇🏽": {}, + "💇🏽\u200d♀️": {}, + "💇🏽\u200d♂️": {}, + "💇🏾": {}, + "💇🏾\u200d♀️": {}, + "💇🏾\u200d♂️": {}, + "💇🏿": {}, + "💇🏿\u200d♀️": {}, + "💇🏿\u200d♂️": {}, + "💈": {}, + "💉": {}, + "💊": {}, + "💋": {}, + "💌": {}, + "💍": {}, + "💎": {}, + "💏": {}, + "💏🏻": {}, + "💏🏼": {}, + "💏🏽": {}, + "💏🏾": {}, + "💏🏿": {}, + "💐": {}, + "💑": {}, + "💑🏻": {}, + "💑🏼": {}, + "💑🏽": {}, + "💑🏾": {}, + "💑🏿": {}, + "💒": {}, + "💓": {}, + "💔": {}, + "💕": {}, + "💖": {}, + "💗": {}, + "💘": {}, + "💙": {}, + "💚": {}, + "💛": {}, + "💜": {}, + "💝": {}, + "💞": {}, + "💟": {}, + "💠": {}, + "💡": {}, + "💢": {}, + "💣": {}, + "💤": {}, + "💥": {}, + "💦": {}, + "💧": {}, + "💨": {}, + "💩": {}, + "💪": {}, + "💪🏻": {}, + "💪🏼": {}, + "💪🏽": {}, + "💪🏾": {}, + "💪🏿": {}, + "💫": {}, + "💬": {}, + "💭": {}, + "💮": {}, + "💯": {}, + "💰": {}, + "💱": {}, + "💲": {}, + "💳": {}, + "💴": {}, + "💵": {}, + "💶": {}, + "💷": {}, + "💸": {}, + "💹": {}, + "💺": {}, + "💻": {}, + "💼": {}, + "💽": {}, + "💾": {}, + "💿": {}, + "📀": {}, + "📁": {}, + "📂": {}, + "📃": {}, + "📄": {}, + "📅": {}, + "📆": {}, + "📇": {}, + "📈": {}, + "📉": {}, + "📊": {}, + "📋": {}, + "📌": {}, + "📍": {}, + "📎": {}, + "📏": {}, + "📐": {}, + "📑": {}, + "📒": {}, + "📓": {}, + "📔": {}, + "📕": {}, + "📖": {}, + "📗": {}, + "📘": {}, + "📙": {}, + "📚": {}, + "📛": {}, + "📜": {}, + "📝": {}, + "📞": {}, + "📟": {}, + "📠": {}, + "📡": {}, + "📢": {}, + "📣": {}, + "📤": {}, + "📥": {}, + "📦": {}, + "📧": {}, + "📨": {}, + "📩": {}, + "📪": {}, + "📫": {}, + "📬": {}, + "📭": {}, + "📮": {}, + "📯": {}, + "📰": {}, + "📱": {}, + "📲": {}, + "📳": {}, + "📴": {}, + "📵": {}, + "📶": {}, + "📷": {}, + "📸": {}, + "📹": {}, + "📺": {}, + "📻": {}, + "📼": {}, + "📽️": {}, + "📿": {}, + "🔀": {}, + "🔁": {}, + "🔂": {}, + "🔃": {}, + "🔄": {}, + "🔅": {}, + "🔆": {}, + "🔇": {}, + "🔈": {}, + "🔉": {}, + "🔊": {}, + "🔋": {}, + "🔌": {}, + "🔍": {}, + "🔎": {}, + "🔏": {}, + "🔐": {}, + "🔑": {}, + "🔒": {}, + "🔓": {}, + "🔔": {}, + "🔕": {}, + "🔖": {}, + "🔗": {}, + "🔘": {}, + "🔙": {}, + "🔚": {}, + "🔛": {}, + "🔜": {}, + "🔝": {}, + "🔞": {}, + "🔟": {}, + "🔠": {}, + "🔡": {}, + "🔢": {}, + "🔣": {}, + "🔤": {}, + "🔥": {}, + "🔦": {}, + "🔧": {}, + "🔨": {}, + "🔩": {}, + "🔪": {}, + "🔫": {}, + "🔬": {}, + "🔭": {}, + "🔮": {}, + "🔯": {}, + "🔰": {}, + "🔱": {}, + "🔲": {}, + "🔳": {}, + "🔴": {}, + "🔵": {}, + "🔶": {}, + "🔷": {}, + "🔸": {}, + "🔹": {}, + "🔺": {}, + "🔻": {}, + "🔼": {}, + "🔽": {}, + "🕉️": {}, + "🕊️": {}, + "🕋": {}, + "🕌": {}, + "🕍": {}, + "🕎": {}, + "🕐": {}, + "🕑": {}, + "🕒": {}, + "🕓": {}, + "🕔": {}, + "🕕": {}, + "🕖": {}, + "🕗": {}, + "🕘": {}, + "🕙": {}, + "🕚": {}, + "🕛": {}, + "🕜": {}, + "🕝": {}, + "🕞": {}, + "🕟": {}, + "🕠": {}, + "🕡": {}, + "🕢": {}, + "🕣": {}, + "🕤": {}, + "🕥": {}, + "🕦": {}, + "🕧": {}, + "🕯️": {}, + "🕰️": {}, + "🕳️": {}, + "🕴️": {}, + "🕴🏻": {}, + "🕴🏼": {}, + "🕴🏽": {}, + "🕴🏾": {}, + "🕴🏿": {}, + "🕵️": {}, + "🕵️\u200d♀️": {}, + "🕵️\u200d♂️": {}, + "🕵🏻": {}, + "🕵🏻\u200d♀️": {}, + "🕵🏻\u200d♂️": {}, + "🕵🏼": {}, + "🕵🏼\u200d♀️": {}, + "🕵🏼\u200d♂️": {}, + "🕵🏽": {}, + "🕵🏽\u200d♀️": {}, + "🕵🏽\u200d♂️": {}, + "🕵🏾": {}, + "🕵🏾\u200d♀️": {}, + "🕵🏾\u200d♂️": {}, + "🕵🏿": {}, + "🕵🏿\u200d♀️": {}, + "🕵🏿\u200d♂️": {}, + "🕶️": {}, + "🕷️": {}, + "🕸️": {}, + "🕹️": {}, + "🕺": {}, + "🕺🏻": {}, + "🕺🏼": {}, + "🕺🏽": {}, + "🕺🏾": {}, + "🕺🏿": {}, + "🖇️": {}, + "🖊️": {}, + "🖋️": {}, + "🖌️": {}, + "🖍️": {}, + "🖐️": {}, + "🖐🏻": {}, + "🖐🏼": {}, + "🖐🏽": {}, + "🖐🏾": {}, + "🖐🏿": {}, + "🖕": {}, + "🖕🏻": {}, + "🖕🏼": {}, + "🖕🏽": {}, + "🖕🏾": {}, + "🖕🏿": {}, + "🖖": {}, + "🖖🏻": {}, + "🖖🏼": {}, + "🖖🏽": {}, + "🖖🏾": {}, + "🖖🏿": {}, + "🖤": {}, + "🖥️": {}, + "🖨️": {}, + "🖱️": {}, + "🖲️": {}, + "🖼️": {}, + "🗂️": {}, + "🗃️": {}, + "🗄️": {}, + "🗑️": {}, + "🗒️": {}, + "🗓️": {}, + "🗜️": {}, + "🗝️": {}, + "🗞️": {}, + "🗡️": {}, + "🗣️": {}, + "🗨️": {}, + "🗯️": {}, + "🗳️": {}, + "🗺️": {}, + "🗻": {}, + "🗼": {}, + "🗽": {}, + "🗾": {}, + "🗿": {}, + "😀": {}, + "😁": {}, + "😂": {}, + "😃": {}, + "😄": {}, + "😅": {}, + "😆": {}, + "😇": {}, + "😈": {}, + "😉": {}, + "😊": {}, + "😋": {}, + "😌": {}, + "😍": {}, + "😎": {}, + "😏": {}, + "😐": {}, + "😑": {}, + "😒": {}, + "😓": {}, + "😔": {}, + "😕": {}, + "😖": {}, + "😗": {}, + "😘": {}, + "😙": {}, + "😚": {}, + "😛": {}, + "😜": {}, + "😝": {}, + "😞": {}, + "😟": {}, + "😠": {}, + "😡": {}, + "😢": {}, + "😣": {}, + "😤": {}, + "😥": {}, + "😦": {}, + "😧": {}, + "😨": {}, + "😩": {}, + "😪": {}, + "😫": {}, + "😬": {}, + "😭": {}, + "😮": {}, + "😮\u200d💨": {}, + "😯": {}, + "😰": {}, + "😱": {}, + "😲": {}, + "😳": {}, + "😴": {}, + "😵": {}, + "😵\u200d💫": {}, + "😶": {}, + "😶\u200d🌫️": {}, + "😷": {}, + "😸": {}, + "😹": {}, + "😺": {}, + "😻": {}, + "😼": {}, + "😽": {}, + "😾": {}, + "😿": {}, + "🙀": {}, + "🙁": {}, + "🙂": {}, + "🙂\u200d↔️": {}, + "🙂\u200d↕️": {}, + "🙃": {}, + "🙄": {}, + "🙅": {}, + "🙅\u200d♀️": {}, + "🙅\u200d♂️": {}, + "🙅🏻": {}, + "🙅🏻\u200d♀️": {}, + "🙅🏻\u200d♂️": {}, + "🙅🏼": {}, + "🙅🏼\u200d♀️": {}, + "🙅🏼\u200d♂️": {}, + "🙅🏽": {}, + "🙅🏽\u200d♀️": {}, + "🙅🏽\u200d♂️": {}, + "🙅🏾": {}, + "🙅🏾\u200d♀️": {}, + "🙅🏾\u200d♂️": {}, + "🙅🏿": {}, + "🙅🏿\u200d♀️": {}, + "🙅🏿\u200d♂️": {}, + "🙆": {}, + "🙆\u200d♀️": {}, + "🙆\u200d♂️": {}, + "🙆🏻": {}, + "🙆🏻\u200d♀️": {}, + "🙆🏻\u200d♂️": {}, + "🙆🏼": {}, + "🙆🏼\u200d♀️": {}, + "🙆🏼\u200d♂️": {}, + "🙆🏽": {}, + "🙆🏽\u200d♀️": {}, + "🙆🏽\u200d♂️": {}, + "🙆🏾": {}, + "🙆🏾\u200d♀️": {}, + "🙆🏾\u200d♂️": {}, + "🙆🏿": {}, + "🙆🏿\u200d♀️": {}, + "🙆🏿\u200d♂️": {}, + "🙇": {}, + "🙇\u200d♀️": {}, + "🙇\u200d♂️": {}, + "🙇🏻": {}, + "🙇🏻\u200d♀️": {}, + "🙇🏻\u200d♂️": {}, + "🙇🏼": {}, + "🙇🏼\u200d♀️": {}, + "🙇🏼\u200d♂️": {}, + "🙇🏽": {}, + "🙇🏽\u200d♀️": {}, + "🙇🏽\u200d♂️": {}, + "🙇🏾": {}, + "🙇🏾\u200d♀️": {}, + "🙇🏾\u200d♂️": {}, + "🙇🏿": {}, + "🙇🏿\u200d♀️": {}, + "🙇🏿\u200d♂️": {}, + "🙈": {}, + "🙉": {}, + "🙊": {}, + "🙋": {}, + "🙋\u200d♀️": {}, + "🙋\u200d♂️": {}, + "🙋🏻": {}, + "🙋🏻\u200d♀️": {}, + "🙋🏻\u200d♂️": {}, + "🙋🏼": {}, + "🙋🏼\u200d♀️": {}, + "🙋🏼\u200d♂️": {}, + "🙋🏽": {}, + "🙋🏽\u200d♀️": {}, + "🙋🏽\u200d♂️": {}, + "🙋🏾": {}, + "🙋🏾\u200d♀️": {}, + "🙋🏾\u200d♂️": {}, + "🙋🏿": {}, + "🙋🏿\u200d♀️": {}, + "🙋🏿\u200d♂️": {}, + "🙌": {}, + "🙌🏻": {}, + "🙌🏼": {}, + "🙌🏽": {}, + "🙌🏾": {}, + "🙌🏿": {}, + "🙍": {}, + "🙍\u200d♀️": {}, + "🙍\u200d♂️": {}, + "🙍🏻": {}, + "🙍🏻\u200d♀️": {}, + "🙍🏻\u200d♂️": {}, + "🙍🏼": {}, + "🙍🏼\u200d♀️": {}, + "🙍🏼\u200d♂️": {}, + "🙍🏽": {}, + "🙍🏽\u200d♀️": {}, + "🙍🏽\u200d♂️": {}, + "🙍🏾": {}, + "🙍🏾\u200d♀️": {}, + "🙍🏾\u200d♂️": {}, + "🙍🏿": {}, + "🙍🏿\u200d♀️": {}, + "🙍🏿\u200d♂️": {}, + "🙎": {}, + "🙎\u200d♀️": {}, + "🙎\u200d♂️": {}, + "🙎🏻": {}, + "🙎🏻\u200d♀️": {}, + "🙎🏻\u200d♂️": {}, + "🙎🏼": {}, + "🙎🏼\u200d♀️": {}, + "🙎🏼\u200d♂️": {}, + "🙎🏽": {}, + "🙎🏽\u200d♀️": {}, + "🙎🏽\u200d♂️": {}, + "🙎🏾": {}, + "🙎🏾\u200d♀️": {}, + "🙎🏾\u200d♂️": {}, + "🙎🏿": {}, + "🙎🏿\u200d♀️": {}, + "🙎🏿\u200d♂️": {}, + "🙏": {}, + "🙏🏻": {}, + "🙏🏼": {}, + "🙏🏽": {}, + "🙏🏾": {}, + "🙏🏿": {}, + "🚀": {}, + "🚁": {}, + "🚂": {}, + "🚃": {}, + "🚄": {}, + "🚅": {}, + "🚆": {}, + "🚇": {}, + "🚈": {}, + "🚉": {}, + "🚊": {}, + "🚋": {}, + "🚌": {}, + "🚍": {}, + "🚎": {}, + "🚏": {}, + "🚐": {}, + "🚑": {}, + "🚒": {}, + "🚓": {}, + "🚔": {}, + "🚕": {}, + "🚖": {}, + "🚗": {}, + "🚘": {}, + "🚙": {}, + "🚚": {}, + "🚛": {}, + "🚜": {}, + "🚝": {}, + "🚞": {}, + "🚟": {}, + "🚠": {}, + "🚡": {}, + "🚢": {}, + "🚣": {}, + "🚣\u200d♀️": {}, + "🚣\u200d♂️": {}, + "🚣🏻": {}, + "🚣🏻\u200d♀️": {}, + "🚣🏻\u200d♂️": {}, + "🚣🏼": {}, + "🚣🏼\u200d♀️": {}, + "🚣🏼\u200d♂️": {}, + "🚣🏽": {}, + "🚣🏽\u200d♀️": {}, + "🚣🏽\u200d♂️": {}, + "🚣🏾": {}, + "🚣🏾\u200d♀️": {}, + "🚣🏾\u200d♂️": {}, + "🚣🏿": {}, + "🚣🏿\u200d♀️": {}, + "🚣🏿\u200d♂️": {}, + "🚤": {}, + "🚥": {}, + "🚦": {}, + "🚧": {}, + "🚨": {}, + "🚩": {}, + "🚪": {}, + "🚫": {}, + "🚬": {}, + "🚭": {}, + "🚮": {}, + "🚯": {}, + "🚰": {}, + "🚱": {}, + "🚲": {}, + "🚳": {}, + "🚴": {}, + "🚴\u200d♀️": {}, + "🚴\u200d♂️": {}, + "🚴🏻": {}, + "🚴🏻\u200d♀️": {}, + "🚴🏻\u200d♂️": {}, + "🚴🏼": {}, + "🚴🏼\u200d♀️": {}, + "🚴🏼\u200d♂️": {}, + "🚴🏽": {}, + "🚴🏽\u200d♀️": {}, + "🚴🏽\u200d♂️": {}, + "🚴🏾": {}, + "🚴🏾\u200d♀️": {}, + "🚴🏾\u200d♂️": {}, + "🚴🏿": {}, + "🚴🏿\u200d♀️": {}, + "🚴🏿\u200d♂️": {}, + "🚵": {}, + "🚵\u200d♀️": {}, + "🚵\u200d♂️": {}, + "🚵🏻": {}, + "🚵🏻\u200d♀️": {}, + "🚵🏻\u200d♂️": {}, + "🚵🏼": {}, + "🚵🏼\u200d♀️": {}, + "🚵🏼\u200d♂️": {}, + "🚵🏽": {}, + "🚵🏽\u200d♀️": {}, + "🚵🏽\u200d♂️": {}, + "🚵🏾": {}, + "🚵🏾\u200d♀️": {}, + "🚵🏾\u200d♂️": {}, + "🚵🏿": {}, + "🚵🏿\u200d♀️": {}, + "🚵🏿\u200d♂️": {}, + "🚶": {}, + "🚶\u200d♀️": {}, + "🚶\u200d♀️\u200d➡️": {}, + "🚶\u200d♂️": {}, + "🚶\u200d♂️\u200d➡️": {}, + "🚶\u200d➡️": {}, + "🚶🏻": {}, + "🚶🏻\u200d♀️": {}, + "🚶🏻\u200d♀️\u200d➡️": {}, + "🚶🏻\u200d♂️": {}, + "🚶🏻\u200d♂️\u200d➡️": {}, + "🚶🏻\u200d➡️": {}, + "🚶🏼": {}, + "🚶🏼\u200d♀️": {}, + "🚶🏼\u200d♀️\u200d➡️": {}, + "🚶🏼\u200d♂️": {}, + "🚶🏼\u200d♂️\u200d➡️": {}, + "🚶🏼\u200d➡️": {}, + "🚶🏽": {}, + "🚶🏽\u200d♀️": {}, + "🚶🏽\u200d♀️\u200d➡️": {}, + "🚶🏽\u200d♂️": {}, + "🚶🏽\u200d♂️\u200d➡️": {}, + "🚶🏽\u200d➡️": {}, + "🚶🏾": {}, + "🚶🏾\u200d♀️": {}, + "🚶🏾\u200d♀️\u200d➡️": {}, + "🚶🏾\u200d♂️": {}, + "🚶🏾\u200d♂️\u200d➡️": {}, + "🚶🏾\u200d➡️": {}, + "🚶🏿": {}, + "🚶🏿\u200d♀️": {}, + "🚶🏿\u200d♀️\u200d➡️": {}, + "🚶🏿\u200d♂️": {}, + "🚶🏿\u200d♂️\u200d➡️": {}, + "🚶🏿\u200d➡️": {}, + "🚷": {}, + "🚸": {}, + "🚹": {}, + "🚺": {}, + "🚻": {}, + "🚼": {}, + "🚽": {}, + "🚾": {}, + "🚿": {}, + "🛀": {}, + "🛀🏻": {}, + "🛀🏼": {}, + "🛀🏽": {}, + "🛀🏾": {}, + "🛀🏿": {}, + "🛁": {}, + "🛂": {}, + "🛃": {}, + "🛄": {}, + "🛅": {}, + "🛋️": {}, + "🛌": {}, + "🛌🏻": {}, + "🛌🏼": {}, + "🛌🏽": {}, + "🛌🏾": {}, + "🛌🏿": {}, + "🛍️": {}, + "🛎️": {}, + "🛏️": {}, + "🛐": {}, + "🛑": {}, + "🛒": {}, + "🛕": {}, + "🛖": {}, + "🛗": {}, + "\U0001f6d8": {}, + "🛜": {}, + "🛝": {}, + "🛞": {}, + "🛟": {}, + "🛠️": {}, + "🛡️": {}, + "🛢️": {}, + "🛣️": {}, + "🛤️": {}, + "🛥️": {}, + "🛩️": {}, + "🛫": {}, + "🛬": {}, + "🛰️": {}, + "🛳️": {}, + "🛴": {}, + "🛵": {}, + "🛶": {}, + "🛷": {}, + "🛸": {}, + "🛹": {}, + "🛺": {}, + "🛻": {}, + "🛼": {}, + "🟠": {}, + "🟡": {}, + "🟢": {}, + "🟣": {}, + "🟤": {}, + "🟥": {}, + "🟦": {}, + "🟧": {}, + "🟨": {}, + "🟩": {}, + "🟪": {}, + "🟫": {}, + "🟰": {}, + "🤌": {}, + "🤌🏻": {}, + "🤌🏼": {}, + "🤌🏽": {}, + "🤌🏾": {}, + "🤌🏿": {}, + "🤍": {}, + "🤎": {}, + "🤏": {}, + "🤏🏻": {}, + "🤏🏼": {}, + "🤏🏽": {}, + "🤏🏾": {}, + "🤏🏿": {}, + "🤐": {}, + "🤑": {}, + "🤒": {}, + "🤓": {}, + "🤔": {}, + "🤕": {}, + "🤖": {}, + "🤗": {}, + "🤘": {}, + "🤘🏻": {}, + "🤘🏼": {}, + "🤘🏽": {}, + "🤘🏾": {}, + "🤘🏿": {}, + "🤙": {}, + "🤙🏻": {}, + "🤙🏼": {}, + "🤙🏽": {}, + "🤙🏾": {}, + "🤙🏿": {}, + "🤚": {}, + "🤚🏻": {}, + "🤚🏼": {}, + "🤚🏽": {}, + "🤚🏾": {}, + "🤚🏿": {}, + "🤛": {}, + "🤛🏻": {}, + "🤛🏼": {}, + "🤛🏽": {}, + "🤛🏾": {}, + "🤛🏿": {}, + "🤜": {}, + "🤜🏻": {}, + "🤜🏼": {}, + "🤜🏽": {}, + "🤜🏾": {}, + "🤜🏿": {}, + "🤝": {}, + "🤝🏻": {}, + "🤝🏼": {}, + "🤝🏽": {}, + "🤝🏾": {}, + "🤝🏿": {}, + "🤞": {}, + "🤞🏻": {}, + "🤞🏼": {}, + "🤞🏽": {}, + "🤞🏾": {}, + "🤞🏿": {}, + "🤟": {}, + "🤟🏻": {}, + "🤟🏼": {}, + "🤟🏽": {}, + "🤟🏾": {}, + "🤟🏿": {}, + "🤠": {}, + "🤡": {}, + "🤢": {}, + "🤣": {}, + "🤤": {}, + "🤥": {}, + "🤦": {}, + "🤦\u200d♀️": {}, + "🤦\u200d♂️": {}, + "🤦🏻": {}, + "🤦🏻\u200d♀️": {}, + "🤦🏻\u200d♂️": {}, + "🤦🏼": {}, + "🤦🏼\u200d♀️": {}, + "🤦🏼\u200d♂️": {}, + "🤦🏽": {}, + "🤦🏽\u200d♀️": {}, + "🤦🏽\u200d♂️": {}, + "🤦🏾": {}, + "🤦🏾\u200d♀️": {}, + "🤦🏾\u200d♂️": {}, + "🤦🏿": {}, + "🤦🏿\u200d♀️": {}, + "🤦🏿\u200d♂️": {}, + "🤧": {}, + "🤨": {}, + "🤩": {}, + "🤪": {}, + "🤫": {}, + "🤬": {}, + "🤭": {}, + "🤮": {}, + "🤯": {}, + "🤰": {}, + "🤰🏻": {}, + "🤰🏼": {}, + "🤰🏽": {}, + "🤰🏾": {}, + "🤰🏿": {}, + "🤱": {}, + "🤱🏻": {}, + "🤱🏼": {}, + "🤱🏽": {}, + "🤱🏾": {}, + "🤱🏿": {}, + "🤲": {}, + "🤲🏻": {}, + "🤲🏼": {}, + "🤲🏽": {}, + "🤲🏾": {}, + "🤲🏿": {}, + "🤳": {}, + "🤳🏻": {}, + "🤳🏼": {}, + "🤳🏽": {}, + "🤳🏾": {}, + "🤳🏿": {}, + "🤴": {}, + "🤴🏻": {}, + "🤴🏼": {}, + "🤴🏽": {}, + "🤴🏾": {}, + "🤴🏿": {}, + "🤵": {}, + "🤵\u200d♀️": {}, + "🤵\u200d♂️": {}, + "🤵🏻": {}, + "🤵🏻\u200d♀️": {}, + "🤵🏻\u200d♂️": {}, + "🤵🏼": {}, + "🤵🏼\u200d♀️": {}, + "🤵🏼\u200d♂️": {}, + "🤵🏽": {}, + "🤵🏽\u200d♀️": {}, + "🤵🏽\u200d♂️": {}, + "🤵🏾": {}, + "🤵🏾\u200d♀️": {}, + "🤵🏾\u200d♂️": {}, + "🤵🏿": {}, + "🤵🏿\u200d♀️": {}, + "🤵🏿\u200d♂️": {}, + "🤶": {}, + "🤶🏻": {}, + "🤶🏼": {}, + "🤶🏽": {}, + "🤶🏾": {}, + "🤶🏿": {}, + "🤷": {}, + "🤷\u200d♀️": {}, + "🤷\u200d♂️": {}, + "🤷🏻": {}, + "🤷🏻\u200d♀️": {}, + "🤷🏻\u200d♂️": {}, + "🤷🏼": {}, + "🤷🏼\u200d♀️": {}, + "🤷🏼\u200d♂️": {}, + "🤷🏽": {}, + "🤷🏽\u200d♀️": {}, + "🤷🏽\u200d♂️": {}, + "🤷🏾": {}, + "🤷🏾\u200d♀️": {}, + "🤷🏾\u200d♂️": {}, + "🤷🏿": {}, + "🤷🏿\u200d♀️": {}, + "🤷🏿\u200d♂️": {}, + "🤸": {}, + "🤸\u200d♀️": {}, + "🤸\u200d♂️": {}, + "🤸🏻": {}, + "🤸🏻\u200d♀️": {}, + "🤸🏻\u200d♂️": {}, + "🤸🏼": {}, + "🤸🏼\u200d♀️": {}, + "🤸🏼\u200d♂️": {}, + "🤸🏽": {}, + "🤸🏽\u200d♀️": {}, + "🤸🏽\u200d♂️": {}, + "🤸🏾": {}, + "🤸🏾\u200d♀️": {}, + "🤸🏾\u200d♂️": {}, + "🤸🏿": {}, + "🤸🏿\u200d♀️": {}, + "🤸🏿\u200d♂️": {}, + "🤹": {}, + "🤹\u200d♀️": {}, + "🤹\u200d♂️": {}, + "🤹🏻": {}, + "🤹🏻\u200d♀️": {}, + "🤹🏻\u200d♂️": {}, + "🤹🏼": {}, + "🤹🏼\u200d♀️": {}, + "🤹🏼\u200d♂️": {}, + "🤹🏽": {}, + "🤹🏽\u200d♀️": {}, + "🤹🏽\u200d♂️": {}, + "🤹🏾": {}, + "🤹🏾\u200d♀️": {}, + "🤹🏾\u200d♂️": {}, + "🤹🏿": {}, + "🤹🏿\u200d♀️": {}, + "🤹🏿\u200d♂️": {}, + "🤺": {}, + "🤼": {}, + "🤼\u200d♀️": {}, + "🤼\u200d♂️": {}, + "🤼🏻": {}, + "🤼🏻\u200d♀️": {}, + "🤼🏻\u200d♂️": {}, + "🤼🏼": {}, + "🤼🏼\u200d♀️": {}, + "🤼🏼\u200d♂️": {}, + "🤼🏽": {}, + "🤼🏽\u200d♀️": {}, + "🤼🏽\u200d♂️": {}, + "🤼🏾": {}, + "🤼🏾\u200d♀️": {}, + "🤼🏾\u200d♂️": {}, + "🤼🏿": {}, + "🤼🏿\u200d♀️": {}, + "🤼🏿\u200d♂️": {}, + "🤽": {}, + "🤽\u200d♀️": {}, + "🤽\u200d♂️": {}, + "🤽🏻": {}, + "🤽🏻\u200d♀️": {}, + "🤽🏻\u200d♂️": {}, + "🤽🏼": {}, + "🤽🏼\u200d♀️": {}, + "🤽🏼\u200d♂️": {}, + "🤽🏽": {}, + "🤽🏽\u200d♀️": {}, + "🤽🏽\u200d♂️": {}, + "🤽🏾": {}, + "🤽🏾\u200d♀️": {}, + "🤽🏾\u200d♂️": {}, + "🤽🏿": {}, + "🤽🏿\u200d♀️": {}, + "🤽🏿\u200d♂️": {}, + "🤾": {}, + "🤾\u200d♀️": {}, + "🤾\u200d♂️": {}, + "🤾🏻": {}, + "🤾🏻\u200d♀️": {}, + "🤾🏻\u200d♂️": {}, + "🤾🏼": {}, + "🤾🏼\u200d♀️": {}, + "🤾🏼\u200d♂️": {}, + "🤾🏽": {}, + "🤾🏽\u200d♀️": {}, + "🤾🏽\u200d♂️": {}, + "🤾🏾": {}, + "🤾🏾\u200d♀️": {}, + "🤾🏾\u200d♂️": {}, + "🤾🏿": {}, + "🤾🏿\u200d♀️": {}, + "🤾🏿\u200d♂️": {}, + "🤿": {}, + "🥀": {}, + "🥁": {}, + "🥂": {}, + "🥃": {}, + "🥄": {}, + "🥅": {}, + "🥇": {}, + "🥈": {}, + "🥉": {}, + "🥊": {}, + "🥋": {}, + "🥌": {}, + "🥍": {}, + "🥎": {}, + "🥏": {}, + "🥐": {}, + "🥑": {}, + "🥒": {}, + "🥓": {}, + "🥔": {}, + "🥕": {}, + "🥖": {}, + "🥗": {}, + "🥘": {}, + "🥙": {}, + "🥚": {}, + "🥛": {}, + "🥜": {}, + "🥝": {}, + "🥞": {}, + "🥟": {}, + "🥠": {}, + "🥡": {}, + "🥢": {}, + "🥣": {}, + "🥤": {}, + "🥥": {}, + "🥦": {}, + "🥧": {}, + "🥨": {}, + "🥩": {}, + "🥪": {}, + "🥫": {}, + "🥬": {}, + "🥭": {}, + "🥮": {}, + "🥯": {}, + "🥰": {}, + "🥱": {}, + "🥲": {}, + "🥳": {}, + "🥴": {}, + "🥵": {}, + "🥶": {}, + "🥷": {}, + "🥷🏻": {}, + "🥷🏼": {}, + "🥷🏽": {}, + "🥷🏾": {}, + "🥷🏿": {}, + "🥸": {}, + "🥹": {}, + "🥺": {}, + "🥻": {}, + "🥼": {}, + "🥽": {}, + "🥾": {}, + "🥿": {}, + "🦀": {}, + "🦁": {}, + "🦂": {}, + "🦃": {}, + "🦄": {}, + "🦅": {}, + "🦆": {}, + "🦇": {}, + "🦈": {}, + "🦉": {}, + "🦊": {}, + "🦋": {}, + "🦌": {}, + "🦍": {}, + "🦎": {}, + "🦏": {}, + "🦐": {}, + "🦑": {}, + "🦒": {}, + "🦓": {}, + "🦔": {}, + "🦕": {}, + "🦖": {}, + "🦗": {}, + "🦘": {}, + "🦙": {}, + "🦚": {}, + "🦛": {}, + "🦜": {}, + "🦝": {}, + "🦞": {}, + "🦟": {}, + "🦠": {}, + "🦡": {}, + "🦢": {}, + "🦣": {}, + "🦤": {}, + "🦥": {}, + "🦦": {}, + "🦧": {}, + "🦨": {}, + "🦩": {}, + "🦪": {}, + "🦫": {}, + "🦬": {}, + "🦭": {}, + "🦮": {}, + "🦯": {}, + "🦴": {}, + "🦵": {}, + "🦵🏻": {}, + "🦵🏼": {}, + "🦵🏽": {}, + "🦵🏾": {}, + "🦵🏿": {}, + "🦶": {}, + "🦶🏻": {}, + "🦶🏼": {}, + "🦶🏽": {}, + "🦶🏾": {}, + "🦶🏿": {}, + "🦷": {}, + "🦸": {}, + "🦸\u200d♀️": {}, + "🦸\u200d♂️": {}, + "🦸🏻": {}, + "🦸🏻\u200d♀️": {}, + "🦸🏻\u200d♂️": {}, + "🦸🏼": {}, + "🦸🏼\u200d♀️": {}, + "🦸🏼\u200d♂️": {}, + "🦸🏽": {}, + "🦸🏽\u200d♀️": {}, + "🦸🏽\u200d♂️": {}, + "🦸🏾": {}, + "🦸🏾\u200d♀️": {}, + "🦸🏾\u200d♂️": {}, + "🦸🏿": {}, + "🦸🏿\u200d♀️": {}, + "🦸🏿\u200d♂️": {}, + "🦹": {}, + "🦹\u200d♀️": {}, + "🦹\u200d♂️": {}, + "🦹🏻": {}, + "🦹🏻\u200d♀️": {}, + "🦹🏻\u200d♂️": {}, + "🦹🏼": {}, + "🦹🏼\u200d♀️": {}, + "🦹🏼\u200d♂️": {}, + "🦹🏽": {}, + "🦹🏽\u200d♀️": {}, + "🦹🏽\u200d♂️": {}, + "🦹🏾": {}, + "🦹🏾\u200d♀️": {}, + "🦹🏾\u200d♂️": {}, + "🦹🏿": {}, + "🦹🏿\u200d♀️": {}, + "🦹🏿\u200d♂️": {}, + "🦺": {}, + "🦻": {}, + "🦻🏻": {}, + "🦻🏼": {}, + "🦻🏽": {}, + "🦻🏾": {}, + "🦻🏿": {}, + "🦼": {}, + "🦽": {}, + "🦾": {}, + "🦿": {}, + "🧀": {}, + "🧁": {}, + "🧂": {}, + "🧃": {}, + "🧄": {}, + "🧅": {}, + "🧆": {}, + "🧇": {}, + "🧈": {}, + "🧉": {}, + "🧊": {}, + "🧋": {}, + "🧌": {}, + "🧍": {}, + "🧍\u200d♀️": {}, + "🧍\u200d♂️": {}, + "🧍🏻": {}, + "🧍🏻\u200d♀️": {}, + "🧍🏻\u200d♂️": {}, + "🧍🏼": {}, + "🧍🏼\u200d♀️": {}, + "🧍🏼\u200d♂️": {}, + "🧍🏽": {}, + "🧍🏽\u200d♀️": {}, + "🧍🏽\u200d♂️": {}, + "🧍🏾": {}, + "🧍🏾\u200d♀️": {}, + "🧍🏾\u200d♂️": {}, + "🧍🏿": {}, + "🧍🏿\u200d♀️": {}, + "🧍🏿\u200d♂️": {}, + "🧎": {}, + "🧎\u200d♀️": {}, + "🧎\u200d♀️\u200d➡️": {}, + "🧎\u200d♂️": {}, + "🧎\u200d♂️\u200d➡️": {}, + "🧎\u200d➡️": {}, + "🧎🏻": {}, + "🧎🏻\u200d♀️": {}, + "🧎🏻\u200d♀️\u200d➡️": {}, + "🧎🏻\u200d♂️": {}, + "🧎🏻\u200d♂️\u200d➡️": {}, + "🧎🏻\u200d➡️": {}, + "🧎🏼": {}, + "🧎🏼\u200d♀️": {}, + "🧎🏼\u200d♀️\u200d➡️": {}, + "🧎🏼\u200d♂️": {}, + "🧎🏼\u200d♂️\u200d➡️": {}, + "🧎🏼\u200d➡️": {}, + "🧎🏽": {}, + "🧎🏽\u200d♀️": {}, + "🧎🏽\u200d♀️\u200d➡️": {}, + "🧎🏽\u200d♂️": {}, + "🧎🏽\u200d♂️\u200d➡️": {}, + "🧎🏽\u200d➡️": {}, + "🧎🏾": {}, + "🧎🏾\u200d♀️": {}, + "🧎🏾\u200d♀️\u200d➡️": {}, + "🧎🏾\u200d♂️": {}, + "🧎🏾\u200d♂️\u200d➡️": {}, + "🧎🏾\u200d➡️": {}, + "🧎🏿": {}, + "🧎🏿\u200d♀️": {}, + "🧎🏿\u200d♀️\u200d➡️": {}, + "🧎🏿\u200d♂️": {}, + "🧎🏿\u200d♂️\u200d➡️": {}, + "🧎🏿\u200d➡️": {}, + "🧏": {}, + "🧏\u200d♀️": {}, + "🧏\u200d♂️": {}, + "🧏🏻": {}, + "🧏🏻\u200d♀️": {}, + "🧏🏻\u200d♂️": {}, + "🧏🏼": {}, + "🧏🏼\u200d♀️": {}, + "🧏🏼\u200d♂️": {}, + "🧏🏽": {}, + "🧏🏽\u200d♀️": {}, + "🧏🏽\u200d♂️": {}, + "🧏🏾": {}, + "🧏🏾\u200d♀️": {}, + "🧏🏾\u200d♂️": {}, + "🧏🏿": {}, + "🧏🏿\u200d♀️": {}, + "🧏🏿\u200d♂️": {}, + "🧐": {}, + "🧑": {}, + "🧑\u200d⚕️": {}, + "🧑\u200d⚖️": {}, + "🧑\u200d✈️": {}, + "🧑\u200d🌾": {}, + "🧑\u200d🍳": {}, + "🧑\u200d🍼": {}, + "🧑\u200d🎄": {}, + "🧑\u200d🎓": {}, + "🧑\u200d🎤": {}, + "🧑\u200d🎨": {}, + "🧑\u200d🏫": {}, + "🧑\u200d🏭": {}, + "🧑\u200d💻": {}, + "🧑\u200d💼": {}, + "🧑\u200d🔧": {}, + "🧑\u200d🔬": {}, + "🧑\u200d🚀": {}, + "🧑\u200d🚒": {}, + "🧑\u200d🤝\u200d🧑": {}, + "🧑\u200d🦯": {}, + "🧑\u200d🦯\u200d➡️": {}, + "🧑\u200d🦰": {}, + "🧑\u200d🦱": {}, + "🧑\u200d🦲": {}, + "🧑\u200d🦳": {}, + "🧑\u200d🦼": {}, + "🧑\u200d🦼\u200d➡️": {}, + "🧑\u200d🦽": {}, + "🧑\u200d🦽\u200d➡️": {}, + "🧑\u200d🧑\u200d🧒": {}, + "🧑\u200d🧑\u200d🧒\u200d🧒": {}, + "🧑\u200d🧒": {}, + "🧑\u200d🧒\u200d🧒": {}, + "🧑\u200d🩰": {}, + "🧑🏻": {}, + "🧑🏻\u200d⚕️": {}, + "🧑🏻\u200d⚖️": {}, + "🧑🏻\u200d✈️": {}, + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏼": {}, + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏽": {}, + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏾": {}, + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏿": {}, + "🧑🏻\u200d❤️\u200d🧑🏼": {}, + "🧑🏻\u200d❤️\u200d🧑🏽": {}, + "🧑🏻\u200d❤️\u200d🧑🏾": {}, + "🧑🏻\u200d❤️\u200d🧑🏿": {}, + "🧑🏻\u200d🌾": {}, + "🧑🏻\u200d🍳": {}, + "🧑🏻\u200d🍼": {}, + "🧑🏻\u200d🎄": {}, + "🧑🏻\u200d🎓": {}, + "🧑🏻\u200d🎤": {}, + "🧑🏻\u200d🎨": {}, + "🧑🏻\u200d🏫": {}, + "🧑🏻\u200d🏭": {}, + "🧑🏻\u200d🐰\u200d🧑🏼": {}, + "🧑🏻\u200d🐰\u200d🧑🏽": {}, + "🧑🏻\u200d🐰\u200d🧑🏾": {}, + "🧑🏻\u200d🐰\u200d🧑🏿": {}, + "🧑🏻\u200d💻": {}, + "🧑🏻\u200d💼": {}, + "🧑🏻\u200d🔧": {}, + "🧑🏻\u200d🔬": {}, + "🧑🏻\u200d🚀": {}, + "🧑🏻\u200d🚒": {}, + "🧑🏻\u200d🤝\u200d🧑🏻": {}, + "🧑🏻\u200d🤝\u200d🧑🏼": {}, + "🧑🏻\u200d🤝\u200d🧑🏽": {}, + "🧑🏻\u200d🤝\u200d🧑🏾": {}, + "🧑🏻\u200d🤝\u200d🧑🏿": {}, + "🧑🏻\u200d🦯": {}, + "🧑🏻\u200d🦯\u200d➡️": {}, + "🧑🏻\u200d🦰": {}, + "🧑🏻\u200d🦱": {}, + "🧑🏻\u200d🦲": {}, + "🧑🏻\u200d🦳": {}, + "🧑🏻\u200d🦼": {}, + "🧑🏻\u200d🦼\u200d➡️": {}, + "🧑🏻\u200d🦽": {}, + "🧑🏻\u200d🦽\u200d➡️": {}, + "🧑🏻\u200d🩰": {}, + "🧑🏻\u200d\U0001faef\u200d🧑🏼": {}, + "🧑🏻\u200d\U0001faef\u200d🧑🏽": {}, + "🧑🏻\u200d\U0001faef\u200d🧑🏾": {}, + "🧑🏻\u200d\U0001faef\u200d🧑🏿": {}, + "🧑🏼": {}, + "🧑🏼\u200d⚕️": {}, + "🧑🏼\u200d⚖️": {}, + "🧑🏼\u200d✈️": {}, + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏻": {}, + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏽": {}, + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏾": {}, + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏿": {}, + "🧑🏼\u200d❤️\u200d🧑🏻": {}, + "🧑🏼\u200d❤️\u200d🧑🏽": {}, + "🧑🏼\u200d❤️\u200d🧑🏾": {}, + "🧑🏼\u200d❤️\u200d🧑🏿": {}, + "🧑🏼\u200d🌾": {}, + "🧑🏼\u200d🍳": {}, + "🧑🏼\u200d🍼": {}, + "🧑🏼\u200d🎄": {}, + "🧑🏼\u200d🎓": {}, + "🧑🏼\u200d🎤": {}, + "🧑🏼\u200d🎨": {}, + "🧑🏼\u200d🏫": {}, + "🧑🏼\u200d🏭": {}, + "🧑🏼\u200d🐰\u200d🧑🏻": {}, + "🧑🏼\u200d🐰\u200d🧑🏽": {}, + "🧑🏼\u200d🐰\u200d🧑🏾": {}, + "🧑🏼\u200d🐰\u200d🧑🏿": {}, + "🧑🏼\u200d💻": {}, + "🧑🏼\u200d💼": {}, + "🧑🏼\u200d🔧": {}, + "🧑🏼\u200d🔬": {}, + "🧑🏼\u200d🚀": {}, + "🧑🏼\u200d🚒": {}, + "🧑🏼\u200d🤝\u200d🧑🏻": {}, + "🧑🏼\u200d🤝\u200d🧑🏼": {}, + "🧑🏼\u200d🤝\u200d🧑🏽": {}, + "🧑🏼\u200d🤝\u200d🧑🏾": {}, + "🧑🏼\u200d🤝\u200d🧑🏿": {}, + "🧑🏼\u200d🦯": {}, + "🧑🏼\u200d🦯\u200d➡️": {}, + "🧑🏼\u200d🦰": {}, + "🧑🏼\u200d🦱": {}, + "🧑🏼\u200d🦲": {}, + "🧑🏼\u200d🦳": {}, + "🧑🏼\u200d🦼": {}, + "🧑🏼\u200d🦼\u200d➡️": {}, + "🧑🏼\u200d🦽": {}, + "🧑🏼\u200d🦽\u200d➡️": {}, + "🧑🏼\u200d🩰": {}, + "🧑🏼\u200d\U0001faef\u200d🧑🏻": {}, + "🧑🏼\u200d\U0001faef\u200d🧑🏽": {}, + "🧑🏼\u200d\U0001faef\u200d🧑🏾": {}, + "🧑🏼\u200d\U0001faef\u200d🧑🏿": {}, + "🧑🏽": {}, + "🧑🏽\u200d⚕️": {}, + "🧑🏽\u200d⚖️": {}, + "🧑🏽\u200d✈️": {}, + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏻": {}, + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏼": {}, + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏾": {}, + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏿": {}, + "🧑🏽\u200d❤️\u200d🧑🏻": {}, + "🧑🏽\u200d❤️\u200d🧑🏼": {}, + "🧑🏽\u200d❤️\u200d🧑🏾": {}, + "🧑🏽\u200d❤️\u200d🧑🏿": {}, + "🧑🏽\u200d🌾": {}, + "🧑🏽\u200d🍳": {}, + "🧑🏽\u200d🍼": {}, + "🧑🏽\u200d🎄": {}, + "🧑🏽\u200d🎓": {}, + "🧑🏽\u200d🎤": {}, + "🧑🏽\u200d🎨": {}, + "🧑🏽\u200d🏫": {}, + "🧑🏽\u200d🏭": {}, + "🧑🏽\u200d🐰\u200d🧑🏻": {}, + "🧑🏽\u200d🐰\u200d🧑🏼": {}, + "🧑🏽\u200d🐰\u200d🧑🏾": {}, + "🧑🏽\u200d🐰\u200d🧑🏿": {}, + "🧑🏽\u200d💻": {}, + "🧑🏽\u200d💼": {}, + "🧑🏽\u200d🔧": {}, + "🧑🏽\u200d🔬": {}, + "🧑🏽\u200d🚀": {}, + "🧑🏽\u200d🚒": {}, + "🧑🏽\u200d🤝\u200d🧑🏻": {}, + "🧑🏽\u200d🤝\u200d🧑🏼": {}, + "🧑🏽\u200d🤝\u200d🧑🏽": {}, + "🧑🏽\u200d🤝\u200d🧑🏾": {}, + "🧑🏽\u200d🤝\u200d🧑🏿": {}, + "🧑🏽\u200d🦯": {}, + "🧑🏽\u200d🦯\u200d➡️": {}, + "🧑🏽\u200d🦰": {}, + "🧑🏽\u200d🦱": {}, + "🧑🏽\u200d🦲": {}, + "🧑🏽\u200d🦳": {}, + "🧑🏽\u200d🦼": {}, + "🧑🏽\u200d🦼\u200d➡️": {}, + "🧑🏽\u200d🦽": {}, + "🧑🏽\u200d🦽\u200d➡️": {}, + "🧑🏽\u200d🩰": {}, + "🧑🏽\u200d\U0001faef\u200d🧑🏻": {}, + "🧑🏽\u200d\U0001faef\u200d🧑🏼": {}, + "🧑🏽\u200d\U0001faef\u200d🧑🏾": {}, + "🧑🏽\u200d\U0001faef\u200d🧑🏿": {}, + "🧑🏾": {}, + "🧑🏾\u200d⚕️": {}, + "🧑🏾\u200d⚖️": {}, + "🧑🏾\u200d✈️": {}, + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏻": {}, + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏼": {}, + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏽": {}, + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏿": {}, + "🧑🏾\u200d❤️\u200d🧑🏻": {}, + "🧑🏾\u200d❤️\u200d🧑🏼": {}, + "🧑🏾\u200d❤️\u200d🧑🏽": {}, + "🧑🏾\u200d❤️\u200d🧑🏿": {}, + "🧑🏾\u200d🌾": {}, + "🧑🏾\u200d🍳": {}, + "🧑🏾\u200d🍼": {}, + "🧑🏾\u200d🎄": {}, + "🧑🏾\u200d🎓": {}, + "🧑🏾\u200d🎤": {}, + "🧑🏾\u200d🎨": {}, + "🧑🏾\u200d🏫": {}, + "🧑🏾\u200d🏭": {}, + "🧑🏾\u200d🐰\u200d🧑🏻": {}, + "🧑🏾\u200d🐰\u200d🧑🏼": {}, + "🧑🏾\u200d🐰\u200d🧑🏽": {}, + "🧑🏾\u200d🐰\u200d🧑🏿": {}, + "🧑🏾\u200d💻": {}, + "🧑🏾\u200d💼": {}, + "🧑🏾\u200d🔧": {}, + "🧑🏾\u200d🔬": {}, + "🧑🏾\u200d🚀": {}, + "🧑🏾\u200d🚒": {}, + "🧑🏾\u200d🤝\u200d🧑🏻": {}, + "🧑🏾\u200d🤝\u200d🧑🏼": {}, + "🧑🏾\u200d🤝\u200d🧑🏽": {}, + "🧑🏾\u200d🤝\u200d🧑🏾": {}, + "🧑🏾\u200d🤝\u200d🧑🏿": {}, + "🧑🏾\u200d🦯": {}, + "🧑🏾\u200d🦯\u200d➡️": {}, + "🧑🏾\u200d🦰": {}, + "🧑🏾\u200d🦱": {}, + "🧑🏾\u200d🦲": {}, + "🧑🏾\u200d🦳": {}, + "🧑🏾\u200d🦼": {}, + "🧑🏾\u200d🦼\u200d➡️": {}, + "🧑🏾\u200d🦽": {}, + "🧑🏾\u200d🦽\u200d➡️": {}, + "🧑🏾\u200d🩰": {}, + "🧑🏾\u200d\U0001faef\u200d🧑🏻": {}, + "🧑🏾\u200d\U0001faef\u200d🧑🏼": {}, + "🧑🏾\u200d\U0001faef\u200d🧑🏽": {}, + "🧑🏾\u200d\U0001faef\u200d🧑🏿": {}, + "🧑🏿": {}, + "🧑🏿\u200d⚕️": {}, + "🧑🏿\u200d⚖️": {}, + "🧑🏿\u200d✈️": {}, + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏻": {}, + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏼": {}, + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏽": {}, + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏾": {}, + "🧑🏿\u200d❤️\u200d🧑🏻": {}, + "🧑🏿\u200d❤️\u200d🧑🏼": {}, + "🧑🏿\u200d❤️\u200d🧑🏽": {}, + "🧑🏿\u200d❤️\u200d🧑🏾": {}, + "🧑🏿\u200d🌾": {}, + "🧑🏿\u200d🍳": {}, + "🧑🏿\u200d🍼": {}, + "🧑🏿\u200d🎄": {}, + "🧑🏿\u200d🎓": {}, + "🧑🏿\u200d🎤": {}, + "🧑🏿\u200d🎨": {}, + "🧑🏿\u200d🏫": {}, + "🧑🏿\u200d🏭": {}, + "🧑🏿\u200d🐰\u200d🧑🏻": {}, + "🧑🏿\u200d🐰\u200d🧑🏼": {}, + "🧑🏿\u200d🐰\u200d🧑🏽": {}, + "🧑🏿\u200d🐰\u200d🧑🏾": {}, + "🧑🏿\u200d💻": {}, + "🧑🏿\u200d💼": {}, + "🧑🏿\u200d🔧": {}, + "🧑🏿\u200d🔬": {}, + "🧑🏿\u200d🚀": {}, + "🧑🏿\u200d🚒": {}, + "🧑🏿\u200d🤝\u200d🧑🏻": {}, + "🧑🏿\u200d🤝\u200d🧑🏼": {}, + "🧑🏿\u200d🤝\u200d🧑🏽": {}, + "🧑🏿\u200d🤝\u200d🧑🏾": {}, + "🧑🏿\u200d🤝\u200d🧑🏿": {}, + "🧑🏿\u200d🦯": {}, + "🧑🏿\u200d🦯\u200d➡️": {}, + "🧑🏿\u200d🦰": {}, + "🧑🏿\u200d🦱": {}, + "🧑🏿\u200d🦲": {}, + "🧑🏿\u200d🦳": {}, + "🧑🏿\u200d🦼": {}, + "🧑🏿\u200d🦼\u200d➡️": {}, + "🧑🏿\u200d🦽": {}, + "🧑🏿\u200d🦽\u200d➡️": {}, + "🧑🏿\u200d🩰": {}, + "🧑🏿\u200d\U0001faef\u200d🧑🏻": {}, + "🧑🏿\u200d\U0001faef\u200d🧑🏼": {}, + "🧑🏿\u200d\U0001faef\u200d🧑🏽": {}, + "🧑🏿\u200d\U0001faef\u200d🧑🏾": {}, + "🧒": {}, + "🧒🏻": {}, + "🧒🏼": {}, + "🧒🏽": {}, + "🧒🏾": {}, + "🧒🏿": {}, + "🧓": {}, + "🧓🏻": {}, + "🧓🏼": {}, + "🧓🏽": {}, + "🧓🏾": {}, + "🧓🏿": {}, + "🧔": {}, + "🧔\u200d♀️": {}, + "🧔\u200d♂️": {}, + "🧔🏻": {}, + "🧔🏻\u200d♀️": {}, + "🧔🏻\u200d♂️": {}, + "🧔🏼": {}, + "🧔🏼\u200d♀️": {}, + "🧔🏼\u200d♂️": {}, + "🧔🏽": {}, + "🧔🏽\u200d♀️": {}, + "🧔🏽\u200d♂️": {}, + "🧔🏾": {}, + "🧔🏾\u200d♀️": {}, + "🧔🏾\u200d♂️": {}, + "🧔🏿": {}, + "🧔🏿\u200d♀️": {}, + "🧔🏿\u200d♂️": {}, + "🧕": {}, + "🧕🏻": {}, + "🧕🏼": {}, + "🧕🏽": {}, + "🧕🏾": {}, + "🧕🏿": {}, + "🧖": {}, + "🧖\u200d♀️": {}, + "🧖\u200d♂️": {}, + "🧖🏻": {}, + "🧖🏻\u200d♀️": {}, + "🧖🏻\u200d♂️": {}, + "🧖🏼": {}, + "🧖🏼\u200d♀️": {}, + "🧖🏼\u200d♂️": {}, + "🧖🏽": {}, + "🧖🏽\u200d♀️": {}, + "🧖🏽\u200d♂️": {}, + "🧖🏾": {}, + "🧖🏾\u200d♀️": {}, + "🧖🏾\u200d♂️": {}, + "🧖🏿": {}, + "🧖🏿\u200d♀️": {}, + "🧖🏿\u200d♂️": {}, + "🧗": {}, + "🧗\u200d♀️": {}, + "🧗\u200d♂️": {}, + "🧗🏻": {}, + "🧗🏻\u200d♀️": {}, + "🧗🏻\u200d♂️": {}, + "🧗🏼": {}, + "🧗🏼\u200d♀️": {}, + "🧗🏼\u200d♂️": {}, + "🧗🏽": {}, + "🧗🏽\u200d♀️": {}, + "🧗🏽\u200d♂️": {}, + "🧗🏾": {}, + "🧗🏾\u200d♀️": {}, + "🧗🏾\u200d♂️": {}, + "🧗🏿": {}, + "🧗🏿\u200d♀️": {}, + "🧗🏿\u200d♂️": {}, + "🧘": {}, + "🧘\u200d♀️": {}, + "🧘\u200d♂️": {}, + "🧘🏻": {}, + "🧘🏻\u200d♀️": {}, + "🧘🏻\u200d♂️": {}, + "🧘🏼": {}, + "🧘🏼\u200d♀️": {}, + "🧘🏼\u200d♂️": {}, + "🧘🏽": {}, + "🧘🏽\u200d♀️": {}, + "🧘🏽\u200d♂️": {}, + "🧘🏾": {}, + "🧘🏾\u200d♀️": {}, + "🧘🏾\u200d♂️": {}, + "🧘🏿": {}, + "🧘🏿\u200d♀️": {}, + "🧘🏿\u200d♂️": {}, + "🧙": {}, + "🧙\u200d♀️": {}, + "🧙\u200d♂️": {}, + "🧙🏻": {}, + "🧙🏻\u200d♀️": {}, + "🧙🏻\u200d♂️": {}, + "🧙🏼": {}, + "🧙🏼\u200d♀️": {}, + "🧙🏼\u200d♂️": {}, + "🧙🏽": {}, + "🧙🏽\u200d♀️": {}, + "🧙🏽\u200d♂️": {}, + "🧙🏾": {}, + "🧙🏾\u200d♀️": {}, + "🧙🏾\u200d♂️": {}, + "🧙🏿": {}, + "🧙🏿\u200d♀️": {}, + "🧙🏿\u200d♂️": {}, + "🧚": {}, + "🧚\u200d♀️": {}, + "🧚\u200d♂️": {}, + "🧚🏻": {}, + "🧚🏻\u200d♀️": {}, + "🧚🏻\u200d♂️": {}, + "🧚🏼": {}, + "🧚🏼\u200d♀️": {}, + "🧚🏼\u200d♂️": {}, + "🧚🏽": {}, + "🧚🏽\u200d♀️": {}, + "🧚🏽\u200d♂️": {}, + "🧚🏾": {}, + "🧚🏾\u200d♀️": {}, + "🧚🏾\u200d♂️": {}, + "🧚🏿": {}, + "🧚🏿\u200d♀️": {}, + "🧚🏿\u200d♂️": {}, + "🧛": {}, + "🧛\u200d♀️": {}, + "🧛\u200d♂️": {}, + "🧛🏻": {}, + "🧛🏻\u200d♀️": {}, + "🧛🏻\u200d♂️": {}, + "🧛🏼": {}, + "🧛🏼\u200d♀️": {}, + "🧛🏼\u200d♂️": {}, + "🧛🏽": {}, + "🧛🏽\u200d♀️": {}, + "🧛🏽\u200d♂️": {}, + "🧛🏾": {}, + "🧛🏾\u200d♀️": {}, + "🧛🏾\u200d♂️": {}, + "🧛🏿": {}, + "🧛🏿\u200d♀️": {}, + "🧛🏿\u200d♂️": {}, + "🧜": {}, + "🧜\u200d♀️": {}, + "🧜\u200d♂️": {}, + "🧜🏻": {}, + "🧜🏻\u200d♀️": {}, + "🧜🏻\u200d♂️": {}, + "🧜🏼": {}, + "🧜🏼\u200d♀️": {}, + "🧜🏼\u200d♂️": {}, + "🧜🏽": {}, + "🧜🏽\u200d♀️": {}, + "🧜🏽\u200d♂️": {}, + "🧜🏾": {}, + "🧜🏾\u200d♀️": {}, + "🧜🏾\u200d♂️": {}, + "🧜🏿": {}, + "🧜🏿\u200d♀️": {}, + "🧜🏿\u200d♂️": {}, + "🧝": {}, + "🧝\u200d♀️": {}, + "🧝\u200d♂️": {}, + "🧝🏻": {}, + "🧝🏻\u200d♀️": {}, + "🧝🏻\u200d♂️": {}, + "🧝🏼": {}, + "🧝🏼\u200d♀️": {}, + "🧝🏼\u200d♂️": {}, + "🧝🏽": {}, + "🧝🏽\u200d♀️": {}, + "🧝🏽\u200d♂️": {}, + "🧝🏾": {}, + "🧝🏾\u200d♀️": {}, + "🧝🏾\u200d♂️": {}, + "🧝🏿": {}, + "🧝🏿\u200d♀️": {}, + "🧝🏿\u200d♂️": {}, + "🧞": {}, + "🧞\u200d♀️": {}, + "🧞\u200d♂️": {}, + "🧟": {}, + "🧟\u200d♀️": {}, + "🧟\u200d♂️": {}, + "🧠": {}, + "🧡": {}, + "🧢": {}, + "🧣": {}, + "🧤": {}, + "🧥": {}, + "🧦": {}, + "🧧": {}, + "🧨": {}, + "🧩": {}, + "🧪": {}, + "🧫": {}, + "🧬": {}, + "🧭": {}, + "🧮": {}, + "🧯": {}, + "🧰": {}, + "🧱": {}, + "🧲": {}, + "🧳": {}, + "🧴": {}, + "🧵": {}, + "🧶": {}, + "🧷": {}, + "🧸": {}, + "🧹": {}, + "🧺": {}, + "🧻": {}, + "🧼": {}, + "🧽": {}, + "🧾": {}, + "🧿": {}, + "🩰": {}, + "🩱": {}, + "🩲": {}, + "🩳": {}, + "🩴": {}, + "🩵": {}, + "🩶": {}, + "🩷": {}, + "🩸": {}, + "🩹": {}, + "🩺": {}, + "🩻": {}, + "🩼": {}, + "🪀": {}, + "🪁": {}, + "🪂": {}, + "🪃": {}, + "🪄": {}, + "🪅": {}, + "🪆": {}, + "🪇": {}, + "🪈": {}, + "\U0001fa89": {}, + "\U0001fa8a": {}, + "\U0001fa8e": {}, + "\U0001fa8f": {}, + "🪐": {}, + "🪑": {}, + "🪒": {}, + "🪓": {}, + "🪔": {}, + "🪕": {}, + "🪖": {}, + "🪗": {}, + "🪘": {}, + "🪙": {}, + "🪚": {}, + "🪛": {}, + "🪜": {}, + "🪝": {}, + "🪞": {}, + "🪟": {}, + "🪠": {}, + "🪡": {}, + "🪢": {}, + "🪣": {}, + "🪤": {}, + "🪥": {}, + "🪦": {}, + "🪧": {}, + "🪨": {}, + "🪩": {}, + "🪪": {}, + "🪫": {}, + "🪬": {}, + "🪭": {}, + "🪮": {}, + "🪯": {}, + "🪰": {}, + "🪱": {}, + "🪲": {}, + "🪳": {}, + "🪴": {}, + "🪵": {}, + "🪶": {}, + "🪷": {}, + "🪸": {}, + "🪹": {}, + "🪺": {}, + "🪻": {}, + "🪼": {}, + "🪽": {}, + "\U0001fabe": {}, + "🪿": {}, + "🫀": {}, + "🫁": {}, + "🫂": {}, + "🫃": {}, + "🫃🏻": {}, + "🫃🏼": {}, + "🫃🏽": {}, + "🫃🏾": {}, + "🫃🏿": {}, + "🫄": {}, + "🫄🏻": {}, + "🫄🏼": {}, + "🫄🏽": {}, + "🫄🏾": {}, + "🫄🏿": {}, + "🫅": {}, + "🫅🏻": {}, + "🫅🏼": {}, + "🫅🏽": {}, + "🫅🏾": {}, + "🫅🏿": {}, + "\U0001fac6": {}, + "\U0001fac8": {}, + "\U0001facd": {}, + "🫎": {}, + "🫏": {}, + "🫐": {}, + "🫑": {}, + "🫒": {}, + "🫓": {}, + "🫔": {}, + "🫕": {}, + "🫖": {}, + "🫗": {}, + "🫘": {}, + "🫙": {}, + "🫚": {}, + "🫛": {}, + "\U0001fadc": {}, + "\U0001fadf": {}, + "🫠": {}, + "🫡": {}, + "🫢": {}, + "🫣": {}, + "🫤": {}, + "🫥": {}, + "🫦": {}, + "🫧": {}, + "🫨": {}, + "\U0001fae9": {}, + "\U0001faea": {}, + "\U0001faef": {}, + "🫰": {}, + "🫰🏻": {}, + "🫰🏼": {}, + "🫰🏽": {}, + "🫰🏾": {}, + "🫰🏿": {}, + "🫱": {}, + "🫱🏻": {}, + "🫱🏻\u200d🫲🏼": {}, + "🫱🏻\u200d🫲🏽": {}, + "🫱🏻\u200d🫲🏾": {}, + "🫱🏻\u200d🫲🏿": {}, + "🫱🏼": {}, + "🫱🏼\u200d🫲🏻": {}, + "🫱🏼\u200d🫲🏽": {}, + "🫱🏼\u200d🫲🏾": {}, + "🫱🏼\u200d🫲🏿": {}, + "🫱🏽": {}, + "🫱🏽\u200d🫲🏻": {}, + "🫱🏽\u200d🫲🏼": {}, + "🫱🏽\u200d🫲🏾": {}, + "🫱🏽\u200d🫲🏿": {}, + "🫱🏾": {}, + "🫱🏾\u200d🫲🏻": {}, + "🫱🏾\u200d🫲🏼": {}, + "🫱🏾\u200d🫲🏽": {}, + "🫱🏾\u200d🫲🏿": {}, + "🫱🏿": {}, + "🫱🏿\u200d🫲🏻": {}, + "🫱🏿\u200d🫲🏼": {}, + "🫱🏿\u200d🫲🏽": {}, + "🫱🏿\u200d🫲🏾": {}, + "🫲": {}, + "🫲🏻": {}, + "🫲🏼": {}, + "🫲🏽": {}, + "🫲🏾": {}, + "🫲🏿": {}, + "🫳": {}, + "🫳🏻": {}, + "🫳🏼": {}, + "🫳🏽": {}, + "🫳🏾": {}, + "🫳🏿": {}, + "🫴": {}, + "🫴🏻": {}, + "🫴🏼": {}, + "🫴🏽": {}, + "🫴🏾": {}, + "🫴🏿": {}, + "🫵": {}, + "🫵🏻": {}, + "🫵🏼": {}, + "🫵🏽": {}, + "🫵🏾": {}, + "🫵🏿": {}, + "🫶": {}, + "🫶🏻": {}, + "🫶🏼": {}, + "🫶🏽": {}, + "🫶🏾": {}, + "🫶🏿": {}, + "🫷": {}, + "🫷🏻": {}, + "🫷🏼": {}, + "🫷🏽": {}, + "🫷🏾": {}, + "🫷🏿": {}, + "🫸": {}, + "🫸🏻": {}, + "🫸🏼": {}, + "🫸🏽": {}, + "🫸🏾": {}, + "🫸🏿": {}, +} + +// propEmoji is the Emoji property. +var propEmoji = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x0023, 0x0023, 1}, + {0x002A, 0x002A, 1}, + {0x0030, 0x0039, 1}, + {0x00A9, 0x00A9, 1}, + {0x00AE, 0x00AE, 1}, + {0x203C, 0x203C, 1}, + {0x2049, 0x2049, 1}, + {0x2122, 0x2122, 1}, + {0x2139, 0x2139, 1}, + {0x2194, 0x2199, 1}, + {0x21A9, 0x21AA, 1}, + {0x231A, 0x231B, 1}, + {0x2328, 0x2328, 1}, + {0x23CF, 0x23CF, 1}, + {0x23E9, 0x23F3, 1}, + {0x23F8, 0x23FA, 1}, + {0x24C2, 0x24C2, 1}, + {0x25AA, 0x25AB, 1}, + {0x25B6, 0x25B6, 1}, + {0x25C0, 0x25C0, 1}, + {0x25FB, 0x25FE, 1}, + {0x2600, 0x2604, 1}, + {0x260E, 0x260E, 1}, + {0x2611, 0x2611, 1}, + {0x2614, 0x2615, 1}, + {0x2618, 0x2618, 1}, + {0x261D, 0x261D, 1}, + {0x2620, 0x2620, 1}, + {0x2622, 0x2623, 1}, + {0x2626, 0x2626, 1}, + {0x262A, 0x262A, 1}, + {0x262E, 0x262F, 1}, + {0x2638, 0x263A, 1}, + {0x2640, 0x2640, 1}, + {0x2642, 0x2642, 1}, + {0x2648, 0x2653, 1}, + {0x265F, 0x2660, 1}, + {0x2663, 0x2663, 1}, + {0x2665, 0x2666, 1}, + {0x2668, 0x2668, 1}, + {0x267B, 0x267B, 1}, + {0x267E, 0x267F, 1}, + {0x2692, 0x2697, 1}, + {0x2699, 0x2699, 1}, + {0x269B, 0x269C, 1}, + {0x26A0, 0x26A1, 1}, + {0x26A7, 0x26A7, 1}, + {0x26AA, 0x26AB, 1}, + {0x26B0, 0x26B1, 1}, + {0x26BD, 0x26BE, 1}, + {0x26C4, 0x26C5, 1}, + {0x26C8, 0x26C8, 1}, + {0x26CE, 0x26CF, 1}, + {0x26D1, 0x26D1, 1}, + {0x26D3, 0x26D4, 1}, + {0x26E9, 0x26EA, 1}, + {0x26F0, 0x26F5, 1}, + {0x26F7, 0x26FA, 1}, + {0x26FD, 0x26FD, 1}, + {0x2702, 0x2702, 1}, + {0x2705, 0x2705, 1}, + {0x2708, 0x270D, 1}, + {0x270F, 0x270F, 1}, + {0x2712, 0x2712, 1}, + {0x2714, 0x2714, 1}, + {0x2716, 0x2716, 1}, + {0x271D, 0x271D, 1}, + {0x2721, 0x2721, 1}, + {0x2728, 0x2728, 1}, + {0x2733, 0x2734, 1}, + {0x2744, 0x2744, 1}, + {0x2747, 0x2747, 1}, + {0x274C, 0x274C, 1}, + {0x274E, 0x274E, 1}, + {0x2753, 0x2755, 1}, + {0x2757, 0x2757, 1}, + {0x2763, 0x2764, 1}, + {0x2795, 0x2797, 1}, + {0x27A1, 0x27A1, 1}, + {0x27B0, 0x27B0, 1}, + {0x27BF, 0x27BF, 1}, + {0x2934, 0x2935, 1}, + {0x2B05, 0x2B07, 1}, + {0x2B1B, 0x2B1C, 1}, + {0x2B50, 0x2B50, 1}, + {0x2B55, 0x2B55, 1}, + {0x3030, 0x3030, 1}, + {0x303D, 0x303D, 1}, + {0x3297, 0x3297, 1}, + {0x3299, 0x3299, 1}, + }, + R32: []unicode.Range32{ + {0x1F004, 0x1F004, 1}, + {0x1F0CF, 0x1F0CF, 1}, + {0x1F170, 0x1F171, 1}, + {0x1F17E, 0x1F17F, 1}, + {0x1F18E, 0x1F18E, 1}, + {0x1F191, 0x1F19A, 1}, + {0x1F1E6, 0x1F1FF, 1}, + {0x1F201, 0x1F202, 1}, + {0x1F21A, 0x1F21A, 1}, + {0x1F22F, 0x1F22F, 1}, + {0x1F232, 0x1F23A, 1}, + {0x1F250, 0x1F251, 1}, + {0x1F300, 0x1F321, 1}, + {0x1F324, 0x1F393, 1}, + {0x1F396, 0x1F397, 1}, + {0x1F399, 0x1F39B, 1}, + {0x1F39E, 0x1F3F0, 1}, + {0x1F3F3, 0x1F3F5, 1}, + {0x1F3F7, 0x1F4FD, 1}, + {0x1F4FF, 0x1F53D, 1}, + {0x1F549, 0x1F54E, 1}, + {0x1F550, 0x1F567, 1}, + {0x1F56F, 0x1F570, 1}, + {0x1F573, 0x1F57A, 1}, + {0x1F587, 0x1F587, 1}, + {0x1F58A, 0x1F58D, 1}, + {0x1F590, 0x1F590, 1}, + {0x1F595, 0x1F596, 1}, + {0x1F5A4, 0x1F5A5, 1}, + {0x1F5A8, 0x1F5A8, 1}, + {0x1F5B1, 0x1F5B2, 1}, + {0x1F5BC, 0x1F5BC, 1}, + {0x1F5C2, 0x1F5C4, 1}, + {0x1F5D1, 0x1F5D3, 1}, + {0x1F5DC, 0x1F5DE, 1}, + {0x1F5E1, 0x1F5E1, 1}, + {0x1F5E3, 0x1F5E3, 1}, + {0x1F5E8, 0x1F5E8, 1}, + {0x1F5EF, 0x1F5EF, 1}, + {0x1F5F3, 0x1F5F3, 1}, + {0x1F5FA, 0x1F64F, 1}, + {0x1F680, 0x1F6C5, 1}, + {0x1F6CB, 0x1F6D2, 1}, + {0x1F6D5, 0x1F6D8, 1}, + {0x1F6DC, 0x1F6E5, 1}, + {0x1F6E9, 0x1F6E9, 1}, + {0x1F6EB, 0x1F6EC, 1}, + {0x1F6F0, 0x1F6F0, 1}, + {0x1F6F3, 0x1F6FC, 1}, + {0x1F7E0, 0x1F7EB, 1}, + {0x1F7F0, 0x1F7F0, 1}, + {0x1F90C, 0x1F93A, 1}, + {0x1F93C, 0x1F945, 1}, + {0x1F947, 0x1F9FF, 1}, + {0x1FA70, 0x1FA7C, 1}, + {0x1FA80, 0x1FA8A, 1}, + {0x1FA8E, 0x1FAC6, 1}, + {0x1FAC8, 0x1FAC8, 1}, + {0x1FACD, 0x1FADC, 1}, + {0x1FADF, 0x1FAEA, 1}, + {0x1FAEF, 0x1FAF8, 1}, + }, +} + +// propEmojiPresentation is the Emoji_Presentation property. +var propEmojiPresentation = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x231A, 0x231B, 1}, + {0x23E9, 0x23EC, 1}, + {0x23F0, 0x23F0, 1}, + {0x23F3, 0x23F3, 1}, + {0x25FD, 0x25FE, 1}, + {0x2614, 0x2615, 1}, + {0x2648, 0x2653, 1}, + {0x267F, 0x267F, 1}, + {0x2693, 0x2693, 1}, + {0x26A1, 0x26A1, 1}, + {0x26AA, 0x26AB, 1}, + {0x26BD, 0x26BE, 1}, + {0x26C4, 0x26C5, 1}, + {0x26CE, 0x26CE, 1}, + {0x26D4, 0x26D4, 1}, + {0x26EA, 0x26EA, 1}, + {0x26F2, 0x26F3, 1}, + {0x26F5, 0x26F5, 1}, + {0x26FA, 0x26FA, 1}, + {0x26FD, 0x26FD, 1}, + {0x2705, 0x2705, 1}, + {0x270A, 0x270B, 1}, + {0x2728, 0x2728, 1}, + {0x274C, 0x274C, 1}, + {0x274E, 0x274E, 1}, + {0x2753, 0x2755, 1}, + {0x2757, 0x2757, 1}, + {0x2795, 0x2797, 1}, + {0x27B0, 0x27B0, 1}, + {0x27BF, 0x27BF, 1}, + {0x2B1B, 0x2B1C, 1}, + {0x2B50, 0x2B50, 1}, + {0x2B55, 0x2B55, 1}, + }, + R32: []unicode.Range32{ + {0x1F004, 0x1F004, 1}, + {0x1F0CF, 0x1F0CF, 1}, + {0x1F18E, 0x1F18E, 1}, + {0x1F191, 0x1F19A, 1}, + {0x1F1E6, 0x1F1FF, 1}, + {0x1F201, 0x1F201, 1}, + {0x1F21A, 0x1F21A, 1}, + {0x1F22F, 0x1F22F, 1}, + {0x1F232, 0x1F236, 1}, + {0x1F238, 0x1F23A, 1}, + {0x1F250, 0x1F251, 1}, + {0x1F300, 0x1F320, 1}, + {0x1F32D, 0x1F335, 1}, + {0x1F337, 0x1F37C, 1}, + {0x1F37E, 0x1F393, 1}, + {0x1F3A0, 0x1F3CA, 1}, + {0x1F3CF, 0x1F3D3, 1}, + {0x1F3E0, 0x1F3F0, 1}, + {0x1F3F4, 0x1F3F4, 1}, + {0x1F3F8, 0x1F43E, 1}, + {0x1F440, 0x1F440, 1}, + {0x1F442, 0x1F4FC, 1}, + {0x1F4FF, 0x1F53D, 1}, + {0x1F54B, 0x1F54E, 1}, + {0x1F550, 0x1F567, 1}, + {0x1F57A, 0x1F57A, 1}, + {0x1F595, 0x1F596, 1}, + {0x1F5A4, 0x1F5A4, 1}, + {0x1F5FB, 0x1F64F, 1}, + {0x1F680, 0x1F6C5, 1}, + {0x1F6CC, 0x1F6CC, 1}, + {0x1F6D0, 0x1F6D2, 1}, + {0x1F6D5, 0x1F6D8, 1}, + {0x1F6DC, 0x1F6DF, 1}, + {0x1F6EB, 0x1F6EC, 1}, + {0x1F6F4, 0x1F6FC, 1}, + {0x1F7E0, 0x1F7EB, 1}, + {0x1F7F0, 0x1F7F0, 1}, + {0x1F90C, 0x1F93A, 1}, + {0x1F93C, 0x1F945, 1}, + {0x1F947, 0x1F9FF, 1}, + {0x1FA70, 0x1FA7C, 1}, + {0x1FA80, 0x1FA8A, 1}, + {0x1FA8E, 0x1FAC6, 1}, + {0x1FAC8, 0x1FAC8, 1}, + {0x1FACD, 0x1FADC, 1}, + {0x1FADF, 0x1FAEA, 1}, + {0x1FAEF, 0x1FAF8, 1}, + }, +} + +// propEmojiModifier is the Emoji_Modifier property. +var propEmojiModifier = &unicode.RangeTable{ + R16: []unicode.Range16{}, + R32: []unicode.Range32{ + {0x1F3FB, 0x1F3FF, 1}, + }, +} + +// propEmojiModifierBase is the Emoji_Modifier_Base property. +var propEmojiModifierBase = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x261D, 0x261D, 1}, + {0x26F9, 0x26F9, 1}, + {0x270A, 0x270D, 1}, + }, + R32: []unicode.Range32{ + {0x1F385, 0x1F385, 1}, + {0x1F3C2, 0x1F3C4, 1}, + {0x1F3C7, 0x1F3C7, 1}, + {0x1F3CA, 0x1F3CC, 1}, + {0x1F442, 0x1F443, 1}, + {0x1F446, 0x1F450, 1}, + {0x1F466, 0x1F478, 1}, + {0x1F47C, 0x1F47C, 1}, + {0x1F481, 0x1F483, 1}, + {0x1F485, 0x1F487, 1}, + {0x1F48F, 0x1F48F, 1}, + {0x1F491, 0x1F491, 1}, + {0x1F4AA, 0x1F4AA, 1}, + {0x1F574, 0x1F575, 1}, + {0x1F57A, 0x1F57A, 1}, + {0x1F590, 0x1F590, 1}, + {0x1F595, 0x1F596, 1}, + {0x1F645, 0x1F647, 1}, + {0x1F64B, 0x1F64F, 1}, + {0x1F6A3, 0x1F6A3, 1}, + {0x1F6B4, 0x1F6B6, 1}, + {0x1F6C0, 0x1F6C0, 1}, + {0x1F6CC, 0x1F6CC, 1}, + {0x1F90C, 0x1F90C, 1}, + {0x1F90F, 0x1F90F, 1}, + {0x1F918, 0x1F91F, 1}, + {0x1F926, 0x1F926, 1}, + {0x1F930, 0x1F939, 1}, + {0x1F93C, 0x1F93E, 1}, + {0x1F977, 0x1F977, 1}, + {0x1F9B5, 0x1F9B6, 1}, + {0x1F9B8, 0x1F9B9, 1}, + {0x1F9BB, 0x1F9BB, 1}, + {0x1F9CD, 0x1F9CF, 1}, + {0x1F9D1, 0x1F9DD, 1}, + {0x1FAC3, 0x1FAC5, 1}, + {0x1FAF0, 0x1FAF8, 1}, + }, +} + +// propEmojiComponent is the Emoji_Component property. +var propEmojiComponent = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x0023, 0x0023, 1}, + {0x002A, 0x002A, 1}, + {0x0030, 0x0039, 1}, + {0x200D, 0x200D, 1}, + {0x20E3, 0x20E3, 1}, + {0xFE0F, 0xFE0F, 1}, + }, + R32: []unicode.Range32{ + {0x1F1E6, 0x1F1FF, 1}, + {0x1F3FB, 0x1F3FF, 1}, + {0x1F9B0, 0x1F9B3, 1}, + {0xE0020, 0xE007F, 1}, + }, +} diff --git a/internal/handlers/hub.go b/internal/handlers/hub.go index 5fa4eb6..aab1f22 100644 --- a/internal/handlers/hub.go +++ b/internal/handlers/hub.go @@ -18,6 +18,7 @@ const ( eventNewMsg = "new_msg" eventDelivered = "delivered" eventRecipientsAdded = "recipients_added" + eventReaction = "reaction" ) // wsEnvelope is the JSON shape of every frame pushed over a WebSocket. The @@ -42,6 +43,12 @@ type Hub struct { // delivered even when the recipient has no live WebSocket. notifyPush func(ctx context.Context, msgID int64, addr string) + // reactionSubject, when non-nil, reports the subject message of msgID + // when msgID is an FMSG-005 reaction. A reaction is pushed to its + // recipients as a reaction event carrying the refreshed subject, never + // as new_msg. + reactionSubject func(ctx context.Context, msgID int64) (int64, bool) + mu sync.RWMutex // registry maps a lower-cased user address to the set of that user's // currently connected clients (a user may have several connections). @@ -57,8 +64,9 @@ func (h *Hub) SetPushNotifier(fn func(ctx context.Context, msgID int64, addr str // NewHub creates a Hub that builds pushed message payloads via msgs. func NewHub(msgs *MessageHandler) *Hub { return &Hub{ - buildItem: msgs.messageItemFor, - registry: make(map[string]map[*wsClient]struct{}), + buildItem: msgs.messageItemFor, + reactionSubject: msgs.reactionSubject, + registry: make(map[string]map[*wsClient]struct{}), } } @@ -147,6 +155,14 @@ func (h *Hub) listen(ctx context.Context, onConnected func()) error { } switch n.Channel { case "new_msg": + if h.reactionSubject != nil { + if subject, ok := h.reactionSubject(ctx, msgID); ok { + // FMSG-005: a reaction is a low-priority event on its + // subject, not a new message. No Web Push is sent. + h.dispatch(ctx, subject, addr, eventReaction) + continue + } + } // Web Push runs in its own goroutine: it is network I/O to many // endpoints and must not stall the single listener. It is // dispatched here, alongside (not inside) dispatch, so a push is diff --git a/internal/handlers/messages.go b/internal/handlers/messages.go index a15c32a..a267228 100644 --- a/internal/handlers/messages.go +++ b/internal/handlers/messages.go @@ -66,6 +66,7 @@ type messageListItem struct { Important bool `json:"important"` NoReply bool `json:"no_reply"` Deflate bool `json:"deflate"` + Terminal bool `json:"terminal"` PID *int64 `json:"pid"` From string `json:"from"` To []string `json:"to"` @@ -79,6 +80,10 @@ type messageListItem struct { Read bool `json:"read"` TimeRead *float64 `json:"time_read"` Attachments []models.Attachment `json:"attachments"` + Reaction *string `json:"reaction"` + Reactions []models.Reaction `json:"reactions"` + + dataPath string // stored body path, for reaction recognition; never serialised } // formatDeliveredISO converts a nullable Unix-epoch-seconds timestamp (as @@ -286,7 +291,7 @@ func (h *MessageHandler) List(c *gin.Context) { ctx := c.Request.Context() rows, err := h.DB.Pool.Query(ctx, - `SELECT m.id, m.version, m.pid, m.no_reply, m.is_important, m.is_deflate, m.time_sent, m.from_addr, m.topic, m.type, m.size, m.filepath, + `SELECT m.id, m.version, m.pid, m.no_reply, m.is_important, m.is_deflate, m.is_terminal, m.time_sent, m.from_addr, m.topic, m.type, m.size, m.filepath, COALESCE( (SELECT mt.time_read FROM msg_to mt WHERE mt.msg_id = m.id AND mt.addr = ANY($1)), (SELECT mat.time_read FROM msg_add_to mat WHERE mat.msg_id = m.id AND mat.addr = ANY($1)) @@ -310,13 +315,14 @@ func (h *MessageHandler) List(c *gin.Context) { for rows.Next() { var m messageListItem var dataPath string - if err := rows.Scan(&m.ID, &m.Version, &m.PID, &m.NoReply, &m.Important, &m.Deflate, &m.Time, &m.From, &m.Topic, &m.Type, &m.Size, &dataPath, &m.TimeRead); err != nil { + if err := rows.Scan(&m.ID, &m.Version, &m.PID, &m.NoReply, &m.Important, &m.Deflate, &m.Terminal, &m.Time, &m.From, &m.Topic, &m.Type, &m.Size, &dataPath, &m.TimeRead); err != nil { log.Printf("list messages scan: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list messages"}) return } m.HasPid = m.PID != nil m.Read = m.TimeRead != nil + m.dataPath = dataPath m.ShortText = h.extractShortText(dataPath, m.Type) messages = append(messages, m) msgIDs = append(msgIDs, m.ID) @@ -361,6 +367,8 @@ func (h *MessageHandler) List(c *gin.Context) { } } + h.populateListReactions(ctx, messages) + c.JSON(http.StatusOK, messages) } @@ -387,7 +395,7 @@ func (h *MessageHandler) Sent(c *gin.Context) { ctx := c.Request.Context() rows, err := h.DB.Pool.Query(ctx, - `SELECT m.id, m.version, m.pid, m.no_reply, m.is_important, m.is_deflate, m.time_sent, m.from_addr, m.topic, m.type, m.size, m.filepath + `SELECT m.id, m.version, m.pid, m.no_reply, m.is_important, m.is_deflate, m.is_terminal, m.time_sent, m.from_addr, m.topic, m.type, m.size, m.filepath FROM msg m WHERE m.from_addr = ANY($1) ORDER BY m.id DESC @@ -406,12 +414,13 @@ func (h *MessageHandler) Sent(c *gin.Context) { for rows.Next() { var m messageListItem var dataPath string - if err := rows.Scan(&m.ID, &m.Version, &m.PID, &m.NoReply, &m.Important, &m.Deflate, &m.Time, &m.From, &m.Topic, &m.Type, &m.Size, &dataPath); err != nil { + if err := rows.Scan(&m.ID, &m.Version, &m.PID, &m.NoReply, &m.Important, &m.Deflate, &m.Terminal, &m.Time, &m.From, &m.Topic, &m.Type, &m.Size, &dataPath); err != nil { log.Printf("list sent messages scan: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list sent messages"}) return } m.HasPid = m.PID != nil + m.dataPath = dataPath m.ShortText = h.extractShortText(dataPath, m.Type) messages = append(messages, m) msgIDs = append(msgIDs, m.ID) @@ -456,6 +465,8 @@ func (h *MessageHandler) Sent(c *gin.Context) { } } + h.populateListReactions(ctx, messages) + c.JSON(http.StatusOK, messages) } @@ -497,19 +508,9 @@ func (h *MessageHandler) Create(c *gin.Context) { ctx := c.Request.Context() - // Validate PID references an existing message. - if msg.PID != nil { - var exists bool - err := h.DB.Pool.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM msg WHERE id = $1)", *msg.PID).Scan(&exists) - if err != nil { - log.Printf("create message: validate pid: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate pid"}) - return - } - if !exists { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("PID %d not found", *msg.PID)}) - return - } + // Validate PID references an existing, non-terminal message. + if !h.validateParent(c, msg.PID) { + return } // Detect zip (deflate) content by checking for the zip magic bytes. @@ -522,10 +523,10 @@ func (h *MessageHandler) Create(c *gin.Context) { dataSize := len(msg.Data) var msgID int64 err := h.DB.Pool.QueryRow(ctx, - `INSERT INTO msg (version, pid, no_reply, is_important, is_deflate, from_addr, topic, type, size, filepath, time_sent) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, '', NULL) + `INSERT INTO msg (version, pid, no_reply, is_important, is_deflate, is_terminal, from_addr, topic, type, size, filepath, time_sent) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, '', NULL) RETURNING id`, - msg.Version, msg.PID, msg.NoReply, msg.Important, msg.Deflate, msg.From, msg.Topic, msg.Type, dataSize, + msg.Version, msg.PID, msg.NoReply, msg.Important, msg.Deflate, msg.Terminal, msg.From, msg.Topic, msg.Type, dataSize, ).Scan(&msgID) if err != nil { log.Printf("create message: insert: %v", err) @@ -595,6 +596,11 @@ func (h *MessageHandler) Get(c *gin.Context) { // Compute ShortText only after authorization has been confirmed. msg.ShortText = h.extractShortText(dataPath, msg.Type) + msg.Reaction = h.reactionOf(msg.HasPid, msg.NoReply, msg.Terminal, msg.Important, msg.HasAddTo, msg.Type, msg.Size, len(msg.Attachments), dataPath) + msg.Reactions = h.loadReactions(ctx, []int64{msgID})[msgID] + if msg.Reactions == nil { + msg.Reactions = []models.Reaction{} + } // Populate per-recipient read state for the calling user. The sender // has no read state of their own. @@ -727,6 +733,10 @@ func (h *MessageHandler) Update(c *gin.Context) { return } + if !h.validateParent(c, msg.PID) { + return + } + if int64(len(msg.Data)) > h.MaxDataSize { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "message data exceeds maximum size"}) return @@ -758,8 +768,8 @@ func (h *MessageHandler) Update(c *gin.Context) { } _, err = h.DB.Pool.Exec(ctx, - `UPDATE msg SET version=$1, pid=$2, no_reply=$3, is_important=$4, is_deflate=$5, topic=$6, type=$7, size=$8, filepath=$9 WHERE id=$10`, - msg.Version, msg.PID, msg.NoReply, msg.Important, msg.Deflate, msg.Topic, msg.Type, len(msg.Data), dataPath, msgID, + `UPDATE msg SET version=$1, pid=$2, no_reply=$3, is_important=$4, is_deflate=$5, is_terminal=$6, topic=$7, type=$8, size=$9, filepath=$10 WHERE id=$11`, + msg.Version, msg.PID, msg.NoReply, msg.Important, msg.Deflate, msg.Terminal, msg.Topic, msg.Type, len(msg.Data), dataPath, msgID, ) if err != nil { log.Printf("update message %d: %v", msgID, err) @@ -1037,9 +1047,10 @@ func (h *MessageHandler) AddRecipients(c *gin.Context) { // own pid is irrelevant here and need not be looked up. var fromAddr string var timeSent *float64 + var terminal bool err := h.DB.Pool.QueryRow(ctx, - "SELECT from_addr, time_sent FROM msg WHERE id = $1", msgID, - ).Scan(&fromAddr, &timeSent) + "SELECT from_addr, time_sent, is_terminal FROM msg WHERE id = $1", msgID, + ).Scan(&fromAddr, &timeSent, &terminal) if err != nil { if errors.Is(err, pgx.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{"error": "message not found"}) @@ -1049,6 +1060,12 @@ func (h *MessageHandler) AddRecipients(c *gin.Context) { } return } + // A terminal message is a leaf: recipients cannot be added to it (fmsg + // Specification v0.6.0 §12). + if terminal { + c.JSON(http.StatusConflict, gin.H{"error": "message is terminal; recipients cannot be added to it"}) + return + } // Verify the requester is an existing participant (from or msg_to). if fromAddr != identity { @@ -1277,7 +1294,7 @@ func (h *MessageHandler) loadAddToBatches(ctx context.Context, msgIDs []int64) m // after performing their own authorization checks. func (h *MessageHandler) fetchMessage(ctx context.Context, msgID int64) (*models.Message, string, error) { row := h.DB.Pool.QueryRow(ctx, - `SELECT version, pid, no_reply, is_important, is_deflate, time_sent, from_addr, topic, type, size, filepath FROM msg WHERE id = $1`, + `SELECT version, pid, no_reply, is_important, is_deflate, is_terminal, time_sent, from_addr, topic, type, size, filepath FROM msg WHERE id = $1`, msgID, ) @@ -1285,7 +1302,7 @@ func (h *MessageHandler) fetchMessage(ctx context.Context, msgID int64) (*models var pid *int64 var timeSent *float64 var dataPath string - if err := row.Scan(&msg.Version, &pid, &msg.NoReply, &msg.Important, &msg.Deflate, &timeSent, &msg.From, &msg.Topic, &msg.Type, &msg.Size, &dataPath); err != nil { + if err := row.Scan(&msg.Version, &pid, &msg.NoReply, &msg.Important, &msg.Deflate, &msg.Terminal, &timeSent, &msg.From, &msg.Topic, &msg.Type, &msg.Size, &dataPath); err != nil { return nil, "", err } msg.PID = pid @@ -1334,6 +1351,7 @@ func (h *MessageHandler) messageItemFor(ctx context.Context, msgID int64, recipi Important: msg.Important, NoReply: msg.NoReply, Deflate: msg.Deflate, + Terminal: msg.Terminal, PID: msg.PID, From: msg.From, To: msg.To, @@ -1345,6 +1363,12 @@ func (h *MessageHandler) messageItemFor(ctx context.Context, msgID int64, recipi Size: msg.Size, ShortText: h.extractShortText(dataPath, msg.Type), Attachments: msg.Attachments, + dataPath: dataPath, + } + item.Reaction = h.reactionOf(item.HasPid, item.NoReply, item.Terminal, item.Important, item.HasAddTo, item.Type, item.Size, len(item.Attachments), dataPath) + item.Reactions = h.loadReactions(ctx, []int64{msgID})[msgID] + if item.Reactions == nil { + item.Reactions = []models.Reaction{} } // Populate the recipient's per-recipient read state, mirroring Get. @@ -1638,6 +1662,33 @@ func validatePidRelations(pid *int64, topic string) error { return nil } +// validateParent checks a draft's pid, when set, references an existing +// message that is not terminal, writing the error response and returning +// false otherwise. A terminal message is a leaf: no message may reference it +// via pid (fmsg Specification v0.6.0 §3), and the database refuses such a row +// too; checking here gives the caller a clear answer instead. +func (h *MessageHandler) validateParent(c *gin.Context, pid *int64) bool { + if pid == nil { + return true + } + var terminal bool + err := h.DB.Pool.QueryRow(c.Request.Context(), "SELECT is_terminal FROM msg WHERE id = $1", *pid).Scan(&terminal) + if errors.Is(err, pgx.ErrNoRows) { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("PID %d not found", *pid)}) + return false + } + if err != nil { + log.Printf("validate pid %d: %v", *pid, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate pid"}) + return false + } + if terminal { + c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("PID %d is terminal; it cannot be replied to", *pid)}) + return false + } + return true +} + // checkDistinctRecipients returns an error if any address in to or addTo // appears more than once (case-insensitive). func checkDistinctRecipients(to, addTo []string) error { diff --git a/internal/handlers/push.go b/internal/handlers/push.go index 1c9a7bf..2d2bb8e 100644 --- a/internal/handlers/push.go +++ b/internal/handlers/push.go @@ -178,6 +178,9 @@ func (h *PushHandler) NotifyNewMsg(_ context.Context, msgID int64, addr string) log.Printf("push notify: build message %d for %s: %v", msgID, addr, err) return } + if item.Reaction != nil { + return // FMSG-005: a reaction does not warrant a notification + } payload, err := json.Marshal(buildPushPayload(item, rootID, h.iconURL)) if err != nil { diff --git a/internal/handlers/reactions.go b/internal/handlers/reactions.go new file mode 100644 index 0000000..525b617 --- /dev/null +++ b/internal/handlers/reactions.go @@ -0,0 +1,434 @@ +package handlers + +import ( + "bytes" + "context" + "errors" + "io" + "log" + "net/http" + "os" + "sort" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5" + "github.com/markmnl/fmsg-webapi/internal/emoji" + "github.com/markmnl/fmsg-webapi/internal/middleware" + "github.com/markmnl/fmsg-webapi/internal/models" +) + +// FMSG-005 Reactions. A reaction is an ordinary message recognised by its +// shape: a reply (pid set) with no reply and terminal set, not important, with +// no add-to batch, body type text/plain;charset=UTF-8 (Common Media Type 56), +// no attachments, and data that is a single emoji or empty to clear. A +// reactor's effective reaction on a subject is their latest reaction message +// by time, ties broken by the greater message hash. + +const ( + // reactionMediaType is Common Media Type 56, the body type of a reaction. + reactionMediaType = "text/plain;charset=UTF-8" + // maxReactionBytes bounds reaction data (FMSG-005 Fields). + maxReactionBytes = 64 +) + +// isReactionMediaType reports whether a stored type string is the reaction +// media type, tolerating case and whitespace differences in the parameter. +func isReactionMediaType(mediaType string) bool { + return strings.EqualFold(strings.ReplaceAll(mediaType, " ", ""), reactionMediaType) +} + +// reactionShape reports whether a message's metadata has the shape of a +// reaction, before its data is examined. +func reactionShape(hasPid, noReply, terminal, important, hasAddTo bool, mediaType string, size, attachments int) bool { + return hasPid && noReply && terminal && !important && !hasAddTo && + size >= 0 && size <= maxReactionBytes && attachments == 0 && + isReactionMediaType(mediaType) +} + +// readReactionData reads a shape-matching message's body and returns the emoji +// it carries ("" clears) and whether the body is a valid reaction body. +func (h *MessageHandler) readReactionData(dataPath string, size int) (string, bool) { + if size == 0 { + return "", true + } + cleanPath, ok := safeDataPath(dataPath, h.DataDir) + if !ok { + return "", false + } + f, err := os.Open(cleanPath) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + log.Printf("reaction data: open %s: %v", cleanPath, err) + } + return "", false + } + defer f.Close() + buf := make([]byte, maxReactionBytes+1) + n, err := io.ReadFull(f, buf) + if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { + log.Printf("reaction data: read %s: %v", cleanPath, err) + return "", false + } + s := string(buf[:n]) + if !emoji.IsSingle(s) { + return "", false + } + return s, true +} + +// reactionOf returns the reaction a message carries, or nil when the message +// is not a reaction. +func (h *MessageHandler) reactionOf(hasPid, noReply, terminal, important, hasAddTo bool, mediaType string, size, attachments int, dataPath string) *string { + if !reactionShape(hasPid, noReply, terminal, important, hasAddTo, mediaType, size, attachments) { + return nil + } + s, ok := h.readReactionData(dataPath, size) + if !ok { + return nil + } + return &s +} + +// reactionSubject reports the subject of msgID when msgID is a reaction. Used +// by the WebSocket hub to route an arriving reaction as a reaction event. +func (h *MessageHandler) reactionSubject(ctx context.Context, msgID int64) (int64, bool) { + var pid *int64 + var noReply, terminal, important, hasAddTo bool + var mediaType, dataPath string + var size, attachments int + err := h.DB.Pool.QueryRow(ctx, ` + SELECT m.pid, m.no_reply, m.is_terminal, m.is_important, m.type, m.size, m.filepath, + (SELECT COUNT(*) FROM msg_attachment a WHERE a.msg_id = m.id), + EXISTS (SELECT 1 FROM msg_add_to_batch b WHERE b.msg_id = m.id) + FROM msg m WHERE m.id = $1`, msgID, + ).Scan(&pid, &noReply, &terminal, &important, &mediaType, &size, &dataPath, &attachments, &hasAddTo) + if err != nil { + if !errors.Is(err, pgx.ErrNoRows) { + log.Printf("reaction subject of %d: %v", msgID, err) + } + return 0, false + } + if pid == nil || h.reactionOf(true, noReply, terminal, important, hasAddTo, mediaType, size, attachments, dataPath) == nil { + return 0, false + } + return *pid, true +} + +// reactionRow is one stored reaction message on a subject. +type reactionRow struct { + subjectID int64 + msgID int64 + from string + time float64 + hash []byte // message hash; nil until the host has computed it + emoji string // "" clears +} + +// newerReaction reports whether a supersedes b as a reactor's effective +// reaction: later time wins, ties broken by the greater message hash. +func newerReaction(a, b reactionRow) bool { + if a.time != b.time { + return a.time > b.time + } + return bytes.Compare(a.hash, b.hash) > 0 +} + +// effectiveReactions reduces reaction rows to each subject's effective +// reactions: one per reactor, grouped by emoji in order of first reaction, +// reactors within a group in time order. A reactor whose effective reaction is +// empty has none and is omitted. +func effectiveReactions(rows []reactionRow) map[int64][]models.Reaction { + bySubject := make(map[int64]map[string]reactionRow) + for _, r := range rows { + reactors := bySubject[r.subjectID] + if reactors == nil { + reactors = make(map[string]reactionRow) + bySubject[r.subjectID] = reactors + } + key := strings.ToLower(r.from) + if cur, ok := reactors[key]; !ok || newerReaction(r, cur) { + reactors[key] = r + } + } + + result := make(map[int64][]models.Reaction, len(bySubject)) + for subject, reactors := range bySubject { + var eff []reactionRow + for _, r := range reactors { + if r.emoji != "" { + eff = append(eff, r) + } + } + sort.Slice(eff, func(i, j int) bool { + if eff[i].time != eff[j].time { + return eff[i].time < eff[j].time + } + return eff[i].msgID < eff[j].msgID + }) + var groups []models.Reaction + idx := make(map[string]int) + for _, r := range eff { + i, ok := idx[r.emoji] + if !ok { + groups = append(groups, models.Reaction{Emoji: r.emoji}) + i = len(groups) - 1 + idx[r.emoji] = i + } + groups[i].From = append(groups[i].From, r.from) + } + if len(groups) > 0 { + result[subject] = groups + } + } + return result +} + +// loadReactionRows loads every sent reaction on the given subjects, or only +// those from one reactor when from is non-empty. Rows whose data is not a +// valid reaction body are ordinary messages and are skipped. +func (h *MessageHandler) loadReactionRows(ctx context.Context, subjectIDs []int64, from string) ([]reactionRow, error) { + if len(subjectIDs) == 0 { + return nil, nil + } + rows, err := h.DB.Pool.Query(ctx, ` + SELECT r.pid, r.id, r.from_addr, r.time_sent, r.sha256, r.size, r.filepath + FROM msg r + WHERE r.pid = ANY($1) + AND r.time_sent IS NOT NULL + AND r.is_terminal AND r.no_reply AND NOT r.is_important + AND r.size <= $2 + AND lower(replace(r.type, ' ', '')) = $3 + AND ($4 = '' OR lower(r.from_addr) = lower($4)) + AND NOT EXISTS (SELECT 1 FROM msg_attachment a WHERE a.msg_id = r.id) + AND NOT EXISTS (SELECT 1 FROM msg_add_to_batch b WHERE b.msg_id = r.id) + ORDER BY r.pid, r.time_sent, r.id`, + subjectIDs, maxReactionBytes, strings.ToLower(reactionMediaType), from, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var out []reactionRow + for rows.Next() { + var r reactionRow + var size int + var dataPath string + if err := rows.Scan(&r.subjectID, &r.msgID, &r.from, &r.time, &r.hash, &size, &dataPath); err != nil { + return nil, err + } + e, ok := h.readReactionData(dataPath, size) + if !ok { + continue + } + r.emoji = e + out = append(out, r) + } + return out, rows.Err() +} + +// loadReactions returns the effective reactions on each of the given subjects. +func (h *MessageHandler) loadReactions(ctx context.Context, subjectIDs []int64) map[int64][]models.Reaction { + rows, err := h.loadReactionRows(ctx, subjectIDs, "") + if err != nil { + log.Printf("load reactions: %v", err) + return nil + } + return effectiveReactions(rows) +} + +// populateListReactions fills Reaction and Reactions on every list item. +func (h *MessageHandler) populateListReactions(ctx context.Context, items []messageListItem) { + ids := make([]int64, len(items)) + for i := range items { + it := &items[i] + it.Reaction = h.reactionOf(it.HasPid, it.NoReply, it.Terminal, it.Important, it.HasAddTo, it.Type, it.Size, len(it.Attachments), it.dataPath) + ids[i] = it.ID + } + reactions := h.loadReactions(ctx, ids) + for i := range items { + items[i].Reactions = reactions[items[i].ID] + if items[i].Reactions == nil { + items[i].Reactions = []models.Reaction{} + } + } +} + +// reactionRecipients returns a subject's participants other than the reactor, +// in message order and deduplicated case-insensitively: the to list of a +// reaction (FMSG-005 Recipients). +func reactionRecipients(subject *models.Message, reactor string) []string { + seen := map[string]bool{strings.ToLower(reactor): true} + var out []string + add := func(addr string) { + key := strings.ToLower(addr) + if seen[key] { + return + } + seen[key] = true + out = append(out, addr) + } + add(subject.From) + for _, a := range subject.To { + add(a) + } + for _, b := range subject.AddTo { + add(b.AddToFrom) + for _, a := range b.To { + add(a) + } + } + return out +} + +// reactInput is the request body of POST /fmsg/:id/react. +type reactInput struct { + Emoji *string `json:"emoji"` // a single emoji; null or "" clears the caller's reaction +} + +// React handles POST /fmsg/:id/react — sets or clears the caller's reaction on +// a message by sending a reaction message (FMSG-005) to every other +// participant. Sending an identical reaction to the current effective one is +// idempotent and sends nothing. +func (h *MessageHandler) React(c *gin.Context) { + identity := middleware.GetIdentity(c) + msgID, ok := parseID(c) + if !ok { + return + } + + var in reactInput + if err := c.ShouldBindJSON(&in); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + want := "" + if in.Emoji != nil { + want = *in.Emoji + } + if want != "" && !emoji.IsSingle(want) { + c.JSON(http.StatusBadRequest, gin.H{"error": "emoji must be a single emoji, or empty to clear"}) + return + } + + ctx := c.Request.Context() + subject, _, err := h.fetchMessage(ctx, msgID) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + c.JSON(http.StatusNotFound, gin.H{"error": "message not found"}) + } else { + log.Printf("react to %d: fetch: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve message"}) + } + return + } + + // Only a participant of the subject may react to it (the host enforces + // the same rule on the wire). + if !strings.EqualFold(subject.From, identity) && !isRecipient(subject.To, identity) && !addToContains(subject.AddTo, identity) { + c.JSON(http.StatusForbidden, gin.H{"error": "only participants may react to a message"}) + return + } + if subject.Time == nil { + c.JSON(http.StatusConflict, gin.H{"error": "cannot react to a draft"}) + return + } + if subject.Terminal { + c.JSON(http.StatusConflict, gin.H{"error": "message is terminal; it cannot be reacted to"}) + return + } + + recipients := reactionRecipients(subject, identity) + if len(recipients) == 0 { + c.JSON(http.StatusConflict, gin.H{"error": "message has no other participants to send a reaction to"}) + return + } + + // Idempotency: the caller's current effective reaction. + mine, err := h.loadReactionRows(ctx, []int64{msgID}, identity) + if err != nil { + log.Printf("react to %d: load reactions: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load reactions"}) + return + } + var current *reactionRow + for i := range mine { + if current == nil || newerReaction(mine[i], *current) { + current = &mine[i] + } + } + switch { + case current != nil && current.emoji == want: + c.JSON(http.StatusOK, gin.H{"id": current.msgID, "time": current.time}) + return + case current == nil && want == "": + c.JSON(http.StatusOK, gin.H{"id": nil, "time": nil}) + return + } + + // As for any reply, refuse when a remote recipient host can never accept + // it because it does not hold the subject (SPEC §10.3, code 6). + if replyDomains := remoteRecipientDomains(&models.Message{To: recipients}, h.LocalDomain); len(replyDomains) > 0 { + parentFromDomain, byDomain, derr := h.parentDeliveryByDomain(ctx, msgID) + if derr != nil { + log.Printf("react to %d: verify delivery: %v", msgID, derr) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify parent delivery"}) + return + } + if blocked := undeliverableReplyDomains(replyDomains, parentFromDomain, byDomain); len(blocked) > 0 { + c.JSON(http.StatusConflict, gin.H{"error": "reaction cannot be accepted by recipient host(s): " + strings.Join(blocked, "; ")}) + return + } + } + + now := float64(time.Now().UnixMicro()) / 1e6 + tx, err := h.DB.Pool.Begin(ctx) + if err != nil { + log.Printf("react to %d: begin: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reaction"}) + return + } + defer tx.Rollback(ctx) + + var reactionID int64 + if err := tx.QueryRow(ctx, + `INSERT INTO msg (version, pid, no_reply, is_important, is_deflate, is_terminal, from_addr, topic, type, size, filepath, time_sent) + VALUES (1, $1, true, false, false, true, $2, '', $3, $4, '', $5) + RETURNING id`, + msgID, identity, reactionMediaType, len(want), now, + ).Scan(&reactionID); err != nil { + log.Printf("react to %d: insert: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reaction"}) + return + } + dataPath, err := h.saveMessageData(identity, reactionID, mimeToExt(reactionMediaType), want) + if err != nil { + log.Printf("react to %d: save data: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save reaction"}) + return + } + if _, err := tx.Exec(ctx, "UPDATE msg SET filepath = $1 WHERE id = $2", dataPath, reactionID); err != nil { + log.Printf("react to %d: update filepath: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reaction"}) + return + } + for _, addr := range recipients { + if _, err := tx.Exec(ctx, "INSERT INTO msg_to (msg_id, addr) VALUES ($1, $2)", reactionID, addr); err != nil { + log.Printf("react to %d: insert recipient %s: %v", msgID, addr, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reaction"}) + return + } + } + if err := tx.Commit(ctx); err != nil { + log.Printf("react to %d: commit: %v", msgID, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send reaction"}) + return + } + + // fmsgd's outbound sender skips the local domain, so local recipients + // have their delivery resolved here, as Send does. + h.resolveLocalDelivery(ctx, "msg_to", reactionID, h.LocalDomain, recipients) + + c.JSON(http.StatusCreated, gin.H{"id": reactionID, "time": now}) +} diff --git a/internal/handlers/reactions_test.go b/internal/handlers/reactions_test.go new file mode 100644 index 0000000..8e2478c --- /dev/null +++ b/internal/handlers/reactions_test.go @@ -0,0 +1,190 @@ +package handlers + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/markmnl/fmsg-webapi/internal/models" +) + +func TestIsReactionMediaType(t *testing.T) { + for _, s := range []string{"text/plain;charset=UTF-8", "text/plain; charset=utf-8", "TEXT/PLAIN;CHARSET=UTF-8"} { + if !isReactionMediaType(s) { + t.Errorf("isReactionMediaType(%q) = false", s) + } + } + for _, s := range []string{"text/plain", "text/markdown;charset=UTF-8", "application/octet-stream", ""} { + if isReactionMediaType(s) { + t.Errorf("isReactionMediaType(%q) = true", s) + } + } +} + +func TestReactionShape(t *testing.T) { + ok := func(hasPid, noReply, terminal, important, hasAddTo bool, typ string, size, att int) bool { + return reactionShape(hasPid, noReply, terminal, important, hasAddTo, typ, size, att) + } + if !ok(true, true, true, false, false, reactionMediaType, 4, 0) { + t.Error("canonical reaction shape rejected") + } + if !ok(true, true, true, false, false, reactionMediaType, 0, 0) { + t.Error("clearing reaction shape rejected") + } + cases := map[string]bool{ + "no pid": ok(false, true, true, false, false, reactionMediaType, 4, 0), + "no no-reply": ok(true, false, true, false, false, reactionMediaType, 4, 0), + "not terminal": ok(true, true, false, false, false, reactionMediaType, 4, 0), + "important": ok(true, true, true, true, false, reactionMediaType, 4, 0), + "add-to": ok(true, true, true, false, true, reactionMediaType, 4, 0), + "wrong type": ok(true, true, true, false, false, "text/markdown", 4, 0), + "too big": ok(true, true, true, false, false, reactionMediaType, maxReactionBytes+1, 0), + "attachment": ok(true, true, true, false, false, reactionMediaType, 4, 1), + "negative size": ok(true, true, true, false, false, reactionMediaType, -1, 0), + } + for name, got := range cases { + if got { + t.Errorf("%s: shape accepted", name) + } + } +} + +func TestReactionOfReadsData(t *testing.T) { + dir := t.TempDir() + h := &MessageHandler{DataDir: dir} + write := func(name, data string) string { + p := filepath.Join(dir, name) + if err := os.WriteFile(p, []byte(data), 0o600); err != nil { + t.Fatal(err) + } + return p + } + thumbs := write("thumbs", "👍") + text := write("text", "ok 👍") + empty := write("empty", "") + + if r := h.reactionOf(true, true, true, false, false, reactionMediaType, 4, 0, thumbs); r == nil || *r != "👍" { + t.Errorf("reactionOf(thumbs) = %v, want 👍", r) + } + if r := h.reactionOf(true, true, true, false, false, reactionMediaType, 6, 0, text); r != nil { + t.Errorf("reactionOf(text) = %q, want nil", *r) + } + if r := h.reactionOf(true, true, true, false, false, reactionMediaType, 0, 0, empty); r == nil || *r != "" { + t.Errorf("reactionOf(empty) = %v, want \"\"", r) + } + if r := h.reactionOf(true, true, true, false, false, reactionMediaType, 4, 0, filepath.Join(dir, "missing")); r != nil { + t.Errorf("reactionOf(missing file) = %q, want nil", *r) + } + if r := h.reactionOf(true, true, true, false, false, reactionMediaType, 4, 0, "/etc/passwd"); r != nil { + t.Errorf("reactionOf(outside data dir) = %q, want nil", *r) + } + if r := h.reactionOf(true, false, true, false, false, reactionMediaType, 4, 0, thumbs); r != nil { + t.Errorf("reactionOf(wrong shape) = %q, want nil", *r) + } +} + +func TestEffectiveReactions(t *testing.T) { + rows := []reactionRow{ + {subjectID: 1, msgID: 10, from: "@bob@example.com", time: 100, emoji: "👍"}, + {subjectID: 1, msgID: 11, from: "@carol@example.edu", time: 101, emoji: "👍"}, + {subjectID: 1, msgID: 12, from: "@Bob@example.com", time: 102, emoji: "❤️"}, // bob changes, case-insensitive + {subjectID: 1, msgID: 13, from: "@dave@example.com", time: 103, emoji: "👍"}, + {subjectID: 1, msgID: 14, from: "@dave@example.com", time: 104, emoji: ""}, // dave clears + {subjectID: 2, msgID: 20, from: "@bob@example.com", time: 100, emoji: "🎉"}, + {subjectID: 3, msgID: 30, from: "@bob@example.com", time: 100, emoji: ""}, // only a clear + } + got := effectiveReactions(rows) + want := map[int64][]models.Reaction{ + 1: { + {Emoji: "👍", From: []string{"@carol@example.edu"}}, + {Emoji: "❤️", From: []string{"@Bob@example.com"}}, + }, + 2: {{Emoji: "🎉", From: []string{"@bob@example.com"}}}, + } + if !reflect.DeepEqual(got, want) { + gj, _ := json.Marshal(got) + wj, _ := json.Marshal(want) + t.Errorf("effectiveReactions = %s, want %s", gj, wj) + } +} + +func TestEffectiveReactionsTieBreaksOnHash(t *testing.T) { + rows := []reactionRow{ + {subjectID: 1, msgID: 10, from: "@bob@example.com", time: 100, hash: []byte{0x01}, emoji: "👍"}, + {subjectID: 1, msgID: 11, from: "@bob@example.com", time: 100, hash: []byte{0x02}, emoji: "❤️"}, + } + got := effectiveReactions(rows) + if len(got[1]) != 1 || got[1][0].Emoji != "❤️" { + t.Errorf("tie at equal time should pick the greater hash, got %+v", got[1]) + } + // Order independence. + got = effectiveReactions([]reactionRow{rows[1], rows[0]}) + if len(got[1]) != 1 || got[1][0].Emoji != "❤️" { + t.Errorf("tie-break must not depend on row order, got %+v", got[1]) + } +} + +func TestReactionRecipients(t *testing.T) { + subject := &models.Message{ + From: "@alice@example.com", + To: []string{"@bob@example.com", "@carol@example.edu"}, + AddTo: []models.AddToBatch{ + {AddToFrom: "@bob@example.com", To: []string{"@dave@example.org", "@Carol@example.edu"}}, + }, + } + got := reactionRecipients(subject, "@BOB@example.com") + want := []string{"@alice@example.com", "@carol@example.edu", "@dave@example.org"} + if !reflect.DeepEqual(got, want) { + t.Errorf("reactionRecipients = %v, want %v", got, want) + } + if got := reactionRecipients(&models.Message{From: "@alice@example.com", To: []string{"@alice@example.com"}}, "@alice@example.com"); len(got) != 0 { + t.Errorf("self-only message should have no recipients, got %v", got) + } +} + +// A reaction arriving as new_msg is routed to its subject as a reaction event. +func TestHubDispatch_ReactionRoutesToSubject(t *testing.T) { + var built []int64 + hub := &Hub{ + buildItem: func(_ context.Context, msgID int64, _ string) (*messageListItem, error) { + built = append(built, msgID) + return &messageListItem{ID: msgID}, nil + }, + reactionSubject: func(_ context.Context, msgID int64) (int64, bool) { + if msgID == 42 { + return 7, true + } + return 0, false + }, + registry: make(map[string]map[*wsClient]struct{}), + } + c := &wsClient{addr: "@bob@example.com", send: make(chan []byte, 4)} + hub.Register(c) + + subject, ok := hub.reactionSubject(context.Background(), 42) + if !ok || subject != 7 { + t.Fatalf("reactionSubject(42) = %d,%v", subject, ok) + } + hub.dispatch(context.Background(), subject, "@bob@example.com", eventReaction) + select { + case frame := <-c.send: + var env struct { + Type string `json:"type"` + Data messageListItem `json:"data"` + } + if err := json.Unmarshal(frame, &env); err != nil { + t.Fatal(err) + } + if env.Type != eventReaction || env.Data.ID != 7 { + t.Errorf("got %s for %d, want %s for 7", env.Type, env.Data.ID, eventReaction) + } + default: + t.Fatal("no frame pushed") + } + if len(built) != 1 || built[0] != 7 { + t.Errorf("built %v, want [7] (the subject, not the reaction)", built) + } +} diff --git a/internal/handlers/thread.go b/internal/handlers/thread.go index 7a164d8..c0b99aa 100644 --- a/internal/handlers/thread.go +++ b/internal/handlers/thread.go @@ -52,6 +52,7 @@ type threadMessage struct { NoReply bool `json:"no_reply,omitempty"` Important bool `json:"important,omitempty"` Deflate bool `json:"deflate,omitempty"` + Terminal bool `json:"terminal,omitempty"` From string `json:"from,omitempty"` To []string `json:"to,omitempty"` AddTo []models.AddToBatch `json:"add_to,omitempty"` @@ -377,18 +378,18 @@ func (h *MessageHandler) ThreadMessages(c *gin.Context) { rows, err := tx.Query(ctx, ` WITH RECURSIVE chain AS ( - SELECT id, version, pid, no_reply, is_important, is_deflate, time_sent, + SELECT id, version, pid, no_reply, is_important, is_deflate, is_terminal, time_sent, from_addr, topic, type, size, filepath, sha256, 0 AS depth FROM msg WHERE id = $1 UNION ALL SELECT m.id, m.version, m.pid, m.no_reply, m.is_important, m.is_deflate, - m.time_sent, m.from_addr, m.topic, m.type, m.size, m.filepath, + m.is_terminal, m.time_sent, m.from_addr, m.topic, m.type, m.size, m.filepath, m.sha256, c.depth + 1 FROM msg m JOIN chain c ON m.id = c.pid WHERE c.depth + 1 < $3 ) SELECT c.id, c.version, c.pid, c.no_reply, c.is_important, c.is_deflate, - c.time_sent, c.from_addr, c.topic, c.type, c.size, c.filepath, + c.is_terminal, c.time_sent, c.from_addr, c.topic, c.type, c.size, c.filepath, encode(c.sha256, 'hex'), (c.from_addr = ANY($2) OR EXISTS (SELECT 1 FROM msg_to t WHERE t.msg_id = c.id AND t.addr = ANY($2)) @@ -404,7 +405,7 @@ func (h *MessageHandler) ThreadMessages(c *gin.Context) { var m threadMessage var hash *string if err = rows.Scan(&m.ID, &m.Version, &m.PID, &m.NoReply, &m.Important, - &m.Deflate, &m.Time, &m.From, &m.Topic, &m.Type, &m.Size, &m.dataPath, + &m.Deflate, &m.Terminal, &m.Time, &m.From, &m.Topic, &m.Type, &m.Size, &m.dataPath, &hash, &m.Visible); err != nil { rows.Close() c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve thread"}) diff --git a/internal/models/models.go b/internal/models/models.go index defda21..c8f3118 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -33,6 +33,7 @@ type Message struct { Important bool `json:"important"` NoReply bool `json:"no_reply"` Deflate bool `json:"deflate"` + Terminal bool `json:"terminal"` PID *int64 `json:"pid"` From string `json:"from"` To []string `json:"to"` @@ -46,4 +47,13 @@ type Message struct { Read bool `json:"read"` TimeRead *float64 `json:"time_read"` Attachments []Attachment `json:"attachments"` + Reaction *string `json:"reaction"` // FMSG-005: the emoji this message carries when it is a reaction ("" clears); nil otherwise + Reactions []Reaction `json:"reactions"` // FMSG-005: effective reactions on this message +} + +// Reaction is one emoji currently reacted on a message and the participants +// whose effective reaction it is (FMSG-005). +type Reaction struct { + Emoji string `json:"emoji"` + From []string `json:"from"` }