diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0a7c08637c9f4968f20289092e3dab317b1cdfb..7ab520cd02563b548eabe925202ef87324725e2c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,7 +43,7 @@ wasm-test: - unset SSH_PRIVATE_KEY - unset $(env | grep '=' | awk -F= '{print $1}' | grep -v PATH | grep -v GO | grep -v HOME) - echo "WASM TESTS DISABLED FOR XX-4522, but will run them just so you can see output" - - GOOS=js GOARCH=wasm go test ./... -v || true + - GOOS=js GOARCH=wasm go test ./... -v build: stage: build @@ -77,22 +77,6 @@ build-workers: - release/ expire_in: 1 hour -emoji-update: - stage: build - except: - - tags - only: - - release - - master - script: - - go mod vendor -v - - mkdir -p release - - go run -ldflags '-w -s' -trimpath ./emoji/... -o emojiSet.json -v 0 - - cp emojiSet.json release/ - artifacts: - paths: - - release/ - expire_in: 1 hour tag: stage: build @@ -116,11 +100,9 @@ combine-artifacts: - echo $PIPELINE_JOBS - BUILD_JOB_JSON=$(echo $PIPELINE_JOBS | jq '.[] | select(.name=="build")') - BUILD_WORKERS_JOB_JSON=$(echo $PIPELINE_JOBS | jq '.[] | select(.name=="build-workers")') - - EMOJI_UPDATE_JOB_JSON=$(echo $PIPELINE_JOBS | jq '.[] | select(.name=="emoji-update")') - BUILD_JOB_ID=$(echo $BUILD_JOB_JSON | jq -r '.["id"]') - BUILD_WORKERS_JOB_ID=$(echo $BUILD_WORKERS_JOB_JSON | jq -r '.["id"]') - - EMOJI_UPDATE_JOB_ID=$(echo $EMOJI_UPDATE_JOB_JSON | jq -r '.["id"]') - rm -rf release - mkdir -p release @@ -133,8 +115,6 @@ combine-artifacts: - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/channelsIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/channelsIndexedDbWorker.js' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/dmIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/dmIndexedDbWorker.js' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/logFileWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/logFileWorker.js' - - 'if [[ $CI_COMMIT_BRANCH =~ ^(release|master)$ ]]; then curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/emojiSet.json $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$EMOJI_UPDATE_JOB_ID/artifacts/release/emojiSet.json; fi' - - ls release artifacts: paths: diff --git a/Makefile b/Makefile index f793672632dd35bd53a4663371d701e3d47cd107..f8c143a6a24f68f6b70d2bc64cc6028b56f4c9af 100644 --- a/Makefile +++ b/Makefile @@ -11,11 +11,11 @@ build: GOOS=js GOARCH=wasm go build ./... update_release: - GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release - GOFLAGS="" go get gitlab.com/elixxir/crypto@release + GOFLAGS="" go get gitlab.com/xx_network/primitives@release GOFLAGS="" go get gitlab.com/elixxir/primitives@release GOFLAGS="" go get gitlab.com/xx_network/crypto@release - GOFLAGS="" go get gitlab.com/xx_network/primitives@release + GOFLAGS="" go get gitlab.com/elixxir/crypto@release + GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release update_master: GOFLAGS="" go get -d gitlab.com/elixxir/client@master @@ -32,9 +32,6 @@ worker_binaries: GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/... GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-logFileWorker.wasm ./logging/workerThread/... -emojis: - go run -ldflags '-w -s' -trimpath ./emoji/... -o emojiSet.json - binaries: binary worker_binaries wasm_tests: diff --git a/emoji/emojiMart.go b/emoji/emojiMart.go deleted file mode 100644 index 74c84172759ae61923e628b2f51309664398ade2..0000000000000000000000000000000000000000 --- a/emoji/emojiMart.go +++ /dev/null @@ -1,65 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package main - -// emojiID represents the alias for an emoji in emoji-mart. For example, the -// alias for the emoji 💯 would be "100". This adheres strictly to how Emoji -// Mart categorizes their emojis within the categories section of their JSON -// file. -type emojiID string - -// codepoint represents the Unicode codepoint or codepoints for an emoji. They -// are in lowercase and if there are multiple codepoints, they are seperated by -// a dash ("-"). For example, the emoji 💯 would have the codepoint "1f4af". -type codepoint string - -// emojiMartSet is a representation of the JSON file format containing the emoji -// list in emoji-mart. It matches the object EmojiMartData: -// https://github.com/missive/emoji-mart/blob/main/packages/emoji-mart-data/index.d.ts -// -// Doc: https://github.com/missive/emoji-mart/ -// JSON example: https://github.com/missive/emoji-mart/blob/main/packages/emoji-mart-data/sets/14/native.json -type emojiMartSet struct { - Categories []category `json:"categories"` - Emojis map[emojiID]emoji `json:"emojis"` - Aliases map[string]emojiID `json:"aliases"` - Sheet sheet `json:"sheet"` -} - -// category adheres to the categories field within the EmojiMartData Javascript -// interface. -type category struct { - ID string `json:"id"` - Emojis []emojiID `json:"emojis"` -} - -// emoji adheres to the emojis field within the EmojiMartData Javascript -// interface. -type emoji struct { - ID emojiID `json:"id"` - Name string `json:"name"` - Keywords []string `json:"keywords"` - Skins []skin `json:"skins"` - Version float64 `json:"version"` - Emoticons []string `json:"emoticons,omitempty"` -} - -// skin adheres to the skins field within the EmojiMartData Javascript interface. -type skin struct { - Unified codepoint `json:"unified"` - Native string `json:"native"` - X float64 `json:"x,omitempty"` - Y float64 `json:"y,omitempty"` -} - -// sheet adheres to the sheet field within the EmojiMartData Javascript -// interface. -type sheet struct { - Cols float64 `json:"cols"` - Rows float64 `json:"rows"` -} diff --git a/emoji/emojiMart.json b/emoji/emojiMart.json deleted file mode 100644 index 1b23125d005428252cbeb125c83a7a74f0b5ed02..0000000000000000000000000000000000000000 --- a/emoji/emojiMart.json +++ /dev/null @@ -1 +0,0 @@ -{"categories":[{"id":"people","emojis":["grinning","smiley","smile","grin","laughing","sweat_smile","rolling_on_the_floor_laughing","joy","slightly_smiling_face","upside_down_face","melting_face","wink","blush","innocent","smiling_face_with_3_hearts","heart_eyes","star-struck","kissing_heart","kissing","relaxed","kissing_closed_eyes","kissing_smiling_eyes","smiling_face_with_tear","yum","stuck_out_tongue","stuck_out_tongue_winking_eye","zany_face","stuck_out_tongue_closed_eyes","money_mouth_face","hugging_face","face_with_hand_over_mouth","face_with_open_eyes_and_hand_over_mouth","face_with_peeking_eye","shushing_face","thinking_face","saluting_face","zipper_mouth_face","face_with_raised_eyebrow","neutral_face","expressionless","no_mouth","dotted_line_face","face_in_clouds","smirk","unamused","face_with_rolling_eyes","grimacing","face_exhaling","lying_face","relieved","pensive","sleepy","drooling_face","sleeping","mask","face_with_thermometer","face_with_head_bandage","nauseated_face","face_vomiting","sneezing_face","hot_face","cold_face","woozy_face","dizzy_face","face_with_spiral_eyes","exploding_head","face_with_cowboy_hat","partying_face","disguised_face","sunglasses","nerd_face","face_with_monocle","confused","face_with_diagonal_mouth","worried","slightly_frowning_face","white_frowning_face","open_mouth","hushed","astonished","flushed","pleading_face","face_holding_back_tears","frowning","anguished","fearful","cold_sweat","disappointed_relieved","cry","sob","scream","confounded","persevere","disappointed","sweat","weary","tired_face","yawning_face","triumph","rage","angry","face_with_symbols_on_mouth","smiling_imp","imp","skull","skull_and_crossbones","hankey","clown_face","japanese_ogre","japanese_goblin","ghost","alien","space_invader","robot_face","wave","raised_back_of_hand","raised_hand_with_fingers_splayed","hand","spock-hand","rightwards_hand","leftwards_hand","palm_down_hand","palm_up_hand","ok_hand","pinched_fingers","pinching_hand","v","crossed_fingers","hand_with_index_finger_and_thumb_crossed","i_love_you_hand_sign","the_horns","call_me_hand","point_left","point_right","point_up_2","middle_finger","point_down","point_up","index_pointing_at_the_viewer","+1","-1","fist","facepunch","left-facing_fist","right-facing_fist","clap","raised_hands","heart_hands","open_hands","palms_up_together","handshake","pray","writing_hand","nail_care","selfie","muscle","mechanical_arm","mechanical_leg","leg","foot","ear","ear_with_hearing_aid","nose","brain","anatomical_heart","lungs","tooth","bone","eyes","eye","tongue","lips","biting_lip","baby","child","boy","girl","adult","person_with_blond_hair","man","bearded_person","man_with_beard","woman_with_beard","red_haired_man","curly_haired_man","white_haired_man","bald_man","woman","red_haired_woman","red_haired_person","curly_haired_woman","curly_haired_person","white_haired_woman","white_haired_person","bald_woman","bald_person","blond-haired-woman","blond-haired-man","older_adult","older_man","older_woman","person_frowning","man-frowning","woman-frowning","person_with_pouting_face","man-pouting","woman-pouting","no_good","man-gesturing-no","woman-gesturing-no","ok_woman","man-gesturing-ok","woman-gesturing-ok","information_desk_person","man-tipping-hand","woman-tipping-hand","raising_hand","man-raising-hand","woman-raising-hand","deaf_person","deaf_man","deaf_woman","bow","man-bowing","woman-bowing","face_palm","man-facepalming","woman-facepalming","shrug","man-shrugging","woman-shrugging","health_worker","male-doctor","female-doctor","student","male-student","female-student","teacher","male-teacher","female-teacher","judge","male-judge","female-judge","farmer","male-farmer","female-farmer","cook","male-cook","female-cook","mechanic","male-mechanic","female-mechanic","factory_worker","male-factory-worker","female-factory-worker","office_worker","male-office-worker","female-office-worker","scientist","male-scientist","female-scientist","technologist","male-technologist","female-technologist","singer","male-singer","female-singer","artist","male-artist","female-artist","pilot","male-pilot","female-pilot","astronaut","male-astronaut","female-astronaut","firefighter","male-firefighter","female-firefighter","cop","male-police-officer","female-police-officer","sleuth_or_spy","male-detective","female-detective","guardsman","male-guard","female-guard","ninja","construction_worker","male-construction-worker","female-construction-worker","person_with_crown","prince","princess","man_with_turban","man-wearing-turban","woman-wearing-turban","man_with_gua_pi_mao","person_with_headscarf","person_in_tuxedo","man_in_tuxedo","woman_in_tuxedo","bride_with_veil","man_with_veil","woman_with_veil","pregnant_woman","pregnant_man","pregnant_person","breast-feeding","woman_feeding_baby","man_feeding_baby","person_feeding_baby","angel","santa","mrs_claus","mx_claus","superhero","male_superhero","female_superhero","supervillain","male_supervillain","female_supervillain","mage","male_mage","female_mage","fairy","male_fairy","female_fairy","vampire","male_vampire","female_vampire","merperson","merman","mermaid","elf","male_elf","female_elf","genie","male_genie","female_genie","zombie","male_zombie","female_zombie","troll","massage","man-getting-massage","woman-getting-massage","haircut","man-getting-haircut","woman-getting-haircut","walking","man-walking","woman-walking","standing_person","man_standing","woman_standing","kneeling_person","man_kneeling","woman_kneeling","person_with_probing_cane","man_with_probing_cane","woman_with_probing_cane","person_in_motorized_wheelchair","man_in_motorized_wheelchair","woman_in_motorized_wheelchair","person_in_manual_wheelchair","man_in_manual_wheelchair","woman_in_manual_wheelchair","runner","man-running","woman-running","dancer","man_dancing","man_in_business_suit_levitating","dancers","men-with-bunny-ears-partying","women-with-bunny-ears-partying","person_in_steamy_room","man_in_steamy_room","woman_in_steamy_room","person_climbing","man_climbing","woman_climbing","fencer","horse_racing","skier","snowboarder","golfer","man-golfing","woman-golfing","surfer","man-surfing","woman-surfing","rowboat","man-rowing-boat","woman-rowing-boat","swimmer","man-swimming","woman-swimming","person_with_ball","man-bouncing-ball","woman-bouncing-ball","weight_lifter","man-lifting-weights","woman-lifting-weights","bicyclist","man-biking","woman-biking","mountain_bicyclist","man-mountain-biking","woman-mountain-biking","person_doing_cartwheel","man-cartwheeling","woman-cartwheeling","wrestlers","man-wrestling","woman-wrestling","water_polo","man-playing-water-polo","woman-playing-water-polo","handball","man-playing-handball","woman-playing-handball","juggling","man-juggling","woman-juggling","person_in_lotus_position","man_in_lotus_position","woman_in_lotus_position","bath","sleeping_accommodation","people_holding_hands","two_women_holding_hands","man_and_woman_holding_hands","two_men_holding_hands","couplekiss","woman-kiss-man","man-kiss-man","woman-kiss-woman","couple_with_heart","woman-heart-man","man-heart-man","woman-heart-woman","family","man-woman-boy","man-woman-girl","man-woman-girl-boy","man-woman-boy-boy","man-woman-girl-girl","man-man-boy","man-man-girl","man-man-girl-boy","man-man-boy-boy","man-man-girl-girl","woman-woman-boy","woman-woman-girl","woman-woman-girl-boy","woman-woman-boy-boy","woman-woman-girl-girl","man-boy","man-boy-boy","man-girl","man-girl-boy","man-girl-girl","woman-boy","woman-boy-boy","woman-girl","woman-girl-boy","woman-girl-girl","speaking_head_in_silhouette","bust_in_silhouette","busts_in_silhouette","people_hugging","footprints","smiley_cat","smile_cat","joy_cat","heart_eyes_cat","smirk_cat","kissing_cat","scream_cat","crying_cat_face","pouting_cat","see_no_evil","hear_no_evil","speak_no_evil","kiss","love_letter","cupid","gift_heart","sparkling_heart","heartpulse","heartbeat","revolving_hearts","two_hearts","heart_decoration","heavy_heart_exclamation_mark_ornament","broken_heart","heart_on_fire","mending_heart","heart","orange_heart","yellow_heart","green_heart","blue_heart","purple_heart","brown_heart","black_heart","white_heart","100","anger","boom","dizzy","sweat_drops","dash","hole","bomb","speech_balloon","eye-in-speech-bubble","left_speech_bubble","right_anger_bubble","thought_balloon","zzz"]},{"id":"nature","emojis":["monkey_face","monkey","gorilla","orangutan","dog","dog2","guide_dog","service_dog","poodle","wolf","fox_face","raccoon","cat","cat2","black_cat","lion_face","tiger","tiger2","leopard","horse","racehorse","unicorn_face","zebra_face","deer","bison","cow","ox","water_buffalo","cow2","pig","pig2","boar","pig_nose","ram","sheep","goat","dromedary_camel","camel","llama","giraffe_face","elephant","mammoth","rhinoceros","hippopotamus","mouse","mouse2","rat","hamster","rabbit","rabbit2","chipmunk","beaver","hedgehog","bat","bear","polar_bear","koala","panda_face","sloth","otter","skunk","kangaroo","badger","feet","turkey","chicken","rooster","hatching_chick","baby_chick","hatched_chick","bird","penguin","dove_of_peace","eagle","duck","swan","owl","dodo","feather","flamingo","peacock","parrot","frog","crocodile","turtle","lizard","snake","dragon_face","dragon","sauropod","t-rex","whale","whale2","dolphin","seal","fish","tropical_fish","blowfish","shark","octopus","shell","coral","snail","butterfly","bug","ant","bee","beetle","ladybug","cricket","cockroach","spider","spider_web","scorpion","mosquito","fly","worm","microbe","bouquet","cherry_blossom","white_flower","lotus","rosette","rose","wilted_flower","hibiscus","sunflower","blossom","tulip","seedling","potted_plant","evergreen_tree","deciduous_tree","palm_tree","cactus","ear_of_rice","herb","shamrock","four_leaf_clover","maple_leaf","fallen_leaf","leaves","empty_nest","nest_with_eggs"]},{"id":"foods","emojis":["grapes","melon","watermelon","tangerine","lemon","banana","pineapple","mango","apple","green_apple","pear","peach","cherries","strawberry","blueberries","kiwifruit","tomato","olive","coconut","avocado","eggplant","potato","carrot","corn","hot_pepper","bell_pepper","cucumber","leafy_green","broccoli","garlic","onion","mushroom","peanuts","beans","chestnut","bread","croissant","baguette_bread","flatbread","pretzel","bagel","pancakes","waffle","cheese_wedge","meat_on_bone","poultry_leg","cut_of_meat","bacon","hamburger","fries","pizza","hotdog","sandwich","taco","burrito","tamale","stuffed_flatbread","falafel","egg","fried_egg","shallow_pan_of_food","stew","fondue","bowl_with_spoon","green_salad","popcorn","butter","salt","canned_food","bento","rice_cracker","rice_ball","rice","curry","ramen","spaghetti","sweet_potato","oden","sushi","fried_shrimp","fish_cake","moon_cake","dango","dumpling","fortune_cookie","takeout_box","crab","lobster","shrimp","squid","oyster","icecream","shaved_ice","ice_cream","doughnut","cookie","birthday","cake","cupcake","pie","chocolate_bar","candy","lollipop","custard","honey_pot","baby_bottle","glass_of_milk","coffee","teapot","tea","sake","champagne","wine_glass","cocktail","tropical_drink","beer","beers","clinking_glasses","tumbler_glass","pouring_liquid","cup_with_straw","bubble_tea","beverage_box","mate_drink","ice_cube","chopsticks","knife_fork_plate","fork_and_knife","spoon","hocho","jar","amphora"]},{"id":"activity","emojis":["jack_o_lantern","christmas_tree","fireworks","sparkler","firecracker","sparkles","balloon","tada","confetti_ball","tanabata_tree","bamboo","dolls","flags","wind_chime","rice_scene","red_envelope","ribbon","gift","reminder_ribbon","admission_tickets","ticket","medal","trophy","sports_medal","first_place_medal","second_place_medal","third_place_medal","soccer","baseball","softball","basketball","volleyball","football","rugby_football","tennis","flying_disc","bowling","cricket_bat_and_ball","field_hockey_stick_and_ball","ice_hockey_stick_and_puck","lacrosse","table_tennis_paddle_and_ball","badminton_racquet_and_shuttlecock","boxing_glove","martial_arts_uniform","goal_net","golf","ice_skate","fishing_pole_and_fish","diving_mask","running_shirt_with_sash","ski","sled","curling_stone","dart","yo-yo","kite","8ball","crystal_ball","magic_wand","nazar_amulet","hamsa","video_game","joystick","slot_machine","game_die","jigsaw","teddy_bear","pinata","mirror_ball","nesting_dolls","spades","hearts","diamonds","clubs","chess_pawn","black_joker","mahjong","flower_playing_cards","performing_arts","frame_with_picture","art","thread","sewing_needle","yarn","knot"]},{"id":"places","emojis":["earth_africa","earth_americas","earth_asia","globe_with_meridians","world_map","japan","compass","snow_capped_mountain","mountain","volcano","mount_fuji","camping","beach_with_umbrella","desert","desert_island","national_park","stadium","classical_building","building_construction","bricks","rock","wood","hut","house_buildings","derelict_house_building","house","house_with_garden","office","post_office","european_post_office","hospital","bank","hotel","love_hotel","convenience_store","school","department_store","factory","japanese_castle","european_castle","wedding","tokyo_tower","statue_of_liberty","church","mosque","hindu_temple","synagogue","shinto_shrine","kaaba","fountain","tent","foggy","night_with_stars","cityscape","sunrise_over_mountains","sunrise","city_sunset","city_sunrise","bridge_at_night","hotsprings","carousel_horse","playground_slide","ferris_wheel","roller_coaster","barber","circus_tent","steam_locomotive","railway_car","bullettrain_side","bullettrain_front","train2","metro","light_rail","station","tram","monorail","mountain_railway","train","bus","oncoming_bus","trolleybus","minibus","ambulance","fire_engine","police_car","oncoming_police_car","taxi","oncoming_taxi","car","oncoming_automobile","blue_car","pickup_truck","truck","articulated_lorry","tractor","racing_car","racing_motorcycle","motor_scooter","manual_wheelchair","motorized_wheelchair","auto_rickshaw","bike","scooter","skateboard","roller_skate","busstop","motorway","railway_track","oil_drum","fuelpump","wheel","rotating_light","traffic_light","vertical_traffic_light","octagonal_sign","construction","anchor","ring_buoy","boat","canoe","speedboat","passenger_ship","ferry","motor_boat","ship","airplane","small_airplane","airplane_departure","airplane_arriving","parachute","seat","helicopter","suspension_railway","mountain_cableway","aerial_tramway","satellite","rocket","flying_saucer","bellhop_bell","luggage","hourglass","hourglass_flowing_sand","watch","alarm_clock","stopwatch","timer_clock","mantelpiece_clock","clock12","clock1230","clock1","clock130","clock2","clock230","clock3","clock330","clock4","clock430","clock5","clock530","clock6","clock630","clock7","clock730","clock8","clock830","clock9","clock930","clock10","clock1030","clock11","clock1130","new_moon","waxing_crescent_moon","first_quarter_moon","moon","full_moon","waning_gibbous_moon","last_quarter_moon","waning_crescent_moon","crescent_moon","new_moon_with_face","first_quarter_moon_with_face","last_quarter_moon_with_face","thermometer","sunny","full_moon_with_face","sun_with_face","ringed_planet","star","star2","stars","milky_way","cloud","partly_sunny","thunder_cloud_and_rain","mostly_sunny","barely_sunny","partly_sunny_rain","rain_cloud","snow_cloud","lightning","tornado","fog","wind_blowing_face","cyclone","rainbow","closed_umbrella","umbrella","umbrella_with_rain_drops","umbrella_on_ground","zap","snowflake","snowman","snowman_without_snow","comet","fire","droplet","ocean"]},{"id":"objects","emojis":["eyeglasses","dark_sunglasses","goggles","lab_coat","safety_vest","necktie","shirt","jeans","scarf","gloves","coat","socks","dress","kimono","sari","one-piece_swimsuit","briefs","shorts","bikini","womans_clothes","purse","handbag","pouch","shopping_bags","school_satchel","thong_sandal","mans_shoe","athletic_shoe","hiking_boot","womans_flat_shoe","high_heel","sandal","ballet_shoes","boot","crown","womans_hat","tophat","mortar_board","billed_cap","military_helmet","helmet_with_white_cross","prayer_beads","lipstick","ring","gem","mute","speaker","sound","loud_sound","loudspeaker","mega","postal_horn","bell","no_bell","musical_score","musical_note","notes","studio_microphone","level_slider","control_knobs","microphone","headphones","radio","saxophone","accordion","guitar","musical_keyboard","trumpet","violin","banjo","drum_with_drumsticks","long_drum","iphone","calling","phone","telephone_receiver","pager","fax","battery","low_battery","electric_plug","computer","desktop_computer","printer","keyboard","three_button_mouse","trackball","minidisc","floppy_disk","cd","dvd","abacus","movie_camera","film_frames","film_projector","clapper","tv","camera","camera_with_flash","video_camera","vhs","mag","mag_right","candle","bulb","flashlight","izakaya_lantern","diya_lamp","notebook_with_decorative_cover","closed_book","book","green_book","blue_book","orange_book","books","notebook","ledger","page_with_curl","scroll","page_facing_up","newspaper","rolled_up_newspaper","bookmark_tabs","bookmark","label","moneybag","coin","yen","dollar","euro","pound","money_with_wings","credit_card","receipt","chart","email","e-mail","incoming_envelope","envelope_with_arrow","outbox_tray","inbox_tray","package","mailbox","mailbox_closed","mailbox_with_mail","mailbox_with_no_mail","postbox","ballot_box_with_ballot","pencil2","black_nib","lower_left_fountain_pen","lower_left_ballpoint_pen","lower_left_paintbrush","lower_left_crayon","memo","briefcase","file_folder","open_file_folder","card_index_dividers","date","calendar","spiral_note_pad","spiral_calendar_pad","card_index","chart_with_upwards_trend","chart_with_downwards_trend","bar_chart","clipboard","pushpin","round_pushpin","paperclip","linked_paperclips","straight_ruler","triangular_ruler","scissors","card_file_box","file_cabinet","wastebasket","lock","unlock","lock_with_ink_pen","closed_lock_with_key","key","old_key","hammer","axe","pick","hammer_and_pick","hammer_and_wrench","dagger_knife","crossed_swords","gun","boomerang","bow_and_arrow","shield","carpentry_saw","wrench","screwdriver","nut_and_bolt","gear","compression","scales","probing_cane","link","chains","hook","toolbox","magnet","ladder","alembic","test_tube","petri_dish","dna","microscope","telescope","satellite_antenna","syringe","drop_of_blood","pill","adhesive_bandage","crutch","stethoscope","x-ray","door","elevator","mirror","window","bed","couch_and_lamp","chair","toilet","plunger","shower","bathtub","mouse_trap","razor","lotion_bottle","safety_pin","broom","basket","roll_of_paper","bucket","soap","bubbles","toothbrush","sponge","fire_extinguisher","shopping_trolley","smoking","coffin","headstone","funeral_urn","moyai","placard","identification_card"]},{"id":"symbols","emojis":["atm","put_litter_in_its_place","potable_water","wheelchair","mens","womens","restroom","baby_symbol","wc","passport_control","customs","baggage_claim","left_luggage","warning","children_crossing","no_entry","no_entry_sign","no_bicycles","no_smoking","do_not_litter","non-potable_water","no_pedestrians","no_mobile_phones","underage","radioactive_sign","biohazard_sign","arrow_up","arrow_upper_right","arrow_right","arrow_lower_right","arrow_down","arrow_lower_left","arrow_left","arrow_upper_left","arrow_up_down","left_right_arrow","leftwards_arrow_with_hook","arrow_right_hook","arrow_heading_up","arrow_heading_down","arrows_clockwise","arrows_counterclockwise","back","end","on","soon","top","place_of_worship","atom_symbol","om_symbol","star_of_david","wheel_of_dharma","yin_yang","latin_cross","orthodox_cross","star_and_crescent","peace_symbol","menorah_with_nine_branches","six_pointed_star","aries","taurus","gemini","cancer","leo","virgo","libra","scorpius","sagittarius","capricorn","aquarius","pisces","ophiuchus","twisted_rightwards_arrows","repeat","repeat_one","arrow_forward","fast_forward","black_right_pointing_double_triangle_with_vertical_bar","black_right_pointing_triangle_with_double_vertical_bar","arrow_backward","rewind","black_left_pointing_double_triangle_with_vertical_bar","arrow_up_small","arrow_double_up","arrow_down_small","arrow_double_down","double_vertical_bar","black_square_for_stop","black_circle_for_record","eject","cinema","low_brightness","high_brightness","signal_strength","vibration_mode","mobile_phone_off","female_sign","male_sign","transgender_symbol","heavy_multiplication_x","heavy_plus_sign","heavy_minus_sign","heavy_division_sign","heavy_equals_sign","infinity","bangbang","interrobang","question","grey_question","grey_exclamation","exclamation","wavy_dash","currency_exchange","heavy_dollar_sign","medical_symbol","recycle","fleur_de_lis","trident","name_badge","beginner","o","white_check_mark","ballot_box_with_check","heavy_check_mark","x","negative_squared_cross_mark","curly_loop","loop","part_alternation_mark","eight_spoked_asterisk","eight_pointed_black_star","sparkle","copyright","registered","tm","hash","keycap_star","zero","one","two","three","four","five","six","seven","eight","nine","keycap_ten","capital_abcd","abcd","1234","symbols","abc","a","ab","b","cl","cool","free","information_source","id","m","new","ng","o2","ok","parking","sos","up","vs","koko","sa","u6708","u6709","u6307","ideograph_advantage","u5272","u7121","u7981","accept","u7533","u5408","u7a7a","congratulations","secret","u55b6","u6e80","red_circle","large_orange_circle","large_yellow_circle","large_green_circle","large_blue_circle","large_purple_circle","large_brown_circle","black_circle","white_circle","large_red_square","large_orange_square","large_yellow_square","large_green_square","large_blue_square","large_purple_square","large_brown_square","black_large_square","white_large_square","black_medium_square","white_medium_square","black_medium_small_square","white_medium_small_square","black_small_square","white_small_square","large_orange_diamond","large_blue_diamond","small_orange_diamond","small_blue_diamond","small_red_triangle","small_red_triangle_down","diamond_shape_with_a_dot_inside","radio_button","white_square_button","black_square_button"]},{"id":"flags","emojis":["checkered_flag","cn","crossed_flags","de","es","flag-ac","flag-ad","flag-ae","flag-af","flag-ag","flag-ai","flag-al","flag-am","flag-ao","flag-aq","flag-ar","flag-as","flag-at","flag-au","flag-aw","flag-ax","flag-az","flag-ba","flag-bb","flag-bd","flag-be","flag-bf","flag-bg","flag-bh","flag-bi","flag-bj","flag-bl","flag-bm","flag-bn","flag-bo","flag-bq","flag-br","flag-bs","flag-bt","flag-bv","flag-bw","flag-by","flag-bz","flag-ca","flag-cc","flag-cd","flag-cf","flag-cg","flag-ch","flag-ci","flag-ck","flag-cl","flag-cm","flag-co","flag-cp","flag-cr","flag-cu","flag-cv","flag-cw","flag-cx","flag-cy","flag-cz","flag-dg","flag-dj","flag-dk","flag-dm","flag-do","flag-dz","flag-ea","flag-ec","flag-ee","flag-eg","flag-eh","flag-england","flag-er","flag-et","flag-eu","flag-fi","flag-fj","flag-fk","flag-fm","flag-fo","flag-ga","flag-gd","flag-ge","flag-gf","flag-gg","flag-gh","flag-gi","flag-gl","flag-gm","flag-gn","flag-gp","flag-gq","flag-gr","flag-gs","flag-gt","flag-gu","flag-gw","flag-gy","flag-hk","flag-hm","flag-hn","flag-hr","flag-ht","flag-hu","flag-ic","flag-id","flag-ie","flag-il","flag-im","flag-in","flag-io","flag-iq","flag-ir","flag-is","flag-je","flag-jm","flag-jo","flag-ke","flag-kg","flag-kh","flag-ki","flag-km","flag-kn","flag-kp","flag-kw","flag-ky","flag-kz","flag-la","flag-lb","flag-lc","flag-li","flag-lk","flag-lr","flag-ls","flag-lt","flag-lu","flag-lv","flag-ly","flag-ma","flag-mc","flag-md","flag-me","flag-mf","flag-mg","flag-mh","flag-mk","flag-ml","flag-mm","flag-mn","flag-mo","flag-mp","flag-mq","flag-mr","flag-ms","flag-mt","flag-mu","flag-mv","flag-mw","flag-mx","flag-my","flag-mz","flag-na","flag-nc","flag-ne","flag-nf","flag-ng","flag-ni","flag-nl","flag-no","flag-np","flag-nr","flag-nu","flag-nz","flag-om","flag-pa","flag-pe","flag-pf","flag-pg","flag-ph","flag-pk","flag-pl","flag-pm","flag-pn","flag-pr","flag-ps","flag-pt","flag-pw","flag-py","flag-qa","flag-re","flag-ro","flag-rs","flag-rw","flag-sa","flag-sb","flag-sc","flag-scotland","flag-sd","flag-se","flag-sg","flag-sh","flag-si","flag-sj","flag-sk","flag-sl","flag-sm","flag-sn","flag-so","flag-sr","flag-ss","flag-st","flag-sv","flag-sx","flag-sy","flag-sz","flag-ta","flag-tc","flag-td","flag-tf","flag-tg","flag-th","flag-tj","flag-tk","flag-tl","flag-tm","flag-tn","flag-to","flag-tr","flag-tt","flag-tv","flag-tw","flag-tz","flag-ua","flag-ug","flag-um","flag-un","flag-uy","flag-uz","flag-va","flag-vc","flag-ve","flag-vg","flag-vi","flag-vn","flag-vu","flag-wales","flag-wf","flag-ws","flag-xk","flag-ye","flag-yt","flag-za","flag-zm","flag-zw","fr","gb","it","jp","kr","pirate_flag","rainbow-flag","ru","transgender_flag","triangular_flag_on_post","us","waving_black_flag","waving_white_flag"]}],"emojis":{"100":{"id":"100","name":"Hundred Points","keywords":["100","score","perfect","numbers","century","exam","quiz","test","pass"],"skins":[{"unified":"1f4af","native":"💯"}],"version":1},"1234":{"id":"1234","name":"Input Numbers","keywords":["1234","blue","square"],"skins":[{"unified":"1f522","native":"🔢"}],"version":1},"grinning":{"id":"grinning","name":"Grinning Face","emoticons":[":D"],"keywords":["smile","happy","joy",":D","grin"],"skins":[{"unified":"1f600","native":"😀"}],"version":1},"smiley":{"id":"smiley","name":"Grinning Face with Big Eyes","emoticons":[":)","=)","=-)"],"keywords":["smiley","happy","joy","haha",":D",":)","smile","funny"],"skins":[{"unified":"1f603","native":"😃"}],"version":1},"smile":{"id":"smile","name":"Grinning Face with Smiling Eyes","emoticons":[":)","C:","c:",":D",":-D"],"keywords":["smile","happy","joy","funny","haha","laugh","like",":D",":)"],"skins":[{"unified":"1f604","native":"😄"}],"version":1},"grin":{"id":"grin","name":"Beaming Face with Smiling Eyes","keywords":["grin","happy","smile","joy","kawaii"],"skins":[{"unified":"1f601","native":"ðŸ˜"}],"version":1},"laughing":{"id":"laughing","name":"Grinning Squinting Face","emoticons":[":>",":->"],"keywords":["laughing","satisfied","happy","joy","lol","haha","glad","XD","laugh"],"skins":[{"unified":"1f606","native":"😆"}],"version":1},"sweat_smile":{"id":"sweat_smile","name":"Grinning Face with Sweat","keywords":["smile","hot","happy","laugh","relief"],"skins":[{"unified":"1f605","native":"😅"}],"version":1},"rolling_on_the_floor_laughing":{"id":"rolling_on_the_floor_laughing","name":"Rolling on the Floor Laughing","keywords":["face","lol","haha","rofl"],"skins":[{"unified":"1f923","native":"🤣"}],"version":3},"joy":{"id":"joy","name":"Face with Tears of Joy","keywords":["cry","weep","happy","happytears","haha"],"skins":[{"unified":"1f602","native":"😂"}],"version":1},"slightly_smiling_face":{"id":"slightly_smiling_face","name":"Slightly Smiling Face","emoticons":[":)","(:",":-)"],"keywords":["smile"],"skins":[{"unified":"1f642","native":"🙂"}],"version":1},"upside_down_face":{"id":"upside_down_face","name":"Upside-Down Face","keywords":["upside","down","flipped","silly","smile"],"skins":[{"unified":"1f643","native":"🙃"}],"version":1},"melting_face":{"id":"melting_face","name":"Melting Face","keywords":["hot","heat"],"skins":[{"unified":"1fae0","native":"🫠"}],"version":14},"wink":{"id":"wink","name":"Winking Face","emoticons":[";)",";-)"],"keywords":["wink","happy","mischievous","secret",";)","smile","eye"],"skins":[{"unified":"1f609","native":"😉"}],"version":1},"blush":{"id":"blush","name":"Smiling Face with Smiling Eyes","emoticons":[":)"],"keywords":["blush","smile","happy","flushed","crush","embarrassed","shy","joy"],"skins":[{"unified":"1f60a","native":"😊"}],"version":1},"innocent":{"id":"innocent","name":"Smiling Face with Halo","keywords":["innocent","angel","heaven"],"skins":[{"unified":"1f607","native":"😇"}],"version":1},"smiling_face_with_3_hearts":{"id":"smiling_face_with_3_hearts","name":"Smiling Face with Hearts","keywords":["3","love","like","affection","valentines","infatuation","crush","adore"],"skins":[{"unified":"1f970","native":"🥰"}],"version":11},"heart_eyes":{"id":"heart_eyes","name":"Smiling Face with Heart-Eyes","keywords":["heart","eyes","love","like","affection","valentines","infatuation","crush"],"skins":[{"unified":"1f60d","native":"ðŸ˜"}],"version":1},"star-struck":{"id":"star-struck","name":"Star-Struck","keywords":["star","struck","grinning","face","with","eyes","smile","starry"],"skins":[{"unified":"1f929","native":"🤩"}],"version":5},"kissing_heart":{"id":"kissing_heart","name":"Face Blowing a Kiss","emoticons":[":*",":-*"],"keywords":["kissing","heart","love","like","affection","valentines","infatuation"],"skins":[{"unified":"1f618","native":"😘"}],"version":1},"kissing":{"id":"kissing","name":"Kissing Face","keywords":["love","like","3","valentines","infatuation","kiss"],"skins":[{"unified":"1f617","native":"😗"}],"version":1},"relaxed":{"id":"relaxed","name":"Smiling Face","keywords":["relaxed","blush","massage","happiness"],"skins":[{"unified":"263a-fe0f","native":"☺ï¸"}],"version":1},"kissing_closed_eyes":{"id":"kissing_closed_eyes","name":"Kissing Face with Closed Eyes","keywords":["love","like","affection","valentines","infatuation","kiss"],"skins":[{"unified":"1f61a","native":"😚"}],"version":1},"kissing_smiling_eyes":{"id":"kissing_smiling_eyes","name":"Kissing Face with Smiling Eyes","keywords":["affection","valentines","infatuation","kiss"],"skins":[{"unified":"1f619","native":"😙"}],"version":1},"smiling_face_with_tear":{"id":"smiling_face_with_tear","name":"Smiling Face with Tear","keywords":["sad","cry","pretend"],"skins":[{"unified":"1f972","native":"🥲"}],"version":13},"yum":{"id":"yum","name":"Face Savoring Food","keywords":["yum","happy","joy","tongue","smile","silly","yummy","nom","delicious","savouring"],"skins":[{"unified":"1f60b","native":"😋"}],"version":1},"stuck_out_tongue":{"id":"stuck_out_tongue","name":"Face with Tongue","emoticons":[":p",":-p",":P",":-P",":b",":-b"],"keywords":["stuck","out","prank","childish","playful","mischievous","smile"],"skins":[{"unified":"1f61b","native":"😛"}],"version":1},"stuck_out_tongue_winking_eye":{"id":"stuck_out_tongue_winking_eye","name":"Winking Face with Tongue","emoticons":[";p",";-p",";b",";-b",";P",";-P"],"keywords":["stuck","out","eye","prank","childish","playful","mischievous","smile","wink"],"skins":[{"unified":"1f61c","native":"😜"}],"version":1},"zany_face":{"id":"zany_face","name":"Zany Face","keywords":["grinning","with","one","large","and","small","eye","goofy","crazy"],"skins":[{"unified":"1f92a","native":"🤪"}],"version":5},"stuck_out_tongue_closed_eyes":{"id":"stuck_out_tongue_closed_eyes","name":"Squinting Face with Tongue","keywords":["stuck","out","closed","eyes","prank","playful","mischievous","smile"],"skins":[{"unified":"1f61d","native":"ðŸ˜"}],"version":1},"money_mouth_face":{"id":"money_mouth_face","name":"Money-Mouth Face","keywords":["money","mouth","rich","dollar"],"skins":[{"unified":"1f911","native":"🤑"}],"version":1},"hugging_face":{"id":"hugging_face","name":"Hugging Face","keywords":["smile","hug"],"skins":[{"unified":"1f917","native":"🤗"}],"version":1},"face_with_hand_over_mouth":{"id":"face_with_hand_over_mouth","name":"Face with Hand over Mouth","keywords":["smiling","eyes","and","covering","whoops","shock","surprise"],"skins":[{"unified":"1f92d","native":"ðŸ¤"}],"version":5},"face_with_open_eyes_and_hand_over_mouth":{"id":"face_with_open_eyes_and_hand_over_mouth","name":"Face with Open Eyes and Hand over Mouth","keywords":["silence","secret","shock","surprise"],"skins":[{"unified":"1fae2","native":"🫢"}],"version":14},"face_with_peeking_eye":{"id":"face_with_peeking_eye","name":"Face with Peeking Eye","keywords":["scared","frightening","embarrassing"],"skins":[{"unified":"1fae3","native":"🫣"}],"version":14},"shushing_face":{"id":"shushing_face","name":"Shushing Face","keywords":["with","finger","covering","closed","lips","quiet","shhh"],"skins":[{"unified":"1f92b","native":"🤫"}],"version":5},"thinking_face":{"id":"thinking_face","name":"Thinking Face","keywords":["hmmm","think","consider"],"skins":[{"unified":"1f914","native":"🤔"}],"version":1},"saluting_face":{"id":"saluting_face","name":"Saluting Face","keywords":["respect","salute"],"skins":[{"unified":"1fae1","native":"🫡"}],"version":14},"zipper_mouth_face":{"id":"zipper_mouth_face","name":"Zipper-Mouth Face","keywords":["zipper","mouth","sealed","secret"],"skins":[{"unified":"1f910","native":"ðŸ¤"}],"version":1},"face_with_raised_eyebrow":{"id":"face_with_raised_eyebrow","name":"Face with Raised Eyebrow","keywords":["one","distrust","scepticism","disapproval","disbelief","surprise"],"skins":[{"unified":"1f928","native":"🤨"}],"version":5},"neutral_face":{"id":"neutral_face","name":"Neutral Face","emoticons":[":|",":-|"],"keywords":["indifference","meh",":",""],"skins":[{"unified":"1f610","native":"ðŸ˜"}],"version":1},"expressionless":{"id":"expressionless","name":"Expressionless Face","emoticons":["-_-"],"keywords":["indifferent","-","","meh","deadpan"],"skins":[{"unified":"1f611","native":"😑"}],"version":1},"no_mouth":{"id":"no_mouth","name":"Face Without Mouth","keywords":["no","hellokitty"],"skins":[{"unified":"1f636","native":"😶"}],"version":1},"dotted_line_face":{"id":"dotted_line_face","name":"Dotted Line Face","keywords":["invisible","lonely","isolation","depression"],"skins":[{"unified":"1fae5","native":"🫥"}],"version":14},"face_in_clouds":{"id":"face_in_clouds","name":"Face in Clouds","keywords":["shower","steam","dream"],"skins":[{"unified":"1f636-200d-1f32b-fe0f","native":"😶â€ðŸŒ«ï¸"}],"version":13.1},"smirk":{"id":"smirk","name":"Smirking Face","keywords":["smirk","smile","mean","prank","smug","sarcasm"],"skins":[{"unified":"1f60f","native":"ðŸ˜"}],"version":1},"unamused":{"id":"unamused","name":"Unamused Face","emoticons":[":("],"keywords":["indifference","bored","straight","serious","sarcasm","unimpressed","skeptical","dubious","side","eye"],"skins":[{"unified":"1f612","native":"😒"}],"version":1},"face_with_rolling_eyes":{"id":"face_with_rolling_eyes","name":"Face with Rolling Eyes","keywords":["eyeroll","frustrated"],"skins":[{"unified":"1f644","native":"🙄"}],"version":1},"grimacing":{"id":"grimacing","name":"Grimacing Face","keywords":["grimace","teeth"],"skins":[{"unified":"1f62c","native":"😬"}],"version":1},"face_exhaling":{"id":"face_exhaling","name":"Face Exhaling","keywords":["relieve","relief","tired","sigh"],"skins":[{"unified":"1f62e-200d-1f4a8","native":"😮â€ðŸ’¨"}],"version":13.1},"lying_face":{"id":"lying_face","name":"Lying Face","keywords":["lie","pinocchio"],"skins":[{"unified":"1f925","native":"🤥"}],"version":3},"relieved":{"id":"relieved","name":"Relieved Face","keywords":["relaxed","phew","massage","happiness"],"skins":[{"unified":"1f60c","native":"😌"}],"version":1},"pensive":{"id":"pensive","name":"Pensive Face","keywords":["sad","depressed","upset"],"skins":[{"unified":"1f614","native":"😔"}],"version":1},"sleepy":{"id":"sleepy","name":"Sleepy Face","keywords":["tired","rest","nap"],"skins":[{"unified":"1f62a","native":"😪"}],"version":1},"drooling_face":{"id":"drooling_face","name":"Drooling Face","keywords":[],"skins":[{"unified":"1f924","native":"🤤"}],"version":3},"sleeping":{"id":"sleeping","name":"Sleeping Face","keywords":["tired","sleepy","night","zzz"],"skins":[{"unified":"1f634","native":"😴"}],"version":1},"mask":{"id":"mask","name":"Face with Medical Mask","keywords":["sick","ill","disease"],"skins":[{"unified":"1f637","native":"😷"}],"version":1},"face_with_thermometer":{"id":"face_with_thermometer","name":"Face with Thermometer","keywords":["sick","temperature","cold","fever"],"skins":[{"unified":"1f912","native":"🤒"}],"version":1},"face_with_head_bandage":{"id":"face_with_head_bandage","name":"Face with Head-Bandage","keywords":["head","bandage","injured","clumsy","hurt"],"skins":[{"unified":"1f915","native":"🤕"}],"version":1},"nauseated_face":{"id":"nauseated_face","name":"Nauseated Face","keywords":["vomit","gross","green","sick","throw","up","ill"],"skins":[{"unified":"1f922","native":"🤢"}],"version":3},"face_vomiting":{"id":"face_vomiting","name":"Face Vomiting","keywords":["with","open","mouth","sick"],"skins":[{"unified":"1f92e","native":"🤮"}],"version":5},"sneezing_face":{"id":"sneezing_face","name":"Sneezing Face","keywords":["gesundheit","sneeze","sick","allergy"],"skins":[{"unified":"1f927","native":"🤧"}],"version":3},"hot_face":{"id":"hot_face","name":"Hot Face","keywords":["feverish","heat","red","sweating"],"skins":[{"unified":"1f975","native":"🥵"}],"version":11},"cold_face":{"id":"cold_face","name":"Cold Face","keywords":["blue","freezing","frozen","frostbite","icicles"],"skins":[{"unified":"1f976","native":"🥶"}],"version":11},"woozy_face":{"id":"woozy_face","name":"Woozy Face","keywords":["dizzy","intoxicated","tipsy","wavy"],"skins":[{"unified":"1f974","native":"🥴"}],"version":11},"dizzy_face":{"id":"dizzy_face","name":"Dizzy Face","keywords":["spent","unconscious","xox"],"skins":[{"unified":"1f635","native":"😵"}],"version":1},"face_with_spiral_eyes":{"id":"face_with_spiral_eyes","name":"Face with Spiral Eyes","keywords":["sick","ill","confused","nauseous","nausea"],"skins":[{"unified":"1f635-200d-1f4ab","native":"😵â€ðŸ’«"}],"version":13.1},"exploding_head":{"id":"exploding_head","name":"Exploding Head","keywords":["shocked","face","with","mind","blown"],"skins":[{"unified":"1f92f","native":"🤯"}],"version":5},"face_with_cowboy_hat":{"id":"face_with_cowboy_hat","name":"Cowboy Hat Face","keywords":["with","cowgirl"],"skins":[{"unified":"1f920","native":"🤠"}],"version":3},"partying_face":{"id":"partying_face","name":"Partying Face","keywords":["celebration","woohoo"],"skins":[{"unified":"1f973","native":"🥳"}],"version":11},"disguised_face":{"id":"disguised_face","name":"Disguised Face","keywords":["pretent","brows","glasses","moustache"],"skins":[{"unified":"1f978","native":"🥸"}],"version":13},"sunglasses":{"id":"sunglasses","name":"Smiling Face with Sunglasses","emoticons":["8)"],"keywords":["cool","smile","summer","beach","sunglass"],"skins":[{"unified":"1f60e","native":"😎"}],"version":1},"nerd_face":{"id":"nerd_face","name":"Nerd Face","keywords":["nerdy","geek","dork"],"skins":[{"unified":"1f913","native":"🤓"}],"version":1},"face_with_monocle":{"id":"face_with_monocle","name":"Face with Monocle","keywords":["stuffy","wealthy"],"skins":[{"unified":"1f9d0","native":"ðŸ§"}],"version":5},"confused":{"id":"confused","name":"Confused Face","emoticons":[":\\",":-\\",":/",":-/"],"keywords":["indifference","huh","weird","hmmm",":/"],"skins":[{"unified":"1f615","native":"😕"}],"version":1},"face_with_diagonal_mouth":{"id":"face_with_diagonal_mouth","name":"Face with Diagonal Mouth","keywords":["skeptic","confuse","frustrated","indifferent"],"skins":[{"unified":"1fae4","native":"🫤"}],"version":14},"worried":{"id":"worried","name":"Worried Face","keywords":["concern","nervous",":("],"skins":[{"unified":"1f61f","native":"😟"}],"version":1},"slightly_frowning_face":{"id":"slightly_frowning_face","name":"Slightly Frowning Face","keywords":["disappointed","sad","upset"],"skins":[{"unified":"1f641","native":"ðŸ™"}],"version":1},"white_frowning_face":{"id":"white_frowning_face","name":"Frowning Face","keywords":["white","sad","upset","frown"],"skins":[{"unified":"2639-fe0f","native":"☹ï¸"}],"version":1},"open_mouth":{"id":"open_mouth","name":"Face with Open Mouth","emoticons":[":o",":-o",":O",":-O"],"keywords":["surprise","impressed","wow","whoa",":O"],"skins":[{"unified":"1f62e","native":"😮"}],"version":1},"hushed":{"id":"hushed","name":"Hushed Face","keywords":["woo","shh"],"skins":[{"unified":"1f62f","native":"😯"}],"version":1},"astonished":{"id":"astonished","name":"Astonished Face","keywords":["xox","surprised","poisoned"],"skins":[{"unified":"1f632","native":"😲"}],"version":1},"flushed":{"id":"flushed","name":"Flushed Face","keywords":["blush","shy","flattered"],"skins":[{"unified":"1f633","native":"😳"}],"version":1},"pleading_face":{"id":"pleading_face","name":"Pleading Face","keywords":["begging","mercy"],"skins":[{"unified":"1f97a","native":"🥺"}],"version":11},"face_holding_back_tears":{"id":"face_holding_back_tears","name":"Face Holding Back Tears","keywords":["touched","gratitude"],"skins":[{"unified":"1f979","native":"🥹"}],"version":14},"frowning":{"id":"frowning","name":"Frowning Face with Open Mouth","keywords":["aw","what"],"skins":[{"unified":"1f626","native":"😦"}],"version":1},"anguished":{"id":"anguished","name":"Anguished Face","emoticons":["D:"],"keywords":["stunned","nervous"],"skins":[{"unified":"1f627","native":"😧"}],"version":1},"fearful":{"id":"fearful","name":"Fearful Face","keywords":["scared","terrified","nervous","oops","huh"],"skins":[{"unified":"1f628","native":"😨"}],"version":1},"cold_sweat":{"id":"cold_sweat","name":"Anxious Face with Sweat","keywords":["cold","nervous"],"skins":[{"unified":"1f630","native":"😰"}],"version":1},"disappointed_relieved":{"id":"disappointed_relieved","name":"Sad but Relieved Face","keywords":["disappointed","phew","sweat","nervous"],"skins":[{"unified":"1f625","native":"😥"}],"version":1},"cry":{"id":"cry","name":"Crying Face","emoticons":[":'("],"keywords":["cry","tears","sad","depressed","upset",":'("],"skins":[{"unified":"1f622","native":"😢"}],"version":1},"sob":{"id":"sob","name":"Loudly Crying Face","emoticons":[":'("],"keywords":["sob","cry","tears","sad","upset","depressed"],"skins":[{"unified":"1f62d","native":"ðŸ˜"}],"version":1},"scream":{"id":"scream","name":"Face Screaming in Fear","keywords":["scream","munch","scared","omg"],"skins":[{"unified":"1f631","native":"😱"}],"version":1},"confounded":{"id":"confounded","name":"Confounded Face","keywords":["confused","sick","unwell","oops",":S"],"skins":[{"unified":"1f616","native":"😖"}],"version":1},"persevere":{"id":"persevere","name":"Persevering Face","keywords":["persevere","sick","no","upset","oops"],"skins":[{"unified":"1f623","native":"😣"}],"version":1},"disappointed":{"id":"disappointed","name":"Disappointed Face","emoticons":["):",":(",":-("],"keywords":["sad","upset","depressed",":("],"skins":[{"unified":"1f61e","native":"😞"}],"version":1},"sweat":{"id":"sweat","name":"Face with Cold Sweat","keywords":["downcast","hot","sad","tired","exercise"],"skins":[{"unified":"1f613","native":"😓"}],"version":1},"weary":{"id":"weary","name":"Weary Face","keywords":["tired","sleepy","sad","frustrated","upset"],"skins":[{"unified":"1f629","native":"😩"}],"version":1},"tired_face":{"id":"tired_face","name":"Tired Face","keywords":["sick","whine","upset","frustrated"],"skins":[{"unified":"1f62b","native":"😫"}],"version":1},"yawning_face":{"id":"yawning_face","name":"Yawning Face","keywords":["tired","sleepy"],"skins":[{"unified":"1f971","native":"🥱"}],"version":12},"triumph":{"id":"triumph","name":"Face with Look of Triumph","keywords":["steam","from","nose","gas","phew","proud","pride"],"skins":[{"unified":"1f624","native":"😤"}],"version":1},"rage":{"id":"rage","name":"Pouting Face","keywords":["rage","angry","mad","hate","despise"],"skins":[{"unified":"1f621","native":"😡"}],"version":1},"angry":{"id":"angry","name":"Angry Face","emoticons":[">:(",">:-("],"keywords":["mad","annoyed","frustrated"],"skins":[{"unified":"1f620","native":"😠"}],"version":1},"face_with_symbols_on_mouth":{"id":"face_with_symbols_on_mouth","name":"Face with Symbols on Mouth","keywords":["serious","covering","swearing","cursing","cussing","profanity","expletive"],"skins":[{"unified":"1f92c","native":"🤬"}],"version":5},"smiling_imp":{"id":"smiling_imp","name":"Smiling Face with Horns","keywords":["imp","devil"],"skins":[{"unified":"1f608","native":"😈"}],"version":1},"imp":{"id":"imp","name":"Imp","keywords":["angry","face","with","horns","devil"],"skins":[{"unified":"1f47f","native":"👿"}],"version":1},"skull":{"id":"skull","name":"Skull","keywords":["dead","skeleton","creepy","death"],"skins":[{"unified":"1f480","native":"💀"}],"version":1},"skull_and_crossbones":{"id":"skull_and_crossbones","name":"Skull and Crossbones","keywords":["poison","danger","deadly","scary","death","pirate","evil"],"skins":[{"unified":"2620-fe0f","native":"☠ï¸"}],"version":1},"hankey":{"id":"hankey","name":"Pile of Poo","keywords":["hankey","poop","shit","shitface","fail","turd"],"skins":[{"unified":"1f4a9","native":"💩"}],"version":1},"clown_face":{"id":"clown_face","name":"Clown Face","keywords":[],"skins":[{"unified":"1f921","native":"🤡"}],"version":3},"japanese_ogre":{"id":"japanese_ogre","name":"Ogre","keywords":["japanese","monster","red","mask","halloween","scary","creepy","devil","demon"],"skins":[{"unified":"1f479","native":"👹"}],"version":1},"japanese_goblin":{"id":"japanese_goblin","name":"Goblin","keywords":["japanese","red","evil","mask","monster","scary","creepy"],"skins":[{"unified":"1f47a","native":"👺"}],"version":1},"ghost":{"id":"ghost","name":"Ghost","keywords":["halloween","spooky","scary"],"skins":[{"unified":"1f47b","native":"👻"}],"version":1},"alien":{"id":"alien","name":"Alien","keywords":["UFO","paul","weird","outer","space"],"skins":[{"unified":"1f47d","native":"👽"}],"version":1},"space_invader":{"id":"space_invader","name":"Alien Monster","keywords":["space","invader","game","arcade","play"],"skins":[{"unified":"1f47e","native":"👾"}],"version":1},"robot_face":{"id":"robot_face","name":"Robot","keywords":["face","computer","machine","bot"],"skins":[{"unified":"1f916","native":"🤖"}],"version":1},"smiley_cat":{"id":"smiley_cat","name":"Grinning Cat","keywords":["smiley","animal","cats","happy","smile"],"skins":[{"unified":"1f63a","native":"😺"}],"version":1},"smile_cat":{"id":"smile_cat","name":"Grinning Cat with Smiling Eyes","keywords":["smile","animal","cats"],"skins":[{"unified":"1f638","native":"😸"}],"version":1},"joy_cat":{"id":"joy_cat","name":"Cat with Tears of Joy","keywords":["animal","cats","haha","happy"],"skins":[{"unified":"1f639","native":"😹"}],"version":1},"heart_eyes_cat":{"id":"heart_eyes_cat","name":"Smiling Cat with Heart-Eyes","keywords":["heart","eyes","animal","love","like","affection","cats","valentines"],"skins":[{"unified":"1f63b","native":"😻"}],"version":1},"smirk_cat":{"id":"smirk_cat","name":"Cat with Wry Smile","keywords":["smirk","animal","cats"],"skins":[{"unified":"1f63c","native":"😼"}],"version":1},"kissing_cat":{"id":"kissing_cat","name":"Kissing Cat","keywords":["animal","cats","kiss"],"skins":[{"unified":"1f63d","native":"😽"}],"version":1},"scream_cat":{"id":"scream_cat","name":"Weary Cat","keywords":["scream","animal","cats","munch","scared"],"skins":[{"unified":"1f640","native":"🙀"}],"version":1},"crying_cat_face":{"id":"crying_cat_face","name":"Crying Cat","keywords":["face","animal","tears","weep","sad","cats","upset","cry"],"skins":[{"unified":"1f63f","native":"😿"}],"version":1},"pouting_cat":{"id":"pouting_cat","name":"Pouting Cat","keywords":["animal","cats"],"skins":[{"unified":"1f63e","native":"😾"}],"version":1},"see_no_evil":{"id":"see_no_evil","name":"See-No-Evil Monkey","keywords":["see","no","evil","animal","nature","haha"],"skins":[{"unified":"1f648","native":"🙈"}],"version":1},"hear_no_evil":{"id":"hear_no_evil","name":"Hear-No-Evil Monkey","keywords":["hear","no","evil","animal","nature"],"skins":[{"unified":"1f649","native":"🙉"}],"version":1},"speak_no_evil":{"id":"speak_no_evil","name":"Speak-No-Evil Monkey","keywords":["speak","no","evil","animal","nature","omg"],"skins":[{"unified":"1f64a","native":"🙊"}],"version":1},"kiss":{"id":"kiss","name":"Kiss Mark","keywords":["face","lips","love","like","affection","valentines"],"skins":[{"unified":"1f48b","native":"💋"}],"version":1},"love_letter":{"id":"love_letter","name":"Love Letter","keywords":["email","like","affection","envelope","valentines"],"skins":[{"unified":"1f48c","native":"💌"}],"version":1},"cupid":{"id":"cupid","name":"Heart with Arrow","keywords":["cupid","love","like","affection","valentines"],"skins":[{"unified":"1f498","native":"💘"}],"version":1},"gift_heart":{"id":"gift_heart","name":"Heart with Ribbon","keywords":["gift","love","valentines"],"skins":[{"unified":"1f49d","native":"ðŸ’"}],"version":1},"sparkling_heart":{"id":"sparkling_heart","name":"Sparkling Heart","keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f496","native":"💖"}],"version":1},"heartpulse":{"id":"heartpulse","name":"Growing Heart","keywords":["heartpulse","like","love","affection","valentines","pink"],"skins":[{"unified":"1f497","native":"💗"}],"version":1},"heartbeat":{"id":"heartbeat","name":"Beating Heart","keywords":["heartbeat","love","like","affection","valentines","pink"],"skins":[{"unified":"1f493","native":"💓"}],"version":1},"revolving_hearts":{"id":"revolving_hearts","name":"Revolving Hearts","keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f49e","native":"💞"}],"version":1},"two_hearts":{"id":"two_hearts","name":"Two Hearts","keywords":["love","like","affection","valentines","heart"],"skins":[{"unified":"1f495","native":"💕"}],"version":1},"heart_decoration":{"id":"heart_decoration","name":"Heart Decoration","keywords":["purple","square","love","like"],"skins":[{"unified":"1f49f","native":"💟"}],"version":1},"heavy_heart_exclamation_mark_ornament":{"id":"heavy_heart_exclamation_mark_ornament","name":"Heart Exclamation","keywords":["heavy","mark","ornament","decoration","love"],"skins":[{"unified":"2763-fe0f","native":"â£ï¸"}],"version":1},"broken_heart":{"id":"broken_heart","name":"Broken Heart","emoticons":["</3"],"keywords":["sad","sorry","break","heartbreak"],"skins":[{"unified":"1f494","native":"💔"}],"version":1},"heart_on_fire":{"id":"heart_on_fire","name":"Heart on Fire","keywords":["passionate","enthusiastic"],"skins":[{"unified":"2764-fe0f-200d-1f525","native":"â¤ï¸â€ðŸ”¥"}],"version":13.1},"mending_heart":{"id":"mending_heart","name":"Mending Heart","keywords":["broken","bandage","wounded"],"skins":[{"unified":"2764-fe0f-200d-1fa79","native":"â¤ï¸â€ðŸ©¹"}],"version":13.1},"heart":{"id":"heart","name":"Red Heart","emoticons":["<3"],"keywords":["love","like","valentines"],"skins":[{"unified":"2764-fe0f","native":"â¤ï¸"}],"version":1},"orange_heart":{"id":"orange_heart","name":"Orange Heart","keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f9e1","native":"🧡"}],"version":5},"yellow_heart":{"id":"yellow_heart","name":"Yellow Heart","emoticons":["<3"],"keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f49b","native":"💛"}],"version":1},"green_heart":{"id":"green_heart","name":"Green Heart","emoticons":["<3"],"keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f49a","native":"💚"}],"version":1},"blue_heart":{"id":"blue_heart","name":"Blue Heart","emoticons":["<3"],"keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f499","native":"💙"}],"version":1},"purple_heart":{"id":"purple_heart","name":"Purple Heart","emoticons":["<3"],"keywords":["love","like","affection","valentines"],"skins":[{"unified":"1f49c","native":"💜"}],"version":1},"brown_heart":{"id":"brown_heart","name":"Brown Heart","keywords":["coffee"],"skins":[{"unified":"1f90e","native":"🤎"}],"version":12},"black_heart":{"id":"black_heart","name":"Black Heart","keywords":["evil"],"skins":[{"unified":"1f5a4","native":"🖤"}],"version":3},"white_heart":{"id":"white_heart","name":"White Heart","keywords":["pure"],"skins":[{"unified":"1f90d","native":"ðŸ¤"}],"version":12},"anger":{"id":"anger","name":"Anger Symbol","keywords":["angry","mad"],"skins":[{"unified":"1f4a2","native":"💢"}],"version":1},"boom":{"id":"boom","name":"Collision","keywords":["boom","bomb","explode","explosion","blown"],"skins":[{"unified":"1f4a5","native":"💥"}],"version":1},"dizzy":{"id":"dizzy","name":"Dizzy","keywords":["star","sparkle","shoot","magic"],"skins":[{"unified":"1f4ab","native":"💫"}],"version":1},"sweat_drops":{"id":"sweat_drops","name":"Sweat Droplets","keywords":["drops","water","drip","oops"],"skins":[{"unified":"1f4a6","native":"💦"}],"version":1},"dash":{"id":"dash","name":"Dash Symbol","keywords":["dashing","away","wind","air","fast","shoo","fart","smoke","puff"],"skins":[{"unified":"1f4a8","native":"💨"}],"version":1},"hole":{"id":"hole","name":"Hole","keywords":["embarrassing"],"skins":[{"unified":"1f573-fe0f","native":"🕳ï¸"}],"version":1},"bomb":{"id":"bomb","name":"Bomb","keywords":["boom","explode","explosion","terrorism"],"skins":[{"unified":"1f4a3","native":"💣"}],"version":1},"speech_balloon":{"id":"speech_balloon","name":"Speech Balloon","keywords":["bubble","words","message","talk","chatting"],"skins":[{"unified":"1f4ac","native":"💬"}],"version":1},"eye-in-speech-bubble":{"id":"eye-in-speech-bubble","name":"Eye in Speech Bubble","keywords":["in-speech-bubble","info"],"skins":[{"unified":"1f441-fe0f-200d-1f5e8-fe0f","native":"ðŸ‘ï¸â€ðŸ—¨ï¸"}],"version":2},"left_speech_bubble":{"id":"left_speech_bubble","name":"Left Speech Bubble","keywords":["words","message","talk","chatting"],"skins":[{"unified":"1f5e8-fe0f","native":"🗨ï¸"}],"version":2},"right_anger_bubble":{"id":"right_anger_bubble","name":"Right Anger Bubble","keywords":["caption","speech","thinking","mad"],"skins":[{"unified":"1f5ef-fe0f","native":"🗯ï¸"}],"version":1},"thought_balloon":{"id":"thought_balloon","name":"Thought Balloon","keywords":["bubble","cloud","speech","thinking","dream"],"skins":[{"unified":"1f4ad","native":"ðŸ’"}],"version":1},"zzz":{"id":"zzz","name":"Zzz","keywords":["sleepy","tired","dream"],"skins":[{"unified":"1f4a4","native":"💤"}],"version":1},"wave":{"id":"wave","name":"Waving Hand","keywords":["wave","hands","gesture","goodbye","solong","farewell","hello","hi","palm"],"skins":[{"unified":"1f44b","native":"👋"},{"unified":"1f44b-1f3fb","native":"👋ðŸ»"},{"unified":"1f44b-1f3fc","native":"👋ðŸ¼"},{"unified":"1f44b-1f3fd","native":"👋ðŸ½"},{"unified":"1f44b-1f3fe","native":"👋ðŸ¾"},{"unified":"1f44b-1f3ff","native":"👋ðŸ¿"}],"version":1},"raised_back_of_hand":{"id":"raised_back_of_hand","name":"Raised Back of Hand","keywords":["fingers","backhand"],"skins":[{"unified":"1f91a","native":"🤚"},{"unified":"1f91a-1f3fb","native":"🤚ðŸ»"},{"unified":"1f91a-1f3fc","native":"🤚ðŸ¼"},{"unified":"1f91a-1f3fd","native":"🤚ðŸ½"},{"unified":"1f91a-1f3fe","native":"🤚ðŸ¾"},{"unified":"1f91a-1f3ff","native":"🤚ðŸ¿"}],"version":3},"raised_hand_with_fingers_splayed":{"id":"raised_hand_with_fingers_splayed","name":"Hand with Fingers Splayed","keywords":["raised","palm"],"skins":[{"unified":"1f590-fe0f","native":"ðŸ–ï¸"},{"unified":"1f590-1f3fb","native":"ðŸ–ðŸ»"},{"unified":"1f590-1f3fc","native":"ðŸ–ðŸ¼"},{"unified":"1f590-1f3fd","native":"ðŸ–ðŸ½"},{"unified":"1f590-1f3fe","native":"ðŸ–ðŸ¾"},{"unified":"1f590-1f3ff","native":"ðŸ–ðŸ¿"}],"version":1},"hand":{"id":"hand","name":"Raised Hand","keywords":["fingers","stop","highfive","high","five","palm","ban"],"skins":[{"unified":"270b","native":"✋"},{"unified":"270b-1f3fb","native":"✋ðŸ»"},{"unified":"270b-1f3fc","native":"✋ðŸ¼"},{"unified":"270b-1f3fd","native":"✋ðŸ½"},{"unified":"270b-1f3fe","native":"✋ðŸ¾"},{"unified":"270b-1f3ff","native":"✋ðŸ¿"}],"version":1},"spock-hand":{"id":"spock-hand","name":"Vulcan Salute","keywords":["spock","hand","fingers","star","trek"],"skins":[{"unified":"1f596","native":"🖖"},{"unified":"1f596-1f3fb","native":"🖖ðŸ»"},{"unified":"1f596-1f3fc","native":"🖖ðŸ¼"},{"unified":"1f596-1f3fd","native":"🖖ðŸ½"},{"unified":"1f596-1f3fe","native":"🖖ðŸ¾"},{"unified":"1f596-1f3ff","native":"🖖ðŸ¿"}],"version":1},"rightwards_hand":{"id":"rightwards_hand","name":"Rightwards Hand","keywords":["palm","offer"],"skins":[{"unified":"1faf1","native":"🫱"},{"unified":"1faf1-1f3fb","native":"🫱ðŸ»"},{"unified":"1faf1-1f3fc","native":"🫱ðŸ¼"},{"unified":"1faf1-1f3fd","native":"🫱ðŸ½"},{"unified":"1faf1-1f3fe","native":"🫱ðŸ¾"},{"unified":"1faf1-1f3ff","native":"🫱ðŸ¿"}],"version":14},"leftwards_hand":{"id":"leftwards_hand","name":"Leftwards Hand","keywords":["palm","offer"],"skins":[{"unified":"1faf2","native":"🫲"},{"unified":"1faf2-1f3fb","native":"🫲ðŸ»"},{"unified":"1faf2-1f3fc","native":"🫲ðŸ¼"},{"unified":"1faf2-1f3fd","native":"🫲ðŸ½"},{"unified":"1faf2-1f3fe","native":"🫲ðŸ¾"},{"unified":"1faf2-1f3ff","native":"🫲ðŸ¿"}],"version":14},"palm_down_hand":{"id":"palm_down_hand","name":"Palm Down Hand","keywords":["drop"],"skins":[{"unified":"1faf3","native":"🫳"},{"unified":"1faf3-1f3fb","native":"🫳ðŸ»"},{"unified":"1faf3-1f3fc","native":"🫳ðŸ¼"},{"unified":"1faf3-1f3fd","native":"🫳ðŸ½"},{"unified":"1faf3-1f3fe","native":"🫳ðŸ¾"},{"unified":"1faf3-1f3ff","native":"🫳ðŸ¿"}],"version":14},"palm_up_hand":{"id":"palm_up_hand","name":"Palm Up Hand","keywords":["lift","offer","demand"],"skins":[{"unified":"1faf4","native":"🫴"},{"unified":"1faf4-1f3fb","native":"🫴ðŸ»"},{"unified":"1faf4-1f3fc","native":"🫴ðŸ¼"},{"unified":"1faf4-1f3fd","native":"🫴ðŸ½"},{"unified":"1faf4-1f3fe","native":"🫴ðŸ¾"},{"unified":"1faf4-1f3ff","native":"🫴ðŸ¿"}],"version":14},"ok_hand":{"id":"ok_hand","name":"Ok Hand","keywords":["fingers","limbs","perfect","okay"],"skins":[{"unified":"1f44c","native":"👌"},{"unified":"1f44c-1f3fb","native":"👌ðŸ»"},{"unified":"1f44c-1f3fc","native":"👌ðŸ¼"},{"unified":"1f44c-1f3fd","native":"👌ðŸ½"},{"unified":"1f44c-1f3fe","native":"👌ðŸ¾"},{"unified":"1f44c-1f3ff","native":"👌ðŸ¿"}],"version":1},"pinched_fingers":{"id":"pinched_fingers","name":"Pinched Fingers","keywords":["size","tiny","small"],"skins":[{"unified":"1f90c","native":"🤌"},{"unified":"1f90c-1f3fb","native":"🤌ðŸ»"},{"unified":"1f90c-1f3fc","native":"🤌ðŸ¼"},{"unified":"1f90c-1f3fd","native":"🤌ðŸ½"},{"unified":"1f90c-1f3fe","native":"🤌ðŸ¾"},{"unified":"1f90c-1f3ff","native":"🤌ðŸ¿"}],"version":13},"pinching_hand":{"id":"pinching_hand","name":"Pinching Hand","keywords":["tiny","small","size"],"skins":[{"unified":"1f90f","native":"ðŸ¤"},{"unified":"1f90f-1f3fb","native":"ðŸ¤ðŸ»"},{"unified":"1f90f-1f3fc","native":"ðŸ¤ðŸ¼"},{"unified":"1f90f-1f3fd","native":"ðŸ¤ðŸ½"},{"unified":"1f90f-1f3fe","native":"ðŸ¤ðŸ¾"},{"unified":"1f90f-1f3ff","native":"ðŸ¤ðŸ¿"}],"version":12},"v":{"id":"v","name":"Victory Hand","keywords":["v","fingers","ohyeah","peace","two"],"skins":[{"unified":"270c-fe0f","native":"✌ï¸"},{"unified":"270c-1f3fb","native":"✌ðŸ»"},{"unified":"270c-1f3fc","native":"✌ðŸ¼"},{"unified":"270c-1f3fd","native":"✌ðŸ½"},{"unified":"270c-1f3fe","native":"✌ðŸ¾"},{"unified":"270c-1f3ff","native":"✌ðŸ¿"}],"version":1},"crossed_fingers":{"id":"crossed_fingers","name":"Crossed Fingers","keywords":["hand","with","index","and","middle","good","lucky"],"skins":[{"unified":"1f91e","native":"🤞"},{"unified":"1f91e-1f3fb","native":"🤞ðŸ»"},{"unified":"1f91e-1f3fc","native":"🤞ðŸ¼"},{"unified":"1f91e-1f3fd","native":"🤞ðŸ½"},{"unified":"1f91e-1f3fe","native":"🤞ðŸ¾"},{"unified":"1f91e-1f3ff","native":"🤞ðŸ¿"}],"version":3},"hand_with_index_finger_and_thumb_crossed":{"id":"hand_with_index_finger_and_thumb_crossed","name":"Hand with Index Finger and Thumb Crossed","keywords":["heart","love","money","expensive"],"skins":[{"unified":"1faf0","native":"🫰"},{"unified":"1faf0-1f3fb","native":"🫰ðŸ»"},{"unified":"1faf0-1f3fc","native":"🫰ðŸ¼"},{"unified":"1faf0-1f3fd","native":"🫰ðŸ½"},{"unified":"1faf0-1f3fe","native":"🫰ðŸ¾"},{"unified":"1faf0-1f3ff","native":"🫰ðŸ¿"}],"version":14},"i_love_you_hand_sign":{"id":"i_love_you_hand_sign","name":"Love-You Gesture","keywords":["i","love","you","hand","sign","fingers"],"skins":[{"unified":"1f91f","native":"🤟"},{"unified":"1f91f-1f3fb","native":"🤟ðŸ»"},{"unified":"1f91f-1f3fc","native":"🤟ðŸ¼"},{"unified":"1f91f-1f3fd","native":"🤟ðŸ½"},{"unified":"1f91f-1f3fe","native":"🤟ðŸ¾"},{"unified":"1f91f-1f3ff","native":"🤟ðŸ¿"}],"version":5},"the_horns":{"id":"the_horns","name":"Sign of the Horns","keywords":["hand","fingers","evil","eye","rock","on"],"skins":[{"unified":"1f918","native":"🤘"},{"unified":"1f918-1f3fb","native":"🤘ðŸ»"},{"unified":"1f918-1f3fc","native":"🤘ðŸ¼"},{"unified":"1f918-1f3fd","native":"🤘ðŸ½"},{"unified":"1f918-1f3fe","native":"🤘ðŸ¾"},{"unified":"1f918-1f3ff","native":"🤘ðŸ¿"}],"version":1},"call_me_hand":{"id":"call_me_hand","name":"Call Me Hand","keywords":["hands","gesture","shaka"],"skins":[{"unified":"1f919","native":"🤙"},{"unified":"1f919-1f3fb","native":"🤙ðŸ»"},{"unified":"1f919-1f3fc","native":"🤙ðŸ¼"},{"unified":"1f919-1f3fd","native":"🤙ðŸ½"},{"unified":"1f919-1f3fe","native":"🤙ðŸ¾"},{"unified":"1f919-1f3ff","native":"🤙ðŸ¿"}],"version":3},"point_left":{"id":"point_left","name":"Backhand Index Pointing Left","keywords":["point","direction","fingers","hand"],"skins":[{"unified":"1f448","native":"👈"},{"unified":"1f448-1f3fb","native":"👈ðŸ»"},{"unified":"1f448-1f3fc","native":"👈ðŸ¼"},{"unified":"1f448-1f3fd","native":"👈ðŸ½"},{"unified":"1f448-1f3fe","native":"👈ðŸ¾"},{"unified":"1f448-1f3ff","native":"👈ðŸ¿"}],"version":1},"point_right":{"id":"point_right","name":"Backhand Index Pointing Right","keywords":["point","fingers","hand","direction"],"skins":[{"unified":"1f449","native":"👉"},{"unified":"1f449-1f3fb","native":"👉ðŸ»"},{"unified":"1f449-1f3fc","native":"👉ðŸ¼"},{"unified":"1f449-1f3fd","native":"👉ðŸ½"},{"unified":"1f449-1f3fe","native":"👉ðŸ¾"},{"unified":"1f449-1f3ff","native":"👉ðŸ¿"}],"version":1},"point_up_2":{"id":"point_up_2","name":"Backhand Index Pointing Up","keywords":["point","2","fingers","hand","direction"],"skins":[{"unified":"1f446","native":"👆"},{"unified":"1f446-1f3fb","native":"👆ðŸ»"},{"unified":"1f446-1f3fc","native":"👆ðŸ¼"},{"unified":"1f446-1f3fd","native":"👆ðŸ½"},{"unified":"1f446-1f3fe","native":"👆ðŸ¾"},{"unified":"1f446-1f3ff","native":"👆ðŸ¿"}],"version":1},"middle_finger":{"id":"middle_finger","name":"Middle Finger","keywords":["reversed","hand","with","extended","fingers","rude","flipping"],"skins":[{"unified":"1f595","native":"🖕"},{"unified":"1f595-1f3fb","native":"🖕ðŸ»"},{"unified":"1f595-1f3fc","native":"🖕ðŸ¼"},{"unified":"1f595-1f3fd","native":"🖕ðŸ½"},{"unified":"1f595-1f3fe","native":"🖕ðŸ¾"},{"unified":"1f595-1f3ff","native":"🖕ðŸ¿"}],"version":1},"point_down":{"id":"point_down","name":"Backhand Index Pointing Down","keywords":["point","fingers","hand","direction"],"skins":[{"unified":"1f447","native":"👇"},{"unified":"1f447-1f3fb","native":"👇ðŸ»"},{"unified":"1f447-1f3fc","native":"👇ðŸ¼"},{"unified":"1f447-1f3fd","native":"👇ðŸ½"},{"unified":"1f447-1f3fe","native":"👇ðŸ¾"},{"unified":"1f447-1f3ff","native":"👇ðŸ¿"}],"version":1},"point_up":{"id":"point_up","name":"Index Pointing Up","keywords":["point","hand","fingers","direction"],"skins":[{"unified":"261d-fe0f","native":"â˜ï¸"},{"unified":"261d-1f3fb","native":"â˜ðŸ»"},{"unified":"261d-1f3fc","native":"â˜ðŸ¼"},{"unified":"261d-1f3fd","native":"â˜ðŸ½"},{"unified":"261d-1f3fe","native":"â˜ðŸ¾"},{"unified":"261d-1f3ff","native":"â˜ðŸ¿"}],"version":1},"index_pointing_at_the_viewer":{"id":"index_pointing_at_the_viewer","name":"Index Pointing at the Viewer","keywords":["you","recruit"],"skins":[{"unified":"1faf5","native":"🫵"},{"unified":"1faf5-1f3fb","native":"🫵ðŸ»"},{"unified":"1faf5-1f3fc","native":"🫵ðŸ¼"},{"unified":"1faf5-1f3fd","native":"🫵ðŸ½"},{"unified":"1faf5-1f3fe","native":"🫵ðŸ¾"},{"unified":"1faf5-1f3ff","native":"🫵ðŸ¿"}],"version":14},"+1":{"id":"+1","name":"Thumbs Up","keywords":["+1","thumbsup","yes","awesome","good","agree","accept","cool","hand","like"],"skins":[{"unified":"1f44d","native":"ðŸ‘"},{"unified":"1f44d-1f3fb","native":"ðŸ‘ðŸ»"},{"unified":"1f44d-1f3fc","native":"ðŸ‘ðŸ¼"},{"unified":"1f44d-1f3fd","native":"ðŸ‘ðŸ½"},{"unified":"1f44d-1f3fe","native":"ðŸ‘ðŸ¾"},{"unified":"1f44d-1f3ff","native":"ðŸ‘ðŸ¿"}],"version":1},"-1":{"id":"-1","name":"Thumbs Down","keywords":["-1","thumbsdown","no","dislike","hand"],"skins":[{"unified":"1f44e","native":"👎"},{"unified":"1f44e-1f3fb","native":"👎ðŸ»"},{"unified":"1f44e-1f3fc","native":"👎ðŸ¼"},{"unified":"1f44e-1f3fd","native":"👎ðŸ½"},{"unified":"1f44e-1f3fe","native":"👎ðŸ¾"},{"unified":"1f44e-1f3ff","native":"👎ðŸ¿"}],"version":1},"fist":{"id":"fist","name":"Raised Fist","keywords":["fingers","hand","grasp"],"skins":[{"unified":"270a","native":"✊"},{"unified":"270a-1f3fb","native":"✊ðŸ»"},{"unified":"270a-1f3fc","native":"✊ðŸ¼"},{"unified":"270a-1f3fd","native":"✊ðŸ½"},{"unified":"270a-1f3fe","native":"✊ðŸ¾"},{"unified":"270a-1f3ff","native":"✊ðŸ¿"}],"version":1},"facepunch":{"id":"facepunch","name":"Oncoming Fist","keywords":["facepunch","punch","angry","violence","hit","attack","hand"],"skins":[{"unified":"1f44a","native":"👊"},{"unified":"1f44a-1f3fb","native":"👊ðŸ»"},{"unified":"1f44a-1f3fc","native":"👊ðŸ¼"},{"unified":"1f44a-1f3fd","native":"👊ðŸ½"},{"unified":"1f44a-1f3fe","native":"👊ðŸ¾"},{"unified":"1f44a-1f3ff","native":"👊ðŸ¿"}],"version":1},"left-facing_fist":{"id":"left-facing_fist","name":"Left-Facing Fist","keywords":["left","facing","hand","fistbump"],"skins":[{"unified":"1f91b","native":"🤛"},{"unified":"1f91b-1f3fb","native":"🤛ðŸ»"},{"unified":"1f91b-1f3fc","native":"🤛ðŸ¼"},{"unified":"1f91b-1f3fd","native":"🤛ðŸ½"},{"unified":"1f91b-1f3fe","native":"🤛ðŸ¾"},{"unified":"1f91b-1f3ff","native":"🤛ðŸ¿"}],"version":3},"right-facing_fist":{"id":"right-facing_fist","name":"Right-Facing Fist","keywords":["right","facing","hand","fistbump"],"skins":[{"unified":"1f91c","native":"🤜"},{"unified":"1f91c-1f3fb","native":"🤜ðŸ»"},{"unified":"1f91c-1f3fc","native":"🤜ðŸ¼"},{"unified":"1f91c-1f3fd","native":"🤜ðŸ½"},{"unified":"1f91c-1f3fe","native":"🤜ðŸ¾"},{"unified":"1f91c-1f3ff","native":"🤜ðŸ¿"}],"version":3},"clap":{"id":"clap","name":"Clapping Hands","keywords":["clap","praise","applause","congrats","yay"],"skins":[{"unified":"1f44f","native":"ðŸ‘"},{"unified":"1f44f-1f3fb","native":"ðŸ‘ðŸ»"},{"unified":"1f44f-1f3fc","native":"ðŸ‘ðŸ¼"},{"unified":"1f44f-1f3fd","native":"ðŸ‘ðŸ½"},{"unified":"1f44f-1f3fe","native":"ðŸ‘ðŸ¾"},{"unified":"1f44f-1f3ff","native":"ðŸ‘ðŸ¿"}],"version":1},"raised_hands":{"id":"raised_hands","name":"Raising Hands","keywords":["raised","gesture","hooray","yea","celebration"],"skins":[{"unified":"1f64c","native":"🙌"},{"unified":"1f64c-1f3fb","native":"🙌ðŸ»"},{"unified":"1f64c-1f3fc","native":"🙌ðŸ¼"},{"unified":"1f64c-1f3fd","native":"🙌ðŸ½"},{"unified":"1f64c-1f3fe","native":"🙌ðŸ¾"},{"unified":"1f64c-1f3ff","native":"🙌ðŸ¿"}],"version":1},"heart_hands":{"id":"heart_hands","name":"Heart Hands","keywords":["love","appreciation","support"],"skins":[{"unified":"1faf6","native":"🫶"},{"unified":"1faf6-1f3fb","native":"🫶ðŸ»"},{"unified":"1faf6-1f3fc","native":"🫶ðŸ¼"},{"unified":"1faf6-1f3fd","native":"🫶ðŸ½"},{"unified":"1faf6-1f3fe","native":"🫶ðŸ¾"},{"unified":"1faf6-1f3ff","native":"🫶ðŸ¿"}],"version":14},"open_hands":{"id":"open_hands","name":"Open Hands","keywords":["fingers","butterfly"],"skins":[{"unified":"1f450","native":"ðŸ‘"},{"unified":"1f450-1f3fb","native":"ðŸ‘ðŸ»"},{"unified":"1f450-1f3fc","native":"ðŸ‘ðŸ¼"},{"unified":"1f450-1f3fd","native":"ðŸ‘ðŸ½"},{"unified":"1f450-1f3fe","native":"ðŸ‘ðŸ¾"},{"unified":"1f450-1f3ff","native":"ðŸ‘ðŸ¿"}],"version":1},"palms_up_together":{"id":"palms_up_together","name":"Palms Up Together","keywords":["hands","gesture","cupped","prayer"],"skins":[{"unified":"1f932","native":"🤲"},{"unified":"1f932-1f3fb","native":"🤲ðŸ»"},{"unified":"1f932-1f3fc","native":"🤲ðŸ¼"},{"unified":"1f932-1f3fd","native":"🤲ðŸ½"},{"unified":"1f932-1f3fe","native":"🤲ðŸ¾"},{"unified":"1f932-1f3ff","native":"🤲ðŸ¿"}],"version":5},"handshake":{"id":"handshake","name":"Handshake","keywords":["agreement","shake"],"skins":[{"unified":"1f91d","native":"ðŸ¤"},{"unified":"1f91d-1f3fb","native":"ðŸ¤ðŸ»"},{"unified":"1f91d-1f3fc","native":"ðŸ¤ðŸ¼"},{"unified":"1f91d-1f3fd","native":"ðŸ¤ðŸ½"},{"unified":"1f91d-1f3fe","native":"ðŸ¤ðŸ¾"},{"unified":"1f91d-1f3ff","native":"ðŸ¤ðŸ¿"}],"version":3},"pray":{"id":"pray","name":"Folded Hands","keywords":["pray","please","hope","wish","namaste","highfive","high","five"],"skins":[{"unified":"1f64f","native":"ðŸ™"},{"unified":"1f64f-1f3fb","native":"ðŸ™ðŸ»"},{"unified":"1f64f-1f3fc","native":"ðŸ™ðŸ¼"},{"unified":"1f64f-1f3fd","native":"ðŸ™ðŸ½"},{"unified":"1f64f-1f3fe","native":"ðŸ™ðŸ¾"},{"unified":"1f64f-1f3ff","native":"ðŸ™ðŸ¿"}],"version":1},"writing_hand":{"id":"writing_hand","name":"Writing Hand","keywords":["lower","left","ballpoint","pen","stationery","write","compose"],"skins":[{"unified":"270d-fe0f","native":"âœï¸"},{"unified":"270d-1f3fb","native":"âœðŸ»"},{"unified":"270d-1f3fc","native":"âœðŸ¼"},{"unified":"270d-1f3fd","native":"âœðŸ½"},{"unified":"270d-1f3fe","native":"âœðŸ¾"},{"unified":"270d-1f3ff","native":"âœðŸ¿"}],"version":1},"nail_care":{"id":"nail_care","name":"Nail Polish","keywords":["care","beauty","manicure","finger","fashion"],"skins":[{"unified":"1f485","native":"💅"},{"unified":"1f485-1f3fb","native":"💅ðŸ»"},{"unified":"1f485-1f3fc","native":"💅ðŸ¼"},{"unified":"1f485-1f3fd","native":"💅ðŸ½"},{"unified":"1f485-1f3fe","native":"💅ðŸ¾"},{"unified":"1f485-1f3ff","native":"💅ðŸ¿"}],"version":1},"selfie":{"id":"selfie","name":"Selfie","keywords":["camera","phone"],"skins":[{"unified":"1f933","native":"🤳"},{"unified":"1f933-1f3fb","native":"🤳ðŸ»"},{"unified":"1f933-1f3fc","native":"🤳ðŸ¼"},{"unified":"1f933-1f3fd","native":"🤳ðŸ½"},{"unified":"1f933-1f3fe","native":"🤳ðŸ¾"},{"unified":"1f933-1f3ff","native":"🤳ðŸ¿"}],"version":3},"muscle":{"id":"muscle","name":"Flexed Biceps","keywords":["muscle","arm","flex","hand","summer","strong"],"skins":[{"unified":"1f4aa","native":"💪"},{"unified":"1f4aa-1f3fb","native":"💪ðŸ»"},{"unified":"1f4aa-1f3fc","native":"💪ðŸ¼"},{"unified":"1f4aa-1f3fd","native":"💪ðŸ½"},{"unified":"1f4aa-1f3fe","native":"💪ðŸ¾"},{"unified":"1f4aa-1f3ff","native":"💪ðŸ¿"}],"version":1},"mechanical_arm":{"id":"mechanical_arm","name":"Mechanical Arm","keywords":["accessibility"],"skins":[{"unified":"1f9be","native":"🦾"}],"version":12},"mechanical_leg":{"id":"mechanical_leg","name":"Mechanical Leg","keywords":["accessibility"],"skins":[{"unified":"1f9bf","native":"🦿"}],"version":12},"leg":{"id":"leg","name":"Leg","keywords":["kick","limb"],"skins":[{"unified":"1f9b5","native":"🦵"},{"unified":"1f9b5-1f3fb","native":"🦵ðŸ»"},{"unified":"1f9b5-1f3fc","native":"🦵ðŸ¼"},{"unified":"1f9b5-1f3fd","native":"🦵ðŸ½"},{"unified":"1f9b5-1f3fe","native":"🦵ðŸ¾"},{"unified":"1f9b5-1f3ff","native":"🦵ðŸ¿"}],"version":11},"foot":{"id":"foot","name":"Foot","keywords":["kick","stomp"],"skins":[{"unified":"1f9b6","native":"🦶"},{"unified":"1f9b6-1f3fb","native":"🦶ðŸ»"},{"unified":"1f9b6-1f3fc","native":"🦶ðŸ¼"},{"unified":"1f9b6-1f3fd","native":"🦶ðŸ½"},{"unified":"1f9b6-1f3fe","native":"🦶ðŸ¾"},{"unified":"1f9b6-1f3ff","native":"🦶ðŸ¿"}],"version":11},"ear":{"id":"ear","name":"Ear","keywords":["face","hear","sound","listen"],"skins":[{"unified":"1f442","native":"👂"},{"unified":"1f442-1f3fb","native":"👂ðŸ»"},{"unified":"1f442-1f3fc","native":"👂ðŸ¼"},{"unified":"1f442-1f3fd","native":"👂ðŸ½"},{"unified":"1f442-1f3fe","native":"👂ðŸ¾"},{"unified":"1f442-1f3ff","native":"👂ðŸ¿"}],"version":1},"ear_with_hearing_aid":{"id":"ear_with_hearing_aid","name":"Ear with Hearing Aid","keywords":["accessibility"],"skins":[{"unified":"1f9bb","native":"🦻"},{"unified":"1f9bb-1f3fb","native":"🦻ðŸ»"},{"unified":"1f9bb-1f3fc","native":"🦻ðŸ¼"},{"unified":"1f9bb-1f3fd","native":"🦻ðŸ½"},{"unified":"1f9bb-1f3fe","native":"🦻ðŸ¾"},{"unified":"1f9bb-1f3ff","native":"🦻ðŸ¿"}],"version":12},"nose":{"id":"nose","name":"Nose","keywords":["smell","sniff"],"skins":[{"unified":"1f443","native":"👃"},{"unified":"1f443-1f3fb","native":"👃ðŸ»"},{"unified":"1f443-1f3fc","native":"👃ðŸ¼"},{"unified":"1f443-1f3fd","native":"👃ðŸ½"},{"unified":"1f443-1f3fe","native":"👃ðŸ¾"},{"unified":"1f443-1f3ff","native":"👃ðŸ¿"}],"version":1},"brain":{"id":"brain","name":"Brain","keywords":["smart","intelligent"],"skins":[{"unified":"1f9e0","native":"🧠"}],"version":5},"anatomical_heart":{"id":"anatomical_heart","name":"Anatomical Heart","keywords":["health","heartbeat"],"skins":[{"unified":"1fac0","native":"🫀"}],"version":13},"lungs":{"id":"lungs","name":"Lungs","keywords":["breathe"],"skins":[{"unified":"1fac1","native":"ðŸ«"}],"version":13},"tooth":{"id":"tooth","name":"Tooth","keywords":["teeth","dentist"],"skins":[{"unified":"1f9b7","native":"🦷"}],"version":11},"bone":{"id":"bone","name":"Bone","keywords":["skeleton"],"skins":[{"unified":"1f9b4","native":"🦴"}],"version":11},"eyes":{"id":"eyes","name":"Eyes","keywords":["look","watch","stalk","peek","see"],"skins":[{"unified":"1f440","native":"👀"}],"version":1},"eye":{"id":"eye","name":"Eye","keywords":["face","look","see","watch","stare"],"skins":[{"unified":"1f441-fe0f","native":"ðŸ‘ï¸"}],"version":1},"tongue":{"id":"tongue","name":"Tongue","keywords":["mouth","playful"],"skins":[{"unified":"1f445","native":"👅"}],"version":1},"lips":{"id":"lips","name":"Mouth","keywords":["lips","kiss"],"skins":[{"unified":"1f444","native":"👄"}],"version":1},"biting_lip":{"id":"biting_lip","name":"Biting Lip","keywords":["flirt","sexy","pain","worry"],"skins":[{"unified":"1fae6","native":"🫦"}],"version":14},"baby":{"id":"baby","name":"Baby","keywords":["child","boy","girl","toddler"],"skins":[{"unified":"1f476","native":"👶"},{"unified":"1f476-1f3fb","native":"👶ðŸ»"},{"unified":"1f476-1f3fc","native":"👶ðŸ¼"},{"unified":"1f476-1f3fd","native":"👶ðŸ½"},{"unified":"1f476-1f3fe","native":"👶ðŸ¾"},{"unified":"1f476-1f3ff","native":"👶ðŸ¿"}],"version":1},"child":{"id":"child","name":"Child","keywords":["gender","neutral","young"],"skins":[{"unified":"1f9d2","native":"🧒"},{"unified":"1f9d2-1f3fb","native":"🧒ðŸ»"},{"unified":"1f9d2-1f3fc","native":"🧒ðŸ¼"},{"unified":"1f9d2-1f3fd","native":"🧒ðŸ½"},{"unified":"1f9d2-1f3fe","native":"🧒ðŸ¾"},{"unified":"1f9d2-1f3ff","native":"🧒ðŸ¿"}],"version":5},"boy":{"id":"boy","name":"Boy","keywords":["man","male","guy","teenager"],"skins":[{"unified":"1f466","native":"👦"},{"unified":"1f466-1f3fb","native":"👦ðŸ»"},{"unified":"1f466-1f3fc","native":"👦ðŸ¼"},{"unified":"1f466-1f3fd","native":"👦ðŸ½"},{"unified":"1f466-1f3fe","native":"👦ðŸ¾"},{"unified":"1f466-1f3ff","native":"👦ðŸ¿"}],"version":1},"girl":{"id":"girl","name":"Girl","keywords":["female","woman","teenager"],"skins":[{"unified":"1f467","native":"👧"},{"unified":"1f467-1f3fb","native":"👧ðŸ»"},{"unified":"1f467-1f3fc","native":"👧ðŸ¼"},{"unified":"1f467-1f3fd","native":"👧ðŸ½"},{"unified":"1f467-1f3fe","native":"👧ðŸ¾"},{"unified":"1f467-1f3ff","native":"👧ðŸ¿"}],"version":1},"adult":{"id":"adult","name":"Adult","keywords":["person","gender","neutral"],"skins":[{"unified":"1f9d1","native":"🧑"},{"unified":"1f9d1-1f3fb","native":"🧑ðŸ»"},{"unified":"1f9d1-1f3fc","native":"🧑ðŸ¼"},{"unified":"1f9d1-1f3fd","native":"🧑ðŸ½"},{"unified":"1f9d1-1f3fe","native":"🧑ðŸ¾"},{"unified":"1f9d1-1f3ff","native":"🧑ðŸ¿"}],"version":5},"person_with_blond_hair":{"id":"person_with_blond_hair","name":"Person Blond Hair","keywords":["with","hairstyle"],"skins":[{"unified":"1f471","native":"👱"},{"unified":"1f471-1f3fb","native":"👱ðŸ»"},{"unified":"1f471-1f3fc","native":"👱ðŸ¼"},{"unified":"1f471-1f3fd","native":"👱ðŸ½"},{"unified":"1f471-1f3fe","native":"👱ðŸ¾"},{"unified":"1f471-1f3ff","native":"👱ðŸ¿"}],"version":1},"man":{"id":"man","name":"Man","keywords":["mustache","father","dad","guy","classy","sir","moustache"],"skins":[{"unified":"1f468","native":"👨"},{"unified":"1f468-1f3fb","native":"👨ðŸ»"},{"unified":"1f468-1f3fc","native":"👨ðŸ¼"},{"unified":"1f468-1f3fd","native":"👨ðŸ½"},{"unified":"1f468-1f3fe","native":"👨ðŸ¾"},{"unified":"1f468-1f3ff","native":"👨ðŸ¿"}],"version":1},"bearded_person":{"id":"bearded_person","name":"Person Beard","keywords":["bearded","man","bewhiskered"],"skins":[{"unified":"1f9d4","native":"🧔"},{"unified":"1f9d4-1f3fb","native":"🧔ðŸ»"},{"unified":"1f9d4-1f3fc","native":"🧔ðŸ¼"},{"unified":"1f9d4-1f3fd","native":"🧔ðŸ½"},{"unified":"1f9d4-1f3fe","native":"🧔ðŸ¾"},{"unified":"1f9d4-1f3ff","native":"🧔ðŸ¿"}],"version":5},"man_with_beard":{"id":"man_with_beard","name":"Man: Beard","keywords":["man","with","facial","hair"],"skins":[{"unified":"1f9d4-200d-2642-fe0f","native":"🧔â€â™‚ï¸"},{"unified":"1f9d4-1f3fb-200d-2642-fe0f","native":"🧔ðŸ»â€â™‚ï¸"},{"unified":"1f9d4-1f3fc-200d-2642-fe0f","native":"🧔ðŸ¼â€â™‚ï¸"},{"unified":"1f9d4-1f3fd-200d-2642-fe0f","native":"🧔ðŸ½â€â™‚ï¸"},{"unified":"1f9d4-1f3fe-200d-2642-fe0f","native":"🧔ðŸ¾â€â™‚ï¸"},{"unified":"1f9d4-1f3ff-200d-2642-fe0f","native":"🧔ðŸ¿â€â™‚ï¸"}],"version":13.1},"woman_with_beard":{"id":"woman_with_beard","name":"Woman: Beard","keywords":["woman","with","facial","hair"],"skins":[{"unified":"1f9d4-200d-2640-fe0f","native":"🧔â€â™€ï¸"},{"unified":"1f9d4-1f3fb-200d-2640-fe0f","native":"🧔ðŸ»â€â™€ï¸"},{"unified":"1f9d4-1f3fc-200d-2640-fe0f","native":"🧔ðŸ¼â€â™€ï¸"},{"unified":"1f9d4-1f3fd-200d-2640-fe0f","native":"🧔ðŸ½â€â™€ï¸"},{"unified":"1f9d4-1f3fe-200d-2640-fe0f","native":"🧔ðŸ¾â€â™€ï¸"},{"unified":"1f9d4-1f3ff-200d-2640-fe0f","native":"🧔ðŸ¿â€â™€ï¸"}],"version":13.1},"red_haired_man":{"id":"red_haired_man","name":"Man: Red Hair","keywords":["haired","man","hairstyle"],"skins":[{"unified":"1f468-200d-1f9b0","native":"👨â€ðŸ¦°"},{"unified":"1f468-1f3fb-200d-1f9b0","native":"👨ðŸ»â€ðŸ¦°"},{"unified":"1f468-1f3fc-200d-1f9b0","native":"👨ðŸ¼â€ðŸ¦°"},{"unified":"1f468-1f3fd-200d-1f9b0","native":"👨ðŸ½â€ðŸ¦°"},{"unified":"1f468-1f3fe-200d-1f9b0","native":"👨ðŸ¾â€ðŸ¦°"},{"unified":"1f468-1f3ff-200d-1f9b0","native":"👨ðŸ¿â€ðŸ¦°"}],"version":11},"curly_haired_man":{"id":"curly_haired_man","name":"Man: Curly Hair","keywords":["haired","man","hairstyle"],"skins":[{"unified":"1f468-200d-1f9b1","native":"👨â€ðŸ¦±"},{"unified":"1f468-1f3fb-200d-1f9b1","native":"👨ðŸ»â€ðŸ¦±"},{"unified":"1f468-1f3fc-200d-1f9b1","native":"👨ðŸ¼â€ðŸ¦±"},{"unified":"1f468-1f3fd-200d-1f9b1","native":"👨ðŸ½â€ðŸ¦±"},{"unified":"1f468-1f3fe-200d-1f9b1","native":"👨ðŸ¾â€ðŸ¦±"},{"unified":"1f468-1f3ff-200d-1f9b1","native":"👨ðŸ¿â€ðŸ¦±"}],"version":11},"white_haired_man":{"id":"white_haired_man","name":"Man: White Hair","keywords":["haired","man","old","elder"],"skins":[{"unified":"1f468-200d-1f9b3","native":"👨â€ðŸ¦³"},{"unified":"1f468-1f3fb-200d-1f9b3","native":"👨ðŸ»â€ðŸ¦³"},{"unified":"1f468-1f3fc-200d-1f9b3","native":"👨ðŸ¼â€ðŸ¦³"},{"unified":"1f468-1f3fd-200d-1f9b3","native":"👨ðŸ½â€ðŸ¦³"},{"unified":"1f468-1f3fe-200d-1f9b3","native":"👨ðŸ¾â€ðŸ¦³"},{"unified":"1f468-1f3ff-200d-1f9b3","native":"👨ðŸ¿â€ðŸ¦³"}],"version":11},"bald_man":{"id":"bald_man","name":"Man: Bald","keywords":["man","hairless"],"skins":[{"unified":"1f468-200d-1f9b2","native":"👨â€ðŸ¦²"},{"unified":"1f468-1f3fb-200d-1f9b2","native":"👨ðŸ»â€ðŸ¦²"},{"unified":"1f468-1f3fc-200d-1f9b2","native":"👨ðŸ¼â€ðŸ¦²"},{"unified":"1f468-1f3fd-200d-1f9b2","native":"👨ðŸ½â€ðŸ¦²"},{"unified":"1f468-1f3fe-200d-1f9b2","native":"👨ðŸ¾â€ðŸ¦²"},{"unified":"1f468-1f3ff-200d-1f9b2","native":"👨ðŸ¿â€ðŸ¦²"}],"version":11},"woman":{"id":"woman","name":"Woman","keywords":["female","girls","lady"],"skins":[{"unified":"1f469","native":"👩"},{"unified":"1f469-1f3fb","native":"👩ðŸ»"},{"unified":"1f469-1f3fc","native":"👩ðŸ¼"},{"unified":"1f469-1f3fd","native":"👩ðŸ½"},{"unified":"1f469-1f3fe","native":"👩ðŸ¾"},{"unified":"1f469-1f3ff","native":"👩ðŸ¿"}],"version":1},"red_haired_woman":{"id":"red_haired_woman","name":"Woman: Red Hair","keywords":["haired","woman","hairstyle"],"skins":[{"unified":"1f469-200d-1f9b0","native":"👩â€ðŸ¦°"},{"unified":"1f469-1f3fb-200d-1f9b0","native":"👩ðŸ»â€ðŸ¦°"},{"unified":"1f469-1f3fc-200d-1f9b0","native":"👩ðŸ¼â€ðŸ¦°"},{"unified":"1f469-1f3fd-200d-1f9b0","native":"👩ðŸ½â€ðŸ¦°"},{"unified":"1f469-1f3fe-200d-1f9b0","native":"👩ðŸ¾â€ðŸ¦°"},{"unified":"1f469-1f3ff-200d-1f9b0","native":"👩ðŸ¿â€ðŸ¦°"}],"version":11},"red_haired_person":{"id":"red_haired_person","name":"Person: Red Hair","keywords":["haired","person","hairstyle"],"skins":[{"unified":"1f9d1-200d-1f9b0","native":"🧑â€ðŸ¦°"},{"unified":"1f9d1-1f3fb-200d-1f9b0","native":"🧑ðŸ»â€ðŸ¦°"},{"unified":"1f9d1-1f3fc-200d-1f9b0","native":"🧑ðŸ¼â€ðŸ¦°"},{"unified":"1f9d1-1f3fd-200d-1f9b0","native":"🧑ðŸ½â€ðŸ¦°"},{"unified":"1f9d1-1f3fe-200d-1f9b0","native":"🧑ðŸ¾â€ðŸ¦°"},{"unified":"1f9d1-1f3ff-200d-1f9b0","native":"🧑ðŸ¿â€ðŸ¦°"}],"version":12.1},"curly_haired_woman":{"id":"curly_haired_woman","name":"Woman: Curly Hair","keywords":["haired","woman","hairstyle"],"skins":[{"unified":"1f469-200d-1f9b1","native":"👩â€ðŸ¦±"},{"unified":"1f469-1f3fb-200d-1f9b1","native":"👩ðŸ»â€ðŸ¦±"},{"unified":"1f469-1f3fc-200d-1f9b1","native":"👩ðŸ¼â€ðŸ¦±"},{"unified":"1f469-1f3fd-200d-1f9b1","native":"👩ðŸ½â€ðŸ¦±"},{"unified":"1f469-1f3fe-200d-1f9b1","native":"👩ðŸ¾â€ðŸ¦±"},{"unified":"1f469-1f3ff-200d-1f9b1","native":"👩ðŸ¿â€ðŸ¦±"}],"version":11},"curly_haired_person":{"id":"curly_haired_person","name":"Person: Curly Hair","keywords":["haired","person","hairstyle"],"skins":[{"unified":"1f9d1-200d-1f9b1","native":"🧑â€ðŸ¦±"},{"unified":"1f9d1-1f3fb-200d-1f9b1","native":"🧑ðŸ»â€ðŸ¦±"},{"unified":"1f9d1-1f3fc-200d-1f9b1","native":"🧑ðŸ¼â€ðŸ¦±"},{"unified":"1f9d1-1f3fd-200d-1f9b1","native":"🧑ðŸ½â€ðŸ¦±"},{"unified":"1f9d1-1f3fe-200d-1f9b1","native":"🧑ðŸ¾â€ðŸ¦±"},{"unified":"1f9d1-1f3ff-200d-1f9b1","native":"🧑ðŸ¿â€ðŸ¦±"}],"version":12.1},"white_haired_woman":{"id":"white_haired_woman","name":"Woman: White Hair","keywords":["haired","woman","old","elder"],"skins":[{"unified":"1f469-200d-1f9b3","native":"👩â€ðŸ¦³"},{"unified":"1f469-1f3fb-200d-1f9b3","native":"👩ðŸ»â€ðŸ¦³"},{"unified":"1f469-1f3fc-200d-1f9b3","native":"👩ðŸ¼â€ðŸ¦³"},{"unified":"1f469-1f3fd-200d-1f9b3","native":"👩ðŸ½â€ðŸ¦³"},{"unified":"1f469-1f3fe-200d-1f9b3","native":"👩ðŸ¾â€ðŸ¦³"},{"unified":"1f469-1f3ff-200d-1f9b3","native":"👩ðŸ¿â€ðŸ¦³"}],"version":11},"white_haired_person":{"id":"white_haired_person","name":"Person: White Hair","keywords":["haired","person","elder","old"],"skins":[{"unified":"1f9d1-200d-1f9b3","native":"🧑â€ðŸ¦³"},{"unified":"1f9d1-1f3fb-200d-1f9b3","native":"🧑ðŸ»â€ðŸ¦³"},{"unified":"1f9d1-1f3fc-200d-1f9b3","native":"🧑ðŸ¼â€ðŸ¦³"},{"unified":"1f9d1-1f3fd-200d-1f9b3","native":"🧑ðŸ½â€ðŸ¦³"},{"unified":"1f9d1-1f3fe-200d-1f9b3","native":"🧑ðŸ¾â€ðŸ¦³"},{"unified":"1f9d1-1f3ff-200d-1f9b3","native":"🧑ðŸ¿â€ðŸ¦³"}],"version":12.1},"bald_woman":{"id":"bald_woman","name":"Woman: Bald","keywords":["woman","hairless"],"skins":[{"unified":"1f469-200d-1f9b2","native":"👩â€ðŸ¦²"},{"unified":"1f469-1f3fb-200d-1f9b2","native":"👩ðŸ»â€ðŸ¦²"},{"unified":"1f469-1f3fc-200d-1f9b2","native":"👩ðŸ¼â€ðŸ¦²"},{"unified":"1f469-1f3fd-200d-1f9b2","native":"👩ðŸ½â€ðŸ¦²"},{"unified":"1f469-1f3fe-200d-1f9b2","native":"👩ðŸ¾â€ðŸ¦²"},{"unified":"1f469-1f3ff-200d-1f9b2","native":"👩ðŸ¿â€ðŸ¦²"}],"version":11},"bald_person":{"id":"bald_person","name":"Person: Bald","keywords":["person","hairless"],"skins":[{"unified":"1f9d1-200d-1f9b2","native":"🧑â€ðŸ¦²"},{"unified":"1f9d1-1f3fb-200d-1f9b2","native":"🧑ðŸ»â€ðŸ¦²"},{"unified":"1f9d1-1f3fc-200d-1f9b2","native":"🧑ðŸ¼â€ðŸ¦²"},{"unified":"1f9d1-1f3fd-200d-1f9b2","native":"🧑ðŸ½â€ðŸ¦²"},{"unified":"1f9d1-1f3fe-200d-1f9b2","native":"🧑ðŸ¾â€ðŸ¦²"},{"unified":"1f9d1-1f3ff-200d-1f9b2","native":"🧑ðŸ¿â€ðŸ¦²"}],"version":12.1},"blond-haired-woman":{"id":"blond-haired-woman","name":"Woman: Blond Hair","keywords":["haired-woman","woman","female","girl","blonde","person"],"skins":[{"unified":"1f471-200d-2640-fe0f","native":"👱â€â™€ï¸"},{"unified":"1f471-1f3fb-200d-2640-fe0f","native":"👱ðŸ»â€â™€ï¸"},{"unified":"1f471-1f3fc-200d-2640-fe0f","native":"👱ðŸ¼â€â™€ï¸"},{"unified":"1f471-1f3fd-200d-2640-fe0f","native":"👱ðŸ½â€â™€ï¸"},{"unified":"1f471-1f3fe-200d-2640-fe0f","native":"👱ðŸ¾â€â™€ï¸"},{"unified":"1f471-1f3ff-200d-2640-fe0f","native":"👱ðŸ¿â€â™€ï¸"}],"version":4},"blond-haired-man":{"id":"blond-haired-man","name":"Man: Blond Hair","keywords":["haired-man","man","male","boy","blonde","guy","person"],"skins":[{"unified":"1f471-200d-2642-fe0f","native":"👱â€â™‚ï¸"},{"unified":"1f471-1f3fb-200d-2642-fe0f","native":"👱ðŸ»â€â™‚ï¸"},{"unified":"1f471-1f3fc-200d-2642-fe0f","native":"👱ðŸ¼â€â™‚ï¸"},{"unified":"1f471-1f3fd-200d-2642-fe0f","native":"👱ðŸ½â€â™‚ï¸"},{"unified":"1f471-1f3fe-200d-2642-fe0f","native":"👱ðŸ¾â€â™‚ï¸"},{"unified":"1f471-1f3ff-200d-2642-fe0f","native":"👱ðŸ¿â€â™‚ï¸"}],"version":4},"older_adult":{"id":"older_adult","name":"Older Adult","keywords":["person","human","elder","senior","gender","neutral"],"skins":[{"unified":"1f9d3","native":"🧓"},{"unified":"1f9d3-1f3fb","native":"🧓ðŸ»"},{"unified":"1f9d3-1f3fc","native":"🧓ðŸ¼"},{"unified":"1f9d3-1f3fd","native":"🧓ðŸ½"},{"unified":"1f9d3-1f3fe","native":"🧓ðŸ¾"},{"unified":"1f9d3-1f3ff","native":"🧓ðŸ¿"}],"version":5},"older_man":{"id":"older_man","name":"Old Man","keywords":["older","human","male","men","elder","senior"],"skins":[{"unified":"1f474","native":"👴"},{"unified":"1f474-1f3fb","native":"👴ðŸ»"},{"unified":"1f474-1f3fc","native":"👴ðŸ¼"},{"unified":"1f474-1f3fd","native":"👴ðŸ½"},{"unified":"1f474-1f3fe","native":"👴ðŸ¾"},{"unified":"1f474-1f3ff","native":"👴ðŸ¿"}],"version":1},"older_woman":{"id":"older_woman","name":"Old Woman","keywords":["older","human","female","women","lady","elder","senior"],"skins":[{"unified":"1f475","native":"👵"},{"unified":"1f475-1f3fb","native":"👵ðŸ»"},{"unified":"1f475-1f3fc","native":"👵ðŸ¼"},{"unified":"1f475-1f3fd","native":"👵ðŸ½"},{"unified":"1f475-1f3fe","native":"👵ðŸ¾"},{"unified":"1f475-1f3ff","native":"👵ðŸ¿"}],"version":1},"person_frowning":{"id":"person_frowning","name":"Person Frowning","keywords":["worried"],"skins":[{"unified":"1f64d","native":"ðŸ™"},{"unified":"1f64d-1f3fb","native":"ðŸ™ðŸ»"},{"unified":"1f64d-1f3fc","native":"ðŸ™ðŸ¼"},{"unified":"1f64d-1f3fd","native":"ðŸ™ðŸ½"},{"unified":"1f64d-1f3fe","native":"ðŸ™ðŸ¾"},{"unified":"1f64d-1f3ff","native":"ðŸ™ðŸ¿"}],"version":1},"man-frowning":{"id":"man-frowning","name":"Man Frowning","keywords":["male","boy","sad","depressed","discouraged","unhappy"],"skins":[{"unified":"1f64d-200d-2642-fe0f","native":"ðŸ™â€â™‚ï¸"},{"unified":"1f64d-1f3fb-200d-2642-fe0f","native":"ðŸ™ðŸ»â€â™‚ï¸"},{"unified":"1f64d-1f3fc-200d-2642-fe0f","native":"ðŸ™ðŸ¼â€â™‚ï¸"},{"unified":"1f64d-1f3fd-200d-2642-fe0f","native":"ðŸ™ðŸ½â€â™‚ï¸"},{"unified":"1f64d-1f3fe-200d-2642-fe0f","native":"ðŸ™ðŸ¾â€â™‚ï¸"},{"unified":"1f64d-1f3ff-200d-2642-fe0f","native":"ðŸ™ðŸ¿â€â™‚ï¸"}],"version":4},"woman-frowning":{"id":"woman-frowning","name":"Woman Frowning","keywords":["female","girl","sad","depressed","discouraged","unhappy"],"skins":[{"unified":"1f64d-200d-2640-fe0f","native":"ðŸ™â€â™€ï¸"},{"unified":"1f64d-1f3fb-200d-2640-fe0f","native":"ðŸ™ðŸ»â€â™€ï¸"},{"unified":"1f64d-1f3fc-200d-2640-fe0f","native":"ðŸ™ðŸ¼â€â™€ï¸"},{"unified":"1f64d-1f3fd-200d-2640-fe0f","native":"ðŸ™ðŸ½â€â™€ï¸"},{"unified":"1f64d-1f3fe-200d-2640-fe0f","native":"ðŸ™ðŸ¾â€â™€ï¸"},{"unified":"1f64d-1f3ff-200d-2640-fe0f","native":"ðŸ™ðŸ¿â€â™€ï¸"}],"version":4},"person_with_pouting_face":{"id":"person_with_pouting_face","name":"Person Pouting","keywords":["with","face","upset"],"skins":[{"unified":"1f64e","native":"🙎"},{"unified":"1f64e-1f3fb","native":"🙎ðŸ»"},{"unified":"1f64e-1f3fc","native":"🙎ðŸ¼"},{"unified":"1f64e-1f3fd","native":"🙎ðŸ½"},{"unified":"1f64e-1f3fe","native":"🙎ðŸ¾"},{"unified":"1f64e-1f3ff","native":"🙎ðŸ¿"}],"version":1},"man-pouting":{"id":"man-pouting","name":"Man Pouting","keywords":["male","boy"],"skins":[{"unified":"1f64e-200d-2642-fe0f","native":"🙎â€â™‚ï¸"},{"unified":"1f64e-1f3fb-200d-2642-fe0f","native":"🙎ðŸ»â€â™‚ï¸"},{"unified":"1f64e-1f3fc-200d-2642-fe0f","native":"🙎ðŸ¼â€â™‚ï¸"},{"unified":"1f64e-1f3fd-200d-2642-fe0f","native":"🙎ðŸ½â€â™‚ï¸"},{"unified":"1f64e-1f3fe-200d-2642-fe0f","native":"🙎ðŸ¾â€â™‚ï¸"},{"unified":"1f64e-1f3ff-200d-2642-fe0f","native":"🙎ðŸ¿â€â™‚ï¸"}],"version":4},"woman-pouting":{"id":"woman-pouting","name":"Woman Pouting","keywords":["female","girl"],"skins":[{"unified":"1f64e-200d-2640-fe0f","native":"🙎â€â™€ï¸"},{"unified":"1f64e-1f3fb-200d-2640-fe0f","native":"🙎ðŸ»â€â™€ï¸"},{"unified":"1f64e-1f3fc-200d-2640-fe0f","native":"🙎ðŸ¼â€â™€ï¸"},{"unified":"1f64e-1f3fd-200d-2640-fe0f","native":"🙎ðŸ½â€â™€ï¸"},{"unified":"1f64e-1f3fe-200d-2640-fe0f","native":"🙎ðŸ¾â€â™€ï¸"},{"unified":"1f64e-1f3ff-200d-2640-fe0f","native":"🙎ðŸ¿â€â™€ï¸"}],"version":4},"no_good":{"id":"no_good","name":"Person Gesturing No","keywords":["good","decline"],"skins":[{"unified":"1f645","native":"🙅"},{"unified":"1f645-1f3fb","native":"🙅ðŸ»"},{"unified":"1f645-1f3fc","native":"🙅ðŸ¼"},{"unified":"1f645-1f3fd","native":"🙅ðŸ½"},{"unified":"1f645-1f3fe","native":"🙅ðŸ¾"},{"unified":"1f645-1f3ff","native":"🙅ðŸ¿"}],"version":1},"man-gesturing-no":{"id":"man-gesturing-no","name":"Man Gesturing No","keywords":["gesturing-no","male","boy","nope"],"skins":[{"unified":"1f645-200d-2642-fe0f","native":"🙅â€â™‚ï¸"},{"unified":"1f645-1f3fb-200d-2642-fe0f","native":"🙅ðŸ»â€â™‚ï¸"},{"unified":"1f645-1f3fc-200d-2642-fe0f","native":"🙅ðŸ¼â€â™‚ï¸"},{"unified":"1f645-1f3fd-200d-2642-fe0f","native":"🙅ðŸ½â€â™‚ï¸"},{"unified":"1f645-1f3fe-200d-2642-fe0f","native":"🙅ðŸ¾â€â™‚ï¸"},{"unified":"1f645-1f3ff-200d-2642-fe0f","native":"🙅ðŸ¿â€â™‚ï¸"}],"version":4},"woman-gesturing-no":{"id":"woman-gesturing-no","name":"Woman Gesturing No","keywords":["gesturing-no","female","girl","nope"],"skins":[{"unified":"1f645-200d-2640-fe0f","native":"🙅â€â™€ï¸"},{"unified":"1f645-1f3fb-200d-2640-fe0f","native":"🙅ðŸ»â€â™€ï¸"},{"unified":"1f645-1f3fc-200d-2640-fe0f","native":"🙅ðŸ¼â€â™€ï¸"},{"unified":"1f645-1f3fd-200d-2640-fe0f","native":"🙅ðŸ½â€â™€ï¸"},{"unified":"1f645-1f3fe-200d-2640-fe0f","native":"🙅ðŸ¾â€â™€ï¸"},{"unified":"1f645-1f3ff-200d-2640-fe0f","native":"🙅ðŸ¿â€â™€ï¸"}],"version":4},"ok_woman":{"id":"ok_woman","name":"Person Gesturing Ok","keywords":["woman","agree"],"skins":[{"unified":"1f646","native":"🙆"},{"unified":"1f646-1f3fb","native":"🙆ðŸ»"},{"unified":"1f646-1f3fc","native":"🙆ðŸ¼"},{"unified":"1f646-1f3fd","native":"🙆ðŸ½"},{"unified":"1f646-1f3fe","native":"🙆ðŸ¾"},{"unified":"1f646-1f3ff","native":"🙆ðŸ¿"}],"version":1},"man-gesturing-ok":{"id":"man-gesturing-ok","name":"Man Gesturing Ok","keywords":["gesturing-ok","men","boy","male","blue","human"],"skins":[{"unified":"1f646-200d-2642-fe0f","native":"🙆â€â™‚ï¸"},{"unified":"1f646-1f3fb-200d-2642-fe0f","native":"🙆ðŸ»â€â™‚ï¸"},{"unified":"1f646-1f3fc-200d-2642-fe0f","native":"🙆ðŸ¼â€â™‚ï¸"},{"unified":"1f646-1f3fd-200d-2642-fe0f","native":"🙆ðŸ½â€â™‚ï¸"},{"unified":"1f646-1f3fe-200d-2642-fe0f","native":"🙆ðŸ¾â€â™‚ï¸"},{"unified":"1f646-1f3ff-200d-2642-fe0f","native":"🙆ðŸ¿â€â™‚ï¸"}],"version":4},"woman-gesturing-ok":{"id":"woman-gesturing-ok","name":"Woman Gesturing Ok","keywords":["gesturing-ok","women","girl","female","pink","human"],"skins":[{"unified":"1f646-200d-2640-fe0f","native":"🙆â€â™€ï¸"},{"unified":"1f646-1f3fb-200d-2640-fe0f","native":"🙆ðŸ»â€â™€ï¸"},{"unified":"1f646-1f3fc-200d-2640-fe0f","native":"🙆ðŸ¼â€â™€ï¸"},{"unified":"1f646-1f3fd-200d-2640-fe0f","native":"🙆ðŸ½â€â™€ï¸"},{"unified":"1f646-1f3fe-200d-2640-fe0f","native":"🙆ðŸ¾â€â™€ï¸"},{"unified":"1f646-1f3ff-200d-2640-fe0f","native":"🙆ðŸ¿â€â™€ï¸"}],"version":4},"information_desk_person":{"id":"information_desk_person","name":"Person Tipping Hand","keywords":["information","desk"],"skins":[{"unified":"1f481","native":"ðŸ’"},{"unified":"1f481-1f3fb","native":"ðŸ’ðŸ»"},{"unified":"1f481-1f3fc","native":"ðŸ’ðŸ¼"},{"unified":"1f481-1f3fd","native":"ðŸ’ðŸ½"},{"unified":"1f481-1f3fe","native":"ðŸ’ðŸ¾"},{"unified":"1f481-1f3ff","native":"ðŸ’ðŸ¿"}],"version":1},"man-tipping-hand":{"id":"man-tipping-hand","name":"Man Tipping Hand","keywords":["tipping-hand","male","boy","human","information"],"skins":[{"unified":"1f481-200d-2642-fe0f","native":"ðŸ’â€â™‚ï¸"},{"unified":"1f481-1f3fb-200d-2642-fe0f","native":"ðŸ’ðŸ»â€â™‚ï¸"},{"unified":"1f481-1f3fc-200d-2642-fe0f","native":"ðŸ’ðŸ¼â€â™‚ï¸"},{"unified":"1f481-1f3fd-200d-2642-fe0f","native":"ðŸ’ðŸ½â€â™‚ï¸"},{"unified":"1f481-1f3fe-200d-2642-fe0f","native":"ðŸ’ðŸ¾â€â™‚ï¸"},{"unified":"1f481-1f3ff-200d-2642-fe0f","native":"ðŸ’ðŸ¿â€â™‚ï¸"}],"version":4},"woman-tipping-hand":{"id":"woman-tipping-hand","name":"Woman Tipping Hand","keywords":["tipping-hand","female","girl","human","information"],"skins":[{"unified":"1f481-200d-2640-fe0f","native":"ðŸ’â€â™€ï¸"},{"unified":"1f481-1f3fb-200d-2640-fe0f","native":"ðŸ’ðŸ»â€â™€ï¸"},{"unified":"1f481-1f3fc-200d-2640-fe0f","native":"ðŸ’ðŸ¼â€â™€ï¸"},{"unified":"1f481-1f3fd-200d-2640-fe0f","native":"ðŸ’ðŸ½â€â™€ï¸"},{"unified":"1f481-1f3fe-200d-2640-fe0f","native":"ðŸ’ðŸ¾â€â™€ï¸"},{"unified":"1f481-1f3ff-200d-2640-fe0f","native":"ðŸ’ðŸ¿â€â™€ï¸"}],"version":4},"raising_hand":{"id":"raising_hand","name":"Person Raising Hand","keywords":["question"],"skins":[{"unified":"1f64b","native":"🙋"},{"unified":"1f64b-1f3fb","native":"🙋ðŸ»"},{"unified":"1f64b-1f3fc","native":"🙋ðŸ¼"},{"unified":"1f64b-1f3fd","native":"🙋ðŸ½"},{"unified":"1f64b-1f3fe","native":"🙋ðŸ¾"},{"unified":"1f64b-1f3ff","native":"🙋ðŸ¿"}],"version":1},"man-raising-hand":{"id":"man-raising-hand","name":"Man Raising Hand","keywords":["raising-hand","male","boy"],"skins":[{"unified":"1f64b-200d-2642-fe0f","native":"🙋â€â™‚ï¸"},{"unified":"1f64b-1f3fb-200d-2642-fe0f","native":"🙋ðŸ»â€â™‚ï¸"},{"unified":"1f64b-1f3fc-200d-2642-fe0f","native":"🙋ðŸ¼â€â™‚ï¸"},{"unified":"1f64b-1f3fd-200d-2642-fe0f","native":"🙋ðŸ½â€â™‚ï¸"},{"unified":"1f64b-1f3fe-200d-2642-fe0f","native":"🙋ðŸ¾â€â™‚ï¸"},{"unified":"1f64b-1f3ff-200d-2642-fe0f","native":"🙋ðŸ¿â€â™‚ï¸"}],"version":4},"woman-raising-hand":{"id":"woman-raising-hand","name":"Woman Raising Hand","keywords":["raising-hand","female","girl"],"skins":[{"unified":"1f64b-200d-2640-fe0f","native":"🙋â€â™€ï¸"},{"unified":"1f64b-1f3fb-200d-2640-fe0f","native":"🙋ðŸ»â€â™€ï¸"},{"unified":"1f64b-1f3fc-200d-2640-fe0f","native":"🙋ðŸ¼â€â™€ï¸"},{"unified":"1f64b-1f3fd-200d-2640-fe0f","native":"🙋ðŸ½â€â™€ï¸"},{"unified":"1f64b-1f3fe-200d-2640-fe0f","native":"🙋ðŸ¾â€â™€ï¸"},{"unified":"1f64b-1f3ff-200d-2640-fe0f","native":"🙋ðŸ¿â€â™€ï¸"}],"version":4},"deaf_person":{"id":"deaf_person","name":"Deaf Person","keywords":["accessibility"],"skins":[{"unified":"1f9cf","native":"ðŸ§"},{"unified":"1f9cf-1f3fb","native":"ðŸ§ðŸ»"},{"unified":"1f9cf-1f3fc","native":"ðŸ§ðŸ¼"},{"unified":"1f9cf-1f3fd","native":"ðŸ§ðŸ½"},{"unified":"1f9cf-1f3fe","native":"ðŸ§ðŸ¾"},{"unified":"1f9cf-1f3ff","native":"ðŸ§ðŸ¿"}],"version":12},"deaf_man":{"id":"deaf_man","name":"Deaf Man","keywords":["accessibility"],"skins":[{"unified":"1f9cf-200d-2642-fe0f","native":"ðŸ§â€â™‚ï¸"},{"unified":"1f9cf-1f3fb-200d-2642-fe0f","native":"ðŸ§ðŸ»â€â™‚ï¸"},{"unified":"1f9cf-1f3fc-200d-2642-fe0f","native":"ðŸ§ðŸ¼â€â™‚ï¸"},{"unified":"1f9cf-1f3fd-200d-2642-fe0f","native":"ðŸ§ðŸ½â€â™‚ï¸"},{"unified":"1f9cf-1f3fe-200d-2642-fe0f","native":"ðŸ§ðŸ¾â€â™‚ï¸"},{"unified":"1f9cf-1f3ff-200d-2642-fe0f","native":"ðŸ§ðŸ¿â€â™‚ï¸"}],"version":12},"deaf_woman":{"id":"deaf_woman","name":"Deaf Woman","keywords":["accessibility"],"skins":[{"unified":"1f9cf-200d-2640-fe0f","native":"ðŸ§â€â™€ï¸"},{"unified":"1f9cf-1f3fb-200d-2640-fe0f","native":"ðŸ§ðŸ»â€â™€ï¸"},{"unified":"1f9cf-1f3fc-200d-2640-fe0f","native":"ðŸ§ðŸ¼â€â™€ï¸"},{"unified":"1f9cf-1f3fd-200d-2640-fe0f","native":"ðŸ§ðŸ½â€â™€ï¸"},{"unified":"1f9cf-1f3fe-200d-2640-fe0f","native":"ðŸ§ðŸ¾â€â™€ï¸"},{"unified":"1f9cf-1f3ff-200d-2640-fe0f","native":"ðŸ§ðŸ¿â€â™€ï¸"}],"version":12},"bow":{"id":"bow","name":"Person Bowing","keywords":["bow","respectiful"],"skins":[{"unified":"1f647","native":"🙇"},{"unified":"1f647-1f3fb","native":"🙇ðŸ»"},{"unified":"1f647-1f3fc","native":"🙇ðŸ¼"},{"unified":"1f647-1f3fd","native":"🙇ðŸ½"},{"unified":"1f647-1f3fe","native":"🙇ðŸ¾"},{"unified":"1f647-1f3ff","native":"🙇ðŸ¿"}],"version":1},"man-bowing":{"id":"man-bowing","name":"Man Bowing","keywords":["male","boy"],"skins":[{"unified":"1f647-200d-2642-fe0f","native":"🙇â€â™‚ï¸"},{"unified":"1f647-1f3fb-200d-2642-fe0f","native":"🙇ðŸ»â€â™‚ï¸"},{"unified":"1f647-1f3fc-200d-2642-fe0f","native":"🙇ðŸ¼â€â™‚ï¸"},{"unified":"1f647-1f3fd-200d-2642-fe0f","native":"🙇ðŸ½â€â™‚ï¸"},{"unified":"1f647-1f3fe-200d-2642-fe0f","native":"🙇ðŸ¾â€â™‚ï¸"},{"unified":"1f647-1f3ff-200d-2642-fe0f","native":"🙇ðŸ¿â€â™‚ï¸"}],"version":4},"woman-bowing":{"id":"woman-bowing","name":"Woman Bowing","keywords":["female","girl"],"skins":[{"unified":"1f647-200d-2640-fe0f","native":"🙇â€â™€ï¸"},{"unified":"1f647-1f3fb-200d-2640-fe0f","native":"🙇ðŸ»â€â™€ï¸"},{"unified":"1f647-1f3fc-200d-2640-fe0f","native":"🙇ðŸ¼â€â™€ï¸"},{"unified":"1f647-1f3fd-200d-2640-fe0f","native":"🙇ðŸ½â€â™€ï¸"},{"unified":"1f647-1f3fe-200d-2640-fe0f","native":"🙇ðŸ¾â€â™€ï¸"},{"unified":"1f647-1f3ff-200d-2640-fe0f","native":"🙇ðŸ¿â€â™€ï¸"}],"version":4},"face_palm":{"id":"face_palm","name":"Face Palm","keywords":["person","facepalming","disappointed"],"skins":[{"unified":"1f926","native":"🤦"},{"unified":"1f926-1f3fb","native":"🤦ðŸ»"},{"unified":"1f926-1f3fc","native":"🤦ðŸ¼"},{"unified":"1f926-1f3fd","native":"🤦ðŸ½"},{"unified":"1f926-1f3fe","native":"🤦ðŸ¾"},{"unified":"1f926-1f3ff","native":"🤦ðŸ¿"}],"version":3},"man-facepalming":{"id":"man-facepalming","name":"Man Facepalming","keywords":["male","boy","disbelief"],"skins":[{"unified":"1f926-200d-2642-fe0f","native":"🤦â€â™‚ï¸"},{"unified":"1f926-1f3fb-200d-2642-fe0f","native":"🤦ðŸ»â€â™‚ï¸"},{"unified":"1f926-1f3fc-200d-2642-fe0f","native":"🤦ðŸ¼â€â™‚ï¸"},{"unified":"1f926-1f3fd-200d-2642-fe0f","native":"🤦ðŸ½â€â™‚ï¸"},{"unified":"1f926-1f3fe-200d-2642-fe0f","native":"🤦ðŸ¾â€â™‚ï¸"},{"unified":"1f926-1f3ff-200d-2642-fe0f","native":"🤦ðŸ¿â€â™‚ï¸"}],"version":4},"woman-facepalming":{"id":"woman-facepalming","name":"Woman Facepalming","keywords":["female","girl","disbelief"],"skins":[{"unified":"1f926-200d-2640-fe0f","native":"🤦â€â™€ï¸"},{"unified":"1f926-1f3fb-200d-2640-fe0f","native":"🤦ðŸ»â€â™€ï¸"},{"unified":"1f926-1f3fc-200d-2640-fe0f","native":"🤦ðŸ¼â€â™€ï¸"},{"unified":"1f926-1f3fd-200d-2640-fe0f","native":"🤦ðŸ½â€â™€ï¸"},{"unified":"1f926-1f3fe-200d-2640-fe0f","native":"🤦ðŸ¾â€â™€ï¸"},{"unified":"1f926-1f3ff-200d-2640-fe0f","native":"🤦ðŸ¿â€â™€ï¸"}],"version":4},"shrug":{"id":"shrug","name":"Shrug","keywords":["person","shrugging","regardless"],"skins":[{"unified":"1f937","native":"🤷"},{"unified":"1f937-1f3fb","native":"🤷ðŸ»"},{"unified":"1f937-1f3fc","native":"🤷ðŸ¼"},{"unified":"1f937-1f3fd","native":"🤷ðŸ½"},{"unified":"1f937-1f3fe","native":"🤷ðŸ¾"},{"unified":"1f937-1f3ff","native":"🤷ðŸ¿"}],"version":3},"man-shrugging":{"id":"man-shrugging","name":"Man Shrugging","keywords":["male","boy","confused","indifferent","doubt"],"skins":[{"unified":"1f937-200d-2642-fe0f","native":"🤷â€â™‚ï¸"},{"unified":"1f937-1f3fb-200d-2642-fe0f","native":"🤷ðŸ»â€â™‚ï¸"},{"unified":"1f937-1f3fc-200d-2642-fe0f","native":"🤷ðŸ¼â€â™‚ï¸"},{"unified":"1f937-1f3fd-200d-2642-fe0f","native":"🤷ðŸ½â€â™‚ï¸"},{"unified":"1f937-1f3fe-200d-2642-fe0f","native":"🤷ðŸ¾â€â™‚ï¸"},{"unified":"1f937-1f3ff-200d-2642-fe0f","native":"🤷ðŸ¿â€â™‚ï¸"}],"version":4},"woman-shrugging":{"id":"woman-shrugging","name":"Woman Shrugging","keywords":["female","girl","confused","indifferent","doubt"],"skins":[{"unified":"1f937-200d-2640-fe0f","native":"🤷â€â™€ï¸"},{"unified":"1f937-1f3fb-200d-2640-fe0f","native":"🤷ðŸ»â€â™€ï¸"},{"unified":"1f937-1f3fc-200d-2640-fe0f","native":"🤷ðŸ¼â€â™€ï¸"},{"unified":"1f937-1f3fd-200d-2640-fe0f","native":"🤷ðŸ½â€â™€ï¸"},{"unified":"1f937-1f3fe-200d-2640-fe0f","native":"🤷ðŸ¾â€â™€ï¸"},{"unified":"1f937-1f3ff-200d-2640-fe0f","native":"🤷ðŸ¿â€â™€ï¸"}],"version":4},"health_worker":{"id":"health_worker","name":"Health Worker","keywords":["hospital"],"skins":[{"unified":"1f9d1-200d-2695-fe0f","native":"🧑â€âš•ï¸"},{"unified":"1f9d1-1f3fb-200d-2695-fe0f","native":"🧑ðŸ»â€âš•ï¸"},{"unified":"1f9d1-1f3fc-200d-2695-fe0f","native":"🧑ðŸ¼â€âš•ï¸"},{"unified":"1f9d1-1f3fd-200d-2695-fe0f","native":"🧑ðŸ½â€âš•ï¸"},{"unified":"1f9d1-1f3fe-200d-2695-fe0f","native":"🧑ðŸ¾â€âš•ï¸"},{"unified":"1f9d1-1f3ff-200d-2695-fe0f","native":"🧑ðŸ¿â€âš•ï¸"}],"version":12.1},"male-doctor":{"id":"male-doctor","name":"Man Health Worker","keywords":["male","doctor","nurse","therapist","healthcare","human"],"skins":[{"unified":"1f468-200d-2695-fe0f","native":"👨â€âš•ï¸"},{"unified":"1f468-1f3fb-200d-2695-fe0f","native":"👨ðŸ»â€âš•ï¸"},{"unified":"1f468-1f3fc-200d-2695-fe0f","native":"👨ðŸ¼â€âš•ï¸"},{"unified":"1f468-1f3fd-200d-2695-fe0f","native":"👨ðŸ½â€âš•ï¸"},{"unified":"1f468-1f3fe-200d-2695-fe0f","native":"👨ðŸ¾â€âš•ï¸"},{"unified":"1f468-1f3ff-200d-2695-fe0f","native":"👨ðŸ¿â€âš•ï¸"}],"version":4},"female-doctor":{"id":"female-doctor","name":"Woman Health Worker","keywords":["female","doctor","nurse","therapist","healthcare","human"],"skins":[{"unified":"1f469-200d-2695-fe0f","native":"👩â€âš•ï¸"},{"unified":"1f469-1f3fb-200d-2695-fe0f","native":"👩ðŸ»â€âš•ï¸"},{"unified":"1f469-1f3fc-200d-2695-fe0f","native":"👩ðŸ¼â€âš•ï¸"},{"unified":"1f469-1f3fd-200d-2695-fe0f","native":"👩ðŸ½â€âš•ï¸"},{"unified":"1f469-1f3fe-200d-2695-fe0f","native":"👩ðŸ¾â€âš•ï¸"},{"unified":"1f469-1f3ff-200d-2695-fe0f","native":"👩ðŸ¿â€âš•ï¸"}],"version":4},"student":{"id":"student","name":"Student","keywords":["learn"],"skins":[{"unified":"1f9d1-200d-1f393","native":"🧑â€ðŸŽ“"},{"unified":"1f9d1-1f3fb-200d-1f393","native":"🧑ðŸ»â€ðŸŽ“"},{"unified":"1f9d1-1f3fc-200d-1f393","native":"🧑ðŸ¼â€ðŸŽ“"},{"unified":"1f9d1-1f3fd-200d-1f393","native":"🧑ðŸ½â€ðŸŽ“"},{"unified":"1f9d1-1f3fe-200d-1f393","native":"🧑ðŸ¾â€ðŸŽ“"},{"unified":"1f9d1-1f3ff-200d-1f393","native":"🧑ðŸ¿â€ðŸŽ“"}],"version":12.1},"male-student":{"id":"male-student","name":"Man Student","keywords":["male","graduate","human"],"skins":[{"unified":"1f468-200d-1f393","native":"👨â€ðŸŽ“"},{"unified":"1f468-1f3fb-200d-1f393","native":"👨ðŸ»â€ðŸŽ“"},{"unified":"1f468-1f3fc-200d-1f393","native":"👨ðŸ¼â€ðŸŽ“"},{"unified":"1f468-1f3fd-200d-1f393","native":"👨ðŸ½â€ðŸŽ“"},{"unified":"1f468-1f3fe-200d-1f393","native":"👨ðŸ¾â€ðŸŽ“"},{"unified":"1f468-1f3ff-200d-1f393","native":"👨ðŸ¿â€ðŸŽ“"}],"version":4},"female-student":{"id":"female-student","name":"Woman Student","keywords":["female","graduate","human"],"skins":[{"unified":"1f469-200d-1f393","native":"👩â€ðŸŽ“"},{"unified":"1f469-1f3fb-200d-1f393","native":"👩ðŸ»â€ðŸŽ“"},{"unified":"1f469-1f3fc-200d-1f393","native":"👩ðŸ¼â€ðŸŽ“"},{"unified":"1f469-1f3fd-200d-1f393","native":"👩ðŸ½â€ðŸŽ“"},{"unified":"1f469-1f3fe-200d-1f393","native":"👩ðŸ¾â€ðŸŽ“"},{"unified":"1f469-1f3ff-200d-1f393","native":"👩ðŸ¿â€ðŸŽ“"}],"version":4},"teacher":{"id":"teacher","name":"Teacher","keywords":["professor"],"skins":[{"unified":"1f9d1-200d-1f3eb","native":"🧑â€ðŸ«"},{"unified":"1f9d1-1f3fb-200d-1f3eb","native":"🧑ðŸ»â€ðŸ«"},{"unified":"1f9d1-1f3fc-200d-1f3eb","native":"🧑ðŸ¼â€ðŸ«"},{"unified":"1f9d1-1f3fd-200d-1f3eb","native":"🧑ðŸ½â€ðŸ«"},{"unified":"1f9d1-1f3fe-200d-1f3eb","native":"🧑ðŸ¾â€ðŸ«"},{"unified":"1f9d1-1f3ff-200d-1f3eb","native":"🧑ðŸ¿â€ðŸ«"}],"version":12.1},"male-teacher":{"id":"male-teacher","name":"Man Teacher","keywords":["male","instructor","professor","human"],"skins":[{"unified":"1f468-200d-1f3eb","native":"👨â€ðŸ«"},{"unified":"1f468-1f3fb-200d-1f3eb","native":"👨ðŸ»â€ðŸ«"},{"unified":"1f468-1f3fc-200d-1f3eb","native":"👨ðŸ¼â€ðŸ«"},{"unified":"1f468-1f3fd-200d-1f3eb","native":"👨ðŸ½â€ðŸ«"},{"unified":"1f468-1f3fe-200d-1f3eb","native":"👨ðŸ¾â€ðŸ«"},{"unified":"1f468-1f3ff-200d-1f3eb","native":"👨ðŸ¿â€ðŸ«"}],"version":4},"female-teacher":{"id":"female-teacher","name":"Woman Teacher","keywords":["female","instructor","professor","human"],"skins":[{"unified":"1f469-200d-1f3eb","native":"👩â€ðŸ«"},{"unified":"1f469-1f3fb-200d-1f3eb","native":"👩ðŸ»â€ðŸ«"},{"unified":"1f469-1f3fc-200d-1f3eb","native":"👩ðŸ¼â€ðŸ«"},{"unified":"1f469-1f3fd-200d-1f3eb","native":"👩ðŸ½â€ðŸ«"},{"unified":"1f469-1f3fe-200d-1f3eb","native":"👩ðŸ¾â€ðŸ«"},{"unified":"1f469-1f3ff-200d-1f3eb","native":"👩ðŸ¿â€ðŸ«"}],"version":4},"judge":{"id":"judge","name":"Judge","keywords":["law"],"skins":[{"unified":"1f9d1-200d-2696-fe0f","native":"🧑â€âš–ï¸"},{"unified":"1f9d1-1f3fb-200d-2696-fe0f","native":"🧑ðŸ»â€âš–ï¸"},{"unified":"1f9d1-1f3fc-200d-2696-fe0f","native":"🧑ðŸ¼â€âš–ï¸"},{"unified":"1f9d1-1f3fd-200d-2696-fe0f","native":"🧑ðŸ½â€âš–ï¸"},{"unified":"1f9d1-1f3fe-200d-2696-fe0f","native":"🧑ðŸ¾â€âš–ï¸"},{"unified":"1f9d1-1f3ff-200d-2696-fe0f","native":"🧑ðŸ¿â€âš–ï¸"}],"version":12.1},"male-judge":{"id":"male-judge","name":"Man Judge","keywords":["male","justice","court","human"],"skins":[{"unified":"1f468-200d-2696-fe0f","native":"👨â€âš–ï¸"},{"unified":"1f468-1f3fb-200d-2696-fe0f","native":"👨ðŸ»â€âš–ï¸"},{"unified":"1f468-1f3fc-200d-2696-fe0f","native":"👨ðŸ¼â€âš–ï¸"},{"unified":"1f468-1f3fd-200d-2696-fe0f","native":"👨ðŸ½â€âš–ï¸"},{"unified":"1f468-1f3fe-200d-2696-fe0f","native":"👨ðŸ¾â€âš–ï¸"},{"unified":"1f468-1f3ff-200d-2696-fe0f","native":"👨ðŸ¿â€âš–ï¸"}],"version":4},"female-judge":{"id":"female-judge","name":"Woman Judge","keywords":["female","justice","court","human"],"skins":[{"unified":"1f469-200d-2696-fe0f","native":"👩â€âš–ï¸"},{"unified":"1f469-1f3fb-200d-2696-fe0f","native":"👩ðŸ»â€âš–ï¸"},{"unified":"1f469-1f3fc-200d-2696-fe0f","native":"👩ðŸ¼â€âš–ï¸"},{"unified":"1f469-1f3fd-200d-2696-fe0f","native":"👩ðŸ½â€âš–ï¸"},{"unified":"1f469-1f3fe-200d-2696-fe0f","native":"👩ðŸ¾â€âš–ï¸"},{"unified":"1f469-1f3ff-200d-2696-fe0f","native":"👩ðŸ¿â€âš–ï¸"}],"version":4},"farmer":{"id":"farmer","name":"Farmer","keywords":["crops"],"skins":[{"unified":"1f9d1-200d-1f33e","native":"🧑â€ðŸŒ¾"},{"unified":"1f9d1-1f3fb-200d-1f33e","native":"🧑ðŸ»â€ðŸŒ¾"},{"unified":"1f9d1-1f3fc-200d-1f33e","native":"🧑ðŸ¼â€ðŸŒ¾"},{"unified":"1f9d1-1f3fd-200d-1f33e","native":"🧑ðŸ½â€ðŸŒ¾"},{"unified":"1f9d1-1f3fe-200d-1f33e","native":"🧑ðŸ¾â€ðŸŒ¾"},{"unified":"1f9d1-1f3ff-200d-1f33e","native":"🧑ðŸ¿â€ðŸŒ¾"}],"version":12.1},"male-farmer":{"id":"male-farmer","name":"Man Farmer","keywords":["male","rancher","gardener","human"],"skins":[{"unified":"1f468-200d-1f33e","native":"👨â€ðŸŒ¾"},{"unified":"1f468-1f3fb-200d-1f33e","native":"👨ðŸ»â€ðŸŒ¾"},{"unified":"1f468-1f3fc-200d-1f33e","native":"👨ðŸ¼â€ðŸŒ¾"},{"unified":"1f468-1f3fd-200d-1f33e","native":"👨ðŸ½â€ðŸŒ¾"},{"unified":"1f468-1f3fe-200d-1f33e","native":"👨ðŸ¾â€ðŸŒ¾"},{"unified":"1f468-1f3ff-200d-1f33e","native":"👨ðŸ¿â€ðŸŒ¾"}],"version":4},"female-farmer":{"id":"female-farmer","name":"Woman Farmer","keywords":["female","rancher","gardener","human"],"skins":[{"unified":"1f469-200d-1f33e","native":"👩â€ðŸŒ¾"},{"unified":"1f469-1f3fb-200d-1f33e","native":"👩ðŸ»â€ðŸŒ¾"},{"unified":"1f469-1f3fc-200d-1f33e","native":"👩ðŸ¼â€ðŸŒ¾"},{"unified":"1f469-1f3fd-200d-1f33e","native":"👩ðŸ½â€ðŸŒ¾"},{"unified":"1f469-1f3fe-200d-1f33e","native":"👩ðŸ¾â€ðŸŒ¾"},{"unified":"1f469-1f3ff-200d-1f33e","native":"👩ðŸ¿â€ðŸŒ¾"}],"version":4},"cook":{"id":"cook","name":"Cook","keywords":["food","kitchen","culinary"],"skins":[{"unified":"1f9d1-200d-1f373","native":"🧑â€ðŸ³"},{"unified":"1f9d1-1f3fb-200d-1f373","native":"🧑ðŸ»â€ðŸ³"},{"unified":"1f9d1-1f3fc-200d-1f373","native":"🧑ðŸ¼â€ðŸ³"},{"unified":"1f9d1-1f3fd-200d-1f373","native":"🧑ðŸ½â€ðŸ³"},{"unified":"1f9d1-1f3fe-200d-1f373","native":"🧑ðŸ¾â€ðŸ³"},{"unified":"1f9d1-1f3ff-200d-1f373","native":"🧑ðŸ¿â€ðŸ³"}],"version":12.1},"male-cook":{"id":"male-cook","name":"Man Cook","keywords":["male","chef","human"],"skins":[{"unified":"1f468-200d-1f373","native":"👨â€ðŸ³"},{"unified":"1f468-1f3fb-200d-1f373","native":"👨ðŸ»â€ðŸ³"},{"unified":"1f468-1f3fc-200d-1f373","native":"👨ðŸ¼â€ðŸ³"},{"unified":"1f468-1f3fd-200d-1f373","native":"👨ðŸ½â€ðŸ³"},{"unified":"1f468-1f3fe-200d-1f373","native":"👨ðŸ¾â€ðŸ³"},{"unified":"1f468-1f3ff-200d-1f373","native":"👨ðŸ¿â€ðŸ³"}],"version":4},"female-cook":{"id":"female-cook","name":"Woman Cook","keywords":["female","chef","human"],"skins":[{"unified":"1f469-200d-1f373","native":"👩â€ðŸ³"},{"unified":"1f469-1f3fb-200d-1f373","native":"👩ðŸ»â€ðŸ³"},{"unified":"1f469-1f3fc-200d-1f373","native":"👩ðŸ¼â€ðŸ³"},{"unified":"1f469-1f3fd-200d-1f373","native":"👩ðŸ½â€ðŸ³"},{"unified":"1f469-1f3fe-200d-1f373","native":"👩ðŸ¾â€ðŸ³"},{"unified":"1f469-1f3ff-200d-1f373","native":"👩ðŸ¿â€ðŸ³"}],"version":4},"mechanic":{"id":"mechanic","name":"Mechanic","keywords":["worker","technician"],"skins":[{"unified":"1f9d1-200d-1f527","native":"🧑â€ðŸ”§"},{"unified":"1f9d1-1f3fb-200d-1f527","native":"🧑ðŸ»â€ðŸ”§"},{"unified":"1f9d1-1f3fc-200d-1f527","native":"🧑ðŸ¼â€ðŸ”§"},{"unified":"1f9d1-1f3fd-200d-1f527","native":"🧑ðŸ½â€ðŸ”§"},{"unified":"1f9d1-1f3fe-200d-1f527","native":"🧑ðŸ¾â€ðŸ”§"},{"unified":"1f9d1-1f3ff-200d-1f527","native":"🧑ðŸ¿â€ðŸ”§"}],"version":12.1},"male-mechanic":{"id":"male-mechanic","name":"Man Mechanic","keywords":["male","plumber","human","wrench"],"skins":[{"unified":"1f468-200d-1f527","native":"👨â€ðŸ”§"},{"unified":"1f468-1f3fb-200d-1f527","native":"👨ðŸ»â€ðŸ”§"},{"unified":"1f468-1f3fc-200d-1f527","native":"👨ðŸ¼â€ðŸ”§"},{"unified":"1f468-1f3fd-200d-1f527","native":"👨ðŸ½â€ðŸ”§"},{"unified":"1f468-1f3fe-200d-1f527","native":"👨ðŸ¾â€ðŸ”§"},{"unified":"1f468-1f3ff-200d-1f527","native":"👨ðŸ¿â€ðŸ”§"}],"version":4},"female-mechanic":{"id":"female-mechanic","name":"Woman Mechanic","keywords":["female","plumber","human","wrench"],"skins":[{"unified":"1f469-200d-1f527","native":"👩â€ðŸ”§"},{"unified":"1f469-1f3fb-200d-1f527","native":"👩ðŸ»â€ðŸ”§"},{"unified":"1f469-1f3fc-200d-1f527","native":"👩ðŸ¼â€ðŸ”§"},{"unified":"1f469-1f3fd-200d-1f527","native":"👩ðŸ½â€ðŸ”§"},{"unified":"1f469-1f3fe-200d-1f527","native":"👩ðŸ¾â€ðŸ”§"},{"unified":"1f469-1f3ff-200d-1f527","native":"👩ðŸ¿â€ðŸ”§"}],"version":4},"factory_worker":{"id":"factory_worker","name":"Factory Worker","keywords":["labor"],"skins":[{"unified":"1f9d1-200d-1f3ed","native":"🧑â€ðŸ"},{"unified":"1f9d1-1f3fb-200d-1f3ed","native":"🧑ðŸ»â€ðŸ"},{"unified":"1f9d1-1f3fc-200d-1f3ed","native":"🧑ðŸ¼â€ðŸ"},{"unified":"1f9d1-1f3fd-200d-1f3ed","native":"🧑ðŸ½â€ðŸ"},{"unified":"1f9d1-1f3fe-200d-1f3ed","native":"🧑ðŸ¾â€ðŸ"},{"unified":"1f9d1-1f3ff-200d-1f3ed","native":"🧑ðŸ¿â€ðŸ"}],"version":12.1},"male-factory-worker":{"id":"male-factory-worker","name":"Man Factory Worker","keywords":["male","factory-worker","assembly","industrial","human"],"skins":[{"unified":"1f468-200d-1f3ed","native":"👨â€ðŸ"},{"unified":"1f468-1f3fb-200d-1f3ed","native":"👨ðŸ»â€ðŸ"},{"unified":"1f468-1f3fc-200d-1f3ed","native":"👨ðŸ¼â€ðŸ"},{"unified":"1f468-1f3fd-200d-1f3ed","native":"👨ðŸ½â€ðŸ"},{"unified":"1f468-1f3fe-200d-1f3ed","native":"👨ðŸ¾â€ðŸ"},{"unified":"1f468-1f3ff-200d-1f3ed","native":"👨ðŸ¿â€ðŸ"}],"version":4},"female-factory-worker":{"id":"female-factory-worker","name":"Woman Factory Worker","keywords":["female","factory-worker","assembly","industrial","human"],"skins":[{"unified":"1f469-200d-1f3ed","native":"👩â€ðŸ"},{"unified":"1f469-1f3fb-200d-1f3ed","native":"👩ðŸ»â€ðŸ"},{"unified":"1f469-1f3fc-200d-1f3ed","native":"👩ðŸ¼â€ðŸ"},{"unified":"1f469-1f3fd-200d-1f3ed","native":"👩ðŸ½â€ðŸ"},{"unified":"1f469-1f3fe-200d-1f3ed","native":"👩ðŸ¾â€ðŸ"},{"unified":"1f469-1f3ff-200d-1f3ed","native":"👩ðŸ¿â€ðŸ"}],"version":4},"office_worker":{"id":"office_worker","name":"Office Worker","keywords":["business"],"skins":[{"unified":"1f9d1-200d-1f4bc","native":"🧑â€ðŸ’¼"},{"unified":"1f9d1-1f3fb-200d-1f4bc","native":"🧑ðŸ»â€ðŸ’¼"},{"unified":"1f9d1-1f3fc-200d-1f4bc","native":"🧑ðŸ¼â€ðŸ’¼"},{"unified":"1f9d1-1f3fd-200d-1f4bc","native":"🧑ðŸ½â€ðŸ’¼"},{"unified":"1f9d1-1f3fe-200d-1f4bc","native":"🧑ðŸ¾â€ðŸ’¼"},{"unified":"1f9d1-1f3ff-200d-1f4bc","native":"🧑ðŸ¿â€ðŸ’¼"}],"version":12.1},"male-office-worker":{"id":"male-office-worker","name":"Man Office Worker","keywords":["male","office-worker","business","manager","human"],"skins":[{"unified":"1f468-200d-1f4bc","native":"👨â€ðŸ’¼"},{"unified":"1f468-1f3fb-200d-1f4bc","native":"👨ðŸ»â€ðŸ’¼"},{"unified":"1f468-1f3fc-200d-1f4bc","native":"👨ðŸ¼â€ðŸ’¼"},{"unified":"1f468-1f3fd-200d-1f4bc","native":"👨ðŸ½â€ðŸ’¼"},{"unified":"1f468-1f3fe-200d-1f4bc","native":"👨ðŸ¾â€ðŸ’¼"},{"unified":"1f468-1f3ff-200d-1f4bc","native":"👨ðŸ¿â€ðŸ’¼"}],"version":4},"female-office-worker":{"id":"female-office-worker","name":"Woman Office Worker","keywords":["female","office-worker","business","manager","human"],"skins":[{"unified":"1f469-200d-1f4bc","native":"👩â€ðŸ’¼"},{"unified":"1f469-1f3fb-200d-1f4bc","native":"👩ðŸ»â€ðŸ’¼"},{"unified":"1f469-1f3fc-200d-1f4bc","native":"👩ðŸ¼â€ðŸ’¼"},{"unified":"1f469-1f3fd-200d-1f4bc","native":"👩ðŸ½â€ðŸ’¼"},{"unified":"1f469-1f3fe-200d-1f4bc","native":"👩ðŸ¾â€ðŸ’¼"},{"unified":"1f469-1f3ff-200d-1f4bc","native":"👩ðŸ¿â€ðŸ’¼"}],"version":4},"scientist":{"id":"scientist","name":"Scientist","keywords":["chemistry"],"skins":[{"unified":"1f9d1-200d-1f52c","native":"🧑â€ðŸ”¬"},{"unified":"1f9d1-1f3fb-200d-1f52c","native":"🧑ðŸ»â€ðŸ”¬"},{"unified":"1f9d1-1f3fc-200d-1f52c","native":"🧑ðŸ¼â€ðŸ”¬"},{"unified":"1f9d1-1f3fd-200d-1f52c","native":"🧑ðŸ½â€ðŸ”¬"},{"unified":"1f9d1-1f3fe-200d-1f52c","native":"🧑ðŸ¾â€ðŸ”¬"},{"unified":"1f9d1-1f3ff-200d-1f52c","native":"🧑ðŸ¿â€ðŸ”¬"}],"version":12.1},"male-scientist":{"id":"male-scientist","name":"Man Scientist","keywords":["male","biologist","chemist","engineer","physicist","human"],"skins":[{"unified":"1f468-200d-1f52c","native":"👨â€ðŸ”¬"},{"unified":"1f468-1f3fb-200d-1f52c","native":"👨ðŸ»â€ðŸ”¬"},{"unified":"1f468-1f3fc-200d-1f52c","native":"👨ðŸ¼â€ðŸ”¬"},{"unified":"1f468-1f3fd-200d-1f52c","native":"👨ðŸ½â€ðŸ”¬"},{"unified":"1f468-1f3fe-200d-1f52c","native":"👨ðŸ¾â€ðŸ”¬"},{"unified":"1f468-1f3ff-200d-1f52c","native":"👨ðŸ¿â€ðŸ”¬"}],"version":4},"female-scientist":{"id":"female-scientist","name":"Woman Scientist","keywords":["female","biologist","chemist","engineer","physicist","human"],"skins":[{"unified":"1f469-200d-1f52c","native":"👩â€ðŸ”¬"},{"unified":"1f469-1f3fb-200d-1f52c","native":"👩ðŸ»â€ðŸ”¬"},{"unified":"1f469-1f3fc-200d-1f52c","native":"👩ðŸ¼â€ðŸ”¬"},{"unified":"1f469-1f3fd-200d-1f52c","native":"👩ðŸ½â€ðŸ”¬"},{"unified":"1f469-1f3fe-200d-1f52c","native":"👩ðŸ¾â€ðŸ”¬"},{"unified":"1f469-1f3ff-200d-1f52c","native":"👩ðŸ¿â€ðŸ”¬"}],"version":4},"technologist":{"id":"technologist","name":"Technologist","keywords":["computer"],"skins":[{"unified":"1f9d1-200d-1f4bb","native":"🧑â€ðŸ’»"},{"unified":"1f9d1-1f3fb-200d-1f4bb","native":"🧑ðŸ»â€ðŸ’»"},{"unified":"1f9d1-1f3fc-200d-1f4bb","native":"🧑ðŸ¼â€ðŸ’»"},{"unified":"1f9d1-1f3fd-200d-1f4bb","native":"🧑ðŸ½â€ðŸ’»"},{"unified":"1f9d1-1f3fe-200d-1f4bb","native":"🧑ðŸ¾â€ðŸ’»"},{"unified":"1f9d1-1f3ff-200d-1f4bb","native":"🧑ðŸ¿â€ðŸ’»"}],"version":12.1},"male-technologist":{"id":"male-technologist","name":"Man Technologist","keywords":["male","coder","developer","engineer","programmer","software","human","laptop","computer"],"skins":[{"unified":"1f468-200d-1f4bb","native":"👨â€ðŸ’»"},{"unified":"1f468-1f3fb-200d-1f4bb","native":"👨ðŸ»â€ðŸ’»"},{"unified":"1f468-1f3fc-200d-1f4bb","native":"👨ðŸ¼â€ðŸ’»"},{"unified":"1f468-1f3fd-200d-1f4bb","native":"👨ðŸ½â€ðŸ’»"},{"unified":"1f468-1f3fe-200d-1f4bb","native":"👨ðŸ¾â€ðŸ’»"},{"unified":"1f468-1f3ff-200d-1f4bb","native":"👨ðŸ¿â€ðŸ’»"}],"version":4},"female-technologist":{"id":"female-technologist","name":"Woman Technologist","keywords":["female","coder","developer","engineer","programmer","software","human","laptop","computer"],"skins":[{"unified":"1f469-200d-1f4bb","native":"👩â€ðŸ’»"},{"unified":"1f469-1f3fb-200d-1f4bb","native":"👩ðŸ»â€ðŸ’»"},{"unified":"1f469-1f3fc-200d-1f4bb","native":"👩ðŸ¼â€ðŸ’»"},{"unified":"1f469-1f3fd-200d-1f4bb","native":"👩ðŸ½â€ðŸ’»"},{"unified":"1f469-1f3fe-200d-1f4bb","native":"👩ðŸ¾â€ðŸ’»"},{"unified":"1f469-1f3ff-200d-1f4bb","native":"👩ðŸ¿â€ðŸ’»"}],"version":4},"singer":{"id":"singer","name":"Singer","keywords":["song","artist","performer"],"skins":[{"unified":"1f9d1-200d-1f3a4","native":"🧑â€ðŸŽ¤"},{"unified":"1f9d1-1f3fb-200d-1f3a4","native":"🧑ðŸ»â€ðŸŽ¤"},{"unified":"1f9d1-1f3fc-200d-1f3a4","native":"🧑ðŸ¼â€ðŸŽ¤"},{"unified":"1f9d1-1f3fd-200d-1f3a4","native":"🧑ðŸ½â€ðŸŽ¤"},{"unified":"1f9d1-1f3fe-200d-1f3a4","native":"🧑ðŸ¾â€ðŸŽ¤"},{"unified":"1f9d1-1f3ff-200d-1f3a4","native":"🧑ðŸ¿â€ðŸŽ¤"}],"version":12.1},"male-singer":{"id":"male-singer","name":"Man Singer","keywords":["male","rockstar","entertainer","human"],"skins":[{"unified":"1f468-200d-1f3a4","native":"👨â€ðŸŽ¤"},{"unified":"1f468-1f3fb-200d-1f3a4","native":"👨ðŸ»â€ðŸŽ¤"},{"unified":"1f468-1f3fc-200d-1f3a4","native":"👨ðŸ¼â€ðŸŽ¤"},{"unified":"1f468-1f3fd-200d-1f3a4","native":"👨ðŸ½â€ðŸŽ¤"},{"unified":"1f468-1f3fe-200d-1f3a4","native":"👨ðŸ¾â€ðŸŽ¤"},{"unified":"1f468-1f3ff-200d-1f3a4","native":"👨ðŸ¿â€ðŸŽ¤"}],"version":4},"female-singer":{"id":"female-singer","name":"Woman Singer","keywords":["female","rockstar","entertainer","human"],"skins":[{"unified":"1f469-200d-1f3a4","native":"👩â€ðŸŽ¤"},{"unified":"1f469-1f3fb-200d-1f3a4","native":"👩ðŸ»â€ðŸŽ¤"},{"unified":"1f469-1f3fc-200d-1f3a4","native":"👩ðŸ¼â€ðŸŽ¤"},{"unified":"1f469-1f3fd-200d-1f3a4","native":"👩ðŸ½â€ðŸŽ¤"},{"unified":"1f469-1f3fe-200d-1f3a4","native":"👩ðŸ¾â€ðŸŽ¤"},{"unified":"1f469-1f3ff-200d-1f3a4","native":"👩ðŸ¿â€ðŸŽ¤"}],"version":4},"artist":{"id":"artist","name":"Artist","keywords":["painting","draw","creativity"],"skins":[{"unified":"1f9d1-200d-1f3a8","native":"🧑â€ðŸŽ¨"},{"unified":"1f9d1-1f3fb-200d-1f3a8","native":"🧑ðŸ»â€ðŸŽ¨"},{"unified":"1f9d1-1f3fc-200d-1f3a8","native":"🧑ðŸ¼â€ðŸŽ¨"},{"unified":"1f9d1-1f3fd-200d-1f3a8","native":"🧑ðŸ½â€ðŸŽ¨"},{"unified":"1f9d1-1f3fe-200d-1f3a8","native":"🧑ðŸ¾â€ðŸŽ¨"},{"unified":"1f9d1-1f3ff-200d-1f3a8","native":"🧑ðŸ¿â€ðŸŽ¨"}],"version":12.1},"male-artist":{"id":"male-artist","name":"Man Artist","keywords":["male","painter","human"],"skins":[{"unified":"1f468-200d-1f3a8","native":"👨â€ðŸŽ¨"},{"unified":"1f468-1f3fb-200d-1f3a8","native":"👨ðŸ»â€ðŸŽ¨"},{"unified":"1f468-1f3fc-200d-1f3a8","native":"👨ðŸ¼â€ðŸŽ¨"},{"unified":"1f468-1f3fd-200d-1f3a8","native":"👨ðŸ½â€ðŸŽ¨"},{"unified":"1f468-1f3fe-200d-1f3a8","native":"👨ðŸ¾â€ðŸŽ¨"},{"unified":"1f468-1f3ff-200d-1f3a8","native":"👨ðŸ¿â€ðŸŽ¨"}],"version":4},"female-artist":{"id":"female-artist","name":"Woman Artist","keywords":["female","painter","human"],"skins":[{"unified":"1f469-200d-1f3a8","native":"👩â€ðŸŽ¨"},{"unified":"1f469-1f3fb-200d-1f3a8","native":"👩ðŸ»â€ðŸŽ¨"},{"unified":"1f469-1f3fc-200d-1f3a8","native":"👩ðŸ¼â€ðŸŽ¨"},{"unified":"1f469-1f3fd-200d-1f3a8","native":"👩ðŸ½â€ðŸŽ¨"},{"unified":"1f469-1f3fe-200d-1f3a8","native":"👩ðŸ¾â€ðŸŽ¨"},{"unified":"1f469-1f3ff-200d-1f3a8","native":"👩ðŸ¿â€ðŸŽ¨"}],"version":4},"pilot":{"id":"pilot","name":"Pilot","keywords":["fly","plane","airplane"],"skins":[{"unified":"1f9d1-200d-2708-fe0f","native":"🧑â€âœˆï¸"},{"unified":"1f9d1-1f3fb-200d-2708-fe0f","native":"🧑ðŸ»â€âœˆï¸"},{"unified":"1f9d1-1f3fc-200d-2708-fe0f","native":"🧑ðŸ¼â€âœˆï¸"},{"unified":"1f9d1-1f3fd-200d-2708-fe0f","native":"🧑ðŸ½â€âœˆï¸"},{"unified":"1f9d1-1f3fe-200d-2708-fe0f","native":"🧑ðŸ¾â€âœˆï¸"},{"unified":"1f9d1-1f3ff-200d-2708-fe0f","native":"🧑ðŸ¿â€âœˆï¸"}],"version":12.1},"male-pilot":{"id":"male-pilot","name":"Man Pilot","keywords":["male","aviator","plane","human"],"skins":[{"unified":"1f468-200d-2708-fe0f","native":"👨â€âœˆï¸"},{"unified":"1f468-1f3fb-200d-2708-fe0f","native":"👨ðŸ»â€âœˆï¸"},{"unified":"1f468-1f3fc-200d-2708-fe0f","native":"👨ðŸ¼â€âœˆï¸"},{"unified":"1f468-1f3fd-200d-2708-fe0f","native":"👨ðŸ½â€âœˆï¸"},{"unified":"1f468-1f3fe-200d-2708-fe0f","native":"👨ðŸ¾â€âœˆï¸"},{"unified":"1f468-1f3ff-200d-2708-fe0f","native":"👨ðŸ¿â€âœˆï¸"}],"version":4},"female-pilot":{"id":"female-pilot","name":"Woman Pilot","keywords":["female","aviator","plane","human"],"skins":[{"unified":"1f469-200d-2708-fe0f","native":"👩â€âœˆï¸"},{"unified":"1f469-1f3fb-200d-2708-fe0f","native":"👩ðŸ»â€âœˆï¸"},{"unified":"1f469-1f3fc-200d-2708-fe0f","native":"👩ðŸ¼â€âœˆï¸"},{"unified":"1f469-1f3fd-200d-2708-fe0f","native":"👩ðŸ½â€âœˆï¸"},{"unified":"1f469-1f3fe-200d-2708-fe0f","native":"👩ðŸ¾â€âœˆï¸"},{"unified":"1f469-1f3ff-200d-2708-fe0f","native":"👩ðŸ¿â€âœˆï¸"}],"version":4},"astronaut":{"id":"astronaut","name":"Astronaut","keywords":["outerspace"],"skins":[{"unified":"1f9d1-200d-1f680","native":"🧑â€ðŸš€"},{"unified":"1f9d1-1f3fb-200d-1f680","native":"🧑ðŸ»â€ðŸš€"},{"unified":"1f9d1-1f3fc-200d-1f680","native":"🧑ðŸ¼â€ðŸš€"},{"unified":"1f9d1-1f3fd-200d-1f680","native":"🧑ðŸ½â€ðŸš€"},{"unified":"1f9d1-1f3fe-200d-1f680","native":"🧑ðŸ¾â€ðŸš€"},{"unified":"1f9d1-1f3ff-200d-1f680","native":"🧑ðŸ¿â€ðŸš€"}],"version":12.1},"male-astronaut":{"id":"male-astronaut","name":"Man Astronaut","keywords":["male","space","rocket","human"],"skins":[{"unified":"1f468-200d-1f680","native":"👨â€ðŸš€"},{"unified":"1f468-1f3fb-200d-1f680","native":"👨ðŸ»â€ðŸš€"},{"unified":"1f468-1f3fc-200d-1f680","native":"👨ðŸ¼â€ðŸš€"},{"unified":"1f468-1f3fd-200d-1f680","native":"👨ðŸ½â€ðŸš€"},{"unified":"1f468-1f3fe-200d-1f680","native":"👨ðŸ¾â€ðŸš€"},{"unified":"1f468-1f3ff-200d-1f680","native":"👨ðŸ¿â€ðŸš€"}],"version":4},"female-astronaut":{"id":"female-astronaut","name":"Woman Astronaut","keywords":["female","space","rocket","human"],"skins":[{"unified":"1f469-200d-1f680","native":"👩â€ðŸš€"},{"unified":"1f469-1f3fb-200d-1f680","native":"👩ðŸ»â€ðŸš€"},{"unified":"1f469-1f3fc-200d-1f680","native":"👩ðŸ¼â€ðŸš€"},{"unified":"1f469-1f3fd-200d-1f680","native":"👩ðŸ½â€ðŸš€"},{"unified":"1f469-1f3fe-200d-1f680","native":"👩ðŸ¾â€ðŸš€"},{"unified":"1f469-1f3ff-200d-1f680","native":"👩ðŸ¿â€ðŸš€"}],"version":4},"firefighter":{"id":"firefighter","name":"Firefighter","keywords":["fire"],"skins":[{"unified":"1f9d1-200d-1f692","native":"🧑â€ðŸš’"},{"unified":"1f9d1-1f3fb-200d-1f692","native":"🧑ðŸ»â€ðŸš’"},{"unified":"1f9d1-1f3fc-200d-1f692","native":"🧑ðŸ¼â€ðŸš’"},{"unified":"1f9d1-1f3fd-200d-1f692","native":"🧑ðŸ½â€ðŸš’"},{"unified":"1f9d1-1f3fe-200d-1f692","native":"🧑ðŸ¾â€ðŸš’"},{"unified":"1f9d1-1f3ff-200d-1f692","native":"🧑ðŸ¿â€ðŸš’"}],"version":12.1},"male-firefighter":{"id":"male-firefighter","name":"Man Firefighter","keywords":["male","fireman","human"],"skins":[{"unified":"1f468-200d-1f692","native":"👨â€ðŸš’"},{"unified":"1f468-1f3fb-200d-1f692","native":"👨ðŸ»â€ðŸš’"},{"unified":"1f468-1f3fc-200d-1f692","native":"👨ðŸ¼â€ðŸš’"},{"unified":"1f468-1f3fd-200d-1f692","native":"👨ðŸ½â€ðŸš’"},{"unified":"1f468-1f3fe-200d-1f692","native":"👨ðŸ¾â€ðŸš’"},{"unified":"1f468-1f3ff-200d-1f692","native":"👨ðŸ¿â€ðŸš’"}],"version":4},"female-firefighter":{"id":"female-firefighter","name":"Woman Firefighter","keywords":["female","fireman","human"],"skins":[{"unified":"1f469-200d-1f692","native":"👩â€ðŸš’"},{"unified":"1f469-1f3fb-200d-1f692","native":"👩ðŸ»â€ðŸš’"},{"unified":"1f469-1f3fc-200d-1f692","native":"👩ðŸ¼â€ðŸš’"},{"unified":"1f469-1f3fd-200d-1f692","native":"👩ðŸ½â€ðŸš’"},{"unified":"1f469-1f3fe-200d-1f692","native":"👩ðŸ¾â€ðŸš’"},{"unified":"1f469-1f3ff-200d-1f692","native":"👩ðŸ¿â€ðŸš’"}],"version":4},"cop":{"id":"cop","name":"Police Officer","keywords":["cop"],"skins":[{"unified":"1f46e","native":"👮"},{"unified":"1f46e-1f3fb","native":"👮ðŸ»"},{"unified":"1f46e-1f3fc","native":"👮ðŸ¼"},{"unified":"1f46e-1f3fd","native":"👮ðŸ½"},{"unified":"1f46e-1f3fe","native":"👮ðŸ¾"},{"unified":"1f46e-1f3ff","native":"👮ðŸ¿"}],"version":1},"male-police-officer":{"id":"male-police-officer","name":"Man Police Officer","keywords":["male","police-officer","law","legal","enforcement","arrest","911"],"skins":[{"unified":"1f46e-200d-2642-fe0f","native":"👮â€â™‚ï¸"},{"unified":"1f46e-1f3fb-200d-2642-fe0f","native":"👮ðŸ»â€â™‚ï¸"},{"unified":"1f46e-1f3fc-200d-2642-fe0f","native":"👮ðŸ¼â€â™‚ï¸"},{"unified":"1f46e-1f3fd-200d-2642-fe0f","native":"👮ðŸ½â€â™‚ï¸"},{"unified":"1f46e-1f3fe-200d-2642-fe0f","native":"👮ðŸ¾â€â™‚ï¸"},{"unified":"1f46e-1f3ff-200d-2642-fe0f","native":"👮ðŸ¿â€â™‚ï¸"}],"version":4},"female-police-officer":{"id":"female-police-officer","name":"Woman Police Officer","keywords":["female","police-officer","law","legal","enforcement","arrest","911"],"skins":[{"unified":"1f46e-200d-2640-fe0f","native":"👮â€â™€ï¸"},{"unified":"1f46e-1f3fb-200d-2640-fe0f","native":"👮ðŸ»â€â™€ï¸"},{"unified":"1f46e-1f3fc-200d-2640-fe0f","native":"👮ðŸ¼â€â™€ï¸"},{"unified":"1f46e-1f3fd-200d-2640-fe0f","native":"👮ðŸ½â€â™€ï¸"},{"unified":"1f46e-1f3fe-200d-2640-fe0f","native":"👮ðŸ¾â€â™€ï¸"},{"unified":"1f46e-1f3ff-200d-2640-fe0f","native":"👮ðŸ¿â€â™€ï¸"}],"version":4},"sleuth_or_spy":{"id":"sleuth_or_spy","name":"Detective","keywords":["sleuth","or","spy","human"],"skins":[{"unified":"1f575-fe0f","native":"🕵ï¸"},{"unified":"1f575-1f3fb","native":"🕵ðŸ»"},{"unified":"1f575-1f3fc","native":"🕵ðŸ¼"},{"unified":"1f575-1f3fd","native":"🕵ðŸ½"},{"unified":"1f575-1f3fe","native":"🕵ðŸ¾"},{"unified":"1f575-1f3ff","native":"🕵ðŸ¿"}],"version":1},"male-detective":{"id":"male-detective","name":"Man Detective","keywords":["male","crime"],"skins":[{"unified":"1f575-fe0f-200d-2642-fe0f","native":"🕵ï¸â€â™‚ï¸"},{"unified":"1f575-1f3fb-200d-2642-fe0f","native":"🕵ðŸ»â€â™‚ï¸"},{"unified":"1f575-1f3fc-200d-2642-fe0f","native":"🕵ðŸ¼â€â™‚ï¸"},{"unified":"1f575-1f3fd-200d-2642-fe0f","native":"🕵ðŸ½â€â™‚ï¸"},{"unified":"1f575-1f3fe-200d-2642-fe0f","native":"🕵ðŸ¾â€â™‚ï¸"},{"unified":"1f575-1f3ff-200d-2642-fe0f","native":"🕵ðŸ¿â€â™‚ï¸"}],"version":4},"female-detective":{"id":"female-detective","name":"Woman Detective","keywords":["female","human","spy"],"skins":[{"unified":"1f575-fe0f-200d-2640-fe0f","native":"🕵ï¸â€â™€ï¸"},{"unified":"1f575-1f3fb-200d-2640-fe0f","native":"🕵ðŸ»â€â™€ï¸"},{"unified":"1f575-1f3fc-200d-2640-fe0f","native":"🕵ðŸ¼â€â™€ï¸"},{"unified":"1f575-1f3fd-200d-2640-fe0f","native":"🕵ðŸ½â€â™€ï¸"},{"unified":"1f575-1f3fe-200d-2640-fe0f","native":"🕵ðŸ¾â€â™€ï¸"},{"unified":"1f575-1f3ff-200d-2640-fe0f","native":"🕵ðŸ¿â€â™€ï¸"}],"version":4},"guardsman":{"id":"guardsman","name":"Guard","keywords":["guardsman","protect"],"skins":[{"unified":"1f482","native":"💂"},{"unified":"1f482-1f3fb","native":"💂ðŸ»"},{"unified":"1f482-1f3fc","native":"💂ðŸ¼"},{"unified":"1f482-1f3fd","native":"💂ðŸ½"},{"unified":"1f482-1f3fe","native":"💂ðŸ¾"},{"unified":"1f482-1f3ff","native":"💂ðŸ¿"}],"version":1},"male-guard":{"id":"male-guard","name":"Man Guard","keywords":["male","uk","gb","british","guy","royal"],"skins":[{"unified":"1f482-200d-2642-fe0f","native":"💂â€â™‚ï¸"},{"unified":"1f482-1f3fb-200d-2642-fe0f","native":"💂ðŸ»â€â™‚ï¸"},{"unified":"1f482-1f3fc-200d-2642-fe0f","native":"💂ðŸ¼â€â™‚ï¸"},{"unified":"1f482-1f3fd-200d-2642-fe0f","native":"💂ðŸ½â€â™‚ï¸"},{"unified":"1f482-1f3fe-200d-2642-fe0f","native":"💂ðŸ¾â€â™‚ï¸"},{"unified":"1f482-1f3ff-200d-2642-fe0f","native":"💂ðŸ¿â€â™‚ï¸"}],"version":4},"female-guard":{"id":"female-guard","name":"Woman Guard","keywords":["female","uk","gb","british","royal"],"skins":[{"unified":"1f482-200d-2640-fe0f","native":"💂â€â™€ï¸"},{"unified":"1f482-1f3fb-200d-2640-fe0f","native":"💂ðŸ»â€â™€ï¸"},{"unified":"1f482-1f3fc-200d-2640-fe0f","native":"💂ðŸ¼â€â™€ï¸"},{"unified":"1f482-1f3fd-200d-2640-fe0f","native":"💂ðŸ½â€â™€ï¸"},{"unified":"1f482-1f3fe-200d-2640-fe0f","native":"💂ðŸ¾â€â™€ï¸"},{"unified":"1f482-1f3ff-200d-2640-fe0f","native":"💂ðŸ¿â€â™€ï¸"}],"version":4},"ninja":{"id":"ninja","name":"Ninja","keywords":["ninjutsu","skills","japanese"],"skins":[{"unified":"1f977","native":"🥷"},{"unified":"1f977-1f3fb","native":"🥷ðŸ»"},{"unified":"1f977-1f3fc","native":"🥷ðŸ¼"},{"unified":"1f977-1f3fd","native":"🥷ðŸ½"},{"unified":"1f977-1f3fe","native":"🥷ðŸ¾"},{"unified":"1f977-1f3ff","native":"🥷ðŸ¿"}],"version":13},"construction_worker":{"id":"construction_worker","name":"Construction Worker","keywords":["labor","build"],"skins":[{"unified":"1f477","native":"👷"},{"unified":"1f477-1f3fb","native":"👷ðŸ»"},{"unified":"1f477-1f3fc","native":"👷ðŸ¼"},{"unified":"1f477-1f3fd","native":"👷ðŸ½"},{"unified":"1f477-1f3fe","native":"👷ðŸ¾"},{"unified":"1f477-1f3ff","native":"👷ðŸ¿"}],"version":1},"male-construction-worker":{"id":"male-construction-worker","name":"Man Construction Worker","keywords":["male","construction-worker","human","wip","guy","build","labor"],"skins":[{"unified":"1f477-200d-2642-fe0f","native":"👷â€â™‚ï¸"},{"unified":"1f477-1f3fb-200d-2642-fe0f","native":"👷ðŸ»â€â™‚ï¸"},{"unified":"1f477-1f3fc-200d-2642-fe0f","native":"👷ðŸ¼â€â™‚ï¸"},{"unified":"1f477-1f3fd-200d-2642-fe0f","native":"👷ðŸ½â€â™‚ï¸"},{"unified":"1f477-1f3fe-200d-2642-fe0f","native":"👷ðŸ¾â€â™‚ï¸"},{"unified":"1f477-1f3ff-200d-2642-fe0f","native":"👷ðŸ¿â€â™‚ï¸"}],"version":4},"female-construction-worker":{"id":"female-construction-worker","name":"Woman Construction Worker","keywords":["female","construction-worker","human","wip","build","labor"],"skins":[{"unified":"1f477-200d-2640-fe0f","native":"👷â€â™€ï¸"},{"unified":"1f477-1f3fb-200d-2640-fe0f","native":"👷ðŸ»â€â™€ï¸"},{"unified":"1f477-1f3fc-200d-2640-fe0f","native":"👷ðŸ¼â€â™€ï¸"},{"unified":"1f477-1f3fd-200d-2640-fe0f","native":"👷ðŸ½â€â™€ï¸"},{"unified":"1f477-1f3fe-200d-2640-fe0f","native":"👷ðŸ¾â€â™€ï¸"},{"unified":"1f477-1f3ff-200d-2640-fe0f","native":"👷ðŸ¿â€â™€ï¸"}],"version":4},"person_with_crown":{"id":"person_with_crown","name":"Person with Crown","keywords":["royalty","power"],"skins":[{"unified":"1fac5","native":"🫅"},{"unified":"1fac5-1f3fb","native":"🫅ðŸ»"},{"unified":"1fac5-1f3fc","native":"🫅ðŸ¼"},{"unified":"1fac5-1f3fd","native":"🫅ðŸ½"},{"unified":"1fac5-1f3fe","native":"🫅ðŸ¾"},{"unified":"1fac5-1f3ff","native":"🫅ðŸ¿"}],"version":14},"prince":{"id":"prince","name":"Prince","keywords":["boy","man","male","crown","royal","king"],"skins":[{"unified":"1f934","native":"🤴"},{"unified":"1f934-1f3fb","native":"🤴ðŸ»"},{"unified":"1f934-1f3fc","native":"🤴ðŸ¼"},{"unified":"1f934-1f3fd","native":"🤴ðŸ½"},{"unified":"1f934-1f3fe","native":"🤴ðŸ¾"},{"unified":"1f934-1f3ff","native":"🤴ðŸ¿"}],"version":3},"princess":{"id":"princess","name":"Princess","keywords":["girl","woman","female","blond","crown","royal","queen"],"skins":[{"unified":"1f478","native":"👸"},{"unified":"1f478-1f3fb","native":"👸ðŸ»"},{"unified":"1f478-1f3fc","native":"👸ðŸ¼"},{"unified":"1f478-1f3fd","native":"👸ðŸ½"},{"unified":"1f478-1f3fe","native":"👸ðŸ¾"},{"unified":"1f478-1f3ff","native":"👸ðŸ¿"}],"version":1},"man_with_turban":{"id":"man_with_turban","name":"Man with Turban","keywords":["person","wearing","headdress"],"skins":[{"unified":"1f473","native":"👳"},{"unified":"1f473-1f3fb","native":"👳ðŸ»"},{"unified":"1f473-1f3fc","native":"👳ðŸ¼"},{"unified":"1f473-1f3fd","native":"👳ðŸ½"},{"unified":"1f473-1f3fe","native":"👳ðŸ¾"},{"unified":"1f473-1f3ff","native":"👳ðŸ¿"}],"version":1},"man-wearing-turban":{"id":"man-wearing-turban","name":"Man Wearing Turban","keywords":["wearing-turban","male","indian","hinduism","arabs"],"skins":[{"unified":"1f473-200d-2642-fe0f","native":"👳â€â™‚ï¸"},{"unified":"1f473-1f3fb-200d-2642-fe0f","native":"👳ðŸ»â€â™‚ï¸"},{"unified":"1f473-1f3fc-200d-2642-fe0f","native":"👳ðŸ¼â€â™‚ï¸"},{"unified":"1f473-1f3fd-200d-2642-fe0f","native":"👳ðŸ½â€â™‚ï¸"},{"unified":"1f473-1f3fe-200d-2642-fe0f","native":"👳ðŸ¾â€â™‚ï¸"},{"unified":"1f473-1f3ff-200d-2642-fe0f","native":"👳ðŸ¿â€â™‚ï¸"}],"version":4},"woman-wearing-turban":{"id":"woman-wearing-turban","name":"Woman Wearing Turban","keywords":["wearing-turban","female","indian","hinduism","arabs"],"skins":[{"unified":"1f473-200d-2640-fe0f","native":"👳â€â™€ï¸"},{"unified":"1f473-1f3fb-200d-2640-fe0f","native":"👳ðŸ»â€â™€ï¸"},{"unified":"1f473-1f3fc-200d-2640-fe0f","native":"👳ðŸ¼â€â™€ï¸"},{"unified":"1f473-1f3fd-200d-2640-fe0f","native":"👳ðŸ½â€â™€ï¸"},{"unified":"1f473-1f3fe-200d-2640-fe0f","native":"👳ðŸ¾â€â™€ï¸"},{"unified":"1f473-1f3ff-200d-2640-fe0f","native":"👳ðŸ¿â€â™€ï¸"}],"version":4},"man_with_gua_pi_mao":{"id":"man_with_gua_pi_mao","name":"Man with Gua Pi Mao","keywords":["skullcap","male","boy","chinese"],"skins":[{"unified":"1f472","native":"👲"},{"unified":"1f472-1f3fb","native":"👲ðŸ»"},{"unified":"1f472-1f3fc","native":"👲ðŸ¼"},{"unified":"1f472-1f3fd","native":"👲ðŸ½"},{"unified":"1f472-1f3fe","native":"👲ðŸ¾"},{"unified":"1f472-1f3ff","native":"👲ðŸ¿"}],"version":1},"person_with_headscarf":{"id":"person_with_headscarf","name":"Woman with Headscarf","keywords":["person","female","hijab","mantilla","tichel"],"skins":[{"unified":"1f9d5","native":"🧕"},{"unified":"1f9d5-1f3fb","native":"🧕ðŸ»"},{"unified":"1f9d5-1f3fc","native":"🧕ðŸ¼"},{"unified":"1f9d5-1f3fd","native":"🧕ðŸ½"},{"unified":"1f9d5-1f3fe","native":"🧕ðŸ¾"},{"unified":"1f9d5-1f3ff","native":"🧕ðŸ¿"}],"version":5},"person_in_tuxedo":{"id":"person_in_tuxedo","name":"Man in Tuxedo","keywords":["person","couple","marriage","wedding","groom"],"skins":[{"unified":"1f935","native":"🤵"},{"unified":"1f935-1f3fb","native":"🤵ðŸ»"},{"unified":"1f935-1f3fc","native":"🤵ðŸ¼"},{"unified":"1f935-1f3fd","native":"🤵ðŸ½"},{"unified":"1f935-1f3fe","native":"🤵ðŸ¾"},{"unified":"1f935-1f3ff","native":"🤵ðŸ¿"}],"version":3},"man_in_tuxedo":{"id":"man_in_tuxedo","name":"Man in Tuxedo","keywords":["formal","fashion"],"skins":[{"unified":"1f935-200d-2642-fe0f","native":"🤵â€â™‚ï¸"},{"unified":"1f935-1f3fb-200d-2642-fe0f","native":"🤵ðŸ»â€â™‚ï¸"},{"unified":"1f935-1f3fc-200d-2642-fe0f","native":"🤵ðŸ¼â€â™‚ï¸"},{"unified":"1f935-1f3fd-200d-2642-fe0f","native":"🤵ðŸ½â€â™‚ï¸"},{"unified":"1f935-1f3fe-200d-2642-fe0f","native":"🤵ðŸ¾â€â™‚ï¸"},{"unified":"1f935-1f3ff-200d-2642-fe0f","native":"🤵ðŸ¿â€â™‚ï¸"}],"version":13},"woman_in_tuxedo":{"id":"woman_in_tuxedo","name":"Woman in Tuxedo","keywords":["formal","fashion"],"skins":[{"unified":"1f935-200d-2640-fe0f","native":"🤵â€â™€ï¸"},{"unified":"1f935-1f3fb-200d-2640-fe0f","native":"🤵ðŸ»â€â™€ï¸"},{"unified":"1f935-1f3fc-200d-2640-fe0f","native":"🤵ðŸ¼â€â™€ï¸"},{"unified":"1f935-1f3fd-200d-2640-fe0f","native":"🤵ðŸ½â€â™€ï¸"},{"unified":"1f935-1f3fe-200d-2640-fe0f","native":"🤵ðŸ¾â€â™€ï¸"},{"unified":"1f935-1f3ff-200d-2640-fe0f","native":"🤵ðŸ¿â€â™€ï¸"}],"version":13},"bride_with_veil":{"id":"bride_with_veil","name":"Bride with Veil","keywords":["couple","marriage","wedding","woman"],"skins":[{"unified":"1f470","native":"👰"},{"unified":"1f470-1f3fb","native":"👰ðŸ»"},{"unified":"1f470-1f3fc","native":"👰ðŸ¼"},{"unified":"1f470-1f3fd","native":"👰ðŸ½"},{"unified":"1f470-1f3fe","native":"👰ðŸ¾"},{"unified":"1f470-1f3ff","native":"👰ðŸ¿"}],"version":1},"man_with_veil":{"id":"man_with_veil","name":"Man with Veil","keywords":["wedding","marriage"],"skins":[{"unified":"1f470-200d-2642-fe0f","native":"👰â€â™‚ï¸"},{"unified":"1f470-1f3fb-200d-2642-fe0f","native":"👰ðŸ»â€â™‚ï¸"},{"unified":"1f470-1f3fc-200d-2642-fe0f","native":"👰ðŸ¼â€â™‚ï¸"},{"unified":"1f470-1f3fd-200d-2642-fe0f","native":"👰ðŸ½â€â™‚ï¸"},{"unified":"1f470-1f3fe-200d-2642-fe0f","native":"👰ðŸ¾â€â™‚ï¸"},{"unified":"1f470-1f3ff-200d-2642-fe0f","native":"👰ðŸ¿â€â™‚ï¸"}],"version":13},"woman_with_veil":{"id":"woman_with_veil","name":"Woman with Veil","keywords":["wedding","marriage"],"skins":[{"unified":"1f470-200d-2640-fe0f","native":"👰â€â™€ï¸"},{"unified":"1f470-1f3fb-200d-2640-fe0f","native":"👰ðŸ»â€â™€ï¸"},{"unified":"1f470-1f3fc-200d-2640-fe0f","native":"👰ðŸ¼â€â™€ï¸"},{"unified":"1f470-1f3fd-200d-2640-fe0f","native":"👰ðŸ½â€â™€ï¸"},{"unified":"1f470-1f3fe-200d-2640-fe0f","native":"👰ðŸ¾â€â™€ï¸"},{"unified":"1f470-1f3ff-200d-2640-fe0f","native":"👰ðŸ¿â€â™€ï¸"}],"version":13},"pregnant_woman":{"id":"pregnant_woman","name":"Pregnant Woman","keywords":["baby"],"skins":[{"unified":"1f930","native":"🤰"},{"unified":"1f930-1f3fb","native":"🤰ðŸ»"},{"unified":"1f930-1f3fc","native":"🤰ðŸ¼"},{"unified":"1f930-1f3fd","native":"🤰ðŸ½"},{"unified":"1f930-1f3fe","native":"🤰ðŸ¾"},{"unified":"1f930-1f3ff","native":"🤰ðŸ¿"}],"version":3},"pregnant_man":{"id":"pregnant_man","name":"Pregnant Man","keywords":["baby","belly"],"skins":[{"unified":"1fac3","native":"🫃"},{"unified":"1fac3-1f3fb","native":"🫃ðŸ»"},{"unified":"1fac3-1f3fc","native":"🫃ðŸ¼"},{"unified":"1fac3-1f3fd","native":"🫃ðŸ½"},{"unified":"1fac3-1f3fe","native":"🫃ðŸ¾"},{"unified":"1fac3-1f3ff","native":"🫃ðŸ¿"}],"version":14},"pregnant_person":{"id":"pregnant_person","name":"Pregnant Person","keywords":["baby","belly"],"skins":[{"unified":"1fac4","native":"🫄"},{"unified":"1fac4-1f3fb","native":"🫄ðŸ»"},{"unified":"1fac4-1f3fc","native":"🫄ðŸ¼"},{"unified":"1fac4-1f3fd","native":"🫄ðŸ½"},{"unified":"1fac4-1f3fe","native":"🫄ðŸ¾"},{"unified":"1fac4-1f3ff","native":"🫄ðŸ¿"}],"version":14},"breast-feeding":{"id":"breast-feeding","name":"Breast-Feeding","keywords":["breast","feeding","nursing","baby"],"skins":[{"unified":"1f931","native":"🤱"},{"unified":"1f931-1f3fb","native":"🤱ðŸ»"},{"unified":"1f931-1f3fc","native":"🤱ðŸ¼"},{"unified":"1f931-1f3fd","native":"🤱ðŸ½"},{"unified":"1f931-1f3fe","native":"🤱ðŸ¾"},{"unified":"1f931-1f3ff","native":"🤱ðŸ¿"}],"version":5},"woman_feeding_baby":{"id":"woman_feeding_baby","name":"Woman Feeding Baby","keywords":["birth","food"],"skins":[{"unified":"1f469-200d-1f37c","native":"👩â€ðŸ¼"},{"unified":"1f469-1f3fb-200d-1f37c","native":"👩ðŸ»â€ðŸ¼"},{"unified":"1f469-1f3fc-200d-1f37c","native":"👩ðŸ¼â€ðŸ¼"},{"unified":"1f469-1f3fd-200d-1f37c","native":"👩ðŸ½â€ðŸ¼"},{"unified":"1f469-1f3fe-200d-1f37c","native":"👩ðŸ¾â€ðŸ¼"},{"unified":"1f469-1f3ff-200d-1f37c","native":"👩ðŸ¿â€ðŸ¼"}],"version":13},"man_feeding_baby":{"id":"man_feeding_baby","name":"Man Feeding Baby","keywords":["birth","food"],"skins":[{"unified":"1f468-200d-1f37c","native":"👨â€ðŸ¼"},{"unified":"1f468-1f3fb-200d-1f37c","native":"👨ðŸ»â€ðŸ¼"},{"unified":"1f468-1f3fc-200d-1f37c","native":"👨ðŸ¼â€ðŸ¼"},{"unified":"1f468-1f3fd-200d-1f37c","native":"👨ðŸ½â€ðŸ¼"},{"unified":"1f468-1f3fe-200d-1f37c","native":"👨ðŸ¾â€ðŸ¼"},{"unified":"1f468-1f3ff-200d-1f37c","native":"👨ðŸ¿â€ðŸ¼"}],"version":13},"person_feeding_baby":{"id":"person_feeding_baby","name":"Person Feeding Baby","keywords":["birth","food"],"skins":[{"unified":"1f9d1-200d-1f37c","native":"🧑â€ðŸ¼"},{"unified":"1f9d1-1f3fb-200d-1f37c","native":"🧑ðŸ»â€ðŸ¼"},{"unified":"1f9d1-1f3fc-200d-1f37c","native":"🧑ðŸ¼â€ðŸ¼"},{"unified":"1f9d1-1f3fd-200d-1f37c","native":"🧑ðŸ½â€ðŸ¼"},{"unified":"1f9d1-1f3fe-200d-1f37c","native":"🧑ðŸ¾â€ðŸ¼"},{"unified":"1f9d1-1f3ff-200d-1f37c","native":"🧑ðŸ¿â€ðŸ¼"}],"version":13},"angel":{"id":"angel","name":"Baby Angel","keywords":["heaven","wings","halo"],"skins":[{"unified":"1f47c","native":"👼"},{"unified":"1f47c-1f3fb","native":"👼ðŸ»"},{"unified":"1f47c-1f3fc","native":"👼ðŸ¼"},{"unified":"1f47c-1f3fd","native":"👼ðŸ½"},{"unified":"1f47c-1f3fe","native":"👼ðŸ¾"},{"unified":"1f47c-1f3ff","native":"👼ðŸ¿"}],"version":1},"santa":{"id":"santa","name":"Santa Claus","keywords":["festival","man","male","xmas","father","christmas"],"skins":[{"unified":"1f385","native":"🎅"},{"unified":"1f385-1f3fb","native":"🎅ðŸ»"},{"unified":"1f385-1f3fc","native":"🎅ðŸ¼"},{"unified":"1f385-1f3fd","native":"🎅ðŸ½"},{"unified":"1f385-1f3fe","native":"🎅ðŸ¾"},{"unified":"1f385-1f3ff","native":"🎅ðŸ¿"}],"version":1},"mrs_claus":{"id":"mrs_claus","name":"Mrs. Claus","keywords":["mrs","mother","christmas","woman","female","xmas"],"skins":[{"unified":"1f936","native":"🤶"},{"unified":"1f936-1f3fb","native":"🤶ðŸ»"},{"unified":"1f936-1f3fc","native":"🤶ðŸ¼"},{"unified":"1f936-1f3fd","native":"🤶ðŸ½"},{"unified":"1f936-1f3fe","native":"🤶ðŸ¾"},{"unified":"1f936-1f3ff","native":"🤶ðŸ¿"}],"version":3},"mx_claus":{"id":"mx_claus","name":"Mx Claus","keywords":["christmas"],"skins":[{"unified":"1f9d1-200d-1f384","native":"🧑â€ðŸŽ„"},{"unified":"1f9d1-1f3fb-200d-1f384","native":"🧑ðŸ»â€ðŸŽ„"},{"unified":"1f9d1-1f3fc-200d-1f384","native":"🧑ðŸ¼â€ðŸŽ„"},{"unified":"1f9d1-1f3fd-200d-1f384","native":"🧑ðŸ½â€ðŸŽ„"},{"unified":"1f9d1-1f3fe-200d-1f384","native":"🧑ðŸ¾â€ðŸŽ„"},{"unified":"1f9d1-1f3ff-200d-1f384","native":"🧑ðŸ¿â€ðŸŽ„"}],"version":13},"superhero":{"id":"superhero","name":"Superhero","keywords":["marvel"],"skins":[{"unified":"1f9b8","native":"🦸"},{"unified":"1f9b8-1f3fb","native":"🦸ðŸ»"},{"unified":"1f9b8-1f3fc","native":"🦸ðŸ¼"},{"unified":"1f9b8-1f3fd","native":"🦸ðŸ½"},{"unified":"1f9b8-1f3fe","native":"🦸ðŸ¾"},{"unified":"1f9b8-1f3ff","native":"🦸ðŸ¿"}],"version":11},"male_superhero":{"id":"male_superhero","name":"Man Superhero","keywords":["male","good","hero","superpowers"],"skins":[{"unified":"1f9b8-200d-2642-fe0f","native":"🦸â€â™‚ï¸"},{"unified":"1f9b8-1f3fb-200d-2642-fe0f","native":"🦸ðŸ»â€â™‚ï¸"},{"unified":"1f9b8-1f3fc-200d-2642-fe0f","native":"🦸ðŸ¼â€â™‚ï¸"},{"unified":"1f9b8-1f3fd-200d-2642-fe0f","native":"🦸ðŸ½â€â™‚ï¸"},{"unified":"1f9b8-1f3fe-200d-2642-fe0f","native":"🦸ðŸ¾â€â™‚ï¸"},{"unified":"1f9b8-1f3ff-200d-2642-fe0f","native":"🦸ðŸ¿â€â™‚ï¸"}],"version":11},"female_superhero":{"id":"female_superhero","name":"Woman Superhero","keywords":["female","good","heroine","superpowers"],"skins":[{"unified":"1f9b8-200d-2640-fe0f","native":"🦸â€â™€ï¸"},{"unified":"1f9b8-1f3fb-200d-2640-fe0f","native":"🦸ðŸ»â€â™€ï¸"},{"unified":"1f9b8-1f3fc-200d-2640-fe0f","native":"🦸ðŸ¼â€â™€ï¸"},{"unified":"1f9b8-1f3fd-200d-2640-fe0f","native":"🦸ðŸ½â€â™€ï¸"},{"unified":"1f9b8-1f3fe-200d-2640-fe0f","native":"🦸ðŸ¾â€â™€ï¸"},{"unified":"1f9b8-1f3ff-200d-2640-fe0f","native":"🦸ðŸ¿â€â™€ï¸"}],"version":11},"supervillain":{"id":"supervillain","name":"Supervillain","keywords":["marvel"],"skins":[{"unified":"1f9b9","native":"🦹"},{"unified":"1f9b9-1f3fb","native":"🦹ðŸ»"},{"unified":"1f9b9-1f3fc","native":"🦹ðŸ¼"},{"unified":"1f9b9-1f3fd","native":"🦹ðŸ½"},{"unified":"1f9b9-1f3fe","native":"🦹ðŸ¾"},{"unified":"1f9b9-1f3ff","native":"🦹ðŸ¿"}],"version":11},"male_supervillain":{"id":"male_supervillain","name":"Man Supervillain","keywords":["male","evil","bad","criminal","hero","superpowers"],"skins":[{"unified":"1f9b9-200d-2642-fe0f","native":"🦹â€â™‚ï¸"},{"unified":"1f9b9-1f3fb-200d-2642-fe0f","native":"🦹ðŸ»â€â™‚ï¸"},{"unified":"1f9b9-1f3fc-200d-2642-fe0f","native":"🦹ðŸ¼â€â™‚ï¸"},{"unified":"1f9b9-1f3fd-200d-2642-fe0f","native":"🦹ðŸ½â€â™‚ï¸"},{"unified":"1f9b9-1f3fe-200d-2642-fe0f","native":"🦹ðŸ¾â€â™‚ï¸"},{"unified":"1f9b9-1f3ff-200d-2642-fe0f","native":"🦹ðŸ¿â€â™‚ï¸"}],"version":11},"female_supervillain":{"id":"female_supervillain","name":"Woman Supervillain","keywords":["female","evil","bad","criminal","heroine","superpowers"],"skins":[{"unified":"1f9b9-200d-2640-fe0f","native":"🦹â€â™€ï¸"},{"unified":"1f9b9-1f3fb-200d-2640-fe0f","native":"🦹ðŸ»â€â™€ï¸"},{"unified":"1f9b9-1f3fc-200d-2640-fe0f","native":"🦹ðŸ¼â€â™€ï¸"},{"unified":"1f9b9-1f3fd-200d-2640-fe0f","native":"🦹ðŸ½â€â™€ï¸"},{"unified":"1f9b9-1f3fe-200d-2640-fe0f","native":"🦹ðŸ¾â€â™€ï¸"},{"unified":"1f9b9-1f3ff-200d-2640-fe0f","native":"🦹ðŸ¿â€â™€ï¸"}],"version":11},"mage":{"id":"mage","name":"Mage","keywords":["magic"],"skins":[{"unified":"1f9d9","native":"🧙"},{"unified":"1f9d9-1f3fb","native":"🧙ðŸ»"},{"unified":"1f9d9-1f3fc","native":"🧙ðŸ¼"},{"unified":"1f9d9-1f3fd","native":"🧙ðŸ½"},{"unified":"1f9d9-1f3fe","native":"🧙ðŸ¾"},{"unified":"1f9d9-1f3ff","native":"🧙ðŸ¿"}],"version":5},"male_mage":{"id":"male_mage","name":"Man Mage","keywords":["male","sorcerer"],"skins":[{"unified":"1f9d9-200d-2642-fe0f","native":"🧙â€â™‚ï¸"},{"unified":"1f9d9-1f3fb-200d-2642-fe0f","native":"🧙ðŸ»â€â™‚ï¸"},{"unified":"1f9d9-1f3fc-200d-2642-fe0f","native":"🧙ðŸ¼â€â™‚ï¸"},{"unified":"1f9d9-1f3fd-200d-2642-fe0f","native":"🧙ðŸ½â€â™‚ï¸"},{"unified":"1f9d9-1f3fe-200d-2642-fe0f","native":"🧙ðŸ¾â€â™‚ï¸"},{"unified":"1f9d9-1f3ff-200d-2642-fe0f","native":"🧙ðŸ¿â€â™‚ï¸"}],"version":5},"female_mage":{"id":"female_mage","name":"Woman Mage","keywords":["female","witch"],"skins":[{"unified":"1f9d9-200d-2640-fe0f","native":"🧙â€â™€ï¸"},{"unified":"1f9d9-1f3fb-200d-2640-fe0f","native":"🧙ðŸ»â€â™€ï¸"},{"unified":"1f9d9-1f3fc-200d-2640-fe0f","native":"🧙ðŸ¼â€â™€ï¸"},{"unified":"1f9d9-1f3fd-200d-2640-fe0f","native":"🧙ðŸ½â€â™€ï¸"},{"unified":"1f9d9-1f3fe-200d-2640-fe0f","native":"🧙ðŸ¾â€â™€ï¸"},{"unified":"1f9d9-1f3ff-200d-2640-fe0f","native":"🧙ðŸ¿â€â™€ï¸"}],"version":5},"fairy":{"id":"fairy","name":"Fairy","keywords":["wings","magical"],"skins":[{"unified":"1f9da","native":"🧚"},{"unified":"1f9da-1f3fb","native":"🧚ðŸ»"},{"unified":"1f9da-1f3fc","native":"🧚ðŸ¼"},{"unified":"1f9da-1f3fd","native":"🧚ðŸ½"},{"unified":"1f9da-1f3fe","native":"🧚ðŸ¾"},{"unified":"1f9da-1f3ff","native":"🧚ðŸ¿"}],"version":5},"male_fairy":{"id":"male_fairy","name":"Man Fairy","keywords":["male"],"skins":[{"unified":"1f9da-200d-2642-fe0f","native":"🧚â€â™‚ï¸"},{"unified":"1f9da-1f3fb-200d-2642-fe0f","native":"🧚ðŸ»â€â™‚ï¸"},{"unified":"1f9da-1f3fc-200d-2642-fe0f","native":"🧚ðŸ¼â€â™‚ï¸"},{"unified":"1f9da-1f3fd-200d-2642-fe0f","native":"🧚ðŸ½â€â™‚ï¸"},{"unified":"1f9da-1f3fe-200d-2642-fe0f","native":"🧚ðŸ¾â€â™‚ï¸"},{"unified":"1f9da-1f3ff-200d-2642-fe0f","native":"🧚ðŸ¿â€â™‚ï¸"}],"version":5},"female_fairy":{"id":"female_fairy","name":"Woman Fairy","keywords":["female"],"skins":[{"unified":"1f9da-200d-2640-fe0f","native":"🧚â€â™€ï¸"},{"unified":"1f9da-1f3fb-200d-2640-fe0f","native":"🧚ðŸ»â€â™€ï¸"},{"unified":"1f9da-1f3fc-200d-2640-fe0f","native":"🧚ðŸ¼â€â™€ï¸"},{"unified":"1f9da-1f3fd-200d-2640-fe0f","native":"🧚ðŸ½â€â™€ï¸"},{"unified":"1f9da-1f3fe-200d-2640-fe0f","native":"🧚ðŸ¾â€â™€ï¸"},{"unified":"1f9da-1f3ff-200d-2640-fe0f","native":"🧚ðŸ¿â€â™€ï¸"}],"version":5},"vampire":{"id":"vampire","name":"Vampire","keywords":["blood","twilight"],"skins":[{"unified":"1f9db","native":"🧛"},{"unified":"1f9db-1f3fb","native":"🧛ðŸ»"},{"unified":"1f9db-1f3fc","native":"🧛ðŸ¼"},{"unified":"1f9db-1f3fd","native":"🧛ðŸ½"},{"unified":"1f9db-1f3fe","native":"🧛ðŸ¾"},{"unified":"1f9db-1f3ff","native":"🧛ðŸ¿"}],"version":5},"male_vampire":{"id":"male_vampire","name":"Man Vampire","keywords":["male","dracula"],"skins":[{"unified":"1f9db-200d-2642-fe0f","native":"🧛â€â™‚ï¸"},{"unified":"1f9db-1f3fb-200d-2642-fe0f","native":"🧛ðŸ»â€â™‚ï¸"},{"unified":"1f9db-1f3fc-200d-2642-fe0f","native":"🧛ðŸ¼â€â™‚ï¸"},{"unified":"1f9db-1f3fd-200d-2642-fe0f","native":"🧛ðŸ½â€â™‚ï¸"},{"unified":"1f9db-1f3fe-200d-2642-fe0f","native":"🧛ðŸ¾â€â™‚ï¸"},{"unified":"1f9db-1f3ff-200d-2642-fe0f","native":"🧛ðŸ¿â€â™‚ï¸"}],"version":5},"female_vampire":{"id":"female_vampire","name":"Woman Vampire","keywords":["female"],"skins":[{"unified":"1f9db-200d-2640-fe0f","native":"🧛â€â™€ï¸"},{"unified":"1f9db-1f3fb-200d-2640-fe0f","native":"🧛ðŸ»â€â™€ï¸"},{"unified":"1f9db-1f3fc-200d-2640-fe0f","native":"🧛ðŸ¼â€â™€ï¸"},{"unified":"1f9db-1f3fd-200d-2640-fe0f","native":"🧛ðŸ½â€â™€ï¸"},{"unified":"1f9db-1f3fe-200d-2640-fe0f","native":"🧛ðŸ¾â€â™€ï¸"},{"unified":"1f9db-1f3ff-200d-2640-fe0f","native":"🧛ðŸ¿â€â™€ï¸"}],"version":5},"merperson":{"id":"merperson","name":"Merperson","keywords":["sea"],"skins":[{"unified":"1f9dc","native":"🧜"},{"unified":"1f9dc-1f3fb","native":"🧜ðŸ»"},{"unified":"1f9dc-1f3fc","native":"🧜ðŸ¼"},{"unified":"1f9dc-1f3fd","native":"🧜ðŸ½"},{"unified":"1f9dc-1f3fe","native":"🧜ðŸ¾"},{"unified":"1f9dc-1f3ff","native":"🧜ðŸ¿"}],"version":5},"merman":{"id":"merman","name":"Merman","keywords":["man","male","triton"],"skins":[{"unified":"1f9dc-200d-2642-fe0f","native":"🧜â€â™‚ï¸"},{"unified":"1f9dc-1f3fb-200d-2642-fe0f","native":"🧜ðŸ»â€â™‚ï¸"},{"unified":"1f9dc-1f3fc-200d-2642-fe0f","native":"🧜ðŸ¼â€â™‚ï¸"},{"unified":"1f9dc-1f3fd-200d-2642-fe0f","native":"🧜ðŸ½â€â™‚ï¸"},{"unified":"1f9dc-1f3fe-200d-2642-fe0f","native":"🧜ðŸ¾â€â™‚ï¸"},{"unified":"1f9dc-1f3ff-200d-2642-fe0f","native":"🧜ðŸ¿â€â™‚ï¸"}],"version":5},"mermaid":{"id":"mermaid","name":"Mermaid","keywords":["woman","female","merwoman","ariel"],"skins":[{"unified":"1f9dc-200d-2640-fe0f","native":"🧜â€â™€ï¸"},{"unified":"1f9dc-1f3fb-200d-2640-fe0f","native":"🧜ðŸ»â€â™€ï¸"},{"unified":"1f9dc-1f3fc-200d-2640-fe0f","native":"🧜ðŸ¼â€â™€ï¸"},{"unified":"1f9dc-1f3fd-200d-2640-fe0f","native":"🧜ðŸ½â€â™€ï¸"},{"unified":"1f9dc-1f3fe-200d-2640-fe0f","native":"🧜ðŸ¾â€â™€ï¸"},{"unified":"1f9dc-1f3ff-200d-2640-fe0f","native":"🧜ðŸ¿â€â™€ï¸"}],"version":5},"elf":{"id":"elf","name":"Elf","keywords":["magical"],"skins":[{"unified":"1f9dd","native":"ðŸ§"},{"unified":"1f9dd-1f3fb","native":"ðŸ§ðŸ»"},{"unified":"1f9dd-1f3fc","native":"ðŸ§ðŸ¼"},{"unified":"1f9dd-1f3fd","native":"ðŸ§ðŸ½"},{"unified":"1f9dd-1f3fe","native":"ðŸ§ðŸ¾"},{"unified":"1f9dd-1f3ff","native":"ðŸ§ðŸ¿"}],"version":5},"male_elf":{"id":"male_elf","name":"Man Elf","keywords":["male"],"skins":[{"unified":"1f9dd-200d-2642-fe0f","native":"ðŸ§â€â™‚ï¸"},{"unified":"1f9dd-1f3fb-200d-2642-fe0f","native":"ðŸ§ðŸ»â€â™‚ï¸"},{"unified":"1f9dd-1f3fc-200d-2642-fe0f","native":"ðŸ§ðŸ¼â€â™‚ï¸"},{"unified":"1f9dd-1f3fd-200d-2642-fe0f","native":"ðŸ§ðŸ½â€â™‚ï¸"},{"unified":"1f9dd-1f3fe-200d-2642-fe0f","native":"ðŸ§ðŸ¾â€â™‚ï¸"},{"unified":"1f9dd-1f3ff-200d-2642-fe0f","native":"ðŸ§ðŸ¿â€â™‚ï¸"}],"version":5},"female_elf":{"id":"female_elf","name":"Woman Elf","keywords":["female"],"skins":[{"unified":"1f9dd-200d-2640-fe0f","native":"ðŸ§â€â™€ï¸"},{"unified":"1f9dd-1f3fb-200d-2640-fe0f","native":"ðŸ§ðŸ»â€â™€ï¸"},{"unified":"1f9dd-1f3fc-200d-2640-fe0f","native":"ðŸ§ðŸ¼â€â™€ï¸"},{"unified":"1f9dd-1f3fd-200d-2640-fe0f","native":"ðŸ§ðŸ½â€â™€ï¸"},{"unified":"1f9dd-1f3fe-200d-2640-fe0f","native":"ðŸ§ðŸ¾â€â™€ï¸"},{"unified":"1f9dd-1f3ff-200d-2640-fe0f","native":"ðŸ§ðŸ¿â€â™€ï¸"}],"version":5},"genie":{"id":"genie","name":"Genie","keywords":["magical","wishes"],"skins":[{"unified":"1f9de","native":"🧞"}],"version":5},"male_genie":{"id":"male_genie","name":"Man Genie","keywords":["male"],"skins":[{"unified":"1f9de-200d-2642-fe0f","native":"🧞â€â™‚ï¸"}],"version":5},"female_genie":{"id":"female_genie","name":"Woman Genie","keywords":["female"],"skins":[{"unified":"1f9de-200d-2640-fe0f","native":"🧞â€â™€ï¸"}],"version":5},"zombie":{"id":"zombie","name":"Zombie","keywords":["dead"],"skins":[{"unified":"1f9df","native":"🧟"}],"version":5},"male_zombie":{"id":"male_zombie","name":"Man Zombie","keywords":["male","dracula","undead","walking","dead"],"skins":[{"unified":"1f9df-200d-2642-fe0f","native":"🧟â€â™‚ï¸"}],"version":5},"female_zombie":{"id":"female_zombie","name":"Woman Zombie","keywords":["female","undead","walking","dead"],"skins":[{"unified":"1f9df-200d-2640-fe0f","native":"🧟â€â™€ï¸"}],"version":5},"troll":{"id":"troll","name":"Troll","keywords":["mystical","monster"],"skins":[{"unified":"1f9cc","native":"🧌"}],"version":14},"massage":{"id":"massage","name":"Face Massage","keywords":["person","getting","relax"],"skins":[{"unified":"1f486","native":"💆"},{"unified":"1f486-1f3fb","native":"💆ðŸ»"},{"unified":"1f486-1f3fc","native":"💆ðŸ¼"},{"unified":"1f486-1f3fd","native":"💆ðŸ½"},{"unified":"1f486-1f3fe","native":"💆ðŸ¾"},{"unified":"1f486-1f3ff","native":"💆ðŸ¿"}],"version":1},"man-getting-massage":{"id":"man-getting-massage","name":"Man Getting Massage","keywords":["getting-massage","male","boy","head"],"skins":[{"unified":"1f486-200d-2642-fe0f","native":"💆â€â™‚ï¸"},{"unified":"1f486-1f3fb-200d-2642-fe0f","native":"💆ðŸ»â€â™‚ï¸"},{"unified":"1f486-1f3fc-200d-2642-fe0f","native":"💆ðŸ¼â€â™‚ï¸"},{"unified":"1f486-1f3fd-200d-2642-fe0f","native":"💆ðŸ½â€â™‚ï¸"},{"unified":"1f486-1f3fe-200d-2642-fe0f","native":"💆ðŸ¾â€â™‚ï¸"},{"unified":"1f486-1f3ff-200d-2642-fe0f","native":"💆ðŸ¿â€â™‚ï¸"}],"version":4},"woman-getting-massage":{"id":"woman-getting-massage","name":"Woman Getting Massage","keywords":["getting-massage","female","girl","head"],"skins":[{"unified":"1f486-200d-2640-fe0f","native":"💆â€â™€ï¸"},{"unified":"1f486-1f3fb-200d-2640-fe0f","native":"💆ðŸ»â€â™€ï¸"},{"unified":"1f486-1f3fc-200d-2640-fe0f","native":"💆ðŸ¼â€â™€ï¸"},{"unified":"1f486-1f3fd-200d-2640-fe0f","native":"💆ðŸ½â€â™€ï¸"},{"unified":"1f486-1f3fe-200d-2640-fe0f","native":"💆ðŸ¾â€â™€ï¸"},{"unified":"1f486-1f3ff-200d-2640-fe0f","native":"💆ðŸ¿â€â™€ï¸"}],"version":4},"haircut":{"id":"haircut","name":"Haircut","keywords":["person","getting","hairstyle"],"skins":[{"unified":"1f487","native":"💇"},{"unified":"1f487-1f3fb","native":"💇ðŸ»"},{"unified":"1f487-1f3fc","native":"💇ðŸ¼"},{"unified":"1f487-1f3fd","native":"💇ðŸ½"},{"unified":"1f487-1f3fe","native":"💇ðŸ¾"},{"unified":"1f487-1f3ff","native":"💇ðŸ¿"}],"version":1},"man-getting-haircut":{"id":"man-getting-haircut","name":"Man Getting Haircut","keywords":["getting-haircut","male","boy"],"skins":[{"unified":"1f487-200d-2642-fe0f","native":"💇â€â™‚ï¸"},{"unified":"1f487-1f3fb-200d-2642-fe0f","native":"💇ðŸ»â€â™‚ï¸"},{"unified":"1f487-1f3fc-200d-2642-fe0f","native":"💇ðŸ¼â€â™‚ï¸"},{"unified":"1f487-1f3fd-200d-2642-fe0f","native":"💇ðŸ½â€â™‚ï¸"},{"unified":"1f487-1f3fe-200d-2642-fe0f","native":"💇ðŸ¾â€â™‚ï¸"},{"unified":"1f487-1f3ff-200d-2642-fe0f","native":"💇ðŸ¿â€â™‚ï¸"}],"version":4},"woman-getting-haircut":{"id":"woman-getting-haircut","name":"Woman Getting Haircut","keywords":["getting-haircut","female","girl"],"skins":[{"unified":"1f487-200d-2640-fe0f","native":"💇â€â™€ï¸"},{"unified":"1f487-1f3fb-200d-2640-fe0f","native":"💇ðŸ»â€â™€ï¸"},{"unified":"1f487-1f3fc-200d-2640-fe0f","native":"💇ðŸ¼â€â™€ï¸"},{"unified":"1f487-1f3fd-200d-2640-fe0f","native":"💇ðŸ½â€â™€ï¸"},{"unified":"1f487-1f3fe-200d-2640-fe0f","native":"💇ðŸ¾â€â™€ï¸"},{"unified":"1f487-1f3ff-200d-2640-fe0f","native":"💇ðŸ¿â€â™€ï¸"}],"version":4},"walking":{"id":"walking","name":"Pedestrian","keywords":["walking","person","move"],"skins":[{"unified":"1f6b6","native":"🚶"},{"unified":"1f6b6-1f3fb","native":"🚶ðŸ»"},{"unified":"1f6b6-1f3fc","native":"🚶ðŸ¼"},{"unified":"1f6b6-1f3fd","native":"🚶ðŸ½"},{"unified":"1f6b6-1f3fe","native":"🚶ðŸ¾"},{"unified":"1f6b6-1f3ff","native":"🚶ðŸ¿"}],"version":1},"man-walking":{"id":"man-walking","name":"Man Walking","keywords":["human","feet","steps"],"skins":[{"unified":"1f6b6-200d-2642-fe0f","native":"🚶â€â™‚ï¸"},{"unified":"1f6b6-1f3fb-200d-2642-fe0f","native":"🚶ðŸ»â€â™‚ï¸"},{"unified":"1f6b6-1f3fc-200d-2642-fe0f","native":"🚶ðŸ¼â€â™‚ï¸"},{"unified":"1f6b6-1f3fd-200d-2642-fe0f","native":"🚶ðŸ½â€â™‚ï¸"},{"unified":"1f6b6-1f3fe-200d-2642-fe0f","native":"🚶ðŸ¾â€â™‚ï¸"},{"unified":"1f6b6-1f3ff-200d-2642-fe0f","native":"🚶ðŸ¿â€â™‚ï¸"}],"version":4},"woman-walking":{"id":"woman-walking","name":"Woman Walking","keywords":["human","feet","steps","female"],"skins":[{"unified":"1f6b6-200d-2640-fe0f","native":"🚶â€â™€ï¸"},{"unified":"1f6b6-1f3fb-200d-2640-fe0f","native":"🚶ðŸ»â€â™€ï¸"},{"unified":"1f6b6-1f3fc-200d-2640-fe0f","native":"🚶ðŸ¼â€â™€ï¸"},{"unified":"1f6b6-1f3fd-200d-2640-fe0f","native":"🚶ðŸ½â€â™€ï¸"},{"unified":"1f6b6-1f3fe-200d-2640-fe0f","native":"🚶ðŸ¾â€â™€ï¸"},{"unified":"1f6b6-1f3ff-200d-2640-fe0f","native":"🚶ðŸ¿â€â™€ï¸"}],"version":4},"standing_person":{"id":"standing_person","name":"Standing Person","keywords":["still"],"skins":[{"unified":"1f9cd","native":"ðŸ§"},{"unified":"1f9cd-1f3fb","native":"ðŸ§ðŸ»"},{"unified":"1f9cd-1f3fc","native":"ðŸ§ðŸ¼"},{"unified":"1f9cd-1f3fd","native":"ðŸ§ðŸ½"},{"unified":"1f9cd-1f3fe","native":"ðŸ§ðŸ¾"},{"unified":"1f9cd-1f3ff","native":"ðŸ§ðŸ¿"}],"version":12},"man_standing":{"id":"man_standing","name":"Man Standing","keywords":["still"],"skins":[{"unified":"1f9cd-200d-2642-fe0f","native":"ðŸ§â€â™‚ï¸"},{"unified":"1f9cd-1f3fb-200d-2642-fe0f","native":"ðŸ§ðŸ»â€â™‚ï¸"},{"unified":"1f9cd-1f3fc-200d-2642-fe0f","native":"ðŸ§ðŸ¼â€â™‚ï¸"},{"unified":"1f9cd-1f3fd-200d-2642-fe0f","native":"ðŸ§ðŸ½â€â™‚ï¸"},{"unified":"1f9cd-1f3fe-200d-2642-fe0f","native":"ðŸ§ðŸ¾â€â™‚ï¸"},{"unified":"1f9cd-1f3ff-200d-2642-fe0f","native":"ðŸ§ðŸ¿â€â™‚ï¸"}],"version":12},"woman_standing":{"id":"woman_standing","name":"Woman Standing","keywords":["still"],"skins":[{"unified":"1f9cd-200d-2640-fe0f","native":"ðŸ§â€â™€ï¸"},{"unified":"1f9cd-1f3fb-200d-2640-fe0f","native":"ðŸ§ðŸ»â€â™€ï¸"},{"unified":"1f9cd-1f3fc-200d-2640-fe0f","native":"ðŸ§ðŸ¼â€â™€ï¸"},{"unified":"1f9cd-1f3fd-200d-2640-fe0f","native":"ðŸ§ðŸ½â€â™€ï¸"},{"unified":"1f9cd-1f3fe-200d-2640-fe0f","native":"ðŸ§ðŸ¾â€â™€ï¸"},{"unified":"1f9cd-1f3ff-200d-2640-fe0f","native":"ðŸ§ðŸ¿â€â™€ï¸"}],"version":12},"kneeling_person":{"id":"kneeling_person","name":"Kneeling Person","keywords":["pray","respectful"],"skins":[{"unified":"1f9ce","native":"🧎"},{"unified":"1f9ce-1f3fb","native":"🧎ðŸ»"},{"unified":"1f9ce-1f3fc","native":"🧎ðŸ¼"},{"unified":"1f9ce-1f3fd","native":"🧎ðŸ½"},{"unified":"1f9ce-1f3fe","native":"🧎ðŸ¾"},{"unified":"1f9ce-1f3ff","native":"🧎ðŸ¿"}],"version":12},"man_kneeling":{"id":"man_kneeling","name":"Man Kneeling","keywords":["pray","respectful"],"skins":[{"unified":"1f9ce-200d-2642-fe0f","native":"🧎â€â™‚ï¸"},{"unified":"1f9ce-1f3fb-200d-2642-fe0f","native":"🧎ðŸ»â€â™‚ï¸"},{"unified":"1f9ce-1f3fc-200d-2642-fe0f","native":"🧎ðŸ¼â€â™‚ï¸"},{"unified":"1f9ce-1f3fd-200d-2642-fe0f","native":"🧎ðŸ½â€â™‚ï¸"},{"unified":"1f9ce-1f3fe-200d-2642-fe0f","native":"🧎ðŸ¾â€â™‚ï¸"},{"unified":"1f9ce-1f3ff-200d-2642-fe0f","native":"🧎ðŸ¿â€â™‚ï¸"}],"version":12},"woman_kneeling":{"id":"woman_kneeling","name":"Woman Kneeling","keywords":["respectful","pray"],"skins":[{"unified":"1f9ce-200d-2640-fe0f","native":"🧎â€â™€ï¸"},{"unified":"1f9ce-1f3fb-200d-2640-fe0f","native":"🧎ðŸ»â€â™€ï¸"},{"unified":"1f9ce-1f3fc-200d-2640-fe0f","native":"🧎ðŸ¼â€â™€ï¸"},{"unified":"1f9ce-1f3fd-200d-2640-fe0f","native":"🧎ðŸ½â€â™€ï¸"},{"unified":"1f9ce-1f3fe-200d-2640-fe0f","native":"🧎ðŸ¾â€â™€ï¸"},{"unified":"1f9ce-1f3ff-200d-2640-fe0f","native":"🧎ðŸ¿â€â™€ï¸"}],"version":12},"person_with_probing_cane":{"id":"person_with_probing_cane","name":"Person with White Cane","keywords":["probing","blind"],"skins":[{"unified":"1f9d1-200d-1f9af","native":"🧑â€ðŸ¦¯"},{"unified":"1f9d1-1f3fb-200d-1f9af","native":"🧑ðŸ»â€ðŸ¦¯"},{"unified":"1f9d1-1f3fc-200d-1f9af","native":"🧑ðŸ¼â€ðŸ¦¯"},{"unified":"1f9d1-1f3fd-200d-1f9af","native":"🧑ðŸ½â€ðŸ¦¯"},{"unified":"1f9d1-1f3fe-200d-1f9af","native":"🧑ðŸ¾â€ðŸ¦¯"},{"unified":"1f9d1-1f3ff-200d-1f9af","native":"🧑ðŸ¿â€ðŸ¦¯"}],"version":12.1},"man_with_probing_cane":{"id":"man_with_probing_cane","name":"Man with White Cane","keywords":["probing","blind"],"skins":[{"unified":"1f468-200d-1f9af","native":"👨â€ðŸ¦¯"},{"unified":"1f468-1f3fb-200d-1f9af","native":"👨ðŸ»â€ðŸ¦¯"},{"unified":"1f468-1f3fc-200d-1f9af","native":"👨ðŸ¼â€ðŸ¦¯"},{"unified":"1f468-1f3fd-200d-1f9af","native":"👨ðŸ½â€ðŸ¦¯"},{"unified":"1f468-1f3fe-200d-1f9af","native":"👨ðŸ¾â€ðŸ¦¯"},{"unified":"1f468-1f3ff-200d-1f9af","native":"👨ðŸ¿â€ðŸ¦¯"}],"version":12},"woman_with_probing_cane":{"id":"woman_with_probing_cane","name":"Woman with White Cane","keywords":["probing","blind"],"skins":[{"unified":"1f469-200d-1f9af","native":"👩â€ðŸ¦¯"},{"unified":"1f469-1f3fb-200d-1f9af","native":"👩ðŸ»â€ðŸ¦¯"},{"unified":"1f469-1f3fc-200d-1f9af","native":"👩ðŸ¼â€ðŸ¦¯"},{"unified":"1f469-1f3fd-200d-1f9af","native":"👩ðŸ½â€ðŸ¦¯"},{"unified":"1f469-1f3fe-200d-1f9af","native":"👩ðŸ¾â€ðŸ¦¯"},{"unified":"1f469-1f3ff-200d-1f9af","native":"👩ðŸ¿â€ðŸ¦¯"}],"version":12},"person_in_motorized_wheelchair":{"id":"person_in_motorized_wheelchair","name":"Person in Motorized Wheelchair","keywords":["disability","accessibility"],"skins":[{"unified":"1f9d1-200d-1f9bc","native":"🧑â€ðŸ¦¼"},{"unified":"1f9d1-1f3fb-200d-1f9bc","native":"🧑ðŸ»â€ðŸ¦¼"},{"unified":"1f9d1-1f3fc-200d-1f9bc","native":"🧑ðŸ¼â€ðŸ¦¼"},{"unified":"1f9d1-1f3fd-200d-1f9bc","native":"🧑ðŸ½â€ðŸ¦¼"},{"unified":"1f9d1-1f3fe-200d-1f9bc","native":"🧑ðŸ¾â€ðŸ¦¼"},{"unified":"1f9d1-1f3ff-200d-1f9bc","native":"🧑ðŸ¿â€ðŸ¦¼"}],"version":12.1},"man_in_motorized_wheelchair":{"id":"man_in_motorized_wheelchair","name":"Man in Motorized Wheelchair","keywords":["disability","accessibility"],"skins":[{"unified":"1f468-200d-1f9bc","native":"👨â€ðŸ¦¼"},{"unified":"1f468-1f3fb-200d-1f9bc","native":"👨ðŸ»â€ðŸ¦¼"},{"unified":"1f468-1f3fc-200d-1f9bc","native":"👨ðŸ¼â€ðŸ¦¼"},{"unified":"1f468-1f3fd-200d-1f9bc","native":"👨ðŸ½â€ðŸ¦¼"},{"unified":"1f468-1f3fe-200d-1f9bc","native":"👨ðŸ¾â€ðŸ¦¼"},{"unified":"1f468-1f3ff-200d-1f9bc","native":"👨ðŸ¿â€ðŸ¦¼"}],"version":12},"woman_in_motorized_wheelchair":{"id":"woman_in_motorized_wheelchair","name":"Woman in Motorized Wheelchair","keywords":["disability","accessibility"],"skins":[{"unified":"1f469-200d-1f9bc","native":"👩â€ðŸ¦¼"},{"unified":"1f469-1f3fb-200d-1f9bc","native":"👩ðŸ»â€ðŸ¦¼"},{"unified":"1f469-1f3fc-200d-1f9bc","native":"👩ðŸ¼â€ðŸ¦¼"},{"unified":"1f469-1f3fd-200d-1f9bc","native":"👩ðŸ½â€ðŸ¦¼"},{"unified":"1f469-1f3fe-200d-1f9bc","native":"👩ðŸ¾â€ðŸ¦¼"},{"unified":"1f469-1f3ff-200d-1f9bc","native":"👩ðŸ¿â€ðŸ¦¼"}],"version":12},"person_in_manual_wheelchair":{"id":"person_in_manual_wheelchair","name":"Person in Manual Wheelchair","keywords":["disability","accessibility"],"skins":[{"unified":"1f9d1-200d-1f9bd","native":"🧑â€ðŸ¦½"},{"unified":"1f9d1-1f3fb-200d-1f9bd","native":"🧑ðŸ»â€ðŸ¦½"},{"unified":"1f9d1-1f3fc-200d-1f9bd","native":"🧑ðŸ¼â€ðŸ¦½"},{"unified":"1f9d1-1f3fd-200d-1f9bd","native":"🧑ðŸ½â€ðŸ¦½"},{"unified":"1f9d1-1f3fe-200d-1f9bd","native":"🧑ðŸ¾â€ðŸ¦½"},{"unified":"1f9d1-1f3ff-200d-1f9bd","native":"🧑ðŸ¿â€ðŸ¦½"}],"version":12.1},"man_in_manual_wheelchair":{"id":"man_in_manual_wheelchair","name":"Man in Manual Wheelchair","keywords":["disability","accessibility"],"skins":[{"unified":"1f468-200d-1f9bd","native":"👨â€ðŸ¦½"},{"unified":"1f468-1f3fb-200d-1f9bd","native":"👨ðŸ»â€ðŸ¦½"},{"unified":"1f468-1f3fc-200d-1f9bd","native":"👨ðŸ¼â€ðŸ¦½"},{"unified":"1f468-1f3fd-200d-1f9bd","native":"👨ðŸ½â€ðŸ¦½"},{"unified":"1f468-1f3fe-200d-1f9bd","native":"👨ðŸ¾â€ðŸ¦½"},{"unified":"1f468-1f3ff-200d-1f9bd","native":"👨ðŸ¿â€ðŸ¦½"}],"version":12},"woman_in_manual_wheelchair":{"id":"woman_in_manual_wheelchair","name":"Woman in Manual Wheelchair","keywords":["disability","accessibility"],"skins":[{"unified":"1f469-200d-1f9bd","native":"👩â€ðŸ¦½"},{"unified":"1f469-1f3fb-200d-1f9bd","native":"👩ðŸ»â€ðŸ¦½"},{"unified":"1f469-1f3fc-200d-1f9bd","native":"👩ðŸ¼â€ðŸ¦½"},{"unified":"1f469-1f3fd-200d-1f9bd","native":"👩ðŸ½â€ðŸ¦½"},{"unified":"1f469-1f3fe-200d-1f9bd","native":"👩ðŸ¾â€ðŸ¦½"},{"unified":"1f469-1f3ff-200d-1f9bd","native":"👩ðŸ¿â€ðŸ¦½"}],"version":12},"runner":{"id":"runner","name":"Runner","keywords":["running","person","move"],"skins":[{"unified":"1f3c3","native":"ðŸƒ"},{"unified":"1f3c3-1f3fb","native":"ðŸƒðŸ»"},{"unified":"1f3c3-1f3fc","native":"ðŸƒðŸ¼"},{"unified":"1f3c3-1f3fd","native":"ðŸƒðŸ½"},{"unified":"1f3c3-1f3fe","native":"ðŸƒðŸ¾"},{"unified":"1f3c3-1f3ff","native":"ðŸƒðŸ¿"}],"version":1},"man-running":{"id":"man-running","name":"Man Running","keywords":["walking","exercise","race"],"skins":[{"unified":"1f3c3-200d-2642-fe0f","native":"ðŸƒâ€â™‚ï¸"},{"unified":"1f3c3-1f3fb-200d-2642-fe0f","native":"ðŸƒðŸ»â€â™‚ï¸"},{"unified":"1f3c3-1f3fc-200d-2642-fe0f","native":"ðŸƒðŸ¼â€â™‚ï¸"},{"unified":"1f3c3-1f3fd-200d-2642-fe0f","native":"ðŸƒðŸ½â€â™‚ï¸"},{"unified":"1f3c3-1f3fe-200d-2642-fe0f","native":"ðŸƒðŸ¾â€â™‚ï¸"},{"unified":"1f3c3-1f3ff-200d-2642-fe0f","native":"ðŸƒðŸ¿â€â™‚ï¸"}],"version":4},"woman-running":{"id":"woman-running","name":"Woman Running","keywords":["walking","exercise","race","female"],"skins":[{"unified":"1f3c3-200d-2640-fe0f","native":"ðŸƒâ€â™€ï¸"},{"unified":"1f3c3-1f3fb-200d-2640-fe0f","native":"ðŸƒðŸ»â€â™€ï¸"},{"unified":"1f3c3-1f3fc-200d-2640-fe0f","native":"ðŸƒðŸ¼â€â™€ï¸"},{"unified":"1f3c3-1f3fd-200d-2640-fe0f","native":"ðŸƒðŸ½â€â™€ï¸"},{"unified":"1f3c3-1f3fe-200d-2640-fe0f","native":"ðŸƒðŸ¾â€â™€ï¸"},{"unified":"1f3c3-1f3ff-200d-2640-fe0f","native":"ðŸƒðŸ¿â€â™€ï¸"}],"version":4},"dancer":{"id":"dancer","name":"Dancer","keywords":["woman","dancing","female","girl","fun"],"skins":[{"unified":"1f483","native":"💃"},{"unified":"1f483-1f3fb","native":"💃ðŸ»"},{"unified":"1f483-1f3fc","native":"💃ðŸ¼"},{"unified":"1f483-1f3fd","native":"💃ðŸ½"},{"unified":"1f483-1f3fe","native":"💃ðŸ¾"},{"unified":"1f483-1f3ff","native":"💃ðŸ¿"}],"version":1},"man_dancing":{"id":"man_dancing","name":"Man Dancing","keywords":["male","boy","fun","dancer"],"skins":[{"unified":"1f57a","native":"🕺"},{"unified":"1f57a-1f3fb","native":"🕺ðŸ»"},{"unified":"1f57a-1f3fc","native":"🕺ðŸ¼"},{"unified":"1f57a-1f3fd","native":"🕺ðŸ½"},{"unified":"1f57a-1f3fe","native":"🕺ðŸ¾"},{"unified":"1f57a-1f3ff","native":"🕺ðŸ¿"}],"version":3},"man_in_business_suit_levitating":{"id":"man_in_business_suit_levitating","name":"Person in Suit Levitating","keywords":["man","business","levitate","hover","jump"],"skins":[{"unified":"1f574-fe0f","native":"🕴ï¸"},{"unified":"1f574-1f3fb","native":"🕴ðŸ»"},{"unified":"1f574-1f3fc","native":"🕴ðŸ¼"},{"unified":"1f574-1f3fd","native":"🕴ðŸ½"},{"unified":"1f574-1f3fe","native":"🕴ðŸ¾"},{"unified":"1f574-1f3ff","native":"🕴ðŸ¿"}],"version":1},"dancers":{"id":"dancers","name":"Woman with Bunny Ears","keywords":["dancers","people","perform","costume"],"skins":[{"unified":"1f46f","native":"👯"}],"version":1},"men-with-bunny-ears-partying":{"id":"men-with-bunny-ears-partying","name":"Men with Bunny Ears","keywords":["with-bunny-ears-partying","man","male","boys"],"skins":[{"unified":"1f46f-200d-2642-fe0f","native":"👯â€â™‚ï¸"}],"version":4},"women-with-bunny-ears-partying":{"id":"women-with-bunny-ears-partying","name":"Women with Bunny Ears","keywords":["with-bunny-ears-partying","woman","female","girls"],"skins":[{"unified":"1f46f-200d-2640-fe0f","native":"👯â€â™€ï¸"}],"version":4},"person_in_steamy_room":{"id":"person_in_steamy_room","name":"Person in Steamy Room","keywords":["relax","spa"],"skins":[{"unified":"1f9d6","native":"🧖"},{"unified":"1f9d6-1f3fb","native":"🧖ðŸ»"},{"unified":"1f9d6-1f3fc","native":"🧖ðŸ¼"},{"unified":"1f9d6-1f3fd","native":"🧖ðŸ½"},{"unified":"1f9d6-1f3fe","native":"🧖ðŸ¾"},{"unified":"1f9d6-1f3ff","native":"🧖ðŸ¿"}],"version":5},"man_in_steamy_room":{"id":"man_in_steamy_room","name":"Man in Steamy Room","keywords":["male","spa","steamroom","sauna"],"skins":[{"unified":"1f9d6-200d-2642-fe0f","native":"🧖â€â™‚ï¸"},{"unified":"1f9d6-1f3fb-200d-2642-fe0f","native":"🧖ðŸ»â€â™‚ï¸"},{"unified":"1f9d6-1f3fc-200d-2642-fe0f","native":"🧖ðŸ¼â€â™‚ï¸"},{"unified":"1f9d6-1f3fd-200d-2642-fe0f","native":"🧖ðŸ½â€â™‚ï¸"},{"unified":"1f9d6-1f3fe-200d-2642-fe0f","native":"🧖ðŸ¾â€â™‚ï¸"},{"unified":"1f9d6-1f3ff-200d-2642-fe0f","native":"🧖ðŸ¿â€â™‚ï¸"}],"version":5},"woman_in_steamy_room":{"id":"woman_in_steamy_room","name":"Woman in Steamy Room","keywords":["female","spa","steamroom","sauna"],"skins":[{"unified":"1f9d6-200d-2640-fe0f","native":"🧖â€â™€ï¸"},{"unified":"1f9d6-1f3fb-200d-2640-fe0f","native":"🧖ðŸ»â€â™€ï¸"},{"unified":"1f9d6-1f3fc-200d-2640-fe0f","native":"🧖ðŸ¼â€â™€ï¸"},{"unified":"1f9d6-1f3fd-200d-2640-fe0f","native":"🧖ðŸ½â€â™€ï¸"},{"unified":"1f9d6-1f3fe-200d-2640-fe0f","native":"🧖ðŸ¾â€â™€ï¸"},{"unified":"1f9d6-1f3ff-200d-2640-fe0f","native":"🧖ðŸ¿â€â™€ï¸"}],"version":5},"person_climbing":{"id":"person_climbing","name":"Person Climbing","keywords":["sport"],"skins":[{"unified":"1f9d7","native":"🧗"},{"unified":"1f9d7-1f3fb","native":"🧗ðŸ»"},{"unified":"1f9d7-1f3fc","native":"🧗ðŸ¼"},{"unified":"1f9d7-1f3fd","native":"🧗ðŸ½"},{"unified":"1f9d7-1f3fe","native":"🧗ðŸ¾"},{"unified":"1f9d7-1f3ff","native":"🧗ðŸ¿"}],"version":5},"man_climbing":{"id":"man_climbing","name":"Man Climbing","keywords":["sports","hobby","male","rock"],"skins":[{"unified":"1f9d7-200d-2642-fe0f","native":"🧗â€â™‚ï¸"},{"unified":"1f9d7-1f3fb-200d-2642-fe0f","native":"🧗ðŸ»â€â™‚ï¸"},{"unified":"1f9d7-1f3fc-200d-2642-fe0f","native":"🧗ðŸ¼â€â™‚ï¸"},{"unified":"1f9d7-1f3fd-200d-2642-fe0f","native":"🧗ðŸ½â€â™‚ï¸"},{"unified":"1f9d7-1f3fe-200d-2642-fe0f","native":"🧗ðŸ¾â€â™‚ï¸"},{"unified":"1f9d7-1f3ff-200d-2642-fe0f","native":"🧗ðŸ¿â€â™‚ï¸"}],"version":5},"woman_climbing":{"id":"woman_climbing","name":"Woman Climbing","keywords":["sports","hobby","female","rock"],"skins":[{"unified":"1f9d7-200d-2640-fe0f","native":"🧗â€â™€ï¸"},{"unified":"1f9d7-1f3fb-200d-2640-fe0f","native":"🧗ðŸ»â€â™€ï¸"},{"unified":"1f9d7-1f3fc-200d-2640-fe0f","native":"🧗ðŸ¼â€â™€ï¸"},{"unified":"1f9d7-1f3fd-200d-2640-fe0f","native":"🧗ðŸ½â€â™€ï¸"},{"unified":"1f9d7-1f3fe-200d-2640-fe0f","native":"🧗ðŸ¾â€â™€ï¸"},{"unified":"1f9d7-1f3ff-200d-2640-fe0f","native":"🧗ðŸ¿â€â™€ï¸"}],"version":5},"fencer":{"id":"fencer","name":"Fencer","keywords":["person","fencing","sports","sword"],"skins":[{"unified":"1f93a","native":"🤺"}],"version":3},"horse_racing":{"id":"horse_racing","name":"Horse Racing","keywords":["animal","betting","competition","gambling","luck"],"skins":[{"unified":"1f3c7","native":"ðŸ‡"},{"unified":"1f3c7-1f3fb","native":"ðŸ‡ðŸ»"},{"unified":"1f3c7-1f3fc","native":"ðŸ‡ðŸ¼"},{"unified":"1f3c7-1f3fd","native":"ðŸ‡ðŸ½"},{"unified":"1f3c7-1f3fe","native":"ðŸ‡ðŸ¾"},{"unified":"1f3c7-1f3ff","native":"ðŸ‡ðŸ¿"}],"version":1},"skier":{"id":"skier","name":"Skier","keywords":["sports","winter","snow"],"skins":[{"unified":"26f7-fe0f","native":"â›·ï¸"}],"version":1},"snowboarder":{"id":"snowboarder","name":"Snowboarder","keywords":["sports","winter"],"skins":[{"unified":"1f3c2","native":"ðŸ‚"},{"unified":"1f3c2-1f3fb","native":"ðŸ‚ðŸ»"},{"unified":"1f3c2-1f3fc","native":"ðŸ‚ðŸ¼"},{"unified":"1f3c2-1f3fd","native":"ðŸ‚ðŸ½"},{"unified":"1f3c2-1f3fe","native":"ðŸ‚ðŸ¾"},{"unified":"1f3c2-1f3ff","native":"ðŸ‚ðŸ¿"}],"version":1},"golfer":{"id":"golfer","name":"Person Golfing","keywords":["golfer","sports","business"],"skins":[{"unified":"1f3cc-fe0f","native":"ðŸŒï¸"},{"unified":"1f3cc-1f3fb","native":"ðŸŒðŸ»"},{"unified":"1f3cc-1f3fc","native":"ðŸŒðŸ¼"},{"unified":"1f3cc-1f3fd","native":"ðŸŒðŸ½"},{"unified":"1f3cc-1f3fe","native":"ðŸŒðŸ¾"},{"unified":"1f3cc-1f3ff","native":"ðŸŒðŸ¿"}],"version":1},"man-golfing":{"id":"man-golfing","name":"Man Golfing","keywords":["sport"],"skins":[{"unified":"1f3cc-fe0f-200d-2642-fe0f","native":"ðŸŒï¸â€â™‚ï¸"},{"unified":"1f3cc-1f3fb-200d-2642-fe0f","native":"ðŸŒðŸ»â€â™‚ï¸"},{"unified":"1f3cc-1f3fc-200d-2642-fe0f","native":"ðŸŒðŸ¼â€â™‚ï¸"},{"unified":"1f3cc-1f3fd-200d-2642-fe0f","native":"ðŸŒðŸ½â€â™‚ï¸"},{"unified":"1f3cc-1f3fe-200d-2642-fe0f","native":"ðŸŒðŸ¾â€â™‚ï¸"},{"unified":"1f3cc-1f3ff-200d-2642-fe0f","native":"ðŸŒðŸ¿â€â™‚ï¸"}],"version":4},"woman-golfing":{"id":"woman-golfing","name":"Woman Golfing","keywords":["sports","business","female"],"skins":[{"unified":"1f3cc-fe0f-200d-2640-fe0f","native":"ðŸŒï¸â€â™€ï¸"},{"unified":"1f3cc-1f3fb-200d-2640-fe0f","native":"ðŸŒðŸ»â€â™€ï¸"},{"unified":"1f3cc-1f3fc-200d-2640-fe0f","native":"ðŸŒðŸ¼â€â™€ï¸"},{"unified":"1f3cc-1f3fd-200d-2640-fe0f","native":"ðŸŒðŸ½â€â™€ï¸"},{"unified":"1f3cc-1f3fe-200d-2640-fe0f","native":"ðŸŒðŸ¾â€â™€ï¸"},{"unified":"1f3cc-1f3ff-200d-2640-fe0f","native":"ðŸŒðŸ¿â€â™€ï¸"}],"version":4},"surfer":{"id":"surfer","name":"Surfer","keywords":["person","surfing","sport","sea"],"skins":[{"unified":"1f3c4","native":"ðŸ„"},{"unified":"1f3c4-1f3fb","native":"ðŸ„ðŸ»"},{"unified":"1f3c4-1f3fc","native":"ðŸ„ðŸ¼"},{"unified":"1f3c4-1f3fd","native":"ðŸ„ðŸ½"},{"unified":"1f3c4-1f3fe","native":"ðŸ„ðŸ¾"},{"unified":"1f3c4-1f3ff","native":"ðŸ„ðŸ¿"}],"version":1},"man-surfing":{"id":"man-surfing","name":"Man Surfing","keywords":["sports","ocean","sea","summer","beach"],"skins":[{"unified":"1f3c4-200d-2642-fe0f","native":"ðŸ„â€â™‚ï¸"},{"unified":"1f3c4-1f3fb-200d-2642-fe0f","native":"ðŸ„ðŸ»â€â™‚ï¸"},{"unified":"1f3c4-1f3fc-200d-2642-fe0f","native":"ðŸ„ðŸ¼â€â™‚ï¸"},{"unified":"1f3c4-1f3fd-200d-2642-fe0f","native":"ðŸ„ðŸ½â€â™‚ï¸"},{"unified":"1f3c4-1f3fe-200d-2642-fe0f","native":"ðŸ„ðŸ¾â€â™‚ï¸"},{"unified":"1f3c4-1f3ff-200d-2642-fe0f","native":"ðŸ„ðŸ¿â€â™‚ï¸"}],"version":4},"woman-surfing":{"id":"woman-surfing","name":"Woman Surfing","keywords":["sports","ocean","sea","summer","beach","female"],"skins":[{"unified":"1f3c4-200d-2640-fe0f","native":"ðŸ„â€â™€ï¸"},{"unified":"1f3c4-1f3fb-200d-2640-fe0f","native":"ðŸ„ðŸ»â€â™€ï¸"},{"unified":"1f3c4-1f3fc-200d-2640-fe0f","native":"ðŸ„ðŸ¼â€â™€ï¸"},{"unified":"1f3c4-1f3fd-200d-2640-fe0f","native":"ðŸ„ðŸ½â€â™€ï¸"},{"unified":"1f3c4-1f3fe-200d-2640-fe0f","native":"ðŸ„ðŸ¾â€â™€ï¸"},{"unified":"1f3c4-1f3ff-200d-2640-fe0f","native":"ðŸ„ðŸ¿â€â™€ï¸"}],"version":4},"rowboat":{"id":"rowboat","name":"Rowboat","keywords":["person","rowing","boat","sport","move"],"skins":[{"unified":"1f6a3","native":"🚣"},{"unified":"1f6a3-1f3fb","native":"🚣ðŸ»"},{"unified":"1f6a3-1f3fc","native":"🚣ðŸ¼"},{"unified":"1f6a3-1f3fd","native":"🚣ðŸ½"},{"unified":"1f6a3-1f3fe","native":"🚣ðŸ¾"},{"unified":"1f6a3-1f3ff","native":"🚣ðŸ¿"}],"version":1},"man-rowing-boat":{"id":"man-rowing-boat","name":"Man Rowing Boat","keywords":["rowing-boat","sports","hobby","water","ship"],"skins":[{"unified":"1f6a3-200d-2642-fe0f","native":"🚣â€â™‚ï¸"},{"unified":"1f6a3-1f3fb-200d-2642-fe0f","native":"🚣ðŸ»â€â™‚ï¸"},{"unified":"1f6a3-1f3fc-200d-2642-fe0f","native":"🚣ðŸ¼â€â™‚ï¸"},{"unified":"1f6a3-1f3fd-200d-2642-fe0f","native":"🚣ðŸ½â€â™‚ï¸"},{"unified":"1f6a3-1f3fe-200d-2642-fe0f","native":"🚣ðŸ¾â€â™‚ï¸"},{"unified":"1f6a3-1f3ff-200d-2642-fe0f","native":"🚣ðŸ¿â€â™‚ï¸"}],"version":4},"woman-rowing-boat":{"id":"woman-rowing-boat","name":"Woman Rowing Boat","keywords":["rowing-boat","sports","hobby","water","ship","female"],"skins":[{"unified":"1f6a3-200d-2640-fe0f","native":"🚣â€â™€ï¸"},{"unified":"1f6a3-1f3fb-200d-2640-fe0f","native":"🚣ðŸ»â€â™€ï¸"},{"unified":"1f6a3-1f3fc-200d-2640-fe0f","native":"🚣ðŸ¼â€â™€ï¸"},{"unified":"1f6a3-1f3fd-200d-2640-fe0f","native":"🚣ðŸ½â€â™€ï¸"},{"unified":"1f6a3-1f3fe-200d-2640-fe0f","native":"🚣ðŸ¾â€â™€ï¸"},{"unified":"1f6a3-1f3ff-200d-2640-fe0f","native":"🚣ðŸ¿â€â™€ï¸"}],"version":4},"swimmer":{"id":"swimmer","name":"Swimmer","keywords":["person","swimming","sport","pool"],"skins":[{"unified":"1f3ca","native":"ðŸŠ"},{"unified":"1f3ca-1f3fb","native":"ðŸŠðŸ»"},{"unified":"1f3ca-1f3fc","native":"ðŸŠðŸ¼"},{"unified":"1f3ca-1f3fd","native":"ðŸŠðŸ½"},{"unified":"1f3ca-1f3fe","native":"ðŸŠðŸ¾"},{"unified":"1f3ca-1f3ff","native":"ðŸŠðŸ¿"}],"version":1},"man-swimming":{"id":"man-swimming","name":"Man Swimming","keywords":["sports","exercise","human","athlete","water","summer"],"skins":[{"unified":"1f3ca-200d-2642-fe0f","native":"ðŸŠâ€â™‚ï¸"},{"unified":"1f3ca-1f3fb-200d-2642-fe0f","native":"ðŸŠðŸ»â€â™‚ï¸"},{"unified":"1f3ca-1f3fc-200d-2642-fe0f","native":"ðŸŠðŸ¼â€â™‚ï¸"},{"unified":"1f3ca-1f3fd-200d-2642-fe0f","native":"ðŸŠðŸ½â€â™‚ï¸"},{"unified":"1f3ca-1f3fe-200d-2642-fe0f","native":"ðŸŠðŸ¾â€â™‚ï¸"},{"unified":"1f3ca-1f3ff-200d-2642-fe0f","native":"ðŸŠðŸ¿â€â™‚ï¸"}],"version":4},"woman-swimming":{"id":"woman-swimming","name":"Woman Swimming","keywords":["sports","exercise","human","athlete","water","summer","female"],"skins":[{"unified":"1f3ca-200d-2640-fe0f","native":"ðŸŠâ€â™€ï¸"},{"unified":"1f3ca-1f3fb-200d-2640-fe0f","native":"ðŸŠðŸ»â€â™€ï¸"},{"unified":"1f3ca-1f3fc-200d-2640-fe0f","native":"ðŸŠðŸ¼â€â™€ï¸"},{"unified":"1f3ca-1f3fd-200d-2640-fe0f","native":"ðŸŠðŸ½â€â™€ï¸"},{"unified":"1f3ca-1f3fe-200d-2640-fe0f","native":"ðŸŠðŸ¾â€â™€ï¸"},{"unified":"1f3ca-1f3ff-200d-2640-fe0f","native":"ðŸŠðŸ¿â€â™€ï¸"}],"version":4},"person_with_ball":{"id":"person_with_ball","name":"Person Bouncing Ball","keywords":["with","sports","human"],"skins":[{"unified":"26f9-fe0f","native":"⛹ï¸"},{"unified":"26f9-1f3fb","native":"⛹ðŸ»"},{"unified":"26f9-1f3fc","native":"⛹ðŸ¼"},{"unified":"26f9-1f3fd","native":"⛹ðŸ½"},{"unified":"26f9-1f3fe","native":"⛹ðŸ¾"},{"unified":"26f9-1f3ff","native":"⛹ðŸ¿"}],"version":1},"man-bouncing-ball":{"id":"man-bouncing-ball","name":"Man Bouncing Ball","keywords":["bouncing-ball","sport"],"skins":[{"unified":"26f9-fe0f-200d-2642-fe0f","native":"⛹ï¸â€â™‚ï¸"},{"unified":"26f9-1f3fb-200d-2642-fe0f","native":"⛹ðŸ»â€â™‚ï¸"},{"unified":"26f9-1f3fc-200d-2642-fe0f","native":"⛹ðŸ¼â€â™‚ï¸"},{"unified":"26f9-1f3fd-200d-2642-fe0f","native":"⛹ðŸ½â€â™‚ï¸"},{"unified":"26f9-1f3fe-200d-2642-fe0f","native":"⛹ðŸ¾â€â™‚ï¸"},{"unified":"26f9-1f3ff-200d-2642-fe0f","native":"⛹ðŸ¿â€â™‚ï¸"}],"version":4},"woman-bouncing-ball":{"id":"woman-bouncing-ball","name":"Woman Bouncing Ball","keywords":["bouncing-ball","sports","human","female"],"skins":[{"unified":"26f9-fe0f-200d-2640-fe0f","native":"⛹ï¸â€â™€ï¸"},{"unified":"26f9-1f3fb-200d-2640-fe0f","native":"⛹ðŸ»â€â™€ï¸"},{"unified":"26f9-1f3fc-200d-2640-fe0f","native":"⛹ðŸ¼â€â™€ï¸"},{"unified":"26f9-1f3fd-200d-2640-fe0f","native":"⛹ðŸ½â€â™€ï¸"},{"unified":"26f9-1f3fe-200d-2640-fe0f","native":"⛹ðŸ¾â€â™€ï¸"},{"unified":"26f9-1f3ff-200d-2640-fe0f","native":"⛹ðŸ¿â€â™€ï¸"}],"version":4},"weight_lifter":{"id":"weight_lifter","name":"Person Lifting Weights","keywords":["weight","lifter","sports","training","exercise"],"skins":[{"unified":"1f3cb-fe0f","native":"ðŸ‹ï¸"},{"unified":"1f3cb-1f3fb","native":"ðŸ‹ðŸ»"},{"unified":"1f3cb-1f3fc","native":"ðŸ‹ðŸ¼"},{"unified":"1f3cb-1f3fd","native":"ðŸ‹ðŸ½"},{"unified":"1f3cb-1f3fe","native":"ðŸ‹ðŸ¾"},{"unified":"1f3cb-1f3ff","native":"ðŸ‹ðŸ¿"}],"version":1},"man-lifting-weights":{"id":"man-lifting-weights","name":"Man Lifting Weights","keywords":["lifting-weights","sport"],"skins":[{"unified":"1f3cb-fe0f-200d-2642-fe0f","native":"ðŸ‹ï¸â€â™‚ï¸"},{"unified":"1f3cb-1f3fb-200d-2642-fe0f","native":"ðŸ‹ðŸ»â€â™‚ï¸"},{"unified":"1f3cb-1f3fc-200d-2642-fe0f","native":"ðŸ‹ðŸ¼â€â™‚ï¸"},{"unified":"1f3cb-1f3fd-200d-2642-fe0f","native":"ðŸ‹ðŸ½â€â™‚ï¸"},{"unified":"1f3cb-1f3fe-200d-2642-fe0f","native":"ðŸ‹ðŸ¾â€â™‚ï¸"},{"unified":"1f3cb-1f3ff-200d-2642-fe0f","native":"ðŸ‹ðŸ¿â€â™‚ï¸"}],"version":4},"woman-lifting-weights":{"id":"woman-lifting-weights","name":"Woman Lifting Weights","keywords":["lifting-weights","sports","training","exercise","female"],"skins":[{"unified":"1f3cb-fe0f-200d-2640-fe0f","native":"ðŸ‹ï¸â€â™€ï¸"},{"unified":"1f3cb-1f3fb-200d-2640-fe0f","native":"ðŸ‹ðŸ»â€â™€ï¸"},{"unified":"1f3cb-1f3fc-200d-2640-fe0f","native":"ðŸ‹ðŸ¼â€â™€ï¸"},{"unified":"1f3cb-1f3fd-200d-2640-fe0f","native":"ðŸ‹ðŸ½â€â™€ï¸"},{"unified":"1f3cb-1f3fe-200d-2640-fe0f","native":"ðŸ‹ðŸ¾â€â™€ï¸"},{"unified":"1f3cb-1f3ff-200d-2640-fe0f","native":"ðŸ‹ðŸ¿â€â™€ï¸"}],"version":4},"bicyclist":{"id":"bicyclist","name":"Bicyclist","keywords":["person","biking","sport","move"],"skins":[{"unified":"1f6b4","native":"🚴"},{"unified":"1f6b4-1f3fb","native":"🚴ðŸ»"},{"unified":"1f6b4-1f3fc","native":"🚴ðŸ¼"},{"unified":"1f6b4-1f3fd","native":"🚴ðŸ½"},{"unified":"1f6b4-1f3fe","native":"🚴ðŸ¾"},{"unified":"1f6b4-1f3ff","native":"🚴ðŸ¿"}],"version":1},"man-biking":{"id":"man-biking","name":"Man Biking","keywords":["sports","bike","exercise","hipster"],"skins":[{"unified":"1f6b4-200d-2642-fe0f","native":"🚴â€â™‚ï¸"},{"unified":"1f6b4-1f3fb-200d-2642-fe0f","native":"🚴ðŸ»â€â™‚ï¸"},{"unified":"1f6b4-1f3fc-200d-2642-fe0f","native":"🚴ðŸ¼â€â™‚ï¸"},{"unified":"1f6b4-1f3fd-200d-2642-fe0f","native":"🚴ðŸ½â€â™‚ï¸"},{"unified":"1f6b4-1f3fe-200d-2642-fe0f","native":"🚴ðŸ¾â€â™‚ï¸"},{"unified":"1f6b4-1f3ff-200d-2642-fe0f","native":"🚴ðŸ¿â€â™‚ï¸"}],"version":4},"woman-biking":{"id":"woman-biking","name":"Woman Biking","keywords":["sports","bike","exercise","hipster","female"],"skins":[{"unified":"1f6b4-200d-2640-fe0f","native":"🚴â€â™€ï¸"},{"unified":"1f6b4-1f3fb-200d-2640-fe0f","native":"🚴ðŸ»â€â™€ï¸"},{"unified":"1f6b4-1f3fc-200d-2640-fe0f","native":"🚴ðŸ¼â€â™€ï¸"},{"unified":"1f6b4-1f3fd-200d-2640-fe0f","native":"🚴ðŸ½â€â™€ï¸"},{"unified":"1f6b4-1f3fe-200d-2640-fe0f","native":"🚴ðŸ¾â€â™€ï¸"},{"unified":"1f6b4-1f3ff-200d-2640-fe0f","native":"🚴ðŸ¿â€â™€ï¸"}],"version":4},"mountain_bicyclist":{"id":"mountain_bicyclist","name":"Mountain Bicyclist","keywords":["person","biking","sport","move"],"skins":[{"unified":"1f6b5","native":"🚵"},{"unified":"1f6b5-1f3fb","native":"🚵ðŸ»"},{"unified":"1f6b5-1f3fc","native":"🚵ðŸ¼"},{"unified":"1f6b5-1f3fd","native":"🚵ðŸ½"},{"unified":"1f6b5-1f3fe","native":"🚵ðŸ¾"},{"unified":"1f6b5-1f3ff","native":"🚵ðŸ¿"}],"version":1},"man-mountain-biking":{"id":"man-mountain-biking","name":"Man Mountain Biking","keywords":["mountain-biking","transportation","sports","human","race","bike"],"skins":[{"unified":"1f6b5-200d-2642-fe0f","native":"🚵â€â™‚ï¸"},{"unified":"1f6b5-1f3fb-200d-2642-fe0f","native":"🚵ðŸ»â€â™‚ï¸"},{"unified":"1f6b5-1f3fc-200d-2642-fe0f","native":"🚵ðŸ¼â€â™‚ï¸"},{"unified":"1f6b5-1f3fd-200d-2642-fe0f","native":"🚵ðŸ½â€â™‚ï¸"},{"unified":"1f6b5-1f3fe-200d-2642-fe0f","native":"🚵ðŸ¾â€â™‚ï¸"},{"unified":"1f6b5-1f3ff-200d-2642-fe0f","native":"🚵ðŸ¿â€â™‚ï¸"}],"version":4},"woman-mountain-biking":{"id":"woman-mountain-biking","name":"Woman Mountain Biking","keywords":["mountain-biking","transportation","sports","human","race","bike","female"],"skins":[{"unified":"1f6b5-200d-2640-fe0f","native":"🚵â€â™€ï¸"},{"unified":"1f6b5-1f3fb-200d-2640-fe0f","native":"🚵ðŸ»â€â™€ï¸"},{"unified":"1f6b5-1f3fc-200d-2640-fe0f","native":"🚵ðŸ¼â€â™€ï¸"},{"unified":"1f6b5-1f3fd-200d-2640-fe0f","native":"🚵ðŸ½â€â™€ï¸"},{"unified":"1f6b5-1f3fe-200d-2640-fe0f","native":"🚵ðŸ¾â€â™€ï¸"},{"unified":"1f6b5-1f3ff-200d-2640-fe0f","native":"🚵ðŸ¿â€â™€ï¸"}],"version":4},"person_doing_cartwheel":{"id":"person_doing_cartwheel","name":"Person Cartwheeling","keywords":["doing","cartwheel","sport","gymnastic"],"skins":[{"unified":"1f938","native":"🤸"},{"unified":"1f938-1f3fb","native":"🤸ðŸ»"},{"unified":"1f938-1f3fc","native":"🤸ðŸ¼"},{"unified":"1f938-1f3fd","native":"🤸ðŸ½"},{"unified":"1f938-1f3fe","native":"🤸ðŸ¾"},{"unified":"1f938-1f3ff","native":"🤸ðŸ¿"}],"version":3},"man-cartwheeling":{"id":"man-cartwheeling","name":"Man Cartwheeling","keywords":["gymnastics"],"skins":[{"unified":"1f938-200d-2642-fe0f","native":"🤸â€â™‚ï¸"},{"unified":"1f938-1f3fb-200d-2642-fe0f","native":"🤸ðŸ»â€â™‚ï¸"},{"unified":"1f938-1f3fc-200d-2642-fe0f","native":"🤸ðŸ¼â€â™‚ï¸"},{"unified":"1f938-1f3fd-200d-2642-fe0f","native":"🤸ðŸ½â€â™‚ï¸"},{"unified":"1f938-1f3fe-200d-2642-fe0f","native":"🤸ðŸ¾â€â™‚ï¸"},{"unified":"1f938-1f3ff-200d-2642-fe0f","native":"🤸ðŸ¿â€â™‚ï¸"}],"version":4},"woman-cartwheeling":{"id":"woman-cartwheeling","name":"Woman Cartwheeling","keywords":["gymnastics"],"skins":[{"unified":"1f938-200d-2640-fe0f","native":"🤸â€â™€ï¸"},{"unified":"1f938-1f3fb-200d-2640-fe0f","native":"🤸ðŸ»â€â™€ï¸"},{"unified":"1f938-1f3fc-200d-2640-fe0f","native":"🤸ðŸ¼â€â™€ï¸"},{"unified":"1f938-1f3fd-200d-2640-fe0f","native":"🤸ðŸ½â€â™€ï¸"},{"unified":"1f938-1f3fe-200d-2640-fe0f","native":"🤸ðŸ¾â€â™€ï¸"},{"unified":"1f938-1f3ff-200d-2640-fe0f","native":"🤸ðŸ¿â€â™€ï¸"}],"version":4},"wrestlers":{"id":"wrestlers","name":"Wrestlers","keywords":["people","wrestling","sport"],"skins":[{"unified":"1f93c","native":"🤼"}],"version":3},"man-wrestling":{"id":"man-wrestling","name":"Men Wrestling","keywords":["man","sports","wrestlers"],"skins":[{"unified":"1f93c-200d-2642-fe0f","native":"🤼â€â™‚ï¸"}],"version":4},"woman-wrestling":{"id":"woman-wrestling","name":"Women Wrestling","keywords":["woman","sports","wrestlers"],"skins":[{"unified":"1f93c-200d-2640-fe0f","native":"🤼â€â™€ï¸"}],"version":4},"water_polo":{"id":"water_polo","name":"Water Polo","keywords":["person","playing","sport"],"skins":[{"unified":"1f93d","native":"🤽"},{"unified":"1f93d-1f3fb","native":"🤽ðŸ»"},{"unified":"1f93d-1f3fc","native":"🤽ðŸ¼"},{"unified":"1f93d-1f3fd","native":"🤽ðŸ½"},{"unified":"1f93d-1f3fe","native":"🤽ðŸ¾"},{"unified":"1f93d-1f3ff","native":"🤽ðŸ¿"}],"version":3},"man-playing-water-polo":{"id":"man-playing-water-polo","name":"Man Playing Water Polo","keywords":["playing-water-polo","sports","pool"],"skins":[{"unified":"1f93d-200d-2642-fe0f","native":"🤽â€â™‚ï¸"},{"unified":"1f93d-1f3fb-200d-2642-fe0f","native":"🤽ðŸ»â€â™‚ï¸"},{"unified":"1f93d-1f3fc-200d-2642-fe0f","native":"🤽ðŸ¼â€â™‚ï¸"},{"unified":"1f93d-1f3fd-200d-2642-fe0f","native":"🤽ðŸ½â€â™‚ï¸"},{"unified":"1f93d-1f3fe-200d-2642-fe0f","native":"🤽ðŸ¾â€â™‚ï¸"},{"unified":"1f93d-1f3ff-200d-2642-fe0f","native":"🤽ðŸ¿â€â™‚ï¸"}],"version":4},"woman-playing-water-polo":{"id":"woman-playing-water-polo","name":"Woman Playing Water Polo","keywords":["playing-water-polo","sports","pool"],"skins":[{"unified":"1f93d-200d-2640-fe0f","native":"🤽â€â™€ï¸"},{"unified":"1f93d-1f3fb-200d-2640-fe0f","native":"🤽ðŸ»â€â™€ï¸"},{"unified":"1f93d-1f3fc-200d-2640-fe0f","native":"🤽ðŸ¼â€â™€ï¸"},{"unified":"1f93d-1f3fd-200d-2640-fe0f","native":"🤽ðŸ½â€â™€ï¸"},{"unified":"1f93d-1f3fe-200d-2640-fe0f","native":"🤽ðŸ¾â€â™€ï¸"},{"unified":"1f93d-1f3ff-200d-2640-fe0f","native":"🤽ðŸ¿â€â™€ï¸"}],"version":4},"handball":{"id":"handball","name":"Handball","keywords":["person","playing","sport"],"skins":[{"unified":"1f93e","native":"🤾"},{"unified":"1f93e-1f3fb","native":"🤾ðŸ»"},{"unified":"1f93e-1f3fc","native":"🤾ðŸ¼"},{"unified":"1f93e-1f3fd","native":"🤾ðŸ½"},{"unified":"1f93e-1f3fe","native":"🤾ðŸ¾"},{"unified":"1f93e-1f3ff","native":"🤾ðŸ¿"}],"version":3},"man-playing-handball":{"id":"man-playing-handball","name":"Man Playing Handball","keywords":["playing-handball","sports"],"skins":[{"unified":"1f93e-200d-2642-fe0f","native":"🤾â€â™‚ï¸"},{"unified":"1f93e-1f3fb-200d-2642-fe0f","native":"🤾ðŸ»â€â™‚ï¸"},{"unified":"1f93e-1f3fc-200d-2642-fe0f","native":"🤾ðŸ¼â€â™‚ï¸"},{"unified":"1f93e-1f3fd-200d-2642-fe0f","native":"🤾ðŸ½â€â™‚ï¸"},{"unified":"1f93e-1f3fe-200d-2642-fe0f","native":"🤾ðŸ¾â€â™‚ï¸"},{"unified":"1f93e-1f3ff-200d-2642-fe0f","native":"🤾ðŸ¿â€â™‚ï¸"}],"version":4},"woman-playing-handball":{"id":"woman-playing-handball","name":"Woman Playing Handball","keywords":["playing-handball","sports"],"skins":[{"unified":"1f93e-200d-2640-fe0f","native":"🤾â€â™€ï¸"},{"unified":"1f93e-1f3fb-200d-2640-fe0f","native":"🤾ðŸ»â€â™€ï¸"},{"unified":"1f93e-1f3fc-200d-2640-fe0f","native":"🤾ðŸ¼â€â™€ï¸"},{"unified":"1f93e-1f3fd-200d-2640-fe0f","native":"🤾ðŸ½â€â™€ï¸"},{"unified":"1f93e-1f3fe-200d-2640-fe0f","native":"🤾ðŸ¾â€â™€ï¸"},{"unified":"1f93e-1f3ff-200d-2640-fe0f","native":"🤾ðŸ¿â€â™€ï¸"}],"version":4},"juggling":{"id":"juggling","name":"Juggling","keywords":["person","performance","balance"],"skins":[{"unified":"1f939","native":"🤹"},{"unified":"1f939-1f3fb","native":"🤹ðŸ»"},{"unified":"1f939-1f3fc","native":"🤹ðŸ¼"},{"unified":"1f939-1f3fd","native":"🤹ðŸ½"},{"unified":"1f939-1f3fe","native":"🤹ðŸ¾"},{"unified":"1f939-1f3ff","native":"🤹ðŸ¿"}],"version":3},"man-juggling":{"id":"man-juggling","name":"Man Juggling","keywords":["juggle","balance","skill","multitask"],"skins":[{"unified":"1f939-200d-2642-fe0f","native":"🤹â€â™‚ï¸"},{"unified":"1f939-1f3fb-200d-2642-fe0f","native":"🤹ðŸ»â€â™‚ï¸"},{"unified":"1f939-1f3fc-200d-2642-fe0f","native":"🤹ðŸ¼â€â™‚ï¸"},{"unified":"1f939-1f3fd-200d-2642-fe0f","native":"🤹ðŸ½â€â™‚ï¸"},{"unified":"1f939-1f3fe-200d-2642-fe0f","native":"🤹ðŸ¾â€â™‚ï¸"},{"unified":"1f939-1f3ff-200d-2642-fe0f","native":"🤹ðŸ¿â€â™‚ï¸"}],"version":4},"woman-juggling":{"id":"woman-juggling","name":"Woman Juggling","keywords":["juggle","balance","skill","multitask"],"skins":[{"unified":"1f939-200d-2640-fe0f","native":"🤹â€â™€ï¸"},{"unified":"1f939-1f3fb-200d-2640-fe0f","native":"🤹ðŸ»â€â™€ï¸"},{"unified":"1f939-1f3fc-200d-2640-fe0f","native":"🤹ðŸ¼â€â™€ï¸"},{"unified":"1f939-1f3fd-200d-2640-fe0f","native":"🤹ðŸ½â€â™€ï¸"},{"unified":"1f939-1f3fe-200d-2640-fe0f","native":"🤹ðŸ¾â€â™€ï¸"},{"unified":"1f939-1f3ff-200d-2640-fe0f","native":"🤹ðŸ¿â€â™€ï¸"}],"version":4},"person_in_lotus_position":{"id":"person_in_lotus_position","name":"Person in Lotus Position","keywords":["meditate"],"skins":[{"unified":"1f9d8","native":"🧘"},{"unified":"1f9d8-1f3fb","native":"🧘ðŸ»"},{"unified":"1f9d8-1f3fc","native":"🧘ðŸ¼"},{"unified":"1f9d8-1f3fd","native":"🧘ðŸ½"},{"unified":"1f9d8-1f3fe","native":"🧘ðŸ¾"},{"unified":"1f9d8-1f3ff","native":"🧘ðŸ¿"}],"version":5},"man_in_lotus_position":{"id":"man_in_lotus_position","name":"Man in Lotus Position","keywords":["male","meditation","yoga","serenity","zen","mindfulness"],"skins":[{"unified":"1f9d8-200d-2642-fe0f","native":"🧘â€â™‚ï¸"},{"unified":"1f9d8-1f3fb-200d-2642-fe0f","native":"🧘ðŸ»â€â™‚ï¸"},{"unified":"1f9d8-1f3fc-200d-2642-fe0f","native":"🧘ðŸ¼â€â™‚ï¸"},{"unified":"1f9d8-1f3fd-200d-2642-fe0f","native":"🧘ðŸ½â€â™‚ï¸"},{"unified":"1f9d8-1f3fe-200d-2642-fe0f","native":"🧘ðŸ¾â€â™‚ï¸"},{"unified":"1f9d8-1f3ff-200d-2642-fe0f","native":"🧘ðŸ¿â€â™‚ï¸"}],"version":5},"woman_in_lotus_position":{"id":"woman_in_lotus_position","name":"Woman in Lotus Position","keywords":["female","meditation","yoga","serenity","zen","mindfulness"],"skins":[{"unified":"1f9d8-200d-2640-fe0f","native":"🧘â€â™€ï¸"},{"unified":"1f9d8-1f3fb-200d-2640-fe0f","native":"🧘ðŸ»â€â™€ï¸"},{"unified":"1f9d8-1f3fc-200d-2640-fe0f","native":"🧘ðŸ¼â€â™€ï¸"},{"unified":"1f9d8-1f3fd-200d-2640-fe0f","native":"🧘ðŸ½â€â™€ï¸"},{"unified":"1f9d8-1f3fe-200d-2640-fe0f","native":"🧘ðŸ¾â€â™€ï¸"},{"unified":"1f9d8-1f3ff-200d-2640-fe0f","native":"🧘ðŸ¿â€â™€ï¸"}],"version":5},"bath":{"id":"bath","name":"Bath","keywords":["person","taking","clean","shower","bathroom"],"skins":[{"unified":"1f6c0","native":"🛀"},{"unified":"1f6c0-1f3fb","native":"🛀ðŸ»"},{"unified":"1f6c0-1f3fc","native":"🛀ðŸ¼"},{"unified":"1f6c0-1f3fd","native":"🛀ðŸ½"},{"unified":"1f6c0-1f3fe","native":"🛀ðŸ¾"},{"unified":"1f6c0-1f3ff","native":"🛀ðŸ¿"}],"version":1},"sleeping_accommodation":{"id":"sleeping_accommodation","name":"Person in Bed","keywords":["sleeping","accommodation","rest"],"skins":[{"unified":"1f6cc","native":"🛌"},{"unified":"1f6cc-1f3fb","native":"🛌ðŸ»"},{"unified":"1f6cc-1f3fc","native":"🛌ðŸ¼"},{"unified":"1f6cc-1f3fd","native":"🛌ðŸ½"},{"unified":"1f6cc-1f3fe","native":"🛌ðŸ¾"},{"unified":"1f6cc-1f3ff","native":"🛌ðŸ¿"}],"version":1},"people_holding_hands":{"id":"people_holding_hands","name":"People Holding Hands","keywords":["friendship"],"skins":[{"unified":"1f9d1-200d-1f91d-200d-1f9d1","native":"🧑â€ðŸ¤â€ðŸ§‘"},{"unified":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb","native":"🧑ðŸ»â€ðŸ¤â€ðŸ§‘ðŸ»"},{"unified":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc","native":"🧑ðŸ¼â€ðŸ¤â€ðŸ§‘ðŸ¼"},{"unified":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd","native":"🧑ðŸ½â€ðŸ¤â€ðŸ§‘ðŸ½"},{"unified":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe","native":"🧑ðŸ¾â€ðŸ¤â€ðŸ§‘ðŸ¾"},{"unified":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff","native":"🧑ðŸ¿â€ðŸ¤â€ðŸ§‘ðŸ¿"}],"version":12},"two_women_holding_hands":{"id":"two_women_holding_hands","name":"Women Holding Hands","keywords":["two","pair","friendship","couple","love","like","female","people","human"],"skins":[{"unified":"1f46d","native":"ðŸ‘"},{"unified":"1f46d-1f3fb","native":"ðŸ‘ðŸ»"},{"unified":"1f46d-1f3fc","native":"ðŸ‘ðŸ¼"},{"unified":"1f46d-1f3fd","native":"ðŸ‘ðŸ½"},{"unified":"1f46d-1f3fe","native":"ðŸ‘ðŸ¾"},{"unified":"1f46d-1f3ff","native":"ðŸ‘ðŸ¿"}],"version":1},"man_and_woman_holding_hands":{"id":"man_and_woman_holding_hands","name":"Man and Woman Holding Hands","keywords":["couple","pair","people","human","love","date","dating","like","affection","valentines","marriage"],"skins":[{"unified":"1f46b","native":"👫"},{"unified":"1f46b-1f3fb","native":"👫ðŸ»"},{"unified":"1f46b-1f3fc","native":"👫ðŸ¼"},{"unified":"1f46b-1f3fd","native":"👫ðŸ½"},{"unified":"1f46b-1f3fe","native":"👫ðŸ¾"},{"unified":"1f46b-1f3ff","native":"👫ðŸ¿"}],"version":1},"two_men_holding_hands":{"id":"two_men_holding_hands","name":"Men Holding Hands","keywords":["two","pair","couple","love","like","bromance","friendship","people","human"],"skins":[{"unified":"1f46c","native":"👬"},{"unified":"1f46c-1f3fb","native":"👬ðŸ»"},{"unified":"1f46c-1f3fc","native":"👬ðŸ¼"},{"unified":"1f46c-1f3fd","native":"👬ðŸ½"},{"unified":"1f46c-1f3fe","native":"👬ðŸ¾"},{"unified":"1f46c-1f3ff","native":"👬ðŸ¿"}],"version":1},"couplekiss":{"id":"couplekiss","name":"Kiss","keywords":["couplekiss","pair","valentines","love","like","dating","marriage"],"skins":[{"unified":"1f48f","native":"ðŸ’"},{"unified":"1f48f-1f3fb","native":"ðŸ’ðŸ»"},{"unified":"1f48f-1f3fc","native":"ðŸ’ðŸ¼"},{"unified":"1f48f-1f3fd","native":"ðŸ’ðŸ½"},{"unified":"1f48f-1f3fe","native":"ðŸ’ðŸ¾"},{"unified":"1f48f-1f3ff","native":"ðŸ’ðŸ¿"}],"version":1},"woman-kiss-man":{"id":"woman-kiss-man","name":"Kiss: Woman, Man","keywords":["woman","kiss-man","kiss","love"],"skins":[{"unified":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468","native":"👩â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨"},{"unified":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","native":"👩ðŸ»â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ»"},{"unified":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","native":"👩ðŸ¼â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ¼"},{"unified":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","native":"👩ðŸ½â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ½"},{"unified":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","native":"👩ðŸ¾â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ¾"},{"unified":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","native":"👩ðŸ¿â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ¿"}],"version":2},"man-kiss-man":{"id":"man-kiss-man","name":"Kiss: Man, Man","keywords":["kiss-man","kiss","pair","valentines","love","like","dating","marriage"],"skins":[{"unified":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","native":"👨â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨"},{"unified":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb","native":"👨ðŸ»â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ»"},{"unified":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc","native":"👨ðŸ¼â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ¼"},{"unified":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd","native":"👨ðŸ½â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ½"},{"unified":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe","native":"👨ðŸ¾â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ¾"},{"unified":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff","native":"👨ðŸ¿â€â¤ï¸â€ðŸ’‹â€ðŸ‘¨ðŸ¿"}],"version":2},"woman-kiss-woman":{"id":"woman-kiss-woman","name":"Kiss: Woman, Woman","keywords":["kiss-woman","kiss","pair","valentines","love","like","dating","marriage"],"skins":[{"unified":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","native":"👩â€â¤ï¸â€ðŸ’‹â€ðŸ‘©"},{"unified":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb","native":"👩ðŸ»â€â¤ï¸â€ðŸ’‹â€ðŸ‘©ðŸ»"},{"unified":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc","native":"👩ðŸ¼â€â¤ï¸â€ðŸ’‹â€ðŸ‘©ðŸ¼"},{"unified":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd","native":"👩ðŸ½â€â¤ï¸â€ðŸ’‹â€ðŸ‘©ðŸ½"},{"unified":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe","native":"👩ðŸ¾â€â¤ï¸â€ðŸ’‹â€ðŸ‘©ðŸ¾"},{"unified":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff","native":"👩ðŸ¿â€â¤ï¸â€ðŸ’‹â€ðŸ‘©ðŸ¿"}],"version":2},"couple_with_heart":{"id":"couple_with_heart","name":"Couple with Heart","keywords":["pair","love","like","affection","human","dating","valentines","marriage"],"skins":[{"unified":"1f491","native":"💑"},{"unified":"1f491-1f3fb","native":"💑ðŸ»"},{"unified":"1f491-1f3fc","native":"💑ðŸ¼"},{"unified":"1f491-1f3fd","native":"💑ðŸ½"},{"unified":"1f491-1f3fe","native":"💑ðŸ¾"},{"unified":"1f491-1f3ff","native":"💑ðŸ¿"}],"version":1},"woman-heart-man":{"id":"woman-heart-man","name":"Couple with Heart: Woman, Man","keywords":["woman","heart-man","heart","love"],"skins":[{"unified":"1f469-200d-2764-fe0f-200d-1f468","native":"👩â€â¤ï¸â€ðŸ‘¨"},{"unified":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","native":"👩ðŸ»â€â¤ï¸â€ðŸ‘¨ðŸ»"},{"unified":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","native":"👩ðŸ¼â€â¤ï¸â€ðŸ‘¨ðŸ¼"},{"unified":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","native":"👩ðŸ½â€â¤ï¸â€ðŸ‘¨ðŸ½"},{"unified":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","native":"👩ðŸ¾â€â¤ï¸â€ðŸ‘¨ðŸ¾"},{"unified":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff","native":"👩ðŸ¿â€â¤ï¸â€ðŸ‘¨ðŸ¿"}],"version":2},"man-heart-man":{"id":"man-heart-man","name":"Couple with Heart: Man, Man","keywords":["heart-man","heart","pair","love","like","affection","human","dating","valentines","marriage"],"skins":[{"unified":"1f468-200d-2764-fe0f-200d-1f468","native":"👨â€â¤ï¸â€ðŸ‘¨"},{"unified":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb","native":"👨ðŸ»â€â¤ï¸â€ðŸ‘¨ðŸ»"},{"unified":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc","native":"👨ðŸ¼â€â¤ï¸â€ðŸ‘¨ðŸ¼"},{"unified":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd","native":"👨ðŸ½â€â¤ï¸â€ðŸ‘¨ðŸ½"},{"unified":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe","native":"👨ðŸ¾â€â¤ï¸â€ðŸ‘¨ðŸ¾"},{"unified":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff","native":"👨ðŸ¿â€â¤ï¸â€ðŸ‘¨ðŸ¿"}],"version":2},"woman-heart-woman":{"id":"woman-heart-woman","name":"Couple with Heart: Woman, Woman","keywords":["heart-woman","heart","pair","love","like","affection","human","dating","valentines","marriage"],"skins":[{"unified":"1f469-200d-2764-fe0f-200d-1f469","native":"👩â€â¤ï¸â€ðŸ‘©"},{"unified":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb","native":"👩ðŸ»â€â¤ï¸â€ðŸ‘©ðŸ»"},{"unified":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc","native":"👩ðŸ¼â€â¤ï¸â€ðŸ‘©ðŸ¼"},{"unified":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd","native":"👩ðŸ½â€â¤ï¸â€ðŸ‘©ðŸ½"},{"unified":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe","native":"👩ðŸ¾â€â¤ï¸â€ðŸ‘©ðŸ¾"},{"unified":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff","native":"👩ðŸ¿â€â¤ï¸â€ðŸ‘©ðŸ¿"}],"version":2},"family":{"id":"family","name":"Family","keywords":["home","parents","child","mom","dad","father","mother","people","human"],"skins":[{"unified":"1f46a","native":"👪"}],"version":1},"man-woman-boy":{"id":"man-woman-boy","name":"Family: Man, Woman, Boy","keywords":["man","woman-boy","family","woman","love"],"skins":[{"unified":"1f468-200d-1f469-200d-1f466","native":"👨â€ðŸ‘©â€ðŸ‘¦"}],"version":2},"man-woman-girl":{"id":"man-woman-girl","name":"Family: Man, Woman, Girl","keywords":["man","woman-girl","family","woman","home","parents","people","human","child"],"skins":[{"unified":"1f468-200d-1f469-200d-1f467","native":"👨â€ðŸ‘©â€ðŸ‘§"}],"version":2},"man-woman-girl-boy":{"id":"man-woman-girl-boy","name":"Family: Man, Woman, Girl, Boy","keywords":["man","woman-girl-boy","family","woman","girl","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f469-200d-1f467-200d-1f466","native":"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦"}],"version":2},"man-woman-boy-boy":{"id":"man-woman-boy-boy","name":"Family: Man, Woman, Boy, Boy","keywords":["man","woman-boy-boy","family","woman","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f469-200d-1f466-200d-1f466","native":"👨â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦"}],"version":2},"man-woman-girl-girl":{"id":"man-woman-girl-girl","name":"Family: Man, Woman, Girl, Girl","keywords":["man","woman-girl-girl","family","woman","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f469-200d-1f467-200d-1f467","native":"👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§"}],"version":2},"man-man-boy":{"id":"man-man-boy","name":"Family: Man, Man, Boy","keywords":["man","man-boy","family","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f468-200d-1f466","native":"👨â€ðŸ‘¨â€ðŸ‘¦"}],"version":2},"man-man-girl":{"id":"man-man-girl","name":"Family: Man, Man, Girl","keywords":["man","man-girl","family","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f468-200d-1f467","native":"👨â€ðŸ‘¨â€ðŸ‘§"}],"version":2},"man-man-girl-boy":{"id":"man-man-girl-boy","name":"Family: Man, Man, Girl, Boy","keywords":["man","man-girl-boy","family","girl","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f468-200d-1f467-200d-1f466","native":"👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘¦"}],"version":2},"man-man-boy-boy":{"id":"man-man-boy-boy","name":"Family: Man, Man, Boy, Boy","keywords":["man","man-boy-boy","family","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f468-200d-1f466-200d-1f466","native":"👨â€ðŸ‘¨â€ðŸ‘¦â€ðŸ‘¦"}],"version":2},"man-man-girl-girl":{"id":"man-man-girl-girl","name":"Family: Man, Man, Girl, Girl","keywords":["man","man-girl-girl","family","home","parents","people","human","children"],"skins":[{"unified":"1f468-200d-1f468-200d-1f467-200d-1f467","native":"👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘§"}],"version":2},"woman-woman-boy":{"id":"woman-woman-boy","name":"Family: Woman, Woman, Boy","keywords":["woman","woman-boy","family","home","parents","people","human","children"],"skins":[{"unified":"1f469-200d-1f469-200d-1f466","native":"👩â€ðŸ‘©â€ðŸ‘¦"}],"version":2},"woman-woman-girl":{"id":"woman-woman-girl","name":"Family: Woman, Woman, Girl","keywords":["woman","woman-girl","family","home","parents","people","human","children"],"skins":[{"unified":"1f469-200d-1f469-200d-1f467","native":"👩â€ðŸ‘©â€ðŸ‘§"}],"version":2},"woman-woman-girl-boy":{"id":"woman-woman-girl-boy","name":"Family: Woman, Woman, Girl, Boy","keywords":["woman","woman-girl-boy","family","girl","home","parents","people","human","children"],"skins":[{"unified":"1f469-200d-1f469-200d-1f467-200d-1f466","native":"👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦"}],"version":2},"woman-woman-boy-boy":{"id":"woman-woman-boy-boy","name":"Family: Woman, Woman, Boy, Boy","keywords":["woman","woman-boy-boy","family","home","parents","people","human","children"],"skins":[{"unified":"1f469-200d-1f469-200d-1f466-200d-1f466","native":"👩â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦"}],"version":2},"woman-woman-girl-girl":{"id":"woman-woman-girl-girl","name":"Family: Woman, Woman, Girl, Girl","keywords":["woman","woman-girl-girl","family","home","parents","people","human","children"],"skins":[{"unified":"1f469-200d-1f469-200d-1f467-200d-1f467","native":"👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§"}],"version":2},"man-boy":{"id":"man-boy","name":"Family: Man, Boy","keywords":["man","family","home","parent","people","human","child"],"skins":[{"unified":"1f468-200d-1f466","native":"👨â€ðŸ‘¦"}],"version":4},"man-boy-boy":{"id":"man-boy-boy","name":"Family: Man, Boy, Boy","keywords":["man","boy-boy","family","home","parent","people","human","children"],"skins":[{"unified":"1f468-200d-1f466-200d-1f466","native":"👨â€ðŸ‘¦â€ðŸ‘¦"}],"version":4},"man-girl":{"id":"man-girl","name":"Family: Man, Girl","keywords":["man","family","home","parent","people","human","child"],"skins":[{"unified":"1f468-200d-1f467","native":"👨â€ðŸ‘§"}],"version":4},"man-girl-boy":{"id":"man-girl-boy","name":"Family: Man, Girl, Boy","keywords":["man","girl-boy","family","girl","home","parent","people","human","children"],"skins":[{"unified":"1f468-200d-1f467-200d-1f466","native":"👨â€ðŸ‘§â€ðŸ‘¦"}],"version":4},"man-girl-girl":{"id":"man-girl-girl","name":"Family: Man, Girl, Girl","keywords":["man","girl-girl","family","home","parent","people","human","children"],"skins":[{"unified":"1f468-200d-1f467-200d-1f467","native":"👨â€ðŸ‘§â€ðŸ‘§"}],"version":4},"woman-boy":{"id":"woman-boy","name":"Family: Woman, Boy","keywords":["woman","family","home","parent","people","human","child"],"skins":[{"unified":"1f469-200d-1f466","native":"👩â€ðŸ‘¦"}],"version":4},"woman-boy-boy":{"id":"woman-boy-boy","name":"Family: Woman, Boy, Boy","keywords":["woman","boy-boy","family","home","parent","people","human","children"],"skins":[{"unified":"1f469-200d-1f466-200d-1f466","native":"👩â€ðŸ‘¦â€ðŸ‘¦"}],"version":4},"woman-girl":{"id":"woman-girl","name":"Family: Woman, Girl","keywords":["woman","family","home","parent","people","human","child"],"skins":[{"unified":"1f469-200d-1f467","native":"👩â€ðŸ‘§"}],"version":4},"woman-girl-boy":{"id":"woman-girl-boy","name":"Family: Woman, Girl, Boy","keywords":["woman","girl-boy","family","girl","home","parent","people","human","children"],"skins":[{"unified":"1f469-200d-1f467-200d-1f466","native":"👩â€ðŸ‘§â€ðŸ‘¦"}],"version":4},"woman-girl-girl":{"id":"woman-girl-girl","name":"Family: Woman, Girl, Girl","keywords":["woman","girl-girl","family","home","parent","people","human","children"],"skins":[{"unified":"1f469-200d-1f467-200d-1f467","native":"👩â€ðŸ‘§â€ðŸ‘§"}],"version":4},"speaking_head_in_silhouette":{"id":"speaking_head_in_silhouette","name":"Speaking Head","keywords":["in","silhouette","user","person","human","sing","say","talk"],"skins":[{"unified":"1f5e3-fe0f","native":"🗣ï¸"}],"version":1},"bust_in_silhouette":{"id":"bust_in_silhouette","name":"Bust in Silhouette","keywords":["user","person","human"],"skins":[{"unified":"1f464","native":"👤"}],"version":1},"busts_in_silhouette":{"id":"busts_in_silhouette","name":"Busts in Silhouette","keywords":["user","person","human","group","team"],"skins":[{"unified":"1f465","native":"👥"}],"version":1},"people_hugging":{"id":"people_hugging","name":"People Hugging","keywords":["care"],"skins":[{"unified":"1fac2","native":"🫂"}],"version":13},"footprints":{"id":"footprints","name":"Footprints","keywords":["feet","tracking","walking","beach"],"skins":[{"unified":"1f463","native":"👣"}],"version":1},"monkey_face":{"id":"monkey_face","name":"Monkey Face","emoticons":[":o)"],"keywords":["animal","nature","circus"],"skins":[{"unified":"1f435","native":"ðŸµ"}],"version":1},"monkey":{"id":"monkey","name":"Monkey","keywords":["animal","nature","banana","circus"],"skins":[{"unified":"1f412","native":"ðŸ’"}],"version":1},"gorilla":{"id":"gorilla","name":"Gorilla","keywords":["animal","nature","circus"],"skins":[{"unified":"1f98d","native":"ðŸ¦"}],"version":3},"orangutan":{"id":"orangutan","name":"Orangutan","keywords":["animal"],"skins":[{"unified":"1f9a7","native":"🦧"}],"version":12},"dog":{"id":"dog","name":"Dog Face","keywords":["animal","friend","nature","woof","puppy","pet","faithful"],"skins":[{"unified":"1f436","native":"ðŸ¶"}],"version":1},"dog2":{"id":"dog2","name":"Dog","keywords":["dog2","animal","nature","friend","doge","pet","faithful"],"skins":[{"unified":"1f415","native":"ðŸ•"}],"version":1},"guide_dog":{"id":"guide_dog","name":"Guide Dog","keywords":["animal","blind"],"skins":[{"unified":"1f9ae","native":"🦮"}],"version":12},"service_dog":{"id":"service_dog","name":"Service Dog","keywords":["blind","animal"],"skins":[{"unified":"1f415-200d-1f9ba","native":"ðŸ•â€ðŸ¦º"}],"version":12},"poodle":{"id":"poodle","name":"Poodle","keywords":["dog","animal","101","nature","pet"],"skins":[{"unified":"1f429","native":"ðŸ©"}],"version":1},"wolf":{"id":"wolf","name":"Wolf","keywords":["animal","nature","wild"],"skins":[{"unified":"1f43a","native":"ðŸº"}],"version":1},"fox_face":{"id":"fox_face","name":"Fox","keywords":["face","animal","nature"],"skins":[{"unified":"1f98a","native":"🦊"}],"version":3},"raccoon":{"id":"raccoon","name":"Raccoon","keywords":["animal","nature"],"skins":[{"unified":"1f99d","native":"ðŸ¦"}],"version":11},"cat":{"id":"cat","name":"Cat Face","keywords":["animal","meow","nature","pet","kitten"],"skins":[{"unified":"1f431","native":"ðŸ±"}],"version":1},"cat2":{"id":"cat2","name":"Cat","keywords":["cat2","animal","meow","pet","cats"],"skins":[{"unified":"1f408","native":"ðŸˆ"}],"version":1},"black_cat":{"id":"black_cat","name":"Black Cat","keywords":["superstition","luck"],"skins":[{"unified":"1f408-200d-2b1b","native":"ðŸˆâ€â¬›"}],"version":13},"lion_face":{"id":"lion_face","name":"Lion","keywords":["face","animal","nature"],"skins":[{"unified":"1f981","native":"ðŸ¦"}],"version":1},"tiger":{"id":"tiger","name":"Tiger Face","keywords":["animal","cat","danger","wild","nature","roar"],"skins":[{"unified":"1f42f","native":"ðŸ¯"}],"version":1},"tiger2":{"id":"tiger2","name":"Tiger","keywords":["tiger2","animal","nature","roar"],"skins":[{"unified":"1f405","native":"ðŸ…"}],"version":1},"leopard":{"id":"leopard","name":"Leopard","keywords":["animal","nature"],"skins":[{"unified":"1f406","native":"ðŸ†"}],"version":1},"horse":{"id":"horse","name":"Horse Face","keywords":["animal","brown","nature"],"skins":[{"unified":"1f434","native":"ðŸ´"}],"version":1},"racehorse":{"id":"racehorse","name":"Horse","keywords":["racehorse","animal","gamble","luck"],"skins":[{"unified":"1f40e","native":"ðŸŽ"}],"version":1},"unicorn_face":{"id":"unicorn_face","name":"Unicorn","keywords":["face","animal","nature","mystical"],"skins":[{"unified":"1f984","native":"🦄"}],"version":1},"zebra_face":{"id":"zebra_face","name":"Zebra","keywords":["face","animal","nature","stripes","safari"],"skins":[{"unified":"1f993","native":"🦓"}],"version":5},"deer":{"id":"deer","name":"Deer","keywords":["animal","nature","horns","venison"],"skins":[{"unified":"1f98c","native":"🦌"}],"version":3},"bison":{"id":"bison","name":"Bison","keywords":["ox"],"skins":[{"unified":"1f9ac","native":"🦬"}],"version":13},"cow":{"id":"cow","name":"Cow Face","keywords":["beef","ox","animal","nature","moo","milk"],"skins":[{"unified":"1f42e","native":"ðŸ®"}],"version":1},"ox":{"id":"ox","name":"Ox","keywords":["animal","cow","beef"],"skins":[{"unified":"1f402","native":"ðŸ‚"}],"version":1},"water_buffalo":{"id":"water_buffalo","name":"Water Buffalo","keywords":["animal","nature","ox","cow"],"skins":[{"unified":"1f403","native":"ðŸƒ"}],"version":1},"cow2":{"id":"cow2","name":"Cow","keywords":["cow2","beef","ox","animal","nature","moo","milk"],"skins":[{"unified":"1f404","native":"ðŸ„"}],"version":1},"pig":{"id":"pig","name":"Pig Face","keywords":["animal","oink","nature"],"skins":[{"unified":"1f437","native":"ðŸ·"}],"version":1},"pig2":{"id":"pig2","name":"Pig","keywords":["pig2","animal","nature"],"skins":[{"unified":"1f416","native":"ðŸ–"}],"version":1},"boar":{"id":"boar","name":"Boar","keywords":["animal","nature"],"skins":[{"unified":"1f417","native":"ðŸ—"}],"version":1},"pig_nose":{"id":"pig_nose","name":"Pig Nose","keywords":["animal","oink"],"skins":[{"unified":"1f43d","native":"ðŸ½"}],"version":1},"ram":{"id":"ram","name":"Ram","keywords":["animal","sheep","nature"],"skins":[{"unified":"1f40f","native":"ðŸ"}],"version":1},"sheep":{"id":"sheep","name":"Ewe","keywords":["sheep","animal","nature","wool","shipit"],"skins":[{"unified":"1f411","native":"ðŸ‘"}],"version":1},"goat":{"id":"goat","name":"Goat","keywords":["animal","nature"],"skins":[{"unified":"1f410","native":"ðŸ"}],"version":1},"dromedary_camel":{"id":"dromedary_camel","name":"Camel","keywords":["dromedary","animal","hot","desert","hump"],"skins":[{"unified":"1f42a","native":"ðŸª"}],"version":1},"camel":{"id":"camel","name":"Bactrian Camel","keywords":["two","hump","animal","nature","hot","desert"],"skins":[{"unified":"1f42b","native":"ðŸ«"}],"version":1},"llama":{"id":"llama","name":"Llama","keywords":["animal","nature","alpaca"],"skins":[{"unified":"1f999","native":"🦙"}],"version":11},"giraffe_face":{"id":"giraffe_face","name":"Giraffe","keywords":["face","animal","nature","spots","safari"],"skins":[{"unified":"1f992","native":"🦒"}],"version":5},"elephant":{"id":"elephant","name":"Elephant","keywords":["animal","nature","nose","th","circus"],"skins":[{"unified":"1f418","native":"ðŸ˜"}],"version":1},"mammoth":{"id":"mammoth","name":"Mammoth","keywords":["elephant","tusks"],"skins":[{"unified":"1f9a3","native":"🦣"}],"version":13},"rhinoceros":{"id":"rhinoceros","name":"Rhinoceros","keywords":["animal","nature","horn"],"skins":[{"unified":"1f98f","native":"ðŸ¦"}],"version":3},"hippopotamus":{"id":"hippopotamus","name":"Hippopotamus","keywords":["animal","nature"],"skins":[{"unified":"1f99b","native":"🦛"}],"version":11},"mouse":{"id":"mouse","name":"Mouse Face","keywords":["animal","nature","cheese","wedge","rodent"],"skins":[{"unified":"1f42d","native":"ðŸ"}],"version":1},"mouse2":{"id":"mouse2","name":"Mouse","keywords":["mouse2","animal","nature","rodent"],"skins":[{"unified":"1f401","native":"ðŸ"}],"version":1},"rat":{"id":"rat","name":"Rat","keywords":["animal","mouse","rodent"],"skins":[{"unified":"1f400","native":"ðŸ€"}],"version":1},"hamster":{"id":"hamster","name":"Hamster","keywords":["animal","nature"],"skins":[{"unified":"1f439","native":"ðŸ¹"}],"version":1},"rabbit":{"id":"rabbit","name":"Rabbit Face","keywords":["animal","nature","pet","spring","magic","bunny"],"skins":[{"unified":"1f430","native":"ðŸ°"}],"version":1},"rabbit2":{"id":"rabbit2","name":"Rabbit","keywords":["rabbit2","animal","nature","pet","magic","spring"],"skins":[{"unified":"1f407","native":"ðŸ‡"}],"version":1},"chipmunk":{"id":"chipmunk","name":"Chipmunk","keywords":["animal","nature","rodent","squirrel"],"skins":[{"unified":"1f43f-fe0f","native":"ðŸ¿ï¸"}],"version":1},"beaver":{"id":"beaver","name":"Beaver","keywords":["animal","rodent"],"skins":[{"unified":"1f9ab","native":"🦫"}],"version":13},"hedgehog":{"id":"hedgehog","name":"Hedgehog","keywords":["animal","nature","spiny"],"skins":[{"unified":"1f994","native":"🦔"}],"version":5},"bat":{"id":"bat","name":"Bat","keywords":["animal","nature","blind","vampire"],"skins":[{"unified":"1f987","native":"🦇"}],"version":3},"bear":{"id":"bear","name":"Bear","keywords":["animal","nature","wild"],"skins":[{"unified":"1f43b","native":"ðŸ»"}],"version":1},"polar_bear":{"id":"polar_bear","name":"Polar Bear","keywords":["animal","arctic"],"skins":[{"unified":"1f43b-200d-2744-fe0f","native":"ðŸ»â€â„ï¸"}],"version":13},"koala":{"id":"koala","name":"Koala","keywords":["animal","nature"],"skins":[{"unified":"1f428","native":"ðŸ¨"}],"version":1},"panda_face":{"id":"panda_face","name":"Panda","keywords":["face","animal","nature"],"skins":[{"unified":"1f43c","native":"ðŸ¼"}],"version":1},"sloth":{"id":"sloth","name":"Sloth","keywords":["animal"],"skins":[{"unified":"1f9a5","native":"🦥"}],"version":12},"otter":{"id":"otter","name":"Otter","keywords":["animal"],"skins":[{"unified":"1f9a6","native":"🦦"}],"version":12},"skunk":{"id":"skunk","name":"Skunk","keywords":["animal"],"skins":[{"unified":"1f9a8","native":"🦨"}],"version":12},"kangaroo":{"id":"kangaroo","name":"Kangaroo","keywords":["animal","nature","australia","joey","hop","marsupial"],"skins":[{"unified":"1f998","native":"🦘"}],"version":11},"badger":{"id":"badger","name":"Badger","keywords":["animal","nature","honey"],"skins":[{"unified":"1f9a1","native":"🦡"}],"version":11},"feet":{"id":"feet","name":"Paw Prints","keywords":["feet","animal","tracking","footprints","dog","cat","pet"],"skins":[{"unified":"1f43e","native":"ðŸ¾"}],"version":1},"turkey":{"id":"turkey","name":"Turkey","keywords":["animal","bird"],"skins":[{"unified":"1f983","native":"🦃"}],"version":1},"chicken":{"id":"chicken","name":"Chicken","keywords":["animal","cluck","nature","bird"],"skins":[{"unified":"1f414","native":"ðŸ”"}],"version":1},"rooster":{"id":"rooster","name":"Rooster","keywords":["animal","nature","chicken"],"skins":[{"unified":"1f413","native":"ðŸ“"}],"version":1},"hatching_chick":{"id":"hatching_chick","name":"Hatching Chick","keywords":["animal","chicken","egg","born","baby","bird"],"skins":[{"unified":"1f423","native":"ðŸ£"}],"version":1},"baby_chick":{"id":"baby_chick","name":"Baby Chick","keywords":["animal","chicken","bird"],"skins":[{"unified":"1f424","native":"ðŸ¤"}],"version":1},"hatched_chick":{"id":"hatched_chick","name":"Front-Facing Baby Chick","keywords":["hatched","front","facing","animal","chicken","bird"],"skins":[{"unified":"1f425","native":"ðŸ¥"}],"version":1},"bird":{"id":"bird","name":"Bird","keywords":["animal","nature","fly","tweet","spring"],"skins":[{"unified":"1f426","native":"ðŸ¦"}],"version":1},"penguin":{"id":"penguin","name":"Penguin","keywords":["animal","nature"],"skins":[{"unified":"1f427","native":"ðŸ§"}],"version":1},"dove_of_peace":{"id":"dove_of_peace","name":"Dove","keywords":["of","peace","animal","bird"],"skins":[{"unified":"1f54a-fe0f","native":"🕊ï¸"}],"version":1},"eagle":{"id":"eagle","name":"Eagle","keywords":["animal","nature","bird"],"skins":[{"unified":"1f985","native":"🦅"}],"version":3},"duck":{"id":"duck","name":"Duck","keywords":["animal","nature","bird","mallard"],"skins":[{"unified":"1f986","native":"🦆"}],"version":3},"swan":{"id":"swan","name":"Swan","keywords":["animal","nature","bird"],"skins":[{"unified":"1f9a2","native":"🦢"}],"version":11},"owl":{"id":"owl","name":"Owl","keywords":["animal","nature","bird","hoot"],"skins":[{"unified":"1f989","native":"🦉"}],"version":3},"dodo":{"id":"dodo","name":"Dodo","keywords":["animal","bird"],"skins":[{"unified":"1f9a4","native":"🦤"}],"version":13},"feather":{"id":"feather","name":"Feather","keywords":["bird","fly"],"skins":[{"unified":"1fab6","native":"🪶"}],"version":13},"flamingo":{"id":"flamingo","name":"Flamingo","keywords":["animal"],"skins":[{"unified":"1f9a9","native":"🦩"}],"version":12},"peacock":{"id":"peacock","name":"Peacock","keywords":["animal","nature","peahen","bird"],"skins":[{"unified":"1f99a","native":"🦚"}],"version":11},"parrot":{"id":"parrot","name":"Parrot","keywords":["animal","nature","bird","pirate","talk"],"skins":[{"unified":"1f99c","native":"🦜"}],"version":11},"frog":{"id":"frog","name":"Frog","keywords":["animal","nature","croak","toad"],"skins":[{"unified":"1f438","native":"ðŸ¸"}],"version":1},"crocodile":{"id":"crocodile","name":"Crocodile","keywords":["animal","nature","reptile","lizard","alligator"],"skins":[{"unified":"1f40a","native":"ðŸŠ"}],"version":1},"turtle":{"id":"turtle","name":"Turtle","keywords":["animal","slow","nature","tortoise"],"skins":[{"unified":"1f422","native":"ðŸ¢"}],"version":1},"lizard":{"id":"lizard","name":"Lizard","keywords":["animal","nature","reptile"],"skins":[{"unified":"1f98e","native":"🦎"}],"version":3},"snake":{"id":"snake","name":"Snake","keywords":["animal","evil","nature","hiss","python"],"skins":[{"unified":"1f40d","native":"ðŸ"}],"version":1},"dragon_face":{"id":"dragon_face","name":"Dragon Face","keywords":["animal","myth","nature","chinese","green"],"skins":[{"unified":"1f432","native":"ðŸ²"}],"version":1},"dragon":{"id":"dragon","name":"Dragon","keywords":["animal","myth","nature","chinese","green"],"skins":[{"unified":"1f409","native":"ðŸ‰"}],"version":1},"sauropod":{"id":"sauropod","name":"Sauropod","keywords":["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"],"skins":[{"unified":"1f995","native":"🦕"}],"version":5},"t-rex":{"id":"t-rex","name":"T-Rex","keywords":["t","rex","animal","nature","dinosaur","tyrannosaurus","extinct"],"skins":[{"unified":"1f996","native":"🦖"}],"version":5},"whale":{"id":"whale","name":"Spouting Whale","keywords":["animal","nature","sea","ocean"],"skins":[{"unified":"1f433","native":"ðŸ³"}],"version":1},"whale2":{"id":"whale2","name":"Whale","keywords":["whale2","animal","nature","sea","ocean"],"skins":[{"unified":"1f40b","native":"ðŸ‹"}],"version":1},"dolphin":{"id":"dolphin","name":"Dolphin","keywords":["flipper","animal","nature","fish","sea","ocean","fins","beach"],"skins":[{"unified":"1f42c","native":"ðŸ¬"}],"version":1},"seal":{"id":"seal","name":"Seal","keywords":["animal","creature","sea"],"skins":[{"unified":"1f9ad","native":"ðŸ¦"}],"version":13},"fish":{"id":"fish","name":"Fish","keywords":["animal","food","nature"],"skins":[{"unified":"1f41f","native":"ðŸŸ"}],"version":1},"tropical_fish":{"id":"tropical_fish","name":"Tropical Fish","keywords":["animal","swim","ocean","beach","nemo"],"skins":[{"unified":"1f420","native":"ðŸ "}],"version":1},"blowfish":{"id":"blowfish","name":"Blowfish","keywords":["animal","nature","food","sea","ocean"],"skins":[{"unified":"1f421","native":"ðŸ¡"}],"version":1},"shark":{"id":"shark","name":"Shark","keywords":["animal","nature","fish","sea","ocean","jaws","fins","beach"],"skins":[{"unified":"1f988","native":"🦈"}],"version":3},"octopus":{"id":"octopus","name":"Octopus","keywords":["animal","creature","ocean","sea","nature","beach"],"skins":[{"unified":"1f419","native":"ðŸ™"}],"version":1},"shell":{"id":"shell","name":"Spiral Shell","keywords":["nature","sea","beach"],"skins":[{"unified":"1f41a","native":"ðŸš"}],"version":1},"coral":{"id":"coral","name":"Coral","keywords":["ocean","sea","reef"],"skins":[{"unified":"1fab8","native":"🪸"}],"version":14},"snail":{"id":"snail","name":"Snail","keywords":["slow","animal","shell"],"skins":[{"unified":"1f40c","native":"ðŸŒ"}],"version":1},"butterfly":{"id":"butterfly","name":"Butterfly","keywords":["animal","insect","nature","caterpillar"],"skins":[{"unified":"1f98b","native":"🦋"}],"version":3},"bug":{"id":"bug","name":"Bug","keywords":["animal","insect","nature","worm"],"skins":[{"unified":"1f41b","native":"ðŸ›"}],"version":1},"ant":{"id":"ant","name":"Ant","keywords":["animal","insect","nature","bug"],"skins":[{"unified":"1f41c","native":"ðŸœ"}],"version":1},"bee":{"id":"bee","name":"Honeybee","keywords":["bee","animal","insect","nature","bug","spring","honey"],"skins":[{"unified":"1f41d","native":"ðŸ"}],"version":1},"beetle":{"id":"beetle","name":"Beetle","keywords":["insect"],"skins":[{"unified":"1fab2","native":"🪲"}],"version":13},"ladybug":{"id":"ladybug","name":"Lady Beetle","keywords":["ladybug","animal","insect","nature"],"skins":[{"unified":"1f41e","native":"ðŸž"}],"version":1},"cricket":{"id":"cricket","name":"Cricket","keywords":["animal","chirp"],"skins":[{"unified":"1f997","native":"🦗"}],"version":5},"cockroach":{"id":"cockroach","name":"Cockroach","keywords":["insect","pests"],"skins":[{"unified":"1fab3","native":"🪳"}],"version":13},"spider":{"id":"spider","name":"Spider","keywords":["animal","arachnid"],"skins":[{"unified":"1f577-fe0f","native":"🕷ï¸"}],"version":1},"spider_web":{"id":"spider_web","name":"Spider Web","keywords":["animal","insect","arachnid","silk"],"skins":[{"unified":"1f578-fe0f","native":"🕸ï¸"}],"version":1},"scorpion":{"id":"scorpion","name":"Scorpion","keywords":["animal","arachnid"],"skins":[{"unified":"1f982","native":"🦂"}],"version":1},"mosquito":{"id":"mosquito","name":"Mosquito","keywords":["animal","nature","insect","malaria"],"skins":[{"unified":"1f99f","native":"🦟"}],"version":11},"fly":{"id":"fly","name":"Fly","keywords":["insect"],"skins":[{"unified":"1fab0","native":"🪰"}],"version":13},"worm":{"id":"worm","name":"Worm","keywords":["animal"],"skins":[{"unified":"1fab1","native":"🪱"}],"version":13},"microbe":{"id":"microbe","name":"Microbe","keywords":["amoeba","bacteria","germs","virus"],"skins":[{"unified":"1f9a0","native":"🦠"}],"version":11},"bouquet":{"id":"bouquet","name":"Bouquet","keywords":["flowers","nature","spring"],"skins":[{"unified":"1f490","native":"ðŸ’"}],"version":1},"cherry_blossom":{"id":"cherry_blossom","name":"Cherry Blossom","keywords":["nature","plant","spring","flower"],"skins":[{"unified":"1f338","native":"🌸"}],"version":1},"white_flower":{"id":"white_flower","name":"White Flower","keywords":["japanese","spring"],"skins":[{"unified":"1f4ae","native":"💮"}],"version":1},"lotus":{"id":"lotus","name":"Lotus","keywords":["flower","calm","meditation"],"skins":[{"unified":"1fab7","native":"🪷"}],"version":14},"rosette":{"id":"rosette","name":"Rosette","keywords":["flower","decoration","military"],"skins":[{"unified":"1f3f5-fe0f","native":"ðŸµï¸"}],"version":1},"rose":{"id":"rose","name":"Rose","keywords":["flowers","valentines","love","spring"],"skins":[{"unified":"1f339","native":"🌹"}],"version":1},"wilted_flower":{"id":"wilted_flower","name":"Wilted Flower","keywords":["plant","nature"],"skins":[{"unified":"1f940","native":"🥀"}],"version":3},"hibiscus":{"id":"hibiscus","name":"Hibiscus","keywords":["plant","vegetable","flowers","beach"],"skins":[{"unified":"1f33a","native":"🌺"}],"version":1},"sunflower":{"id":"sunflower","name":"Sunflower","keywords":["nature","plant","fall"],"skins":[{"unified":"1f33b","native":"🌻"}],"version":1},"blossom":{"id":"blossom","name":"Blossom","keywords":["nature","flowers","yellow"],"skins":[{"unified":"1f33c","native":"🌼"}],"version":1},"tulip":{"id":"tulip","name":"Tulip","keywords":["flowers","plant","nature","summer","spring"],"skins":[{"unified":"1f337","native":"🌷"}],"version":1},"seedling":{"id":"seedling","name":"Seedling","keywords":["plant","nature","grass","lawn","spring"],"skins":[{"unified":"1f331","native":"🌱"}],"version":1},"potted_plant":{"id":"potted_plant","name":"Potted Plant","keywords":["greenery","house"],"skins":[{"unified":"1fab4","native":"🪴"}],"version":13},"evergreen_tree":{"id":"evergreen_tree","name":"Evergreen Tree","keywords":["plant","nature"],"skins":[{"unified":"1f332","native":"🌲"}],"version":1},"deciduous_tree":{"id":"deciduous_tree","name":"Deciduous Tree","keywords":["plant","nature"],"skins":[{"unified":"1f333","native":"🌳"}],"version":1},"palm_tree":{"id":"palm_tree","name":"Palm Tree","keywords":["plant","vegetable","nature","summer","beach","mojito","tropical"],"skins":[{"unified":"1f334","native":"🌴"}],"version":1},"cactus":{"id":"cactus","name":"Cactus","keywords":["vegetable","plant","nature"],"skins":[{"unified":"1f335","native":"🌵"}],"version":1},"ear_of_rice":{"id":"ear_of_rice","name":"Ear of Rice","keywords":["sheaf","nature","plant"],"skins":[{"unified":"1f33e","native":"🌾"}],"version":1},"herb":{"id":"herb","name":"Herb","keywords":["vegetable","plant","medicine","weed","grass","lawn"],"skins":[{"unified":"1f33f","native":"🌿"}],"version":1},"shamrock":{"id":"shamrock","name":"Shamrock","keywords":["vegetable","plant","nature","irish","clover"],"skins":[{"unified":"2618-fe0f","native":"☘ï¸"}],"version":1},"four_leaf_clover":{"id":"four_leaf_clover","name":"Four Leaf Clover","keywords":["vegetable","plant","nature","lucky","irish"],"skins":[{"unified":"1f340","native":"ðŸ€"}],"version":1},"maple_leaf":{"id":"maple_leaf","name":"Maple Leaf","keywords":["nature","plant","vegetable","ca","fall"],"skins":[{"unified":"1f341","native":"ðŸ"}],"version":1},"fallen_leaf":{"id":"fallen_leaf","name":"Fallen Leaf","keywords":["nature","plant","vegetable","leaves"],"skins":[{"unified":"1f342","native":"ðŸ‚"}],"version":1},"leaves":{"id":"leaves","name":"Leaf Fluttering in Wind","keywords":["leaves","nature","plant","tree","vegetable","grass","lawn","spring"],"skins":[{"unified":"1f343","native":"ðŸƒ"}],"version":1},"empty_nest":{"id":"empty_nest","name":"Empty Nest","keywords":["bird"],"skins":[{"unified":"1fab9","native":"🪹"}],"version":14},"nest_with_eggs":{"id":"nest_with_eggs","name":"Nest with Eggs","keywords":["bird"],"skins":[{"unified":"1faba","native":"🪺"}],"version":14},"grapes":{"id":"grapes","name":"Grapes","keywords":["fruit","food","wine"],"skins":[{"unified":"1f347","native":"ðŸ‡"}],"version":1},"melon":{"id":"melon","name":"Melon","keywords":["fruit","nature","food"],"skins":[{"unified":"1f348","native":"ðŸˆ"}],"version":1},"watermelon":{"id":"watermelon","name":"Watermelon","keywords":["fruit","food","picnic","summer"],"skins":[{"unified":"1f349","native":"ðŸ‰"}],"version":1},"tangerine":{"id":"tangerine","name":"Tangerine","keywords":["food","fruit","nature","orange"],"skins":[{"unified":"1f34a","native":"ðŸŠ"}],"version":1},"lemon":{"id":"lemon","name":"Lemon","keywords":["fruit","nature"],"skins":[{"unified":"1f34b","native":"ðŸ‹"}],"version":1},"banana":{"id":"banana","name":"Banana","keywords":["fruit","food","monkey"],"skins":[{"unified":"1f34c","native":"ðŸŒ"}],"version":1},"pineapple":{"id":"pineapple","name":"Pineapple","keywords":["fruit","nature","food"],"skins":[{"unified":"1f34d","native":"ðŸ"}],"version":1},"mango":{"id":"mango","name":"Mango","keywords":["fruit","food","tropical"],"skins":[{"unified":"1f96d","native":"ðŸ¥"}],"version":11},"apple":{"id":"apple","name":"Red Apple","keywords":["fruit","mac","school"],"skins":[{"unified":"1f34e","native":"ðŸŽ"}],"version":1},"green_apple":{"id":"green_apple","name":"Green Apple","keywords":["fruit","nature"],"skins":[{"unified":"1f34f","native":"ðŸ"}],"version":1},"pear":{"id":"pear","name":"Pear","keywords":["fruit","nature","food"],"skins":[{"unified":"1f350","native":"ðŸ"}],"version":1},"peach":{"id":"peach","name":"Peach","keywords":["fruit","nature","food"],"skins":[{"unified":"1f351","native":"ðŸ‘"}],"version":1},"cherries":{"id":"cherries","name":"Cherries","keywords":["food","fruit"],"skins":[{"unified":"1f352","native":"ðŸ’"}],"version":1},"strawberry":{"id":"strawberry","name":"Strawberry","keywords":["fruit","food","nature"],"skins":[{"unified":"1f353","native":"ðŸ“"}],"version":1},"blueberries":{"id":"blueberries","name":"Blueberries","keywords":["fruit"],"skins":[{"unified":"1fad0","native":"ðŸ«"}],"version":13},"kiwifruit":{"id":"kiwifruit","name":"Kiwifruit","keywords":["kiwi","fruit","food"],"skins":[{"unified":"1f95d","native":"ðŸ¥"}],"version":3},"tomato":{"id":"tomato","name":"Tomato","keywords":["fruit","vegetable","nature","food"],"skins":[{"unified":"1f345","native":"ðŸ…"}],"version":1},"olive":{"id":"olive","name":"Olive","keywords":["fruit"],"skins":[{"unified":"1fad2","native":"🫒"}],"version":13},"coconut":{"id":"coconut","name":"Coconut","keywords":["fruit","nature","food","palm"],"skins":[{"unified":"1f965","native":"🥥"}],"version":5},"avocado":{"id":"avocado","name":"Avocado","keywords":["fruit","food"],"skins":[{"unified":"1f951","native":"🥑"}],"version":3},"eggplant":{"id":"eggplant","name":"Eggplant","keywords":["vegetable","nature","food","aubergine"],"skins":[{"unified":"1f346","native":"ðŸ†"}],"version":1},"potato":{"id":"potato","name":"Potato","keywords":["food","tuber","vegatable","starch"],"skins":[{"unified":"1f954","native":"🥔"}],"version":3},"carrot":{"id":"carrot","name":"Carrot","keywords":["vegetable","food","orange"],"skins":[{"unified":"1f955","native":"🥕"}],"version":3},"corn":{"id":"corn","name":"Ear of Corn","keywords":["food","vegetable","plant"],"skins":[{"unified":"1f33d","native":"🌽"}],"version":1},"hot_pepper":{"id":"hot_pepper","name":"Hot Pepper","keywords":["food","spicy","chilli","chili"],"skins":[{"unified":"1f336-fe0f","native":"🌶ï¸"}],"version":1},"bell_pepper":{"id":"bell_pepper","name":"Bell Pepper","keywords":["fruit","plant"],"skins":[{"unified":"1fad1","native":"🫑"}],"version":13},"cucumber":{"id":"cucumber","name":"Cucumber","keywords":["fruit","food","pickle"],"skins":[{"unified":"1f952","native":"🥒"}],"version":3},"leafy_green":{"id":"leafy_green","name":"Leafy Green","keywords":["food","vegetable","plant","bok","choy","cabbage","kale","lettuce"],"skins":[{"unified":"1f96c","native":"🥬"}],"version":11},"broccoli":{"id":"broccoli","name":"Broccoli","keywords":["fruit","food","vegetable"],"skins":[{"unified":"1f966","native":"🥦"}],"version":5},"garlic":{"id":"garlic","name":"Garlic","keywords":["food","spice","cook"],"skins":[{"unified":"1f9c4","native":"🧄"}],"version":12},"onion":{"id":"onion","name":"Onion","keywords":["cook","food","spice"],"skins":[{"unified":"1f9c5","native":"🧅"}],"version":12},"mushroom":{"id":"mushroom","name":"Mushroom","keywords":["plant","vegetable"],"skins":[{"unified":"1f344","native":"ðŸ„"}],"version":1},"peanuts":{"id":"peanuts","name":"Peanuts","keywords":["food","nut"],"skins":[{"unified":"1f95c","native":"🥜"}],"version":3},"beans":{"id":"beans","name":"Beans","keywords":["food"],"skins":[{"unified":"1fad8","native":"🫘"}],"version":14},"chestnut":{"id":"chestnut","name":"Chestnut","keywords":["food","squirrel"],"skins":[{"unified":"1f330","native":"🌰"}],"version":1},"bread":{"id":"bread","name":"Bread","keywords":["food","wheat","breakfast","toast"],"skins":[{"unified":"1f35e","native":"ðŸž"}],"version":1},"croissant":{"id":"croissant","name":"Croissant","keywords":["food","bread","french"],"skins":[{"unified":"1f950","native":"ðŸ¥"}],"version":3},"baguette_bread":{"id":"baguette_bread","name":"Baguette Bread","keywords":["food","french"],"skins":[{"unified":"1f956","native":"🥖"}],"version":3},"flatbread":{"id":"flatbread","name":"Flatbread","keywords":["flour","food"],"skins":[{"unified":"1fad3","native":"🫓"}],"version":13},"pretzel":{"id":"pretzel","name":"Pretzel","keywords":["food","bread","twisted"],"skins":[{"unified":"1f968","native":"🥨"}],"version":5},"bagel":{"id":"bagel","name":"Bagel","keywords":["food","bread","bakery","schmear"],"skins":[{"unified":"1f96f","native":"🥯"}],"version":11},"pancakes":{"id":"pancakes","name":"Pancakes","keywords":["food","breakfast","flapjacks","hotcakes"],"skins":[{"unified":"1f95e","native":"🥞"}],"version":3},"waffle":{"id":"waffle","name":"Waffle","keywords":["food","breakfast"],"skins":[{"unified":"1f9c7","native":"🧇"}],"version":12},"cheese_wedge":{"id":"cheese_wedge","name":"Cheese Wedge","keywords":["food","chadder"],"skins":[{"unified":"1f9c0","native":"🧀"}],"version":1},"meat_on_bone":{"id":"meat_on_bone","name":"Meat on Bone","keywords":["good","food","drumstick"],"skins":[{"unified":"1f356","native":"ðŸ–"}],"version":1},"poultry_leg":{"id":"poultry_leg","name":"Poultry Leg","keywords":["food","meat","drumstick","bird","chicken","turkey"],"skins":[{"unified":"1f357","native":"ðŸ—"}],"version":1},"cut_of_meat":{"id":"cut_of_meat","name":"Cut of Meat","keywords":["food","cow","chop","lambchop","porkchop"],"skins":[{"unified":"1f969","native":"🥩"}],"version":5},"bacon":{"id":"bacon","name":"Bacon","keywords":["food","breakfast","pork","pig","meat"],"skins":[{"unified":"1f953","native":"🥓"}],"version":3},"hamburger":{"id":"hamburger","name":"Hamburger","keywords":["meat","fast","food","beef","cheeseburger","mcdonalds","burger","king"],"skins":[{"unified":"1f354","native":"ðŸ”"}],"version":1},"fries":{"id":"fries","name":"French Fries","keywords":["chips","snack","fast","food"],"skins":[{"unified":"1f35f","native":"ðŸŸ"}],"version":1},"pizza":{"id":"pizza","name":"Pizza","keywords":["food","party"],"skins":[{"unified":"1f355","native":"ðŸ•"}],"version":1},"hotdog":{"id":"hotdog","name":"Hot Dog","keywords":["hotdog","food","frankfurter"],"skins":[{"unified":"1f32d","native":"ðŸŒ"}],"version":1},"sandwich":{"id":"sandwich","name":"Sandwich","keywords":["food","lunch","bread"],"skins":[{"unified":"1f96a","native":"🥪"}],"version":5},"taco":{"id":"taco","name":"Taco","keywords":["food","mexican"],"skins":[{"unified":"1f32e","native":"🌮"}],"version":1},"burrito":{"id":"burrito","name":"Burrito","keywords":["food","mexican"],"skins":[{"unified":"1f32f","native":"🌯"}],"version":1},"tamale":{"id":"tamale","name":"Tamale","keywords":["food","masa"],"skins":[{"unified":"1fad4","native":"🫔"}],"version":13},"stuffed_flatbread":{"id":"stuffed_flatbread","name":"Stuffed Flatbread","keywords":["food","gyro"],"skins":[{"unified":"1f959","native":"🥙"}],"version":3},"falafel":{"id":"falafel","name":"Falafel","keywords":["food"],"skins":[{"unified":"1f9c6","native":"🧆"}],"version":12},"egg":{"id":"egg","name":"Egg","keywords":["food","chicken","breakfast"],"skins":[{"unified":"1f95a","native":"🥚"}],"version":3},"fried_egg":{"id":"fried_egg","name":"Cooking","keywords":["fried","egg","food","breakfast","kitchen"],"skins":[{"unified":"1f373","native":"ðŸ³"}],"version":1},"shallow_pan_of_food":{"id":"shallow_pan_of_food","name":"Shallow Pan of Food","keywords":["cooking","casserole","paella"],"skins":[{"unified":"1f958","native":"🥘"}],"version":3},"stew":{"id":"stew","name":"Pot of Food","keywords":["stew","meat","soup"],"skins":[{"unified":"1f372","native":"ðŸ²"}],"version":1},"fondue":{"id":"fondue","name":"Fondue","keywords":["cheese","pot","food"],"skins":[{"unified":"1fad5","native":"🫕"}],"version":13},"bowl_with_spoon":{"id":"bowl_with_spoon","name":"Bowl with Spoon","keywords":["food","breakfast","cereal","oatmeal","porridge"],"skins":[{"unified":"1f963","native":"🥣"}],"version":5},"green_salad":{"id":"green_salad","name":"Green Salad","keywords":["food","healthy","lettuce"],"skins":[{"unified":"1f957","native":"🥗"}],"version":3},"popcorn":{"id":"popcorn","name":"Popcorn","keywords":["food","movie","theater","films","snack"],"skins":[{"unified":"1f37f","native":"ðŸ¿"}],"version":1},"butter":{"id":"butter","name":"Butter","keywords":["food","cook"],"skins":[{"unified":"1f9c8","native":"🧈"}],"version":12},"salt":{"id":"salt","name":"Salt","keywords":["condiment","shaker"],"skins":[{"unified":"1f9c2","native":"🧂"}],"version":11},"canned_food":{"id":"canned_food","name":"Canned Food","keywords":["soup"],"skins":[{"unified":"1f96b","native":"🥫"}],"version":5},"bento":{"id":"bento","name":"Bento Box","keywords":["food","japanese"],"skins":[{"unified":"1f371","native":"ðŸ±"}],"version":1},"rice_cracker":{"id":"rice_cracker","name":"Rice Cracker","keywords":["food","japanese"],"skins":[{"unified":"1f358","native":"ðŸ˜"}],"version":1},"rice_ball":{"id":"rice_ball","name":"Rice Ball","keywords":["food","japanese"],"skins":[{"unified":"1f359","native":"ðŸ™"}],"version":1},"rice":{"id":"rice","name":"Cooked Rice","keywords":["food","china","asian"],"skins":[{"unified":"1f35a","native":"ðŸš"}],"version":1},"curry":{"id":"curry","name":"Curry Rice","keywords":["food","spicy","hot","indian"],"skins":[{"unified":"1f35b","native":"ðŸ›"}],"version":1},"ramen":{"id":"ramen","name":"Steaming Bowl","keywords":["ramen","food","japanese","noodle","chopsticks"],"skins":[{"unified":"1f35c","native":"ðŸœ"}],"version":1},"spaghetti":{"id":"spaghetti","name":"Spaghetti","keywords":["food","italian","noodle"],"skins":[{"unified":"1f35d","native":"ðŸ"}],"version":1},"sweet_potato":{"id":"sweet_potato","name":"Roasted Sweet Potato","keywords":["food","nature"],"skins":[{"unified":"1f360","native":"ðŸ "}],"version":1},"oden":{"id":"oden","name":"Oden","keywords":["food","japanese"],"skins":[{"unified":"1f362","native":"ðŸ¢"}],"version":1},"sushi":{"id":"sushi","name":"Sushi","keywords":["food","fish","japanese","rice"],"skins":[{"unified":"1f363","native":"ðŸ£"}],"version":1},"fried_shrimp":{"id":"fried_shrimp","name":"Fried Shrimp","keywords":["food","animal","appetizer","summer"],"skins":[{"unified":"1f364","native":"ðŸ¤"}],"version":1},"fish_cake":{"id":"fish_cake","name":"Fish Cake with Swirl","keywords":["food","japan","sea","beach","narutomaki","pink","kamaboko","surimi","ramen"],"skins":[{"unified":"1f365","native":"ðŸ¥"}],"version":1},"moon_cake":{"id":"moon_cake","name":"Moon Cake","keywords":["food","autumn"],"skins":[{"unified":"1f96e","native":"🥮"}],"version":11},"dango":{"id":"dango","name":"Dango","keywords":["food","dessert","sweet","japanese","barbecue","meat"],"skins":[{"unified":"1f361","native":"ðŸ¡"}],"version":1},"dumpling":{"id":"dumpling","name":"Dumpling","keywords":["food","empanada","pierogi","potsticker"],"skins":[{"unified":"1f95f","native":"🥟"}],"version":5},"fortune_cookie":{"id":"fortune_cookie","name":"Fortune Cookie","keywords":["food","prophecy"],"skins":[{"unified":"1f960","native":"🥠"}],"version":5},"takeout_box":{"id":"takeout_box","name":"Takeout Box","keywords":["food","leftovers"],"skins":[{"unified":"1f961","native":"🥡"}],"version":5},"crab":{"id":"crab","name":"Crab","keywords":["animal","crustacean"],"skins":[{"unified":"1f980","native":"🦀"}],"version":1},"lobster":{"id":"lobster","name":"Lobster","keywords":["animal","nature","bisque","claws","seafood"],"skins":[{"unified":"1f99e","native":"🦞"}],"version":11},"shrimp":{"id":"shrimp","name":"Shrimp","keywords":["animal","ocean","nature","seafood"],"skins":[{"unified":"1f990","native":"ðŸ¦"}],"version":3},"squid":{"id":"squid","name":"Squid","keywords":["animal","nature","ocean","sea"],"skins":[{"unified":"1f991","native":"🦑"}],"version":3},"oyster":{"id":"oyster","name":"Oyster","keywords":["food"],"skins":[{"unified":"1f9aa","native":"🦪"}],"version":12},"icecream":{"id":"icecream","name":"Soft Ice Cream","keywords":["icecream","food","hot","dessert","summer"],"skins":[{"unified":"1f366","native":"ðŸ¦"}],"version":1},"shaved_ice":{"id":"shaved_ice","name":"Shaved Ice","keywords":["hot","dessert","summer"],"skins":[{"unified":"1f367","native":"ðŸ§"}],"version":1},"ice_cream":{"id":"ice_cream","name":"Ice Cream","keywords":["food","hot","dessert"],"skins":[{"unified":"1f368","native":"ðŸ¨"}],"version":1},"doughnut":{"id":"doughnut","name":"Doughnut","keywords":["food","dessert","snack","sweet","donut"],"skins":[{"unified":"1f369","native":"ðŸ©"}],"version":1},"cookie":{"id":"cookie","name":"Cookie","keywords":["food","snack","oreo","chocolate","sweet","dessert"],"skins":[{"unified":"1f36a","native":"ðŸª"}],"version":1},"birthday":{"id":"birthday","name":"Birthday Cake","keywords":["food","dessert"],"skins":[{"unified":"1f382","native":"🎂"}],"version":1},"cake":{"id":"cake","name":"Shortcake","keywords":["cake","food","dessert"],"skins":[{"unified":"1f370","native":"ðŸ°"}],"version":1},"cupcake":{"id":"cupcake","name":"Cupcake","keywords":["food","dessert","bakery","sweet"],"skins":[{"unified":"1f9c1","native":"ðŸ§"}],"version":11},"pie":{"id":"pie","name":"Pie","keywords":["food","dessert","pastry"],"skins":[{"unified":"1f967","native":"🥧"}],"version":5},"chocolate_bar":{"id":"chocolate_bar","name":"Chocolate Bar","keywords":["food","snack","dessert","sweet"],"skins":[{"unified":"1f36b","native":"ðŸ«"}],"version":1},"candy":{"id":"candy","name":"Candy","keywords":["snack","dessert","sweet","lolly"],"skins":[{"unified":"1f36c","native":"ðŸ¬"}],"version":1},"lollipop":{"id":"lollipop","name":"Lollipop","keywords":["food","snack","candy","sweet"],"skins":[{"unified":"1f36d","native":"ðŸ"}],"version":1},"custard":{"id":"custard","name":"Custard","keywords":["dessert","food"],"skins":[{"unified":"1f36e","native":"ðŸ®"}],"version":1},"honey_pot":{"id":"honey_pot","name":"Honey Pot","keywords":["bees","sweet","kitchen"],"skins":[{"unified":"1f36f","native":"ðŸ¯"}],"version":1},"baby_bottle":{"id":"baby_bottle","name":"Baby Bottle","keywords":["food","container","milk"],"skins":[{"unified":"1f37c","native":"ðŸ¼"}],"version":1},"glass_of_milk":{"id":"glass_of_milk","name":"Glass of Milk","keywords":["beverage","drink","cow"],"skins":[{"unified":"1f95b","native":"🥛"}],"version":3},"coffee":{"id":"coffee","name":"Hot Beverage","keywords":["coffee","caffeine","latte","espresso"],"skins":[{"unified":"2615","native":"☕"}],"version":1},"teapot":{"id":"teapot","name":"Teapot","keywords":["drink","hot"],"skins":[{"unified":"1fad6","native":"🫖"}],"version":13},"tea":{"id":"tea","name":"Teacup Without Handle","keywords":["tea","drink","bowl","breakfast","green","british"],"skins":[{"unified":"1f375","native":"ðŸµ"}],"version":1},"sake":{"id":"sake","name":"Sake","keywords":["wine","drink","drunk","beverage","japanese","alcohol","booze"],"skins":[{"unified":"1f376","native":"ðŸ¶"}],"version":1},"champagne":{"id":"champagne","name":"Bottle with Popping Cork","keywords":["champagne","drink","wine","celebration"],"skins":[{"unified":"1f37e","native":"ðŸ¾"}],"version":1},"wine_glass":{"id":"wine_glass","name":"Wine Glass","keywords":["drink","beverage","drunk","alcohol","booze"],"skins":[{"unified":"1f377","native":"ðŸ·"}],"version":1},"cocktail":{"id":"cocktail","name":"Cocktail Glass","keywords":["drink","drunk","alcohol","beverage","booze","mojito"],"skins":[{"unified":"1f378","native":"ðŸ¸"}],"version":1},"tropical_drink":{"id":"tropical_drink","name":"Tropical Drink","keywords":["beverage","cocktail","summer","beach","alcohol","booze","mojito"],"skins":[{"unified":"1f379","native":"ðŸ¹"}],"version":1},"beer":{"id":"beer","name":"Beer Mug","keywords":["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],"skins":[{"unified":"1f37a","native":"ðŸº"}],"version":1},"beers":{"id":"beers","name":"Clinking Beer Mugs","keywords":["beers","relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"],"skins":[{"unified":"1f37b","native":"ðŸ»"}],"version":1},"clinking_glasses":{"id":"clinking_glasses","name":"Clinking Glasses","keywords":["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"],"skins":[{"unified":"1f942","native":"🥂"}],"version":3},"tumbler_glass":{"id":"tumbler_glass","name":"Tumbler Glass","keywords":["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","shot"],"skins":[{"unified":"1f943","native":"🥃"}],"version":3},"pouring_liquid":{"id":"pouring_liquid","name":"Pouring Liquid","keywords":["cup","water"],"skins":[{"unified":"1fad7","native":"🫗"}],"version":14},"cup_with_straw":{"id":"cup_with_straw","name":"Cup with Straw","keywords":["drink","soda"],"skins":[{"unified":"1f964","native":"🥤"}],"version":5},"bubble_tea":{"id":"bubble_tea","name":"Bubble Tea","keywords":["taiwan","boba","milk","straw"],"skins":[{"unified":"1f9cb","native":"🧋"}],"version":13},"beverage_box":{"id":"beverage_box","name":"Beverage Box","keywords":["drink"],"skins":[{"unified":"1f9c3","native":"🧃"}],"version":12},"mate_drink":{"id":"mate_drink","name":"Mate","keywords":["drink","tea","beverage"],"skins":[{"unified":"1f9c9","native":"🧉"}],"version":12},"ice_cube":{"id":"ice_cube","name":"Ice","keywords":["cube","water","cold"],"skins":[{"unified":"1f9ca","native":"🧊"}],"version":12},"chopsticks":{"id":"chopsticks","name":"Chopsticks","keywords":["food"],"skins":[{"unified":"1f962","native":"🥢"}],"version":5},"knife_fork_plate":{"id":"knife_fork_plate","name":"Fork and Knife with Plate","keywords":["food","eat","meal","lunch","dinner","restaurant"],"skins":[{"unified":"1f37d-fe0f","native":"ðŸ½ï¸"}],"version":1},"fork_and_knife":{"id":"fork_and_knife","name":"Fork and Knife","keywords":["cutlery","kitchen"],"skins":[{"unified":"1f374","native":"ðŸ´"}],"version":1},"spoon":{"id":"spoon","name":"Spoon","keywords":["cutlery","kitchen","tableware"],"skins":[{"unified":"1f944","native":"🥄"}],"version":3},"hocho":{"id":"hocho","name":"Hocho","keywords":["knife","kitchen","blade","cutlery","weapon"],"skins":[{"unified":"1f52a","native":"🔪"}],"version":1},"jar":{"id":"jar","name":"Jar","keywords":["container","sauce"],"skins":[{"unified":"1fad9","native":"🫙"}],"version":14},"amphora":{"id":"amphora","name":"Amphora","keywords":["vase","jar"],"skins":[{"unified":"1f3fa","native":"ðŸº"}],"version":1},"earth_africa":{"id":"earth_africa","name":"Earth Globe Europe-Africa","keywords":["africa","showing","europe","world","international"],"skins":[{"unified":"1f30d","native":"ðŸŒ"}],"version":1},"earth_americas":{"id":"earth_americas","name":"Earth Globe Americas","keywords":["showing","world","USA","international"],"skins":[{"unified":"1f30e","native":"🌎"}],"version":1},"earth_asia":{"id":"earth_asia","name":"Earth Globe Asia-Australia","keywords":["asia","showing","australia","world","east","international"],"skins":[{"unified":"1f30f","native":"ðŸŒ"}],"version":1},"globe_with_meridians":{"id":"globe_with_meridians","name":"Globe with Meridians","keywords":["earth","international","world","internet","interweb","i18n"],"skins":[{"unified":"1f310","native":"ðŸŒ"}],"version":1},"world_map":{"id":"world_map","name":"World Map","keywords":["location","direction"],"skins":[{"unified":"1f5fa-fe0f","native":"🗺ï¸"}],"version":1},"japan":{"id":"japan","name":"Map of Japan","keywords":["nation","country","japanese","asia"],"skins":[{"unified":"1f5fe","native":"🗾"}],"version":1},"compass":{"id":"compass","name":"Compass","keywords":["magnetic","navigation","orienteering"],"skins":[{"unified":"1f9ed","native":"ðŸ§"}],"version":11},"snow_capped_mountain":{"id":"snow_capped_mountain","name":"Snow-Capped Mountain","keywords":["snow","capped","photo","nature","environment","winter","cold"],"skins":[{"unified":"1f3d4-fe0f","native":"ðŸ”ï¸"}],"version":1},"mountain":{"id":"mountain","name":"Mountain","keywords":["photo","nature","environment"],"skins":[{"unified":"26f0-fe0f","native":"â›°ï¸"}],"version":1},"volcano":{"id":"volcano","name":"Volcano","keywords":["photo","nature","disaster"],"skins":[{"unified":"1f30b","native":"🌋"}],"version":1},"mount_fuji":{"id":"mount_fuji","name":"Mount Fuji","keywords":["photo","mountain","nature","japanese"],"skins":[{"unified":"1f5fb","native":"🗻"}],"version":1},"camping":{"id":"camping","name":"Camping","keywords":["photo","outdoors","tent"],"skins":[{"unified":"1f3d5-fe0f","native":"ðŸ•ï¸"}],"version":1},"beach_with_umbrella":{"id":"beach_with_umbrella","name":"Beach with Umbrella","keywords":["weather","summer","sunny","sand","mojito"],"skins":[{"unified":"1f3d6-fe0f","native":"ðŸ–ï¸"}],"version":1},"desert":{"id":"desert","name":"Desert","keywords":["photo","warm","saharah"],"skins":[{"unified":"1f3dc-fe0f","native":"ðŸœï¸"}],"version":1},"desert_island":{"id":"desert_island","name":"Desert Island","keywords":["photo","tropical","mojito"],"skins":[{"unified":"1f3dd-fe0f","native":"ðŸï¸"}],"version":1},"national_park":{"id":"national_park","name":"National Park","keywords":["photo","environment","nature"],"skins":[{"unified":"1f3de-fe0f","native":"ðŸžï¸"}],"version":1},"stadium":{"id":"stadium","name":"Stadium","keywords":["photo","place","sports","concert","venue"],"skins":[{"unified":"1f3df-fe0f","native":"ðŸŸï¸"}],"version":1},"classical_building":{"id":"classical_building","name":"Classical Building","keywords":["art","culture","history"],"skins":[{"unified":"1f3db-fe0f","native":"ðŸ›ï¸"}],"version":1},"building_construction":{"id":"building_construction","name":"Building Construction","keywords":["wip","working","progress"],"skins":[{"unified":"1f3d7-fe0f","native":"ðŸ—ï¸"}],"version":1},"bricks":{"id":"bricks","name":"Brick","keywords":["bricks"],"skins":[{"unified":"1f9f1","native":"🧱"}],"version":11},"rock":{"id":"rock","name":"Rock","keywords":["stone"],"skins":[{"unified":"1faa8","native":"🪨"}],"version":13},"wood":{"id":"wood","name":"Wood","keywords":["nature","timber","trunk"],"skins":[{"unified":"1fab5","native":"🪵"}],"version":13},"hut":{"id":"hut","name":"Hut","keywords":["house","structure"],"skins":[{"unified":"1f6d6","native":"🛖"}],"version":13},"house_buildings":{"id":"house_buildings","name":"Houses","keywords":["house","buildings","photo"],"skins":[{"unified":"1f3d8-fe0f","native":"ðŸ˜ï¸"}],"version":1},"derelict_house_building":{"id":"derelict_house_building","name":"Derelict House","keywords":["building","abandon","evict","broken"],"skins":[{"unified":"1f3da-fe0f","native":"ðŸšï¸"}],"version":1},"house":{"id":"house","name":"House","keywords":["building","home"],"skins":[{"unified":"1f3e0","native":"ðŸ "}],"version":1},"house_with_garden":{"id":"house_with_garden","name":"House with Garden","keywords":["home","plant","nature"],"skins":[{"unified":"1f3e1","native":"ðŸ¡"}],"version":1},"office":{"id":"office","name":"Office Building","keywords":["bureau","work"],"skins":[{"unified":"1f3e2","native":"ðŸ¢"}],"version":1},"post_office":{"id":"post_office","name":"Japanese Post Office","keywords":["building","envelope","communication"],"skins":[{"unified":"1f3e3","native":"ðŸ£"}],"version":1},"european_post_office":{"id":"european_post_office","name":"Post Office","keywords":["european","building","email"],"skins":[{"unified":"1f3e4","native":"ðŸ¤"}],"version":1},"hospital":{"id":"hospital","name":"Hospital","keywords":["building","health","surgery","doctor"],"skins":[{"unified":"1f3e5","native":"ðŸ¥"}],"version":1},"bank":{"id":"bank","name":"Bank","keywords":["building","money","sales","cash","business","enterprise"],"skins":[{"unified":"1f3e6","native":"ðŸ¦"}],"version":1},"hotel":{"id":"hotel","name":"Hotel","keywords":["building","accomodation","checkin"],"skins":[{"unified":"1f3e8","native":"ðŸ¨"}],"version":1},"love_hotel":{"id":"love_hotel","name":"Love Hotel","keywords":["like","affection","dating"],"skins":[{"unified":"1f3e9","native":"ðŸ©"}],"version":1},"convenience_store":{"id":"convenience_store","name":"Convenience Store","keywords":["building","shopping","groceries"],"skins":[{"unified":"1f3ea","native":"ðŸª"}],"version":1},"school":{"id":"school","name":"School","keywords":["building","student","education","learn","teach"],"skins":[{"unified":"1f3eb","native":"ðŸ«"}],"version":1},"department_store":{"id":"department_store","name":"Department Store","keywords":["building","shopping","mall"],"skins":[{"unified":"1f3ec","native":"ðŸ¬"}],"version":1},"factory":{"id":"factory","name":"Factory","keywords":["building","industry","pollution","smoke"],"skins":[{"unified":"1f3ed","native":"ðŸ"}],"version":1},"japanese_castle":{"id":"japanese_castle","name":"Japanese Castle","keywords":["photo","building"],"skins":[{"unified":"1f3ef","native":"ðŸ¯"}],"version":1},"european_castle":{"id":"european_castle","name":"Castle","keywords":["european","building","royalty","history"],"skins":[{"unified":"1f3f0","native":"ðŸ°"}],"version":1},"wedding":{"id":"wedding","name":"Wedding","keywords":["love","like","affection","couple","marriage","bride","groom"],"skins":[{"unified":"1f492","native":"💒"}],"version":1},"tokyo_tower":{"id":"tokyo_tower","name":"Tokyo Tower","keywords":["photo","japanese"],"skins":[{"unified":"1f5fc","native":"🗼"}],"version":1},"statue_of_liberty":{"id":"statue_of_liberty","name":"Statue of Liberty","keywords":["american","newyork"],"skins":[{"unified":"1f5fd","native":"🗽"}],"version":1},"church":{"id":"church","name":"Church","keywords":["building","religion","christ"],"skins":[{"unified":"26ea","native":"⛪"}],"version":1},"mosque":{"id":"mosque","name":"Mosque","keywords":["islam","worship","minaret"],"skins":[{"unified":"1f54c","native":"🕌"}],"version":1},"hindu_temple":{"id":"hindu_temple","name":"Hindu Temple","keywords":["religion"],"skins":[{"unified":"1f6d5","native":"🛕"}],"version":12},"synagogue":{"id":"synagogue","name":"Synagogue","keywords":["judaism","worship","temple","jewish"],"skins":[{"unified":"1f54d","native":"ðŸ•"}],"version":1},"shinto_shrine":{"id":"shinto_shrine","name":"Shinto Shrine","keywords":["temple","japan","kyoto"],"skins":[{"unified":"26e9-fe0f","native":"⛩ï¸"}],"version":1},"kaaba":{"id":"kaaba","name":"Kaaba","keywords":["mecca","mosque","islam"],"skins":[{"unified":"1f54b","native":"🕋"}],"version":1},"fountain":{"id":"fountain","name":"Fountain","keywords":["photo","summer","water","fresh"],"skins":[{"unified":"26f2","native":"⛲"}],"version":1},"tent":{"id":"tent","name":"Tent","keywords":["photo","camping","outdoors"],"skins":[{"unified":"26fa","native":"⛺"}],"version":1},"foggy":{"id":"foggy","name":"Foggy","keywords":["photo","mountain"],"skins":[{"unified":"1f301","native":"ðŸŒ"}],"version":1},"night_with_stars":{"id":"night_with_stars","name":"Night with Stars","keywords":["evening","city","downtown"],"skins":[{"unified":"1f303","native":"🌃"}],"version":1},"cityscape":{"id":"cityscape","name":"Cityscape","keywords":["photo","night","life","urban"],"skins":[{"unified":"1f3d9-fe0f","native":"ðŸ™ï¸"}],"version":1},"sunrise_over_mountains":{"id":"sunrise_over_mountains","name":"Sunrise over Mountains","keywords":["view","vacation","photo"],"skins":[{"unified":"1f304","native":"🌄"}],"version":1},"sunrise":{"id":"sunrise","name":"Sunrise","keywords":["morning","view","vacation","photo"],"skins":[{"unified":"1f305","native":"🌅"}],"version":1},"city_sunset":{"id":"city_sunset","name":"Cityscape at Dusk","keywords":["city","sunset","photo","evening","sky","buildings"],"skins":[{"unified":"1f306","native":"🌆"}],"version":1},"city_sunrise":{"id":"city_sunrise","name":"Sunset","keywords":["city","sunrise","photo","good","morning","dawn"],"skins":[{"unified":"1f307","native":"🌇"}],"version":1},"bridge_at_night":{"id":"bridge_at_night","name":"Bridge at Night","keywords":["photo","sanfrancisco"],"skins":[{"unified":"1f309","native":"🌉"}],"version":1},"hotsprings":{"id":"hotsprings","name":"Hot Springs","keywords":["hotsprings","bath","warm","relax"],"skins":[{"unified":"2668-fe0f","native":"♨ï¸"}],"version":1},"carousel_horse":{"id":"carousel_horse","name":"Carousel Horse","keywords":["photo","carnival"],"skins":[{"unified":"1f3a0","native":"🎠"}],"version":1},"playground_slide":{"id":"playground_slide","name":"Playground Slide","keywords":["fun","park"],"skins":[{"unified":"1f6dd","native":"ðŸ›"}],"version":14},"ferris_wheel":{"id":"ferris_wheel","name":"Ferris Wheel","keywords":["photo","carnival","londoneye"],"skins":[{"unified":"1f3a1","native":"🎡"}],"version":1},"roller_coaster":{"id":"roller_coaster","name":"Roller Coaster","keywords":["carnival","playground","photo","fun"],"skins":[{"unified":"1f3a2","native":"🎢"}],"version":1},"barber":{"id":"barber","name":"Barber Pole","keywords":["hair","salon","style"],"skins":[{"unified":"1f488","native":"💈"}],"version":1},"circus_tent":{"id":"circus_tent","name":"Circus Tent","keywords":["festival","carnival","party"],"skins":[{"unified":"1f3aa","native":"🎪"}],"version":1},"steam_locomotive":{"id":"steam_locomotive","name":"Locomotive","keywords":["steam","transportation","vehicle","train"],"skins":[{"unified":"1f682","native":"🚂"}],"version":1},"railway_car":{"id":"railway_car","name":"Railway Car","keywords":["transportation","vehicle"],"skins":[{"unified":"1f683","native":"🚃"}],"version":1},"bullettrain_side":{"id":"bullettrain_side","name":"High-Speed Train","keywords":["bullettrain","side","high","speed","transportation","vehicle"],"skins":[{"unified":"1f684","native":"🚄"}],"version":1},"bullettrain_front":{"id":"bullettrain_front","name":"Bullet Train","keywords":["bullettrain","front","transportation","vehicle","speed","fast","public","travel"],"skins":[{"unified":"1f685","native":"🚅"}],"version":1},"train2":{"id":"train2","name":"Train","keywords":["train2","transportation","vehicle"],"skins":[{"unified":"1f686","native":"🚆"}],"version":1},"metro":{"id":"metro","name":"Metro","keywords":["transportation","blue","square","mrt","underground","tube"],"skins":[{"unified":"1f687","native":"🚇"}],"version":1},"light_rail":{"id":"light_rail","name":"Light Rail","keywords":["transportation","vehicle"],"skins":[{"unified":"1f688","native":"🚈"}],"version":1},"station":{"id":"station","name":"Station","keywords":["transportation","vehicle","public"],"skins":[{"unified":"1f689","native":"🚉"}],"version":1},"tram":{"id":"tram","name":"Tram","keywords":["transportation","vehicle"],"skins":[{"unified":"1f68a","native":"🚊"}],"version":1},"monorail":{"id":"monorail","name":"Monorail","keywords":["transportation","vehicle"],"skins":[{"unified":"1f69d","native":"ðŸš"}],"version":1},"mountain_railway":{"id":"mountain_railway","name":"Mountain Railway","keywords":["transportation","vehicle"],"skins":[{"unified":"1f69e","native":"🚞"}],"version":1},"train":{"id":"train","name":"Tram Car","keywords":["train","transportation","vehicle","carriage","public","travel"],"skins":[{"unified":"1f68b","native":"🚋"}],"version":1},"bus":{"id":"bus","name":"Bus","keywords":["car","vehicle","transportation"],"skins":[{"unified":"1f68c","native":"🚌"}],"version":1},"oncoming_bus":{"id":"oncoming_bus","name":"Oncoming Bus","keywords":["vehicle","transportation"],"skins":[{"unified":"1f68d","native":"ðŸš"}],"version":1},"trolleybus":{"id":"trolleybus","name":"Trolleybus","keywords":["bart","transportation","vehicle"],"skins":[{"unified":"1f68e","native":"🚎"}],"version":1},"minibus":{"id":"minibus","name":"Minibus","keywords":["vehicle","car","transportation"],"skins":[{"unified":"1f690","native":"ðŸš"}],"version":1},"ambulance":{"id":"ambulance","name":"Ambulance","keywords":["health","911","hospital"],"skins":[{"unified":"1f691","native":"🚑"}],"version":1},"fire_engine":{"id":"fire_engine","name":"Fire Engine","keywords":["transportation","cars","vehicle"],"skins":[{"unified":"1f692","native":"🚒"}],"version":1},"police_car":{"id":"police_car","name":"Police Car","keywords":["vehicle","cars","transportation","law","legal","enforcement"],"skins":[{"unified":"1f693","native":"🚓"}],"version":1},"oncoming_police_car":{"id":"oncoming_police_car","name":"Oncoming Police Car","keywords":["vehicle","law","legal","enforcement","911"],"skins":[{"unified":"1f694","native":"🚔"}],"version":1},"taxi":{"id":"taxi","name":"Taxi","keywords":["uber","vehicle","cars","transportation"],"skins":[{"unified":"1f695","native":"🚕"}],"version":1},"oncoming_taxi":{"id":"oncoming_taxi","name":"Oncoming Taxi","keywords":["vehicle","cars","uber"],"skins":[{"unified":"1f696","native":"🚖"}],"version":1},"car":{"id":"car","name":"Automobile","keywords":["car","red","transportation","vehicle"],"skins":[{"unified":"1f697","native":"🚗"}],"version":1},"oncoming_automobile":{"id":"oncoming_automobile","name":"Oncoming Automobile","keywords":["car","vehicle","transportation"],"skins":[{"unified":"1f698","native":"🚘"}],"version":1},"blue_car":{"id":"blue_car","name":"Recreational Vehicle","keywords":["blue","car","sport","utility","transportation"],"skins":[{"unified":"1f699","native":"🚙"}],"version":1},"pickup_truck":{"id":"pickup_truck","name":"Pickup Truck","keywords":["car","transportation"],"skins":[{"unified":"1f6fb","native":"🛻"}],"version":13},"truck":{"id":"truck","name":"Delivery Truck","keywords":["cars","transportation"],"skins":[{"unified":"1f69a","native":"🚚"}],"version":1},"articulated_lorry":{"id":"articulated_lorry","name":"Articulated Lorry","keywords":["vehicle","cars","transportation","express"],"skins":[{"unified":"1f69b","native":"🚛"}],"version":1},"tractor":{"id":"tractor","name":"Tractor","keywords":["vehicle","car","farming","agriculture"],"skins":[{"unified":"1f69c","native":"🚜"}],"version":1},"racing_car":{"id":"racing_car","name":"Racing Car","keywords":["sports","race","fast","formula","f1"],"skins":[{"unified":"1f3ce-fe0f","native":"ðŸŽï¸"}],"version":1},"racing_motorcycle":{"id":"racing_motorcycle","name":"Motorcycle","keywords":["racing","race","sports","fast"],"skins":[{"unified":"1f3cd-fe0f","native":"ðŸï¸"}],"version":1},"motor_scooter":{"id":"motor_scooter","name":"Motor Scooter","keywords":["vehicle","vespa","sasha"],"skins":[{"unified":"1f6f5","native":"🛵"}],"version":3},"manual_wheelchair":{"id":"manual_wheelchair","name":"Manual Wheelchair","keywords":["accessibility"],"skins":[{"unified":"1f9bd","native":"🦽"}],"version":12},"motorized_wheelchair":{"id":"motorized_wheelchair","name":"Motorized Wheelchair","keywords":["accessibility"],"skins":[{"unified":"1f9bc","native":"🦼"}],"version":12},"auto_rickshaw":{"id":"auto_rickshaw","name":"Auto Rickshaw","keywords":["move","transportation"],"skins":[{"unified":"1f6fa","native":"🛺"}],"version":12},"bike":{"id":"bike","name":"Bicycle","keywords":["bike","sports","exercise","hipster"],"skins":[{"unified":"1f6b2","native":"🚲"}],"version":1},"scooter":{"id":"scooter","name":"Scooter","keywords":["kick","vehicle","razor"],"skins":[{"unified":"1f6f4","native":"🛴"}],"version":3},"skateboard":{"id":"skateboard","name":"Skateboard","keywords":["board"],"skins":[{"unified":"1f6f9","native":"🛹"}],"version":11},"roller_skate":{"id":"roller_skate","name":"Roller Skate","keywords":["footwear","sports"],"skins":[{"unified":"1f6fc","native":"🛼"}],"version":13},"busstop":{"id":"busstop","name":"Bus Stop","keywords":["busstop","transportation","wait"],"skins":[{"unified":"1f68f","native":"ðŸš"}],"version":1},"motorway":{"id":"motorway","name":"Motorway","keywords":["road","cupertino","interstate","highway"],"skins":[{"unified":"1f6e3-fe0f","native":"🛣ï¸"}],"version":1},"railway_track":{"id":"railway_track","name":"Railway Track","keywords":["train","transportation"],"skins":[{"unified":"1f6e4-fe0f","native":"🛤ï¸"}],"version":1},"oil_drum":{"id":"oil_drum","name":"Oil Drum","keywords":["barrell"],"skins":[{"unified":"1f6e2-fe0f","native":"🛢ï¸"}],"version":1},"fuelpump":{"id":"fuelpump","name":"Fuel Pump","keywords":["fuelpump","gas","station","petroleum"],"skins":[{"unified":"26fd","native":"⛽"}],"version":1},"wheel":{"id":"wheel","name":"Wheel","keywords":["car","transport"],"skins":[{"unified":"1f6de","native":"🛞"}],"version":14},"rotating_light":{"id":"rotating_light","name":"Police Car Light","keywords":["rotating","ambulance","911","emergency","alert","error","pinged","law","legal"],"skins":[{"unified":"1f6a8","native":"🚨"}],"version":1},"traffic_light":{"id":"traffic_light","name":"Horizontal Traffic Light","keywords":["transportation","signal"],"skins":[{"unified":"1f6a5","native":"🚥"}],"version":1},"vertical_traffic_light":{"id":"vertical_traffic_light","name":"Vertical Traffic Light","keywords":["transportation","driving"],"skins":[{"unified":"1f6a6","native":"🚦"}],"version":1},"octagonal_sign":{"id":"octagonal_sign","name":"Stop Sign","keywords":["octagonal"],"skins":[{"unified":"1f6d1","native":"🛑"}],"version":3},"construction":{"id":"construction","name":"Construction","keywords":["wip","progress","caution","warning"],"skins":[{"unified":"1f6a7","native":"🚧"}],"version":1},"anchor":{"id":"anchor","name":"Anchor","keywords":["ship","ferry","sea","boat"],"skins":[{"unified":"2693","native":"âš“"}],"version":1},"ring_buoy":{"id":"ring_buoy","name":"Ring Buoy","keywords":["life","saver","preserver"],"skins":[{"unified":"1f6df","native":"🛟"}],"version":14},"boat":{"id":"boat","name":"Sailboat","keywords":["boat","ship","summer","transportation","water","sailing"],"skins":[{"unified":"26f5","native":"⛵"}],"version":1},"canoe":{"id":"canoe","name":"Canoe","keywords":["boat","paddle","water","ship"],"skins":[{"unified":"1f6f6","native":"🛶"}],"version":3},"speedboat":{"id":"speedboat","name":"Speedboat","keywords":["ship","transportation","vehicle","summer"],"skins":[{"unified":"1f6a4","native":"🚤"}],"version":1},"passenger_ship":{"id":"passenger_ship","name":"Passenger Ship","keywords":["yacht","cruise","ferry"],"skins":[{"unified":"1f6f3-fe0f","native":"🛳ï¸"}],"version":1},"ferry":{"id":"ferry","name":"Ferry","keywords":["boat","ship","yacht"],"skins":[{"unified":"26f4-fe0f","native":"â›´ï¸"}],"version":1},"motor_boat":{"id":"motor_boat","name":"Motor Boat","keywords":["ship"],"skins":[{"unified":"1f6e5-fe0f","native":"🛥ï¸"}],"version":1},"ship":{"id":"ship","name":"Ship","keywords":["transportation","titanic","deploy"],"skins":[{"unified":"1f6a2","native":"🚢"}],"version":1},"airplane":{"id":"airplane","name":"Airplane","keywords":["vehicle","transportation","flight","fly"],"skins":[{"unified":"2708-fe0f","native":"✈ï¸"}],"version":1},"small_airplane":{"id":"small_airplane","name":"Small Airplane","keywords":["flight","transportation","fly","vehicle"],"skins":[{"unified":"1f6e9-fe0f","native":"🛩ï¸"}],"version":1},"airplane_departure":{"id":"airplane_departure","name":"Airplane Departure","keywords":["airport","flight","landing"],"skins":[{"unified":"1f6eb","native":"🛫"}],"version":1},"airplane_arriving":{"id":"airplane_arriving","name":"Airplane Arrival","keywords":["arriving","airport","flight","boarding"],"skins":[{"unified":"1f6ec","native":"🛬"}],"version":1},"parachute":{"id":"parachute","name":"Parachute","keywords":["fly","glide"],"skins":[{"unified":"1fa82","native":"🪂"}],"version":12},"seat":{"id":"seat","name":"Seat","keywords":["sit","airplane","transport","bus","flight","fly"],"skins":[{"unified":"1f4ba","native":"💺"}],"version":1},"helicopter":{"id":"helicopter","name":"Helicopter","keywords":["transportation","vehicle","fly"],"skins":[{"unified":"1f681","native":"ðŸš"}],"version":1},"suspension_railway":{"id":"suspension_railway","name":"Suspension Railway","keywords":["vehicle","transportation"],"skins":[{"unified":"1f69f","native":"🚟"}],"version":1},"mountain_cableway":{"id":"mountain_cableway","name":"Mountain Cableway","keywords":["transportation","vehicle","ski"],"skins":[{"unified":"1f6a0","native":"🚠"}],"version":1},"aerial_tramway":{"id":"aerial_tramway","name":"Aerial Tramway","keywords":["transportation","vehicle","ski"],"skins":[{"unified":"1f6a1","native":"🚡"}],"version":1},"satellite":{"id":"satellite","name":"Satellite","keywords":["communication","gps","orbit","spaceflight","NASA","ISS"],"skins":[{"unified":"1f6f0-fe0f","native":"🛰ï¸"}],"version":1},"rocket":{"id":"rocket","name":"Rocket","keywords":["launch","ship","staffmode","NASA","outer","space","fly"],"skins":[{"unified":"1f680","native":"🚀"}],"version":1},"flying_saucer":{"id":"flying_saucer","name":"Flying Saucer","keywords":["transportation","vehicle","ufo"],"skins":[{"unified":"1f6f8","native":"🛸"}],"version":5},"bellhop_bell":{"id":"bellhop_bell","name":"Bellhop Bell","keywords":["service"],"skins":[{"unified":"1f6ce-fe0f","native":"🛎ï¸"}],"version":1},"luggage":{"id":"luggage","name":"Luggage","keywords":["packing","travel"],"skins":[{"unified":"1f9f3","native":"🧳"}],"version":11},"hourglass":{"id":"hourglass","name":"Hourglass","keywords":["done","time","clock","oldschool","limit","exam","quiz","test"],"skins":[{"unified":"231b","native":"⌛"}],"version":1},"hourglass_flowing_sand":{"id":"hourglass_flowing_sand","name":"Hourglass Not Done","keywords":["flowing","sand","oldschool","time","countdown"],"skins":[{"unified":"23f3","native":"â³"}],"version":1},"watch":{"id":"watch","name":"Watch","keywords":["time","accessories"],"skins":[{"unified":"231a","native":"⌚"}],"version":1},"alarm_clock":{"id":"alarm_clock","name":"Alarm Clock","keywords":["time","wake"],"skins":[{"unified":"23f0","native":"â°"}],"version":1},"stopwatch":{"id":"stopwatch","name":"Stopwatch","keywords":["time","deadline"],"skins":[{"unified":"23f1-fe0f","native":"â±ï¸"}],"version":1},"timer_clock":{"id":"timer_clock","name":"Timer Clock","keywords":["alarm"],"skins":[{"unified":"23f2-fe0f","native":"â²ï¸"}],"version":1},"mantelpiece_clock":{"id":"mantelpiece_clock","name":"Mantelpiece Clock","keywords":["time"],"skins":[{"unified":"1f570-fe0f","native":"🕰ï¸"}],"version":1},"clock12":{"id":"clock12","name":"Twelve O’clock","keywords":["clock12","o","clock","time","noon","midnight","midday","late","early","schedule"],"skins":[{"unified":"1f55b","native":"🕛"}],"version":1},"clock1230":{"id":"clock1230","name":"Twelve-Thirty","keywords":["clock1230","twelve","thirty","time","late","early","schedule"],"skins":[{"unified":"1f567","native":"🕧"}],"version":1},"clock1":{"id":"clock1","name":"One O’clock","keywords":["clock1","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f550","native":"ðŸ•"}],"version":1},"clock130":{"id":"clock130","name":"One-Thirty","keywords":["clock130","one","thirty","time","late","early","schedule"],"skins":[{"unified":"1f55c","native":"🕜"}],"version":1},"clock2":{"id":"clock2","name":"Two O’clock","keywords":["clock2","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f551","native":"🕑"}],"version":1},"clock230":{"id":"clock230","name":"Two-Thirty","keywords":["clock230","two","thirty","time","late","early","schedule"],"skins":[{"unified":"1f55d","native":"ðŸ•"}],"version":1},"clock3":{"id":"clock3","name":"Three O’clock","keywords":["clock3","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f552","native":"🕒"}],"version":1},"clock330":{"id":"clock330","name":"Three-Thirty","keywords":["clock330","three","thirty","time","late","early","schedule"],"skins":[{"unified":"1f55e","native":"🕞"}],"version":1},"clock4":{"id":"clock4","name":"Four O’clock","keywords":["clock4","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f553","native":"🕓"}],"version":1},"clock430":{"id":"clock430","name":"Four-Thirty","keywords":["clock430","four","thirty","time","late","early","schedule"],"skins":[{"unified":"1f55f","native":"🕟"}],"version":1},"clock5":{"id":"clock5","name":"Five O’clock","keywords":["clock5","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f554","native":"🕔"}],"version":1},"clock530":{"id":"clock530","name":"Five-Thirty","keywords":["clock530","five","thirty","time","late","early","schedule"],"skins":[{"unified":"1f560","native":"🕠"}],"version":1},"clock6":{"id":"clock6","name":"Six O’clock","keywords":["clock6","o","clock","time","late","early","schedule","dawn","dusk"],"skins":[{"unified":"1f555","native":"🕕"}],"version":1},"clock630":{"id":"clock630","name":"Six-Thirty","keywords":["clock630","six","thirty","time","late","early","schedule"],"skins":[{"unified":"1f561","native":"🕡"}],"version":1},"clock7":{"id":"clock7","name":"Seven O’clock","keywords":["clock7","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f556","native":"🕖"}],"version":1},"clock730":{"id":"clock730","name":"Seven-Thirty","keywords":["clock730","seven","thirty","time","late","early","schedule"],"skins":[{"unified":"1f562","native":"🕢"}],"version":1},"clock8":{"id":"clock8","name":"Eight O’clock","keywords":["clock8","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f557","native":"🕗"}],"version":1},"clock830":{"id":"clock830","name":"Eight-Thirty","keywords":["clock830","eight","thirty","time","late","early","schedule"],"skins":[{"unified":"1f563","native":"🕣"}],"version":1},"clock9":{"id":"clock9","name":"Nine O’clock","keywords":["clock9","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f558","native":"🕘"}],"version":1},"clock930":{"id":"clock930","name":"Nine-Thirty","keywords":["clock930","nine","thirty","time","late","early","schedule"],"skins":[{"unified":"1f564","native":"🕤"}],"version":1},"clock10":{"id":"clock10","name":"Ten O’clock","keywords":["clock10","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f559","native":"🕙"}],"version":1},"clock1030":{"id":"clock1030","name":"Ten-Thirty","keywords":["clock1030","ten","thirty","time","late","early","schedule"],"skins":[{"unified":"1f565","native":"🕥"}],"version":1},"clock11":{"id":"clock11","name":"Eleven O’clock","keywords":["clock11","o","clock","time","late","early","schedule"],"skins":[{"unified":"1f55a","native":"🕚"}],"version":1},"clock1130":{"id":"clock1130","name":"Eleven-Thirty","keywords":["clock1130","eleven","thirty","time","late","early","schedule"],"skins":[{"unified":"1f566","native":"🕦"}],"version":1},"new_moon":{"id":"new_moon","name":"New Moon","keywords":["nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f311","native":"🌑"}],"version":1},"waxing_crescent_moon":{"id":"waxing_crescent_moon","name":"Waxing Crescent Moon","keywords":["nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f312","native":"🌒"}],"version":1},"first_quarter_moon":{"id":"first_quarter_moon","name":"First Quarter Moon","keywords":["nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f313","native":"🌓"}],"version":1},"moon":{"id":"moon","name":"Waxing Gibbous Moon","keywords":["nature","night","sky","gray","twilight","planet","space","evening","sleep"],"skins":[{"unified":"1f314","native":"🌔"}],"version":1},"full_moon":{"id":"full_moon","name":"Full Moon","keywords":["nature","yellow","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f315","native":"🌕"}],"version":1},"waning_gibbous_moon":{"id":"waning_gibbous_moon","name":"Waning Gibbous Moon","keywords":["nature","twilight","planet","space","night","evening","sleep","waxing"],"skins":[{"unified":"1f316","native":"🌖"}],"version":1},"last_quarter_moon":{"id":"last_quarter_moon","name":"Last Quarter Moon","keywords":["nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f317","native":"🌗"}],"version":1},"waning_crescent_moon":{"id":"waning_crescent_moon","name":"Waning Crescent Moon","keywords":["nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f318","native":"🌘"}],"version":1},"crescent_moon":{"id":"crescent_moon","name":"Crescent Moon","keywords":["night","sleep","sky","evening","magic"],"skins":[{"unified":"1f319","native":"🌙"}],"version":1},"new_moon_with_face":{"id":"new_moon_with_face","name":"New Moon Face","keywords":["with","nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f31a","native":"🌚"}],"version":1},"first_quarter_moon_with_face":{"id":"first_quarter_moon_with_face","name":"First Quarter Moon Face","keywords":["with","nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f31b","native":"🌛"}],"version":1},"last_quarter_moon_with_face":{"id":"last_quarter_moon_with_face","name":"Last Quarter Moon Face","keywords":["with","nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f31c","native":"🌜"}],"version":1},"thermometer":{"id":"thermometer","name":"Thermometer","keywords":["weather","temperature","hot","cold"],"skins":[{"unified":"1f321-fe0f","native":"🌡ï¸"}],"version":1},"sunny":{"id":"sunny","name":"Sun","keywords":["sunny","weather","nature","brightness","summer","beach","spring"],"skins":[{"unified":"2600-fe0f","native":"☀ï¸"}],"version":1},"full_moon_with_face":{"id":"full_moon_with_face","name":"Full Moon Face","keywords":["with","nature","twilight","planet","space","night","evening","sleep"],"skins":[{"unified":"1f31d","native":"ðŸŒ"}],"version":1},"sun_with_face":{"id":"sun_with_face","name":"Sun with Face","keywords":["nature","morning","sky"],"skins":[{"unified":"1f31e","native":"🌞"}],"version":1},"ringed_planet":{"id":"ringed_planet","name":"Ringed Planet","keywords":["outerspace"],"skins":[{"unified":"1fa90","native":"ðŸª"}],"version":12},"star":{"id":"star","name":"Star","keywords":["night","yellow"],"skins":[{"unified":"2b50","native":"â"}],"version":1},"star2":{"id":"star2","name":"Glowing Star","keywords":["star2","night","sparkle","awesome","good","magic"],"skins":[{"unified":"1f31f","native":"🌟"}],"version":1},"stars":{"id":"stars","name":"Shooting Star","keywords":["stars","night","photo"],"skins":[{"unified":"1f320","native":"🌠"}],"version":1},"milky_way":{"id":"milky_way","name":"Milky Way","keywords":["photo","space","stars"],"skins":[{"unified":"1f30c","native":"🌌"}],"version":1},"cloud":{"id":"cloud","name":"Cloud","keywords":["weather","sky"],"skins":[{"unified":"2601-fe0f","native":"â˜ï¸"}],"version":1},"partly_sunny":{"id":"partly_sunny","name":"Sun Behind Cloud","keywords":["partly","sunny","weather","nature","cloudy","morning","fall","spring"],"skins":[{"unified":"26c5","native":"â›…"}],"version":1},"thunder_cloud_and_rain":{"id":"thunder_cloud_and_rain","name":"Cloud with Lightning and Rain","keywords":["thunder","weather"],"skins":[{"unified":"26c8-fe0f","native":"⛈ï¸"}],"version":1},"mostly_sunny":{"id":"mostly_sunny","name":"Sun Behind Small Cloud","keywords":["mostly","sunny","weather"],"skins":[{"unified":"1f324-fe0f","native":"🌤ï¸"}],"version":1},"barely_sunny":{"id":"barely_sunny","name":"Sun Behind Large Cloud","keywords":["barely","sunny","weather"],"skins":[{"unified":"1f325-fe0f","native":"🌥ï¸"}],"version":1},"partly_sunny_rain":{"id":"partly_sunny_rain","name":"Sun Behind Rain Cloud","keywords":["partly","sunny","weather"],"skins":[{"unified":"1f326-fe0f","native":"🌦ï¸"}],"version":1},"rain_cloud":{"id":"rain_cloud","name":"Cloud with Rain","keywords":["weather"],"skins":[{"unified":"1f327-fe0f","native":"🌧ï¸"}],"version":1},"snow_cloud":{"id":"snow_cloud","name":"Cloud with Snow","keywords":["weather"],"skins":[{"unified":"1f328-fe0f","native":"🌨ï¸"}],"version":1},"lightning":{"id":"lightning","name":"Cloud with Lightning","keywords":["weather","thunder"],"skins":[{"unified":"1f329-fe0f","native":"🌩ï¸"}],"version":1},"tornado":{"id":"tornado","name":"Tornado","keywords":["cloud","weather","cyclone","twister"],"skins":[{"unified":"1f32a-fe0f","native":"🌪ï¸"}],"version":1},"fog":{"id":"fog","name":"Fog","keywords":["weather"],"skins":[{"unified":"1f32b-fe0f","native":"🌫ï¸"}],"version":1},"wind_blowing_face":{"id":"wind_blowing_face","name":"Wind Face","keywords":["blowing","gust","air"],"skins":[{"unified":"1f32c-fe0f","native":"🌬ï¸"}],"version":1},"cyclone":{"id":"cyclone","name":"Cyclone","keywords":["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"],"skins":[{"unified":"1f300","native":"🌀"}],"version":1},"rainbow":{"id":"rainbow","name":"Rainbow","keywords":["nature","happy","unicorn","face","photo","sky","spring"],"skins":[{"unified":"1f308","native":"🌈"}],"version":1},"closed_umbrella":{"id":"closed_umbrella","name":"Closed Umbrella","keywords":["weather","rain","drizzle"],"skins":[{"unified":"1f302","native":"🌂"}],"version":1},"umbrella":{"id":"umbrella","name":"Umbrella","keywords":["weather","spring"],"skins":[{"unified":"2602-fe0f","native":"☂ï¸"}],"version":1},"umbrella_with_rain_drops":{"id":"umbrella_with_rain_drops","name":"Umbrella with Rain Drops","keywords":["rainy","weather","spring"],"skins":[{"unified":"2614","native":"☔"}],"version":1},"umbrella_on_ground":{"id":"umbrella_on_ground","name":"Umbrella on Ground","keywords":["weather","summer"],"skins":[{"unified":"26f1-fe0f","native":"â›±ï¸"}],"version":1},"zap":{"id":"zap","name":"High Voltage","keywords":["zap","thunder","weather","lightning","bolt","fast"],"skins":[{"unified":"26a1","native":"âš¡"}],"version":1},"snowflake":{"id":"snowflake","name":"Snowflake","keywords":["winter","season","cold","weather","christmas","xmas"],"skins":[{"unified":"2744-fe0f","native":"â„ï¸"}],"version":1},"snowman":{"id":"snowman","name":"Snowman","keywords":["winter","season","cold","weather","christmas","xmas","frozen"],"skins":[{"unified":"2603-fe0f","native":"☃ï¸"}],"version":1},"snowman_without_snow":{"id":"snowman_without_snow","name":"Snowman Without Snow","keywords":["winter","season","cold","weather","christmas","xmas","frozen"],"skins":[{"unified":"26c4","native":"⛄"}],"version":1},"comet":{"id":"comet","name":"Comet","keywords":["space"],"skins":[{"unified":"2604-fe0f","native":"☄ï¸"}],"version":1},"fire":{"id":"fire","name":"Fire","keywords":["hot","cook","flame"],"skins":[{"unified":"1f525","native":"🔥"}],"version":1},"droplet":{"id":"droplet","name":"Droplet","keywords":["water","drip","faucet","spring"],"skins":[{"unified":"1f4a7","native":"💧"}],"version":1},"ocean":{"id":"ocean","name":"Water Wave","keywords":["ocean","sea","nature","tsunami","disaster"],"skins":[{"unified":"1f30a","native":"🌊"}],"version":1},"jack_o_lantern":{"id":"jack_o_lantern","name":"Jack-O-Lantern","keywords":["jack","o","lantern","halloween","light","pumpkin","creepy","fall"],"skins":[{"unified":"1f383","native":"🎃"}],"version":1},"christmas_tree":{"id":"christmas_tree","name":"Christmas Tree","keywords":["festival","vacation","december","xmas","celebration"],"skins":[{"unified":"1f384","native":"🎄"}],"version":1},"fireworks":{"id":"fireworks","name":"Fireworks","keywords":["photo","festival","carnival","congratulations"],"skins":[{"unified":"1f386","native":"🎆"}],"version":1},"sparkler":{"id":"sparkler","name":"Sparkler","keywords":["stars","night","shine"],"skins":[{"unified":"1f387","native":"🎇"}],"version":1},"firecracker":{"id":"firecracker","name":"Firecracker","keywords":["dynamite","boom","explode","explosion","explosive"],"skins":[{"unified":"1f9e8","native":"🧨"}],"version":11},"sparkles":{"id":"sparkles","name":"Sparkles","keywords":["stars","shine","shiny","cool","awesome","good","magic"],"skins":[{"unified":"2728","native":"✨"}],"version":1},"balloon":{"id":"balloon","name":"Balloon","keywords":["party","celebration","birthday","circus"],"skins":[{"unified":"1f388","native":"🎈"}],"version":1},"tada":{"id":"tada","name":"Party Popper","keywords":["tada","congratulations","birthday","magic","circus","celebration"],"skins":[{"unified":"1f389","native":"🎉"}],"version":1},"confetti_ball":{"id":"confetti_ball","name":"Confetti Ball","keywords":["festival","party","birthday","circus"],"skins":[{"unified":"1f38a","native":"🎊"}],"version":1},"tanabata_tree":{"id":"tanabata_tree","name":"Tanabata Tree","keywords":["plant","nature","branch","summer"],"skins":[{"unified":"1f38b","native":"🎋"}],"version":1},"bamboo":{"id":"bamboo","name":"Pine Decoration","keywords":["bamboo","plant","nature","vegetable","panda"],"skins":[{"unified":"1f38d","native":"ðŸŽ"}],"version":1},"dolls":{"id":"dolls","name":"Japanese Dolls","keywords":["toy","kimono"],"skins":[{"unified":"1f38e","native":"🎎"}],"version":1},"flags":{"id":"flags","name":"Carp Streamer","keywords":["flags","fish","japanese","koinobori","banner"],"skins":[{"unified":"1f38f","native":"ðŸŽ"}],"version":1},"wind_chime":{"id":"wind_chime","name":"Wind Chime","keywords":["nature","ding","spring","bell"],"skins":[{"unified":"1f390","native":"ðŸŽ"}],"version":1},"rice_scene":{"id":"rice_scene","name":"Moon Viewing Ceremony","keywords":["rice","scene","photo","japan","asia","tsukimi"],"skins":[{"unified":"1f391","native":"🎑"}],"version":1},"red_envelope":{"id":"red_envelope","name":"Red Envelope","keywords":["gift"],"skins":[{"unified":"1f9e7","native":"🧧"}],"version":11},"ribbon":{"id":"ribbon","name":"Ribbon","keywords":["decoration","pink","girl","bowtie"],"skins":[{"unified":"1f380","native":"🎀"}],"version":1},"gift":{"id":"gift","name":"Wrapped Gift","keywords":["present","birthday","christmas","xmas"],"skins":[{"unified":"1f381","native":"ðŸŽ"}],"version":1},"reminder_ribbon":{"id":"reminder_ribbon","name":"Reminder Ribbon","keywords":["sports","cause","support","awareness"],"skins":[{"unified":"1f397-fe0f","native":"🎗ï¸"}],"version":1},"admission_tickets":{"id":"admission_tickets","name":"Admission Tickets","keywords":["sports","concert","entrance"],"skins":[{"unified":"1f39f-fe0f","native":"🎟ï¸"}],"version":1},"ticket":{"id":"ticket","name":"Ticket","keywords":["event","concert","pass"],"skins":[{"unified":"1f3ab","native":"🎫"}],"version":1},"medal":{"id":"medal","name":"Military Medal","keywords":["award","winning","army"],"skins":[{"unified":"1f396-fe0f","native":"🎖ï¸"}],"version":1},"trophy":{"id":"trophy","name":"Trophy","keywords":["win","award","contest","place","ftw","ceremony"],"skins":[{"unified":"1f3c6","native":"ðŸ†"}],"version":1},"sports_medal":{"id":"sports_medal","name":"Sports Medal","keywords":["award","winning"],"skins":[{"unified":"1f3c5","native":"ðŸ…"}],"version":1},"first_place_medal":{"id":"first_place_medal","name":"1st Place Medal","keywords":["first","award","winning"],"skins":[{"unified":"1f947","native":"🥇"}],"version":3},"second_place_medal":{"id":"second_place_medal","name":"2nd Place Medal","keywords":["second","award"],"skins":[{"unified":"1f948","native":"🥈"}],"version":3},"third_place_medal":{"id":"third_place_medal","name":"3rd Place Medal","keywords":["third","award"],"skins":[{"unified":"1f949","native":"🥉"}],"version":3},"soccer":{"id":"soccer","name":"Soccer Ball","keywords":["sports","football"],"skins":[{"unified":"26bd","native":"âš½"}],"version":1},"baseball":{"id":"baseball","name":"Baseball","keywords":["sports","balls"],"skins":[{"unified":"26be","native":"âš¾"}],"version":1},"softball":{"id":"softball","name":"Softball","keywords":["sports","balls"],"skins":[{"unified":"1f94e","native":"🥎"}],"version":11},"basketball":{"id":"basketball","name":"Basketball","keywords":["sports","balls","NBA"],"skins":[{"unified":"1f3c0","native":"ðŸ€"}],"version":1},"volleyball":{"id":"volleyball","name":"Volleyball","keywords":["sports","balls"],"skins":[{"unified":"1f3d0","native":"ðŸ"}],"version":1},"football":{"id":"football","name":"American Football","keywords":["sports","balls","NFL"],"skins":[{"unified":"1f3c8","native":"ðŸˆ"}],"version":1},"rugby_football":{"id":"rugby_football","name":"Rugby Football","keywords":["sports","team"],"skins":[{"unified":"1f3c9","native":"ðŸ‰"}],"version":1},"tennis":{"id":"tennis","name":"Tennis","keywords":["sports","balls","green"],"skins":[{"unified":"1f3be","native":"🎾"}],"version":1},"flying_disc":{"id":"flying_disc","name":"Flying Disc","keywords":["sports","frisbee","ultimate"],"skins":[{"unified":"1f94f","native":"ðŸ¥"}],"version":11},"bowling":{"id":"bowling","name":"Bowling","keywords":["sports","fun","play"],"skins":[{"unified":"1f3b3","native":"🎳"}],"version":1},"cricket_bat_and_ball":{"id":"cricket_bat_and_ball","name":"Cricket Game","keywords":["bat","and","ball","sports"],"skins":[{"unified":"1f3cf","native":"ðŸ"}],"version":1},"field_hockey_stick_and_ball":{"id":"field_hockey_stick_and_ball","name":"Field Hockey","keywords":["stick","and","ball","sports"],"skins":[{"unified":"1f3d1","native":"ðŸ‘"}],"version":1},"ice_hockey_stick_and_puck":{"id":"ice_hockey_stick_and_puck","name":"Ice Hockey","keywords":["stick","and","puck","sports"],"skins":[{"unified":"1f3d2","native":"ðŸ’"}],"version":1},"lacrosse":{"id":"lacrosse","name":"Lacrosse","keywords":["sports","ball","stick"],"skins":[{"unified":"1f94d","native":"ðŸ¥"}],"version":11},"table_tennis_paddle_and_ball":{"id":"table_tennis_paddle_and_ball","name":"Ping Pong","keywords":["table","tennis","paddle","and","ball","sports","pingpong"],"skins":[{"unified":"1f3d3","native":"ðŸ“"}],"version":1},"badminton_racquet_and_shuttlecock":{"id":"badminton_racquet_and_shuttlecock","name":"Badminton","keywords":["racquet","and","shuttlecock","sports"],"skins":[{"unified":"1f3f8","native":"ðŸ¸"}],"version":1},"boxing_glove":{"id":"boxing_glove","name":"Boxing Glove","keywords":["sports","fighting"],"skins":[{"unified":"1f94a","native":"🥊"}],"version":3},"martial_arts_uniform":{"id":"martial_arts_uniform","name":"Martial Arts Uniform","keywords":["judo","karate","taekwondo"],"skins":[{"unified":"1f94b","native":"🥋"}],"version":3},"goal_net":{"id":"goal_net","name":"Goal Net","keywords":["sports"],"skins":[{"unified":"1f945","native":"🥅"}],"version":3},"golf":{"id":"golf","name":"Flag in Hole","keywords":["golf","sports","business","summer"],"skins":[{"unified":"26f3","native":"⛳"}],"version":1},"ice_skate":{"id":"ice_skate","name":"Ice Skate","keywords":["sports"],"skins":[{"unified":"26f8-fe0f","native":"⛸ï¸"}],"version":1},"fishing_pole_and_fish":{"id":"fishing_pole_and_fish","name":"Fishing Pole","keywords":["and","fish","food","hobby","summer"],"skins":[{"unified":"1f3a3","native":"🎣"}],"version":1},"diving_mask":{"id":"diving_mask","name":"Diving Mask","keywords":["sport","ocean"],"skins":[{"unified":"1f93f","native":"🤿"}],"version":12},"running_shirt_with_sash":{"id":"running_shirt_with_sash","name":"Running Shirt","keywords":["with","sash","play","pageant"],"skins":[{"unified":"1f3bd","native":"🎽"}],"version":1},"ski":{"id":"ski","name":"Skis","keywords":["ski","sports","winter","cold","snow"],"skins":[{"unified":"1f3bf","native":"🎿"}],"version":1},"sled":{"id":"sled","name":"Sled","keywords":["sleigh","luge","toboggan"],"skins":[{"unified":"1f6f7","native":"🛷"}],"version":5},"curling_stone":{"id":"curling_stone","name":"Curling Stone","keywords":["sports"],"skins":[{"unified":"1f94c","native":"🥌"}],"version":5},"dart":{"id":"dart","name":"Bullseye","keywords":["dart","direct","hit","game","play","bar","target"],"skins":[{"unified":"1f3af","native":"🎯"}],"version":1},"yo-yo":{"id":"yo-yo","name":"Yo-Yo","keywords":["yo","toy"],"skins":[{"unified":"1fa80","native":"🪀"}],"version":12},"kite":{"id":"kite","name":"Kite","keywords":["wind","fly"],"skins":[{"unified":"1fa81","native":"ðŸª"}],"version":12},"8ball":{"id":"8ball","name":"Billiards","keywords":["8ball","pool","8","ball","hobby","game","luck","magic"],"skins":[{"unified":"1f3b1","native":"🎱"}],"version":1},"crystal_ball":{"id":"crystal_ball","name":"Crystal Ball","keywords":["disco","party","magic","circus","fortune","teller"],"skins":[{"unified":"1f52e","native":"🔮"}],"version":1},"magic_wand":{"id":"magic_wand","name":"Magic Wand","keywords":["supernature","power"],"skins":[{"unified":"1fa84","native":"🪄"}],"version":13},"nazar_amulet":{"id":"nazar_amulet","name":"Nazar Amulet","keywords":["bead","charm"],"skins":[{"unified":"1f9ff","native":"🧿"}],"version":11},"hamsa":{"id":"hamsa","name":"Hamsa","keywords":["religion","protection"],"skins":[{"unified":"1faac","native":"🪬"}],"version":14},"video_game":{"id":"video_game","name":"Video Game","keywords":["play","console","PS4","controller"],"skins":[{"unified":"1f3ae","native":"🎮"}],"version":1},"joystick":{"id":"joystick","name":"Joystick","keywords":["game","play"],"skins":[{"unified":"1f579-fe0f","native":"🕹ï¸"}],"version":1},"slot_machine":{"id":"slot_machine","name":"Slot Machine","keywords":["bet","gamble","vegas","fruit","luck","casino"],"skins":[{"unified":"1f3b0","native":"🎰"}],"version":1},"game_die":{"id":"game_die","name":"Game Die","keywords":["dice","random","tabletop","play","luck"],"skins":[{"unified":"1f3b2","native":"🎲"}],"version":1},"jigsaw":{"id":"jigsaw","name":"Puzzle Piece","keywords":["jigsaw","interlocking"],"skins":[{"unified":"1f9e9","native":"🧩"}],"version":11},"teddy_bear":{"id":"teddy_bear","name":"Teddy Bear","keywords":["plush","stuffed"],"skins":[{"unified":"1f9f8","native":"🧸"}],"version":11},"pinata":{"id":"pinata","name":"Pinata","keywords":["mexico","candy","celebration"],"skins":[{"unified":"1fa85","native":"🪅"}],"version":13},"mirror_ball":{"id":"mirror_ball","name":"Mirror Ball","keywords":["disco","dance","party"],"skins":[{"unified":"1faa9","native":"🪩"}],"version":14},"nesting_dolls":{"id":"nesting_dolls","name":"Nesting Dolls","keywords":["matryoshka","toy"],"skins":[{"unified":"1fa86","native":"🪆"}],"version":13},"spades":{"id":"spades","name":"Spade Suit","keywords":["spades","poker","cards","suits","magic"],"skins":[{"unified":"2660-fe0f","native":"â™ ï¸"}],"version":1},"hearts":{"id":"hearts","name":"Heart Suit","keywords":["hearts","poker","cards","magic","suits"],"skins":[{"unified":"2665-fe0f","native":"♥ï¸"}],"version":1},"diamonds":{"id":"diamonds","name":"Diamond Suit","keywords":["diamonds","poker","cards","magic","suits"],"skins":[{"unified":"2666-fe0f","native":"♦ï¸"}],"version":1},"clubs":{"id":"clubs","name":"Club Suit","keywords":["clubs","poker","cards","magic","suits"],"skins":[{"unified":"2663-fe0f","native":"♣ï¸"}],"version":1},"chess_pawn":{"id":"chess_pawn","name":"Chess Pawn","keywords":["expendable"],"skins":[{"unified":"265f-fe0f","native":"♟ï¸"}],"version":11},"black_joker":{"id":"black_joker","name":"Joker","keywords":["black","poker","cards","game","play","magic"],"skins":[{"unified":"1f0cf","native":"ðŸƒ"}],"version":1},"mahjong":{"id":"mahjong","name":"Mahjong Red Dragon","keywords":["game","play","chinese","kanji"],"skins":[{"unified":"1f004","native":"🀄"}],"version":1},"flower_playing_cards":{"id":"flower_playing_cards","name":"Flower Playing Cards","keywords":["game","sunset","red"],"skins":[{"unified":"1f3b4","native":"🎴"}],"version":1},"performing_arts":{"id":"performing_arts","name":"Performing Arts","keywords":["acting","theater","drama"],"skins":[{"unified":"1f3ad","native":"ðŸŽ"}],"version":1},"frame_with_picture":{"id":"frame_with_picture","name":"Framed Picture","keywords":["frame","with","photography"],"skins":[{"unified":"1f5bc-fe0f","native":"🖼ï¸"}],"version":1},"art":{"id":"art","name":"Artist Palette","keywords":["art","design","paint","draw","colors"],"skins":[{"unified":"1f3a8","native":"🎨"}],"version":1},"thread":{"id":"thread","name":"Thread","keywords":["needle","sewing","spool","string"],"skins":[{"unified":"1f9f5","native":"🧵"}],"version":11},"sewing_needle":{"id":"sewing_needle","name":"Sewing Needle","keywords":["stitches"],"skins":[{"unified":"1faa1","native":"🪡"}],"version":13},"yarn":{"id":"yarn","name":"Yarn","keywords":["ball","crochet","knit"],"skins":[{"unified":"1f9f6","native":"🧶"}],"version":11},"knot":{"id":"knot","name":"Knot","keywords":["rope","scout"],"skins":[{"unified":"1faa2","native":"🪢"}],"version":13},"eyeglasses":{"id":"eyeglasses","name":"Glasses","keywords":["eyeglasses","fashion","accessories","eyesight","nerdy","dork","geek"],"skins":[{"unified":"1f453","native":"👓"}],"version":1},"dark_sunglasses":{"id":"dark_sunglasses","name":"Sunglasses","keywords":["dark","face","cool","accessories"],"skins":[{"unified":"1f576-fe0f","native":"🕶ï¸"}],"version":1},"goggles":{"id":"goggles","name":"Goggles","keywords":["eyes","protection","safety"],"skins":[{"unified":"1f97d","native":"🥽"}],"version":11},"lab_coat":{"id":"lab_coat","name":"Lab Coat","keywords":["doctor","experiment","scientist","chemist"],"skins":[{"unified":"1f97c","native":"🥼"}],"version":11},"safety_vest":{"id":"safety_vest","name":"Safety Vest","keywords":["protection"],"skins":[{"unified":"1f9ba","native":"🦺"}],"version":12},"necktie":{"id":"necktie","name":"Necktie","keywords":["shirt","suitup","formal","fashion","cloth","business"],"skins":[{"unified":"1f454","native":"👔"}],"version":1},"shirt":{"id":"shirt","name":"T-Shirt","keywords":["shirt","tshirt","t","fashion","cloth","casual","tee"],"skins":[{"unified":"1f455","native":"👕"}],"version":1},"jeans":{"id":"jeans","name":"Jeans","keywords":["fashion","shopping"],"skins":[{"unified":"1f456","native":"👖"}],"version":1},"scarf":{"id":"scarf","name":"Scarf","keywords":["neck","winter","clothes"],"skins":[{"unified":"1f9e3","native":"🧣"}],"version":5},"gloves":{"id":"gloves","name":"Gloves","keywords":["hands","winter","clothes"],"skins":[{"unified":"1f9e4","native":"🧤"}],"version":5},"coat":{"id":"coat","name":"Coat","keywords":["jacket"],"skins":[{"unified":"1f9e5","native":"🧥"}],"version":5},"socks":{"id":"socks","name":"Socks","keywords":["stockings","clothes"],"skins":[{"unified":"1f9e6","native":"🧦"}],"version":5},"dress":{"id":"dress","name":"Dress","keywords":["clothes","fashion","shopping"],"skins":[{"unified":"1f457","native":"👗"}],"version":1},"kimono":{"id":"kimono","name":"Kimono","keywords":["dress","fashion","women","female","japanese"],"skins":[{"unified":"1f458","native":"👘"}],"version":1},"sari":{"id":"sari","name":"Sari","keywords":["dress"],"skins":[{"unified":"1f97b","native":"🥻"}],"version":12},"one-piece_swimsuit":{"id":"one-piece_swimsuit","name":"One-Piece Swimsuit","keywords":["one","piece","fashion"],"skins":[{"unified":"1fa71","native":"🩱"}],"version":12},"briefs":{"id":"briefs","name":"Briefs","keywords":["clothing"],"skins":[{"unified":"1fa72","native":"🩲"}],"version":12},"shorts":{"id":"shorts","name":"Shorts","keywords":["clothing"],"skins":[{"unified":"1fa73","native":"🩳"}],"version":12},"bikini":{"id":"bikini","name":"Bikini","keywords":["swimming","female","woman","girl","fashion","beach","summer"],"skins":[{"unified":"1f459","native":"👙"}],"version":1},"womans_clothes":{"id":"womans_clothes","name":"Womans Clothes","keywords":["woman","s","fashion","shopping","bags","female"],"skins":[{"unified":"1f45a","native":"👚"}],"version":1},"purse":{"id":"purse","name":"Purse","keywords":["fashion","accessories","money","sales","shopping"],"skins":[{"unified":"1f45b","native":"👛"}],"version":1},"handbag":{"id":"handbag","name":"Handbag","keywords":["fashion","accessory","accessories","shopping"],"skins":[{"unified":"1f45c","native":"👜"}],"version":1},"pouch":{"id":"pouch","name":"Pouch","keywords":["clutch","bag","accessories","shopping"],"skins":[{"unified":"1f45d","native":"ðŸ‘"}],"version":1},"shopping_bags":{"id":"shopping_bags","name":"Shopping Bags","keywords":["mall","buy","purchase"],"skins":[{"unified":"1f6cd-fe0f","native":"ðŸ›ï¸"}],"version":1},"school_satchel":{"id":"school_satchel","name":"Backpack","keywords":["school","satchel","student","education","bag"],"skins":[{"unified":"1f392","native":"🎒"}],"version":1},"thong_sandal":{"id":"thong_sandal","name":"Thong Sandal","keywords":["footwear","summer"],"skins":[{"unified":"1fa74","native":"🩴"}],"version":13},"mans_shoe":{"id":"mans_shoe","name":"Mans Shoe","keywords":["man","s","fashion","male"],"skins":[{"unified":"1f45e","native":"👞"}],"version":1},"athletic_shoe":{"id":"athletic_shoe","name":"Running Shoe","keywords":["athletic","shoes","sports","sneakers"],"skins":[{"unified":"1f45f","native":"👟"}],"version":1},"hiking_boot":{"id":"hiking_boot","name":"Hiking Boot","keywords":["backpacking","camping"],"skins":[{"unified":"1f97e","native":"🥾"}],"version":11},"womans_flat_shoe":{"id":"womans_flat_shoe","name":"Flat Shoe","keywords":["womans","ballet","slip","on","slipper"],"skins":[{"unified":"1f97f","native":"🥿"}],"version":11},"high_heel":{"id":"high_heel","name":"High-Heeled Shoe","keywords":["high","heel","heeled","fashion","shoes","female","pumps","stiletto"],"skins":[{"unified":"1f460","native":"👠"}],"version":1},"sandal":{"id":"sandal","name":"Womans Sandal","keywords":["woman","s","shoes","fashion","flip","flops"],"skins":[{"unified":"1f461","native":"👡"}],"version":1},"ballet_shoes":{"id":"ballet_shoes","name":"Ballet Shoes","keywords":["dance"],"skins":[{"unified":"1fa70","native":"🩰"}],"version":12},"boot":{"id":"boot","name":"Womans Boots","keywords":["boot","woman","s","shoes","fashion"],"skins":[{"unified":"1f462","native":"👢"}],"version":1},"crown":{"id":"crown","name":"Crown","keywords":["king","kod","leader","royalty","lord"],"skins":[{"unified":"1f451","native":"👑"}],"version":1},"womans_hat":{"id":"womans_hat","name":"Womans Hat","keywords":["woman","s","fashion","accessories","female","lady","spring"],"skins":[{"unified":"1f452","native":"👒"}],"version":1},"tophat":{"id":"tophat","name":"Top Hat","keywords":["tophat","magic","gentleman","classy","circus"],"skins":[{"unified":"1f3a9","native":"🎩"}],"version":1},"mortar_board":{"id":"mortar_board","name":"Graduation Cap","keywords":["mortar","board","school","college","degree","university","hat","legal","learn","education"],"skins":[{"unified":"1f393","native":"🎓"}],"version":1},"billed_cap":{"id":"billed_cap","name":"Billed Cap","keywords":["baseball"],"skins":[{"unified":"1f9e2","native":"🧢"}],"version":5},"military_helmet":{"id":"military_helmet","name":"Military Helmet","keywords":["army","protection"],"skins":[{"unified":"1fa96","native":"🪖"}],"version":13},"helmet_with_white_cross":{"id":"helmet_with_white_cross","name":"Rescue Worker’s Helmet","keywords":["with","white","cross","worker","s","construction","build"],"skins":[{"unified":"26d1-fe0f","native":"⛑ï¸"}],"version":1},"prayer_beads":{"id":"prayer_beads","name":"Prayer Beads","keywords":["dhikr","religious"],"skins":[{"unified":"1f4ff","native":"📿"}],"version":1},"lipstick":{"id":"lipstick","name":"Lipstick","keywords":["female","girl","fashion","woman"],"skins":[{"unified":"1f484","native":"💄"}],"version":1},"ring":{"id":"ring","name":"Ring","keywords":["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"],"skins":[{"unified":"1f48d","native":"ðŸ’"}],"version":1},"gem":{"id":"gem","name":"Gem Stone","keywords":["blue","ruby","diamond","jewelry"],"skins":[{"unified":"1f48e","native":"💎"}],"version":1},"mute":{"id":"mute","name":"Muted Speaker","keywords":["mute","sound","volume","silence","quiet"],"skins":[{"unified":"1f507","native":"🔇"}],"version":1},"speaker":{"id":"speaker","name":"Speaker","keywords":["low","volume","sound","silence","broadcast"],"skins":[{"unified":"1f508","native":"🔈"}],"version":1},"sound":{"id":"sound","name":"Speaker Medium Volume","keywords":["sound","broadcast"],"skins":[{"unified":"1f509","native":"🔉"}],"version":1},"loud_sound":{"id":"loud_sound","name":"Speaker High Volume","keywords":["loud","sound","noise","noisy","broadcast"],"skins":[{"unified":"1f50a","native":"🔊"}],"version":1},"loudspeaker":{"id":"loudspeaker","name":"Loudspeaker","keywords":["volume","sound"],"skins":[{"unified":"1f4e2","native":"📢"}],"version":1},"mega":{"id":"mega","name":"Megaphone","keywords":["mega","sound","speaker","volume"],"skins":[{"unified":"1f4e3","native":"📣"}],"version":1},"postal_horn":{"id":"postal_horn","name":"Postal Horn","keywords":["instrument","music"],"skins":[{"unified":"1f4ef","native":"📯"}],"version":1},"bell":{"id":"bell","name":"Bell","keywords":["sound","notification","christmas","xmas","chime"],"skins":[{"unified":"1f514","native":"🔔"}],"version":1},"no_bell":{"id":"no_bell","name":"Bell with Slash","keywords":["no","sound","volume","mute","quiet","silent"],"skins":[{"unified":"1f515","native":"🔕"}],"version":1},"musical_score":{"id":"musical_score","name":"Musical Score","keywords":["treble","clef","compose"],"skins":[{"unified":"1f3bc","native":"🎼"}],"version":1},"musical_note":{"id":"musical_note","name":"Musical Note","keywords":["score","tone","sound"],"skins":[{"unified":"1f3b5","native":"🎵"}],"version":1},"notes":{"id":"notes","name":"Musical Notes","keywords":["music","score"],"skins":[{"unified":"1f3b6","native":"🎶"}],"version":1},"studio_microphone":{"id":"studio_microphone","name":"Studio Microphone","keywords":["sing","recording","artist","talkshow"],"skins":[{"unified":"1f399-fe0f","native":"🎙ï¸"}],"version":1},"level_slider":{"id":"level_slider","name":"Level Slider","keywords":["scale"],"skins":[{"unified":"1f39a-fe0f","native":"🎚ï¸"}],"version":1},"control_knobs":{"id":"control_knobs","name":"Control Knobs","keywords":["dial"],"skins":[{"unified":"1f39b-fe0f","native":"🎛ï¸"}],"version":1},"microphone":{"id":"microphone","name":"Microphone","keywords":["sound","music","PA","sing","talkshow"],"skins":[{"unified":"1f3a4","native":"🎤"}],"version":1},"headphones":{"id":"headphones","name":"Headphone","keywords":["headphones","music","score","gadgets"],"skins":[{"unified":"1f3a7","native":"🎧"}],"version":1},"radio":{"id":"radio","name":"Radio","keywords":["communication","music","podcast","program"],"skins":[{"unified":"1f4fb","native":"📻"}],"version":1},"saxophone":{"id":"saxophone","name":"Saxophone","keywords":["music","instrument","jazz","blues"],"skins":[{"unified":"1f3b7","native":"🎷"}],"version":1},"accordion":{"id":"accordion","name":"Accordion","keywords":["music"],"skins":[{"unified":"1fa97","native":"🪗"}],"version":13},"guitar":{"id":"guitar","name":"Guitar","keywords":["music","instrument"],"skins":[{"unified":"1f3b8","native":"🎸"}],"version":1},"musical_keyboard":{"id":"musical_keyboard","name":"Musical Keyboard","keywords":["piano","instrument","compose"],"skins":[{"unified":"1f3b9","native":"🎹"}],"version":1},"trumpet":{"id":"trumpet","name":"Trumpet","keywords":["music","brass"],"skins":[{"unified":"1f3ba","native":"🎺"}],"version":1},"violin":{"id":"violin","name":"Violin","keywords":["music","instrument","orchestra","symphony"],"skins":[{"unified":"1f3bb","native":"🎻"}],"version":1},"banjo":{"id":"banjo","name":"Banjo","keywords":["music","instructment"],"skins":[{"unified":"1fa95","native":"🪕"}],"version":12},"drum_with_drumsticks":{"id":"drum_with_drumsticks","name":"Drum","keywords":["with","drumsticks","music","instrument","snare"],"skins":[{"unified":"1f941","native":"ðŸ¥"}],"version":3},"long_drum":{"id":"long_drum","name":"Long Drum","keywords":["music"],"skins":[{"unified":"1fa98","native":"🪘"}],"version":13},"iphone":{"id":"iphone","name":"Mobile Phone","keywords":["iphone","technology","apple","gadgets","dial"],"skins":[{"unified":"1f4f1","native":"📱"}],"version":1},"calling":{"id":"calling","name":"Mobile Phone with Arrow","keywords":["calling","iphone","incoming"],"skins":[{"unified":"1f4f2","native":"📲"}],"version":1},"phone":{"id":"phone","name":"Telephone","keywords":["phone","technology","communication","dial"],"skins":[{"unified":"260e-fe0f","native":"☎ï¸"}],"version":1},"telephone_receiver":{"id":"telephone_receiver","name":"Telephone Receiver","keywords":["technology","communication","dial"],"skins":[{"unified":"1f4de","native":"📞"}],"version":1},"pager":{"id":"pager","name":"Pager","keywords":["bbcall","oldschool","90s"],"skins":[{"unified":"1f4df","native":"📟"}],"version":1},"fax":{"id":"fax","name":"Fax Machine","keywords":["communication","technology"],"skins":[{"unified":"1f4e0","native":"📠"}],"version":1},"battery":{"id":"battery","name":"Battery","keywords":["power","energy","sustain"],"skins":[{"unified":"1f50b","native":"🔋"}],"version":1},"low_battery":{"id":"low_battery","name":"Low Battery","keywords":["drained","dead"],"skins":[{"unified":"1faab","native":"🪫"}],"version":14},"electric_plug":{"id":"electric_plug","name":"Electric Plug","keywords":["charger","power"],"skins":[{"unified":"1f50c","native":"🔌"}],"version":1},"computer":{"id":"computer","name":"Laptop","keywords":["computer","technology","screen","display","monitor"],"skins":[{"unified":"1f4bb","native":"💻"}],"version":1},"desktop_computer":{"id":"desktop_computer","name":"Desktop Computer","keywords":["technology","computing","screen"],"skins":[{"unified":"1f5a5-fe0f","native":"🖥ï¸"}],"version":1},"printer":{"id":"printer","name":"Printer","keywords":["paper","ink"],"skins":[{"unified":"1f5a8-fe0f","native":"🖨ï¸"}],"version":1},"keyboard":{"id":"keyboard","name":"Keyboard","keywords":["technology","computer","type","input","text"],"skins":[{"unified":"2328-fe0f","native":"⌨ï¸"}],"version":1},"three_button_mouse":{"id":"three_button_mouse","name":"Computer Mouse","keywords":["three","button","click"],"skins":[{"unified":"1f5b1-fe0f","native":"🖱ï¸"}],"version":1},"trackball":{"id":"trackball","name":"Trackball","keywords":["technology","trackpad"],"skins":[{"unified":"1f5b2-fe0f","native":"🖲ï¸"}],"version":1},"minidisc":{"id":"minidisc","name":"Minidisc","keywords":["computer","disk","technology","record","data","90s"],"skins":[{"unified":"1f4bd","native":"💽"}],"version":1},"floppy_disk":{"id":"floppy_disk","name":"Floppy Disk","keywords":["oldschool","technology","save","90s","80s"],"skins":[{"unified":"1f4be","native":"💾"}],"version":1},"cd":{"id":"cd","name":"Optical Disc","keywords":["cd","disk","technology","dvd","90s"],"skins":[{"unified":"1f4bf","native":"💿"}],"version":1},"dvd":{"id":"dvd","name":"Dvd","keywords":["cd","disk","disc"],"skins":[{"unified":"1f4c0","native":"📀"}],"version":1},"abacus":{"id":"abacus","name":"Abacus","keywords":["calculation"],"skins":[{"unified":"1f9ee","native":"🧮"}],"version":11},"movie_camera":{"id":"movie_camera","name":"Movie Camera","keywords":["film","record"],"skins":[{"unified":"1f3a5","native":"🎥"}],"version":1},"film_frames":{"id":"film_frames","name":"Film Frames","keywords":["movie"],"skins":[{"unified":"1f39e-fe0f","native":"🎞ï¸"}],"version":1},"film_projector":{"id":"film_projector","name":"Film Projector","keywords":["video","tape","record","movie"],"skins":[{"unified":"1f4fd-fe0f","native":"📽ï¸"}],"version":1},"clapper":{"id":"clapper","name":"Clapper Board","keywords":["movie","film","record"],"skins":[{"unified":"1f3ac","native":"🎬"}],"version":1},"tv":{"id":"tv","name":"Television","keywords":["tv","technology","program","oldschool","show"],"skins":[{"unified":"1f4fa","native":"📺"}],"version":1},"camera":{"id":"camera","name":"Camera","keywords":["gadgets","photography"],"skins":[{"unified":"1f4f7","native":"📷"}],"version":1},"camera_with_flash":{"id":"camera_with_flash","name":"Camera with Flash","keywords":["photography","gadgets"],"skins":[{"unified":"1f4f8","native":"📸"}],"version":1},"video_camera":{"id":"video_camera","name":"Video Camera","keywords":["film","record"],"skins":[{"unified":"1f4f9","native":"📹"}],"version":1},"vhs":{"id":"vhs","name":"Videocassette","keywords":["vhs","record","video","oldschool","90s","80s"],"skins":[{"unified":"1f4fc","native":"📼"}],"version":1},"mag":{"id":"mag","name":"Magnifying Glass Tilted Left","keywords":["mag","search","zoom","find","detective"],"skins":[{"unified":"1f50d","native":"ðŸ”"}],"version":1},"mag_right":{"id":"mag_right","name":"Magnifying Glass Tilted Right","keywords":["mag","search","zoom","find","detective"],"skins":[{"unified":"1f50e","native":"🔎"}],"version":1},"candle":{"id":"candle","name":"Candle","keywords":["fire","wax"],"skins":[{"unified":"1f56f-fe0f","native":"🕯ï¸"}],"version":1},"bulb":{"id":"bulb","name":"Light Bulb","keywords":["electricity","idea"],"skins":[{"unified":"1f4a1","native":"💡"}],"version":1},"flashlight":{"id":"flashlight","name":"Flashlight","keywords":["dark","camping","sight","night"],"skins":[{"unified":"1f526","native":"🔦"}],"version":1},"izakaya_lantern":{"id":"izakaya_lantern","name":"Izakaya Lantern","keywords":["red","paper","light","halloween","spooky"],"skins":[{"unified":"1f3ee","native":"ðŸ®"}],"version":1},"diya_lamp":{"id":"diya_lamp","name":"Diya Lamp","keywords":["lighting"],"skins":[{"unified":"1fa94","native":"🪔"}],"version":12},"notebook_with_decorative_cover":{"id":"notebook_with_decorative_cover","name":"Notebook with Decorative Cover","keywords":["classroom","notes","record","paper","study"],"skins":[{"unified":"1f4d4","native":"📔"}],"version":1},"closed_book":{"id":"closed_book","name":"Closed Book","keywords":["read","library","knowledge","textbook","learn"],"skins":[{"unified":"1f4d5","native":"📕"}],"version":1},"book":{"id":"book","name":"Open Book","keywords":["read","library","knowledge","literature","learn","study"],"skins":[{"unified":"1f4d6","native":"📖"}],"version":1},"green_book":{"id":"green_book","name":"Green Book","keywords":["read","library","knowledge","study"],"skins":[{"unified":"1f4d7","native":"📗"}],"version":1},"blue_book":{"id":"blue_book","name":"Blue Book","keywords":["read","library","knowledge","learn","study"],"skins":[{"unified":"1f4d8","native":"📘"}],"version":1},"orange_book":{"id":"orange_book","name":"Orange Book","keywords":["read","library","knowledge","textbook","study"],"skins":[{"unified":"1f4d9","native":"📙"}],"version":1},"books":{"id":"books","name":"Books","keywords":["literature","library","study"],"skins":[{"unified":"1f4da","native":"📚"}],"version":1},"notebook":{"id":"notebook","name":"Notebook","keywords":["stationery","record","notes","paper","study"],"skins":[{"unified":"1f4d3","native":"📓"}],"version":1},"ledger":{"id":"ledger","name":"Ledger","keywords":["notes","paper"],"skins":[{"unified":"1f4d2","native":"📒"}],"version":1},"page_with_curl":{"id":"page_with_curl","name":"Page with Curl","keywords":["documents","office","paper"],"skins":[{"unified":"1f4c3","native":"📃"}],"version":1},"scroll":{"id":"scroll","name":"Scroll","keywords":["documents","ancient","history","paper"],"skins":[{"unified":"1f4dc","native":"📜"}],"version":1},"page_facing_up":{"id":"page_facing_up","name":"Page Facing Up","keywords":["documents","office","paper","information"],"skins":[{"unified":"1f4c4","native":"📄"}],"version":1},"newspaper":{"id":"newspaper","name":"Newspaper","keywords":["press","headline"],"skins":[{"unified":"1f4f0","native":"📰"}],"version":1},"rolled_up_newspaper":{"id":"rolled_up_newspaper","name":"Rolled-Up Newspaper","keywords":["rolled","up","press","headline"],"skins":[{"unified":"1f5de-fe0f","native":"🗞ï¸"}],"version":1},"bookmark_tabs":{"id":"bookmark_tabs","name":"Bookmark Tabs","keywords":["favorite","save","order","tidy"],"skins":[{"unified":"1f4d1","native":"📑"}],"version":1},"bookmark":{"id":"bookmark","name":"Bookmark","keywords":["favorite","label","save"],"skins":[{"unified":"1f516","native":"🔖"}],"version":1},"label":{"id":"label","name":"Label","keywords":["sale","tag"],"skins":[{"unified":"1f3f7-fe0f","native":"ðŸ·ï¸"}],"version":1},"moneybag":{"id":"moneybag","name":"Money Bag","keywords":["moneybag","dollar","payment","coins","sale"],"skins":[{"unified":"1f4b0","native":"💰"}],"version":1},"coin":{"id":"coin","name":"Coin","keywords":["money","currency"],"skins":[{"unified":"1fa99","native":"🪙"}],"version":13},"yen":{"id":"yen","name":"Yen Banknote","keywords":["money","sales","japanese","dollar","currency"],"skins":[{"unified":"1f4b4","native":"💴"}],"version":1},"dollar":{"id":"dollar","name":"Dollar Banknote","keywords":["money","sales","bill","currency"],"skins":[{"unified":"1f4b5","native":"💵"}],"version":1},"euro":{"id":"euro","name":"Euro Banknote","keywords":["money","sales","dollar","currency"],"skins":[{"unified":"1f4b6","native":"💶"}],"version":1},"pound":{"id":"pound","name":"Pound Banknote","keywords":["british","sterling","money","sales","bills","uk","england","currency"],"skins":[{"unified":"1f4b7","native":"💷"}],"version":1},"money_with_wings":{"id":"money_with_wings","name":"Money with Wings","keywords":["dollar","bills","payment","sale"],"skins":[{"unified":"1f4b8","native":"💸"}],"version":1},"credit_card":{"id":"credit_card","name":"Credit Card","keywords":["money","sales","dollar","bill","payment","shopping"],"skins":[{"unified":"1f4b3","native":"💳"}],"version":1},"receipt":{"id":"receipt","name":"Receipt","keywords":["accounting","expenses"],"skins":[{"unified":"1f9fe","native":"🧾"}],"version":11},"chart":{"id":"chart","name":"Chart Increasing with Yen","keywords":["green","square","graph","presentation","stats"],"skins":[{"unified":"1f4b9","native":"💹"}],"version":1},"email":{"id":"email","name":"Envelope","keywords":["email","letter","postal","inbox","communication"],"skins":[{"unified":"2709-fe0f","native":"✉ï¸"}],"version":1},"e-mail":{"id":"e-mail","name":"E-Mail","keywords":["e","mail","communication","inbox"],"skins":[{"unified":"1f4e7","native":"📧"}],"version":1},"incoming_envelope":{"id":"incoming_envelope","name":"Incoming Envelope","keywords":["email","inbox"],"skins":[{"unified":"1f4e8","native":"📨"}],"version":1},"envelope_with_arrow":{"id":"envelope_with_arrow","name":"Envelope with Arrow","keywords":["email","communication"],"skins":[{"unified":"1f4e9","native":"📩"}],"version":1},"outbox_tray":{"id":"outbox_tray","name":"Outbox Tray","keywords":["inbox","email"],"skins":[{"unified":"1f4e4","native":"📤"}],"version":1},"inbox_tray":{"id":"inbox_tray","name":"Inbox Tray","keywords":["email","documents"],"skins":[{"unified":"1f4e5","native":"📥"}],"version":1},"package":{"id":"package","name":"Package","keywords":["mail","gift","cardboard","box","moving"],"skins":[{"unified":"1f4e6","native":"📦"}],"version":1},"mailbox":{"id":"mailbox","name":"Closed Mailbox with Raised Flag","keywords":["email","inbox","communication"],"skins":[{"unified":"1f4eb","native":"📫"}],"version":1},"mailbox_closed":{"id":"mailbox_closed","name":"Closed Mailbox with Lowered Flag","keywords":["email","communication","inbox"],"skins":[{"unified":"1f4ea","native":"📪"}],"version":1},"mailbox_with_mail":{"id":"mailbox_with_mail","name":"Open Mailbox with Raised Flag","keywords":["mail","email","inbox","communication"],"skins":[{"unified":"1f4ec","native":"📬"}],"version":1},"mailbox_with_no_mail":{"id":"mailbox_with_no_mail","name":"Open Mailbox with Lowered Flag","keywords":["no","mail","email","inbox"],"skins":[{"unified":"1f4ed","native":"ðŸ“"}],"version":1},"postbox":{"id":"postbox","name":"Postbox","keywords":["email","letter","envelope"],"skins":[{"unified":"1f4ee","native":"📮"}],"version":1},"ballot_box_with_ballot":{"id":"ballot_box_with_ballot","name":"Ballot Box with Ballot","keywords":["election","vote"],"skins":[{"unified":"1f5f3-fe0f","native":"🗳ï¸"}],"version":1},"pencil2":{"id":"pencil2","name":"Pencil","keywords":["pencil2","stationery","write","paper","writing","school","study"],"skins":[{"unified":"270f-fe0f","native":"âœï¸"}],"version":1},"black_nib":{"id":"black_nib","name":"Black Nib","keywords":["pen","stationery","writing","write"],"skins":[{"unified":"2712-fe0f","native":"✒ï¸"}],"version":1},"lower_left_fountain_pen":{"id":"lower_left_fountain_pen","name":"Fountain Pen","keywords":["lower","left","stationery","writing","write"],"skins":[{"unified":"1f58b-fe0f","native":"🖋ï¸"}],"version":1},"lower_left_ballpoint_pen":{"id":"lower_left_ballpoint_pen","name":"Pen","keywords":["lower","left","ballpoint","stationery","writing","write"],"skins":[{"unified":"1f58a-fe0f","native":"🖊ï¸"}],"version":1},"lower_left_paintbrush":{"id":"lower_left_paintbrush","name":"Paintbrush","keywords":["lower","left","drawing","creativity","art"],"skins":[{"unified":"1f58c-fe0f","native":"🖌ï¸"}],"version":1},"lower_left_crayon":{"id":"lower_left_crayon","name":"Crayon","keywords":["lower","left","drawing","creativity"],"skins":[{"unified":"1f58d-fe0f","native":"ðŸ–ï¸"}],"version":1},"memo":{"id":"memo","name":"Memo","keywords":["pencil","write","documents","stationery","paper","writing","legal","exam","quiz","test","study","compose"],"skins":[{"unified":"1f4dd","native":"ðŸ“"}],"version":1},"briefcase":{"id":"briefcase","name":"Briefcase","keywords":["business","documents","work","law","legal","job","career"],"skins":[{"unified":"1f4bc","native":"💼"}],"version":1},"file_folder":{"id":"file_folder","name":"File Folder","keywords":["documents","business","office"],"skins":[{"unified":"1f4c1","native":"ðŸ“"}],"version":1},"open_file_folder":{"id":"open_file_folder","name":"Open File Folder","keywords":["documents","load"],"skins":[{"unified":"1f4c2","native":"📂"}],"version":1},"card_index_dividers":{"id":"card_index_dividers","name":"Card Index Dividers","keywords":["organizing","business","stationery"],"skins":[{"unified":"1f5c2-fe0f","native":"🗂ï¸"}],"version":1},"date":{"id":"date","name":"Calendar","keywords":["date","schedule"],"skins":[{"unified":"1f4c5","native":"📅"}],"version":1},"calendar":{"id":"calendar","name":"Tear-off Calendar","keywords":["tear","off","schedule","date","planning"],"skins":[{"unified":"1f4c6","native":"📆"}],"version":1},"spiral_note_pad":{"id":"spiral_note_pad","name":"Spiral Notepad","keywords":["note","pad","memo","stationery"],"skins":[{"unified":"1f5d2-fe0f","native":"🗒ï¸"}],"version":1},"spiral_calendar_pad":{"id":"spiral_calendar_pad","name":"Spiral Calendar","keywords":["pad","date","schedule","planning"],"skins":[{"unified":"1f5d3-fe0f","native":"🗓ï¸"}],"version":1},"card_index":{"id":"card_index","name":"Card Index","keywords":["business","stationery"],"skins":[{"unified":"1f4c7","native":"📇"}],"version":1},"chart_with_upwards_trend":{"id":"chart_with_upwards_trend","name":"Chart Increasing","keywords":["with","upwards","trend","graph","presentation","stats","recovery","business","economics","money","sales","good","success"],"skins":[{"unified":"1f4c8","native":"📈"}],"version":1},"chart_with_downwards_trend":{"id":"chart_with_downwards_trend","name":"Chart Decreasing","keywords":["with","downwards","trend","graph","presentation","stats","recession","business","economics","money","sales","bad","failure"],"skins":[{"unified":"1f4c9","native":"📉"}],"version":1},"bar_chart":{"id":"bar_chart","name":"Bar Chart","keywords":["graph","presentation","stats"],"skins":[{"unified":"1f4ca","native":"📊"}],"version":1},"clipboard":{"id":"clipboard","name":"Clipboard","keywords":["stationery","documents"],"skins":[{"unified":"1f4cb","native":"📋"}],"version":1},"pushpin":{"id":"pushpin","name":"Pushpin","keywords":["stationery","mark","here"],"skins":[{"unified":"1f4cc","native":"📌"}],"version":1},"round_pushpin":{"id":"round_pushpin","name":"Round Pushpin","keywords":["stationery","location","map","here"],"skins":[{"unified":"1f4cd","native":"ðŸ“"}],"version":1},"paperclip":{"id":"paperclip","name":"Paperclip","keywords":["documents","stationery"],"skins":[{"unified":"1f4ce","native":"📎"}],"version":1},"linked_paperclips":{"id":"linked_paperclips","name":"Linked Paperclips","keywords":["documents","stationery"],"skins":[{"unified":"1f587-fe0f","native":"🖇ï¸"}],"version":1},"straight_ruler":{"id":"straight_ruler","name":"Straight Ruler","keywords":["stationery","calculate","length","math","school","drawing","architect","sketch"],"skins":[{"unified":"1f4cf","native":"ðŸ“"}],"version":1},"triangular_ruler":{"id":"triangular_ruler","name":"Triangular Ruler","keywords":["stationery","math","architect","sketch"],"skins":[{"unified":"1f4d0","native":"ðŸ“"}],"version":1},"scissors":{"id":"scissors","name":"Scissors","keywords":["stationery","cut"],"skins":[{"unified":"2702-fe0f","native":"✂ï¸"}],"version":1},"card_file_box":{"id":"card_file_box","name":"Card File Box","keywords":["business","stationery"],"skins":[{"unified":"1f5c3-fe0f","native":"🗃ï¸"}],"version":1},"file_cabinet":{"id":"file_cabinet","name":"File Cabinet","keywords":["filing","organizing"],"skins":[{"unified":"1f5c4-fe0f","native":"🗄ï¸"}],"version":1},"wastebasket":{"id":"wastebasket","name":"Wastebasket","keywords":["bin","trash","rubbish","garbage","toss"],"skins":[{"unified":"1f5d1-fe0f","native":"🗑ï¸"}],"version":1},"lock":{"id":"lock","name":"Lock","keywords":["locked","security","password","padlock"],"skins":[{"unified":"1f512","native":"🔒"}],"version":1},"unlock":{"id":"unlock","name":"Unlocked","keywords":["unlock","privacy","security"],"skins":[{"unified":"1f513","native":"🔓"}],"version":1},"lock_with_ink_pen":{"id":"lock_with_ink_pen","name":"Locked with Pen","keywords":["lock","ink","security","secret"],"skins":[{"unified":"1f50f","native":"ðŸ”"}],"version":1},"closed_lock_with_key":{"id":"closed_lock_with_key","name":"Locked with Key","keywords":["closed","lock","security","privacy"],"skins":[{"unified":"1f510","native":"ðŸ”"}],"version":1},"key":{"id":"key","name":"Key","keywords":["lock","door","password"],"skins":[{"unified":"1f511","native":"🔑"}],"version":1},"old_key":{"id":"old_key","name":"Old Key","keywords":["lock","door","password"],"skins":[{"unified":"1f5dd-fe0f","native":"ðŸ—ï¸"}],"version":1},"hammer":{"id":"hammer","name":"Hammer","keywords":["tools","build","create"],"skins":[{"unified":"1f528","native":"🔨"}],"version":1},"axe":{"id":"axe","name":"Axe","keywords":["tool","chop","cut"],"skins":[{"unified":"1fa93","native":"🪓"}],"version":12},"pick":{"id":"pick","name":"Pick","keywords":["tools","dig"],"skins":[{"unified":"26cf-fe0f","native":"â›ï¸"}],"version":1},"hammer_and_pick":{"id":"hammer_and_pick","name":"Hammer and Pick","keywords":["tools","build","create"],"skins":[{"unified":"2692-fe0f","native":"âš’ï¸"}],"version":1},"hammer_and_wrench":{"id":"hammer_and_wrench","name":"Hammer and Wrench","keywords":["tools","build","create"],"skins":[{"unified":"1f6e0-fe0f","native":"🛠ï¸"}],"version":1},"dagger_knife":{"id":"dagger_knife","name":"Dagger","keywords":["knife","weapon"],"skins":[{"unified":"1f5e1-fe0f","native":"🗡ï¸"}],"version":1},"crossed_swords":{"id":"crossed_swords","name":"Crossed Swords","keywords":["weapon"],"skins":[{"unified":"2694-fe0f","native":"âš”ï¸"}],"version":1},"gun":{"id":"gun","name":"Pistol","keywords":["gun","violence","weapon","revolver"],"skins":[{"unified":"1f52b","native":"🔫"}],"version":1},"boomerang":{"id":"boomerang","name":"Boomerang","keywords":["weapon"],"skins":[{"unified":"1fa83","native":"🪃"}],"version":13},"bow_and_arrow":{"id":"bow_and_arrow","name":"Bow and Arrow","keywords":["sports"],"skins":[{"unified":"1f3f9","native":"ðŸ¹"}],"version":1},"shield":{"id":"shield","name":"Shield","keywords":["protection","security"],"skins":[{"unified":"1f6e1-fe0f","native":"🛡ï¸"}],"version":1},"carpentry_saw":{"id":"carpentry_saw","name":"Carpentry Saw","keywords":["cut","chop"],"skins":[{"unified":"1fa9a","native":"🪚"}],"version":13},"wrench":{"id":"wrench","name":"Wrench","keywords":["tools","diy","ikea","fix","maintainer"],"skins":[{"unified":"1f527","native":"🔧"}],"version":1},"screwdriver":{"id":"screwdriver","name":"Screwdriver","keywords":["tools"],"skins":[{"unified":"1fa9b","native":"🪛"}],"version":13},"nut_and_bolt":{"id":"nut_and_bolt","name":"Nut and Bolt","keywords":["handy","tools","fix"],"skins":[{"unified":"1f529","native":"🔩"}],"version":1},"gear":{"id":"gear","name":"Gear","keywords":["cog"],"skins":[{"unified":"2699-fe0f","native":"âš™ï¸"}],"version":1},"compression":{"id":"compression","name":"Clamp","keywords":["compression","tool"],"skins":[{"unified":"1f5dc-fe0f","native":"🗜ï¸"}],"version":1},"scales":{"id":"scales","name":"Balance Scale","keywords":["scales","law","fairness","weight"],"skins":[{"unified":"2696-fe0f","native":"âš–ï¸"}],"version":1},"probing_cane":{"id":"probing_cane","name":"White Cane","keywords":["probing","accessibility"],"skins":[{"unified":"1f9af","native":"🦯"}],"version":12},"link":{"id":"link","name":"Link","keywords":["rings","url"],"skins":[{"unified":"1f517","native":"🔗"}],"version":1},"chains":{"id":"chains","name":"Chains","keywords":["lock","arrest"],"skins":[{"unified":"26d3-fe0f","native":"⛓ï¸"}],"version":1},"hook":{"id":"hook","name":"Hook","keywords":["tools"],"skins":[{"unified":"1fa9d","native":"ðŸª"}],"version":13},"toolbox":{"id":"toolbox","name":"Toolbox","keywords":["tools","diy","fix","maintainer","mechanic"],"skins":[{"unified":"1f9f0","native":"🧰"}],"version":11},"magnet":{"id":"magnet","name":"Magnet","keywords":["attraction","magnetic"],"skins":[{"unified":"1f9f2","native":"🧲"}],"version":11},"ladder":{"id":"ladder","name":"Ladder","keywords":["tools"],"skins":[{"unified":"1fa9c","native":"🪜"}],"version":13},"alembic":{"id":"alembic","name":"Alembic","keywords":["distilling","science","experiment","chemistry"],"skins":[{"unified":"2697-fe0f","native":"âš—ï¸"}],"version":1},"test_tube":{"id":"test_tube","name":"Test Tube","keywords":["chemistry","experiment","lab","science"],"skins":[{"unified":"1f9ea","native":"🧪"}],"version":11},"petri_dish":{"id":"petri_dish","name":"Petri Dish","keywords":["bacteria","biology","culture","lab"],"skins":[{"unified":"1f9eb","native":"🧫"}],"version":11},"dna":{"id":"dna","name":"Dna","keywords":["biologist","genetics","life"],"skins":[{"unified":"1f9ec","native":"🧬"}],"version":11},"microscope":{"id":"microscope","name":"Microscope","keywords":["laboratory","experiment","zoomin","science","study"],"skins":[{"unified":"1f52c","native":"🔬"}],"version":1},"telescope":{"id":"telescope","name":"Telescope","keywords":["stars","space","zoom","science","astronomy"],"skins":[{"unified":"1f52d","native":"ðŸ”"}],"version":1},"satellite_antenna":{"id":"satellite_antenna","name":"Satellite Antenna","keywords":["communication","future","radio","space"],"skins":[{"unified":"1f4e1","native":"📡"}],"version":1},"syringe":{"id":"syringe","name":"Syringe","keywords":["health","hospital","drugs","blood","medicine","needle","doctor","nurse"],"skins":[{"unified":"1f489","native":"💉"}],"version":1},"drop_of_blood":{"id":"drop_of_blood","name":"Drop of Blood","keywords":["period","hurt","harm","wound"],"skins":[{"unified":"1fa78","native":"🩸"}],"version":12},"pill":{"id":"pill","name":"Pill","keywords":["health","medicine","doctor","pharmacy","drug"],"skins":[{"unified":"1f48a","native":"💊"}],"version":1},"adhesive_bandage":{"id":"adhesive_bandage","name":"Adhesive Bandage","keywords":["heal"],"skins":[{"unified":"1fa79","native":"🩹"}],"version":12},"crutch":{"id":"crutch","name":"Crutch","keywords":["accessibility","assist"],"skins":[{"unified":"1fa7c","native":"🩼"}],"version":14},"stethoscope":{"id":"stethoscope","name":"Stethoscope","keywords":["health"],"skins":[{"unified":"1fa7a","native":"🩺"}],"version":12},"x-ray":{"id":"x-ray","name":"X-Ray","keywords":["x","ray","skeleton","medicine"],"skins":[{"unified":"1fa7b","native":"🩻"}],"version":14},"door":{"id":"door","name":"Door","keywords":["house","entry","exit"],"skins":[{"unified":"1f6aa","native":"🚪"}],"version":1},"elevator":{"id":"elevator","name":"Elevator","keywords":["lift"],"skins":[{"unified":"1f6d7","native":"🛗"}],"version":13},"mirror":{"id":"mirror","name":"Mirror","keywords":["reflection"],"skins":[{"unified":"1fa9e","native":"🪞"}],"version":13},"window":{"id":"window","name":"Window","keywords":["scenery"],"skins":[{"unified":"1fa9f","native":"🪟"}],"version":13},"bed":{"id":"bed","name":"Bed","keywords":["sleep","rest"],"skins":[{"unified":"1f6cf-fe0f","native":"ðŸ›ï¸"}],"version":1},"couch_and_lamp":{"id":"couch_and_lamp","name":"Couch and Lamp","keywords":["read","chill"],"skins":[{"unified":"1f6cb-fe0f","native":"🛋ï¸"}],"version":1},"chair":{"id":"chair","name":"Chair","keywords":["sit","furniture"],"skins":[{"unified":"1fa91","native":"🪑"}],"version":12},"toilet":{"id":"toilet","name":"Toilet","keywords":["restroom","wc","washroom","bathroom","potty"],"skins":[{"unified":"1f6bd","native":"🚽"}],"version":1},"plunger":{"id":"plunger","name":"Plunger","keywords":["toilet"],"skins":[{"unified":"1faa0","native":"🪠"}],"version":13},"shower":{"id":"shower","name":"Shower","keywords":["clean","water","bathroom"],"skins":[{"unified":"1f6bf","native":"🚿"}],"version":1},"bathtub":{"id":"bathtub","name":"Bathtub","keywords":["clean","shower","bathroom"],"skins":[{"unified":"1f6c1","native":"ðŸ›"}],"version":1},"mouse_trap":{"id":"mouse_trap","name":"Mouse Trap","keywords":["cheese"],"skins":[{"unified":"1faa4","native":"🪤"}],"version":13},"razor":{"id":"razor","name":"Razor","keywords":["cut"],"skins":[{"unified":"1fa92","native":"🪒"}],"version":12},"lotion_bottle":{"id":"lotion_bottle","name":"Lotion Bottle","keywords":["moisturizer","sunscreen"],"skins":[{"unified":"1f9f4","native":"🧴"}],"version":11},"safety_pin":{"id":"safety_pin","name":"Safety Pin","keywords":["diaper"],"skins":[{"unified":"1f9f7","native":"🧷"}],"version":11},"broom":{"id":"broom","name":"Broom","keywords":["cleaning","sweeping","witch"],"skins":[{"unified":"1f9f9","native":"🧹"}],"version":11},"basket":{"id":"basket","name":"Basket","keywords":["laundry"],"skins":[{"unified":"1f9fa","native":"🧺"}],"version":11},"roll_of_paper":{"id":"roll_of_paper","name":"Roll of Paper","keywords":[],"skins":[{"unified":"1f9fb","native":"🧻"}],"version":11},"bucket":{"id":"bucket","name":"Bucket","keywords":["water","container"],"skins":[{"unified":"1faa3","native":"🪣"}],"version":13},"soap":{"id":"soap","name":"Soap","keywords":["bar","bathing","cleaning","lather"],"skins":[{"unified":"1f9fc","native":"🧼"}],"version":11},"bubbles":{"id":"bubbles","name":"Bubbles","keywords":["soap","fun","carbonation","sparkling"],"skins":[{"unified":"1fae7","native":"🫧"}],"version":14},"toothbrush":{"id":"toothbrush","name":"Toothbrush","keywords":["hygiene","dental"],"skins":[{"unified":"1faa5","native":"🪥"}],"version":13},"sponge":{"id":"sponge","name":"Sponge","keywords":["absorbing","cleaning","porous"],"skins":[{"unified":"1f9fd","native":"🧽"}],"version":11},"fire_extinguisher":{"id":"fire_extinguisher","name":"Fire Extinguisher","keywords":["quench"],"skins":[{"unified":"1f9ef","native":"🧯"}],"version":11},"shopping_trolley":{"id":"shopping_trolley","name":"Shopping Cart","keywords":["trolley"],"skins":[{"unified":"1f6d2","native":"🛒"}],"version":3},"smoking":{"id":"smoking","name":"Cigarette","keywords":["smoking","kills","tobacco","joint","smoke"],"skins":[{"unified":"1f6ac","native":"🚬"}],"version":1},"coffin":{"id":"coffin","name":"Coffin","keywords":["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"],"skins":[{"unified":"26b0-fe0f","native":"âš°ï¸"}],"version":1},"headstone":{"id":"headstone","name":"Headstone","keywords":["death","rip","grave"],"skins":[{"unified":"1faa6","native":"🪦"}],"version":13},"funeral_urn":{"id":"funeral_urn","name":"Funeral Urn","keywords":["dead","die","death","rip","ashes"],"skins":[{"unified":"26b1-fe0f","native":"âš±ï¸"}],"version":1},"moyai":{"id":"moyai","name":"Moai","keywords":["moyai","rock","easter","island"],"skins":[{"unified":"1f5ff","native":"🗿"}],"version":1},"placard":{"id":"placard","name":"Placard","keywords":["announcement"],"skins":[{"unified":"1faa7","native":"🪧"}],"version":13},"identification_card":{"id":"identification_card","name":"Identification Card","keywords":["document"],"skins":[{"unified":"1faaa","native":"🪪"}],"version":14},"atm":{"id":"atm","name":"Atm Sign","keywords":["money","sales","cash","blue","square","payment","bank"],"skins":[{"unified":"1f3e7","native":"ðŸ§"}],"version":1},"put_litter_in_its_place":{"id":"put_litter_in_its_place","name":"Litter in Bin Sign","keywords":["put","its","place","blue","square","human","info"],"skins":[{"unified":"1f6ae","native":"🚮"}],"version":1},"potable_water":{"id":"potable_water","name":"Potable Water","keywords":["blue","square","liquid","restroom","cleaning","faucet"],"skins":[{"unified":"1f6b0","native":"🚰"}],"version":1},"wheelchair":{"id":"wheelchair","name":"Wheelchair Symbol","keywords":["blue","square","disabled","accessibility"],"skins":[{"unified":"267f","native":"♿"}],"version":1},"mens":{"id":"mens","name":"Men’s Room","keywords":["mens","men","s","toilet","restroom","wc","blue","square","gender","male"],"skins":[{"unified":"1f6b9","native":"🚹"}],"version":1},"womens":{"id":"womens","name":"Women’s Room","keywords":["womens","women","s","purple","square","woman","female","toilet","loo","restroom","gender"],"skins":[{"unified":"1f6ba","native":"🚺"}],"version":1},"restroom":{"id":"restroom","name":"Restroom","keywords":["blue","square","toilet","refresh","wc","gender"],"skins":[{"unified":"1f6bb","native":"🚻"}],"version":1},"baby_symbol":{"id":"baby_symbol","name":"Baby Symbol","keywords":["orange","square","child"],"skins":[{"unified":"1f6bc","native":"🚼"}],"version":1},"wc":{"id":"wc","name":"Water Closet","keywords":["wc","toilet","restroom","blue","square"],"skins":[{"unified":"1f6be","native":"🚾"}],"version":1},"passport_control":{"id":"passport_control","name":"Passport Control","keywords":["custom","blue","square"],"skins":[{"unified":"1f6c2","native":"🛂"}],"version":1},"customs":{"id":"customs","name":"Customs","keywords":["passport","border","blue","square"],"skins":[{"unified":"1f6c3","native":"🛃"}],"version":1},"baggage_claim":{"id":"baggage_claim","name":"Baggage Claim","keywords":["blue","square","airport","transport"],"skins":[{"unified":"1f6c4","native":"🛄"}],"version":1},"left_luggage":{"id":"left_luggage","name":"Left Luggage","keywords":["blue","square","travel"],"skins":[{"unified":"1f6c5","native":"🛅"}],"version":1},"warning":{"id":"warning","name":"Warning","keywords":["exclamation","wip","alert","error","problem","issue"],"skins":[{"unified":"26a0-fe0f","native":"âš ï¸"}],"version":1},"children_crossing":{"id":"children_crossing","name":"Children Crossing","keywords":["school","warning","danger","sign","driving","yellow","diamond"],"skins":[{"unified":"1f6b8","native":"🚸"}],"version":1},"no_entry":{"id":"no_entry","name":"No Entry","keywords":["limit","security","privacy","bad","denied","stop","circle"],"skins":[{"unified":"26d4","native":"â›”"}],"version":1},"no_entry_sign":{"id":"no_entry_sign","name":"Prohibited","keywords":["no","entry","sign","forbid","stop","limit","denied","disallow","circle"],"skins":[{"unified":"1f6ab","native":"🚫"}],"version":1},"no_bicycles":{"id":"no_bicycles","name":"No Bicycles","keywords":["cyclist","prohibited","circle"],"skins":[{"unified":"1f6b3","native":"🚳"}],"version":1},"no_smoking":{"id":"no_smoking","name":"No Smoking","keywords":["cigarette","blue","square","smell","smoke"],"skins":[{"unified":"1f6ad","native":"ðŸš"}],"version":1},"do_not_litter":{"id":"do_not_litter","name":"No Littering","keywords":["do","not","litter","trash","bin","garbage","circle"],"skins":[{"unified":"1f6af","native":"🚯"}],"version":1},"non-potable_water":{"id":"non-potable_water","name":"Non-Potable Water","keywords":["non","potable","drink","faucet","tap","circle"],"skins":[{"unified":"1f6b1","native":"🚱"}],"version":1},"no_pedestrians":{"id":"no_pedestrians","name":"No Pedestrians","keywords":["rules","crossing","walking","circle"],"skins":[{"unified":"1f6b7","native":"🚷"}],"version":1},"no_mobile_phones":{"id":"no_mobile_phones","name":"No Mobile Phones","keywords":["iphone","mute","circle"],"skins":[{"unified":"1f4f5","native":"📵"}],"version":1},"underage":{"id":"underage","name":"No One Under Eighteen","keywords":["underage","18","drink","pub","night","minor","circle"],"skins":[{"unified":"1f51e","native":"🔞"}],"version":1},"radioactive_sign":{"id":"radioactive_sign","name":"Radioactive","keywords":["sign","nuclear","danger"],"skins":[{"unified":"2622-fe0f","native":"☢ï¸"}],"version":1},"biohazard_sign":{"id":"biohazard_sign","name":"Biohazard","keywords":["sign","danger"],"skins":[{"unified":"2623-fe0f","native":"☣ï¸"}],"version":1},"arrow_up":{"id":"arrow_up","name":"Up Arrow","keywords":["blue","square","continue","top","direction"],"skins":[{"unified":"2b06-fe0f","native":"⬆ï¸"}],"version":1},"arrow_upper_right":{"id":"arrow_upper_right","name":"Up-Right Arrow","keywords":["upper","right","up","blue","square","point","direction","diagonal","northeast"],"skins":[{"unified":"2197-fe0f","native":"↗ï¸"}],"version":1},"arrow_right":{"id":"arrow_right","name":"Right Arrow","keywords":["blue","square","next"],"skins":[{"unified":"27a1-fe0f","native":"âž¡ï¸"}],"version":1},"arrow_lower_right":{"id":"arrow_lower_right","name":"South East Arrow","keywords":["lower","right","down","blue","square","direction","diagonal","southeast"],"skins":[{"unified":"2198-fe0f","native":"↘ï¸"}],"version":1},"arrow_down":{"id":"arrow_down","name":"Down Arrow","keywords":["blue","square","direction","bottom"],"skins":[{"unified":"2b07-fe0f","native":"⬇ï¸"}],"version":1},"arrow_lower_left":{"id":"arrow_lower_left","name":"Down-Left Arrow","keywords":["lower","left","down","blue","square","direction","diagonal","southwest"],"skins":[{"unified":"2199-fe0f","native":"↙ï¸"}],"version":1},"arrow_left":{"id":"arrow_left","name":"Left Arrow","keywords":["blue","square","previous","back"],"skins":[{"unified":"2b05-fe0f","native":"⬅ï¸"}],"version":1},"arrow_upper_left":{"id":"arrow_upper_left","name":"Up-Left Arrow","keywords":["upper","left","up","blue","square","point","direction","diagonal","northwest"],"skins":[{"unified":"2196-fe0f","native":"↖ï¸"}],"version":1},"arrow_up_down":{"id":"arrow_up_down","name":"Up Down Arrow","keywords":["blue","square","direction","way","vertical"],"skins":[{"unified":"2195-fe0f","native":"↕ï¸"}],"version":1},"left_right_arrow":{"id":"left_right_arrow","name":"Left Right Arrow","keywords":["shape","direction","horizontal","sideways"],"skins":[{"unified":"2194-fe0f","native":"↔ï¸"}],"version":1},"leftwards_arrow_with_hook":{"id":"leftwards_arrow_with_hook","name":"Right Arrow Curving Left","keywords":["leftwards","with","hook","back","return","blue","square","undo","enter"],"skins":[{"unified":"21a9-fe0f","native":"↩ï¸"}],"version":1},"arrow_right_hook":{"id":"arrow_right_hook","name":"Left Arrow Curving Right","keywords":["hook","blue","square","return","rotate","direction"],"skins":[{"unified":"21aa-fe0f","native":"↪ï¸"}],"version":1},"arrow_heading_up":{"id":"arrow_heading_up","name":"Right Arrow Curving Up","keywords":["heading","blue","square","direction","top"],"skins":[{"unified":"2934-fe0f","native":"⤴ï¸"}],"version":1},"arrow_heading_down":{"id":"arrow_heading_down","name":"Right Arrow Curving Down","keywords":["heading","blue","square","direction","bottom"],"skins":[{"unified":"2935-fe0f","native":"⤵ï¸"}],"version":1},"arrows_clockwise":{"id":"arrows_clockwise","name":"Clockwise Vertical Arrows","keywords":["sync","cycle","round","repeat"],"skins":[{"unified":"1f503","native":"🔃"}],"version":1},"arrows_counterclockwise":{"id":"arrows_counterclockwise","name":"Counterclockwise Arrows Button","keywords":["blue","square","sync","cycle"],"skins":[{"unified":"1f504","native":"🔄"}],"version":1},"back":{"id":"back","name":"Back Arrow","keywords":["words","return"],"skins":[{"unified":"1f519","native":"🔙"}],"version":1},"end":{"id":"end","name":"End Arrow","keywords":["words"],"skins":[{"unified":"1f51a","native":"🔚"}],"version":1},"on":{"id":"on","name":"On! Arrow","keywords":["on","words"],"skins":[{"unified":"1f51b","native":"🔛"}],"version":1},"soon":{"id":"soon","name":"Soon Arrow","keywords":["words"],"skins":[{"unified":"1f51c","native":"🔜"}],"version":1},"top":{"id":"top","name":"Top Arrow","keywords":["words","blue","square"],"skins":[{"unified":"1f51d","native":"ðŸ”"}],"version":1},"place_of_worship":{"id":"place_of_worship","name":"Place of Worship","keywords":["religion","church","temple","prayer"],"skins":[{"unified":"1f6d0","native":"ðŸ›"}],"version":1},"atom_symbol":{"id":"atom_symbol","name":"Atom Symbol","keywords":["science","physics","chemistry"],"skins":[{"unified":"269b-fe0f","native":"âš›ï¸"}],"version":1},"om_symbol":{"id":"om_symbol","name":"Om","keywords":["symbol","hinduism","buddhism","sikhism","jainism"],"skins":[{"unified":"1f549-fe0f","native":"🕉ï¸"}],"version":1},"star_of_david":{"id":"star_of_david","name":"Star of David","keywords":["judaism"],"skins":[{"unified":"2721-fe0f","native":"✡ï¸"}],"version":1},"wheel_of_dharma":{"id":"wheel_of_dharma","name":"Wheel of Dharma","keywords":["hinduism","buddhism","sikhism","jainism"],"skins":[{"unified":"2638-fe0f","native":"☸ï¸"}],"version":1},"yin_yang":{"id":"yin_yang","name":"Yin Yang","keywords":["balance"],"skins":[{"unified":"262f-fe0f","native":"☯ï¸"}],"version":1},"latin_cross":{"id":"latin_cross","name":"Latin Cross","keywords":["christianity"],"skins":[{"unified":"271d-fe0f","native":"âœï¸"}],"version":1},"orthodox_cross":{"id":"orthodox_cross","name":"Orthodox Cross","keywords":["suppedaneum","religion"],"skins":[{"unified":"2626-fe0f","native":"☦ï¸"}],"version":1},"star_and_crescent":{"id":"star_and_crescent","name":"Star and Crescent","keywords":["islam"],"skins":[{"unified":"262a-fe0f","native":"☪ï¸"}],"version":1},"peace_symbol":{"id":"peace_symbol","name":"Peace Symbol","keywords":["hippie"],"skins":[{"unified":"262e-fe0f","native":"☮ï¸"}],"version":1},"menorah_with_nine_branches":{"id":"menorah_with_nine_branches","name":"Menorah","keywords":["with","nine","branches","hanukkah","candles","jewish"],"skins":[{"unified":"1f54e","native":"🕎"}],"version":1},"six_pointed_star":{"id":"six_pointed_star","name":"Dotted Six-Pointed Star","keywords":["six","pointed","purple","square","religion","jewish","hexagram"],"skins":[{"unified":"1f52f","native":"🔯"}],"version":1},"aries":{"id":"aries","name":"Aries","keywords":["sign","purple","square","zodiac","astrology"],"skins":[{"unified":"2648","native":"♈"}],"version":1},"taurus":{"id":"taurus","name":"Taurus","keywords":["purple","square","sign","zodiac","astrology"],"skins":[{"unified":"2649","native":"♉"}],"version":1},"gemini":{"id":"gemini","name":"Gemini","keywords":["sign","zodiac","purple","square","astrology"],"skins":[{"unified":"264a","native":"♊"}],"version":1},"cancer":{"id":"cancer","name":"Cancer","keywords":["sign","zodiac","purple","square","astrology"],"skins":[{"unified":"264b","native":"♋"}],"version":1},"leo":{"id":"leo","name":"Leo","keywords":["sign","purple","square","zodiac","astrology"],"skins":[{"unified":"264c","native":"♌"}],"version":1},"virgo":{"id":"virgo","name":"Virgo","keywords":["sign","zodiac","purple","square","astrology"],"skins":[{"unified":"264d","native":"â™"}],"version":1},"libra":{"id":"libra","name":"Libra","keywords":["sign","purple","square","zodiac","astrology"],"skins":[{"unified":"264e","native":"♎"}],"version":1},"scorpius":{"id":"scorpius","name":"Scorpio","keywords":["scorpius","sign","zodiac","purple","square","astrology"],"skins":[{"unified":"264f","native":"â™"}],"version":1},"sagittarius":{"id":"sagittarius","name":"Sagittarius","keywords":["sign","zodiac","purple","square","astrology"],"skins":[{"unified":"2650","native":"â™"}],"version":1},"capricorn":{"id":"capricorn","name":"Capricorn","keywords":["sign","zodiac","purple","square","astrology"],"skins":[{"unified":"2651","native":"♑"}],"version":1},"aquarius":{"id":"aquarius","name":"Aquarius","keywords":["sign","purple","square","zodiac","astrology"],"skins":[{"unified":"2652","native":"â™’"}],"version":1},"pisces":{"id":"pisces","name":"Pisces","keywords":["purple","square","sign","zodiac","astrology"],"skins":[{"unified":"2653","native":"♓"}],"version":1},"ophiuchus":{"id":"ophiuchus","name":"Ophiuchus","keywords":["sign","purple","square","constellation","astrology"],"skins":[{"unified":"26ce","native":"⛎"}],"version":1},"twisted_rightwards_arrows":{"id":"twisted_rightwards_arrows","name":"Shuffle Tracks Button","keywords":["twisted","rightwards","arrows","blue","square","music","random"],"skins":[{"unified":"1f500","native":"🔀"}],"version":1},"repeat":{"id":"repeat","name":"Repeat Button","keywords":["loop","record"],"skins":[{"unified":"1f501","native":"ðŸ”"}],"version":1},"repeat_one":{"id":"repeat_one","name":"Repeat Single Button","keywords":["one","blue","square","loop"],"skins":[{"unified":"1f502","native":"🔂"}],"version":1},"arrow_forward":{"id":"arrow_forward","name":"Play Button","keywords":["arrow","forward","blue","square","right","direction"],"skins":[{"unified":"25b6-fe0f","native":"â–¶ï¸"}],"version":1},"fast_forward":{"id":"fast_forward","name":"Fast-Forward Button","keywords":["fast","forward","blue","square","play","speed","continue"],"skins":[{"unified":"23e9","native":"â©"}],"version":1},"black_right_pointing_double_triangle_with_vertical_bar":{"id":"black_right_pointing_double_triangle_with_vertical_bar","name":"Next Track Button","keywords":["black","right","pointing","double","triangle","with","vertical","bar","forward","blue","square"],"skins":[{"unified":"23ed-fe0f","native":"âï¸"}],"version":1},"black_right_pointing_triangle_with_double_vertical_bar":{"id":"black_right_pointing_triangle_with_double_vertical_bar","name":"Play or Pause Button","keywords":["black","right","pointing","triangle","with","double","vertical","bar","blue","square"],"skins":[{"unified":"23ef-fe0f","native":"â¯ï¸"}],"version":1},"arrow_backward":{"id":"arrow_backward","name":"Reverse Button","keywords":["arrow","backward","blue","square","left","direction"],"skins":[{"unified":"25c0-fe0f","native":"â—€ï¸"}],"version":1},"rewind":{"id":"rewind","name":"Fast Reverse Button","keywords":["rewind","play","blue","square"],"skins":[{"unified":"23ea","native":"âª"}],"version":1},"black_left_pointing_double_triangle_with_vertical_bar":{"id":"black_left_pointing_double_triangle_with_vertical_bar","name":"Last Track Button","keywords":["black","left","pointing","double","triangle","with","vertical","bar","backward"],"skins":[{"unified":"23ee-fe0f","native":"â®ï¸"}],"version":1},"arrow_up_small":{"id":"arrow_up_small","name":"Upwards Button","keywords":["arrow","up","small","blue","square","triangle","direction","point","forward","top"],"skins":[{"unified":"1f53c","native":"🔼"}],"version":1},"arrow_double_up":{"id":"arrow_double_up","name":"Fast Up Button","keywords":["arrow","double","blue","square","direction","top"],"skins":[{"unified":"23eb","native":"â«"}],"version":1},"arrow_down_small":{"id":"arrow_down_small","name":"Downwards Button","keywords":["arrow","down","small","blue","square","direction","bottom"],"skins":[{"unified":"1f53d","native":"🔽"}],"version":1},"arrow_double_down":{"id":"arrow_double_down","name":"Fast Down Button","keywords":["arrow","double","blue","square","direction","bottom"],"skins":[{"unified":"23ec","native":"â¬"}],"version":1},"double_vertical_bar":{"id":"double_vertical_bar","name":"Pause Button","keywords":["double","vertical","bar","blue","square"],"skins":[{"unified":"23f8-fe0f","native":"â¸ï¸"}],"version":1},"black_square_for_stop":{"id":"black_square_for_stop","name":"Stop Button","keywords":["black","square","for","blue"],"skins":[{"unified":"23f9-fe0f","native":"â¹ï¸"}],"version":1},"black_circle_for_record":{"id":"black_circle_for_record","name":"Record Button","keywords":["black","circle","for","blue","square"],"skins":[{"unified":"23fa-fe0f","native":"âºï¸"}],"version":1},"eject":{"id":"eject","name":"Eject Button","keywords":["blue","square"],"skins":[{"unified":"23cf-fe0f","native":"âï¸"}],"version":1},"cinema":{"id":"cinema","name":"Cinema","keywords":["blue","square","record","film","movie","curtain","stage","theater"],"skins":[{"unified":"1f3a6","native":"🎦"}],"version":1},"low_brightness":{"id":"low_brightness","name":"Dim Button","keywords":["low","brightness","sun","afternoon","warm","summer"],"skins":[{"unified":"1f505","native":"🔅"}],"version":1},"high_brightness":{"id":"high_brightness","name":"Bright Button","keywords":["high","brightness","sun","light"],"skins":[{"unified":"1f506","native":"🔆"}],"version":1},"signal_strength":{"id":"signal_strength","name":"Antenna Bars","keywords":["signal","strength","blue","square","reception","phone","internet","connection","wifi","bluetooth"],"skins":[{"unified":"1f4f6","native":"📶"}],"version":1},"vibration_mode":{"id":"vibration_mode","name":"Vibration Mode","keywords":["orange","square","phone"],"skins":[{"unified":"1f4f3","native":"📳"}],"version":1},"mobile_phone_off":{"id":"mobile_phone_off","name":"Mobile Phone off","keywords":["mute","orange","square","silence","quiet"],"skins":[{"unified":"1f4f4","native":"📴"}],"version":1},"female_sign":{"id":"female_sign","name":"Female Sign","keywords":["woman","women","lady","girl"],"skins":[{"unified":"2640-fe0f","native":"♀ï¸"}],"version":4},"male_sign":{"id":"male_sign","name":"Male Sign","keywords":["man","boy","men"],"skins":[{"unified":"2642-fe0f","native":"♂ï¸"}],"version":4},"transgender_symbol":{"id":"transgender_symbol","name":"Transgender Symbol","keywords":["lgbtq"],"skins":[{"unified":"26a7-fe0f","native":"⚧ï¸"}],"version":13},"heavy_multiplication_x":{"id":"heavy_multiplication_x","name":"Multiply","keywords":["heavy","multiplication","x","sign","math","calculation"],"skins":[{"unified":"2716-fe0f","native":"✖ï¸"}],"version":1},"heavy_plus_sign":{"id":"heavy_plus_sign","name":"Plus","keywords":["heavy","sign","math","calculation","addition","more","increase"],"skins":[{"unified":"2795","native":"âž•"}],"version":1},"heavy_minus_sign":{"id":"heavy_minus_sign","name":"Minus","keywords":["heavy","sign","math","calculation","subtract","less"],"skins":[{"unified":"2796","native":"âž–"}],"version":1},"heavy_division_sign":{"id":"heavy_division_sign","name":"Divide","keywords":["heavy","division","sign","math","calculation"],"skins":[{"unified":"2797","native":"âž—"}],"version":1},"heavy_equals_sign":{"id":"heavy_equals_sign","name":"Heavy Equals Sign","keywords":["math"],"skins":[{"unified":"1f7f0","native":"🟰"}],"version":14},"infinity":{"id":"infinity","name":"Infinity","keywords":["forever"],"skins":[{"unified":"267e-fe0f","native":"♾ï¸"}],"version":11},"bangbang":{"id":"bangbang","name":"Double Exclamation Mark","keywords":["bangbang","surprise"],"skins":[{"unified":"203c-fe0f","native":"‼ï¸"}],"version":1},"interrobang":{"id":"interrobang","name":"Exclamation Question Mark","keywords":["interrobang","wat","punctuation","surprise"],"skins":[{"unified":"2049-fe0f","native":"â‰ï¸"}],"version":1},"question":{"id":"question","name":"Red Question Mark","keywords":["doubt","confused"],"skins":[{"unified":"2753","native":"â“"}],"version":1},"grey_question":{"id":"grey_question","name":"White Question Mark","keywords":["grey","doubts","gray","huh","confused"],"skins":[{"unified":"2754","native":"â”"}],"version":1},"grey_exclamation":{"id":"grey_exclamation","name":"White Exclamation Mark","keywords":["grey","surprise","punctuation","gray","wow","warning"],"skins":[{"unified":"2755","native":"â•"}],"version":1},"exclamation":{"id":"exclamation","name":"Red Exclamation Mark","keywords":["heavy","danger","surprise","punctuation","wow","warning"],"skins":[{"unified":"2757","native":"â—"}],"version":1},"wavy_dash":{"id":"wavy_dash","name":"Wavy Dash","keywords":["draw","line","moustache","mustache","squiggle","scribble"],"skins":[{"unified":"3030-fe0f","native":"〰ï¸"}],"version":1},"currency_exchange":{"id":"currency_exchange","name":"Currency Exchange","keywords":["money","sales","dollar","travel"],"skins":[{"unified":"1f4b1","native":"💱"}],"version":1},"heavy_dollar_sign":{"id":"heavy_dollar_sign","name":"Heavy Dollar Sign","keywords":["money","sales","payment","currency","buck"],"skins":[{"unified":"1f4b2","native":"💲"}],"version":1},"medical_symbol":{"id":"medical_symbol","name":"Medical Symbol","keywords":["staff","of","aesculapius","health","hospital"],"skins":[{"unified":"2695-fe0f","native":"âš•ï¸"}],"version":4},"recycle":{"id":"recycle","name":"Recycling Symbol","keywords":["recycle","arrow","environment","garbage","trash"],"skins":[{"unified":"267b-fe0f","native":"â™»ï¸"}],"version":1},"fleur_de_lis":{"id":"fleur_de_lis","name":"Fleur-De-Lis","keywords":["fleur","de","lis","decorative","scout"],"skins":[{"unified":"269c-fe0f","native":"âšœï¸"}],"version":1},"trident":{"id":"trident","name":"Trident Emblem","keywords":["weapon","spear"],"skins":[{"unified":"1f531","native":"🔱"}],"version":1},"name_badge":{"id":"name_badge","name":"Name Badge","keywords":["fire","forbid"],"skins":[{"unified":"1f4db","native":"📛"}],"version":1},"beginner":{"id":"beginner","name":"Japanese Symbol for Beginner","keywords":["badge","shield"],"skins":[{"unified":"1f530","native":"🔰"}],"version":1},"o":{"id":"o","name":"Hollow Red Circle","keywords":["o","round"],"skins":[{"unified":"2b55","native":"â•"}],"version":1},"white_check_mark":{"id":"white_check_mark","name":"Check Mark Button","keywords":["white","green","square","ok","agree","vote","election","answer","tick"],"skins":[{"unified":"2705","native":"✅"}],"version":1},"ballot_box_with_check":{"id":"ballot_box_with_check","name":"Check Box with Check","keywords":["ballot","ok","agree","confirm","black","square","vote","election","yes","tick"],"skins":[{"unified":"2611-fe0f","native":"☑ï¸"}],"version":1},"heavy_check_mark":{"id":"heavy_check_mark","name":"Check Mark","keywords":["heavy","ok","nike","answer","yes","tick"],"skins":[{"unified":"2714-fe0f","native":"✔ï¸"}],"version":1},"x":{"id":"x","name":"Cross Mark","keywords":["x","no","delete","remove","cancel","red"],"skins":[{"unified":"274c","native":"âŒ"}],"version":1},"negative_squared_cross_mark":{"id":"negative_squared_cross_mark","name":"Cross Mark Button","keywords":["negative","squared","x","green","square","no","deny"],"skins":[{"unified":"274e","native":"âŽ"}],"version":1},"curly_loop":{"id":"curly_loop","name":"Curly Loop","keywords":["scribble","draw","shape","squiggle"],"skins":[{"unified":"27b0","native":"âž°"}],"version":1},"loop":{"id":"loop","name":"Double Curly Loop","keywords":["tape","cassette"],"skins":[{"unified":"27bf","native":"âž¿"}],"version":1},"part_alternation_mark":{"id":"part_alternation_mark","name":"Part Alternation Mark","keywords":["graph","presentation","stats","business","economics","bad"],"skins":[{"unified":"303d-fe0f","native":"〽ï¸"}],"version":1},"eight_spoked_asterisk":{"id":"eight_spoked_asterisk","name":"Eight Spoked Asterisk","keywords":["star","sparkle","green","square"],"skins":[{"unified":"2733-fe0f","native":"✳ï¸"}],"version":1},"eight_pointed_black_star":{"id":"eight_pointed_black_star","name":"Eight-Pointed Star","keywords":["eight","pointed","black","orange","square","shape","polygon"],"skins":[{"unified":"2734-fe0f","native":"✴ï¸"}],"version":1},"sparkle":{"id":"sparkle","name":"Sparkle","keywords":["stars","green","square","awesome","good","fireworks"],"skins":[{"unified":"2747-fe0f","native":"â‡ï¸"}],"version":1},"copyright":{"id":"copyright","name":"Copyright","keywords":["ip","license","circle","law","legal"],"skins":[{"unified":"00a9-fe0f","native":"©ï¸"}],"version":1},"registered":{"id":"registered","name":"Registered","keywords":["alphabet","circle"],"skins":[{"unified":"00ae-fe0f","native":"®ï¸"}],"version":1},"tm":{"id":"tm","name":"Trade Mark","keywords":["tm","trademark","brand","law","legal"],"skins":[{"unified":"2122-fe0f","native":"â„¢ï¸"}],"version":1},"hash":{"id":"hash","name":"Hash Key","keywords":["keycap","","symbol","blue","square","twitter"],"skins":[{"unified":"0023-fe0f-20e3","native":"#ï¸âƒ£"}],"version":1},"keycap_star":{"id":"keycap_star","name":"Keycap: *","keywords":["keycap","star",""],"skins":[{"unified":"002a-fe0f-20e3","native":"*ï¸âƒ£"}],"version":2},"zero":{"id":"zero","name":"Keycap 0","keywords":["zero","numbers","blue","square","null"],"skins":[{"unified":"0030-fe0f-20e3","native":"0ï¸âƒ£"}],"version":1},"one":{"id":"one","name":"Keycap 1","keywords":["one","blue","square","numbers"],"skins":[{"unified":"0031-fe0f-20e3","native":"1ï¸âƒ£"}],"version":1},"two":{"id":"two","name":"Keycap 2","keywords":["two","numbers","prime","blue","square"],"skins":[{"unified":"0032-fe0f-20e3","native":"2ï¸âƒ£"}],"version":1},"three":{"id":"three","name":"Keycap 3","keywords":["three","numbers","prime","blue","square"],"skins":[{"unified":"0033-fe0f-20e3","native":"3ï¸âƒ£"}],"version":1},"four":{"id":"four","name":"Keycap 4","keywords":["four","numbers","blue","square"],"skins":[{"unified":"0034-fe0f-20e3","native":"4ï¸âƒ£"}],"version":1},"five":{"id":"five","name":"Keycap 5","keywords":["five","numbers","blue","square","prime"],"skins":[{"unified":"0035-fe0f-20e3","native":"5ï¸âƒ£"}],"version":1},"six":{"id":"six","name":"Keycap 6","keywords":["six","numbers","blue","square"],"skins":[{"unified":"0036-fe0f-20e3","native":"6ï¸âƒ£"}],"version":1},"seven":{"id":"seven","name":"Keycap 7","keywords":["seven","numbers","blue","square","prime"],"skins":[{"unified":"0037-fe0f-20e3","native":"7ï¸âƒ£"}],"version":1},"eight":{"id":"eight","name":"Keycap 8","keywords":["eight","blue","square","numbers"],"skins":[{"unified":"0038-fe0f-20e3","native":"8ï¸âƒ£"}],"version":1},"nine":{"id":"nine","name":"Keycap 9","keywords":["nine","blue","square","numbers"],"skins":[{"unified":"0039-fe0f-20e3","native":"9ï¸âƒ£"}],"version":1},"keycap_ten":{"id":"keycap_ten","name":"Keycap 10","keywords":["ten","numbers","blue","square"],"skins":[{"unified":"1f51f","native":"🔟"}],"version":1},"capital_abcd":{"id":"capital_abcd","name":"Input Latin Uppercase","keywords":["capital","abcd","alphabet","words","blue","square"],"skins":[{"unified":"1f520","native":"🔠"}],"version":1},"abcd":{"id":"abcd","name":"Input Latin Lowercase","keywords":["abcd","blue","square","alphabet"],"skins":[{"unified":"1f521","native":"🔡"}],"version":1},"symbols":{"id":"symbols","name":"Input Symbols","keywords":["blue","square","music","note","ampersand","percent","glyphs","characters"],"skins":[{"unified":"1f523","native":"🔣"}],"version":1},"abc":{"id":"abc","name":"Input Latin Letters","keywords":["abc","blue","square","alphabet"],"skins":[{"unified":"1f524","native":"🔤"}],"version":1},"a":{"id":"a","name":"A Button (blood Type)","keywords":["red","square","alphabet","letter"],"skins":[{"unified":"1f170-fe0f","native":"🅰ï¸"}],"version":1},"ab":{"id":"ab","name":"Negative Squared Ab","keywords":["button","red","square","alphabet"],"skins":[{"unified":"1f18e","native":"🆎"}],"version":1},"b":{"id":"b","name":"B Button (blood Type)","keywords":["red","square","alphabet","letter"],"skins":[{"unified":"1f171-fe0f","native":"🅱ï¸"}],"version":1},"cl":{"id":"cl","name":"Cl Button","keywords":["alphabet","words","red","square"],"skins":[{"unified":"1f191","native":"🆑"}],"version":1},"cool":{"id":"cool","name":"Cool Button","keywords":["words","blue","square"],"skins":[{"unified":"1f192","native":"🆒"}],"version":1},"free":{"id":"free","name":"Free Button","keywords":["blue","square","words"],"skins":[{"unified":"1f193","native":"🆓"}],"version":1},"information_source":{"id":"information_source","name":"Information","keywords":["source","blue","square","alphabet","letter"],"skins":[{"unified":"2139-fe0f","native":"ℹï¸"}],"version":1},"id":{"id":"id","name":"Id Button","keywords":["purple","square","words"],"skins":[{"unified":"1f194","native":"🆔"}],"version":1},"m":{"id":"m","name":"Circled M","keywords":["alphabet","blue","circle","letter"],"skins":[{"unified":"24c2-fe0f","native":"â“‚ï¸"}],"version":1},"new":{"id":"new","name":"New Button","keywords":["blue","square","words","start"],"skins":[{"unified":"1f195","native":"🆕"}],"version":1},"ng":{"id":"ng","name":"Ng Button","keywords":["blue","square","words","shape","icon"],"skins":[{"unified":"1f196","native":"🆖"}],"version":1},"o2":{"id":"o2","name":"O Button (blood Type)","keywords":["o2","alphabet","red","square","letter"],"skins":[{"unified":"1f17e-fe0f","native":"🅾ï¸"}],"version":1},"ok":{"id":"ok","name":"Ok Button","keywords":["good","agree","yes","blue","square"],"skins":[{"unified":"1f197","native":"🆗"}],"version":1},"parking":{"id":"parking","name":"P Button","keywords":["parking","cars","blue","square","alphabet","letter"],"skins":[{"unified":"1f17f-fe0f","native":"🅿ï¸"}],"version":1},"sos":{"id":"sos","name":"Sos Button","keywords":["help","red","square","words","emergency","911"],"skins":[{"unified":"1f198","native":"🆘"}],"version":1},"up":{"id":"up","name":"Up! Button","keywords":["up","blue","square","above","high"],"skins":[{"unified":"1f199","native":"🆙"}],"version":1},"vs":{"id":"vs","name":"Vs Button","keywords":["words","orange","square"],"skins":[{"unified":"1f19a","native":"🆚"}],"version":1},"koko":{"id":"koko","name":"Squared Katakana Koko","keywords":["japanese","here","button","blue","square","destination"],"skins":[{"unified":"1f201","native":"ðŸˆ"}],"version":1},"sa":{"id":"sa","name":"Squared Katakana Sa","keywords":["japanese","service","charge","button","blue","square"],"skins":[{"unified":"1f202-fe0f","native":"🈂ï¸"}],"version":1},"u6708":{"id":"u6708","name":"Japanese “monthly Amount†Button","keywords":["u6708","monthly","amount","chinese","month","moon","orange","square","kanji"],"skins":[{"unified":"1f237-fe0f","native":"🈷ï¸"}],"version":1},"u6709":{"id":"u6709","name":"Squared Cjk Unified Ideograph-6709","keywords":["u6709","japanese","not","free","of","charge","button","orange","square","chinese","have","kanji"],"skins":[{"unified":"1f236","native":"🈶"}],"version":1},"u6307":{"id":"u6307","name":"Japanese “reserved†Button","keywords":["u6307","reserved","chinese","point","green","square","kanji"],"skins":[{"unified":"1f22f","native":"🈯"}],"version":1},"ideograph_advantage":{"id":"ideograph_advantage","name":"Japanese “bargain†Button","keywords":["ideograph","advantage","bargain","chinese","kanji","obtain","get","circle"],"skins":[{"unified":"1f250","native":"ðŸ‰"}],"version":1},"u5272":{"id":"u5272","name":"Japanese “discount†Button","keywords":["u5272","discount","cut","divide","chinese","kanji","pink","square"],"skins":[{"unified":"1f239","native":"🈹"}],"version":1},"u7121":{"id":"u7121","name":"Japanese “free of Charge†Button","keywords":["u7121","free","charge","nothing","chinese","kanji","orange","square"],"skins":[{"unified":"1f21a","native":"🈚"}],"version":1},"u7981":{"id":"u7981","name":"Japanese “prohibited†Button","keywords":["u7981","prohibited","kanji","chinese","forbidden","limit","restricted","red","square"],"skins":[{"unified":"1f232","native":"🈲"}],"version":1},"accept":{"id":"accept","name":"Circled Ideograph Accept","keywords":["japanese","acceptable","button","ok","good","chinese","kanji","agree","yes","orange","circle"],"skins":[{"unified":"1f251","native":"🉑"}],"version":1},"u7533":{"id":"u7533","name":"Japanese “application†Button","keywords":["u7533","application","chinese","kanji","orange","square"],"skins":[{"unified":"1f238","native":"🈸"}],"version":1},"u5408":{"id":"u5408","name":"Japanese “passing Grade†Button","keywords":["u5408","passing","grade","chinese","join","kanji","red","square"],"skins":[{"unified":"1f234","native":"🈴"}],"version":1},"u7a7a":{"id":"u7a7a","name":"Japanese “vacancy†Button","keywords":["u7a7a","vacancy","kanji","chinese","empty","sky","blue","square"],"skins":[{"unified":"1f233","native":"🈳"}],"version":1},"congratulations":{"id":"congratulations","name":"Circled Ideograph Congratulation","keywords":["congratulations","japanese","button","chinese","kanji","red","circle"],"skins":[{"unified":"3297-fe0f","native":"㊗ï¸"}],"version":1},"secret":{"id":"secret","name":"Circled Ideograph Secret","keywords":["japanese","button","privacy","chinese","sshh","kanji","red","circle"],"skins":[{"unified":"3299-fe0f","native":"㊙ï¸"}],"version":1},"u55b6":{"id":"u55b6","name":"Squared Cjk Unified Ideograph-55b6","keywords":["u55b6","japanese","open","for","business","button","opening","hours","orange","square"],"skins":[{"unified":"1f23a","native":"🈺"}],"version":1},"u6e80":{"id":"u6e80","name":"Japanese “no Vacancy†Button","keywords":["u6e80","no","vacancy","full","chinese","red","square","kanji"],"skins":[{"unified":"1f235","native":"🈵"}],"version":1},"red_circle":{"id":"red_circle","name":"Red Circle","keywords":["shape","error","danger"],"skins":[{"unified":"1f534","native":"🔴"}],"version":1},"large_orange_circle":{"id":"large_orange_circle","name":"Orange Circle","keywords":["large","round"],"skins":[{"unified":"1f7e0","native":"🟠"}],"version":12},"large_yellow_circle":{"id":"large_yellow_circle","name":"Yellow Circle","keywords":["large","round"],"skins":[{"unified":"1f7e1","native":"🟡"}],"version":12},"large_green_circle":{"id":"large_green_circle","name":"Green Circle","keywords":["large","round"],"skins":[{"unified":"1f7e2","native":"🟢"}],"version":12},"large_blue_circle":{"id":"large_blue_circle","name":"Blue Circle","keywords":["large","shape","icon","button"],"skins":[{"unified":"1f535","native":"🔵"}],"version":1},"large_purple_circle":{"id":"large_purple_circle","name":"Purple Circle","keywords":["large","round"],"skins":[{"unified":"1f7e3","native":"🟣"}],"version":12},"large_brown_circle":{"id":"large_brown_circle","name":"Brown Circle","keywords":["large","round"],"skins":[{"unified":"1f7e4","native":"🟤"}],"version":12},"black_circle":{"id":"black_circle","name":"Black Circle","keywords":["shape","button","round"],"skins":[{"unified":"26ab","native":"âš«"}],"version":1},"white_circle":{"id":"white_circle","name":"White Circle","keywords":["shape","round"],"skins":[{"unified":"26aa","native":"⚪"}],"version":1},"large_red_square":{"id":"large_red_square","name":"Red Square","keywords":["large"],"skins":[{"unified":"1f7e5","native":"🟥"}],"version":12},"large_orange_square":{"id":"large_orange_square","name":"Orange Square","keywords":["large"],"skins":[{"unified":"1f7e7","native":"🟧"}],"version":12},"large_yellow_square":{"id":"large_yellow_square","name":"Yellow Square","keywords":["large"],"skins":[{"unified":"1f7e8","native":"🟨"}],"version":12},"large_green_square":{"id":"large_green_square","name":"Green Square","keywords":["large"],"skins":[{"unified":"1f7e9","native":"🟩"}],"version":12},"large_blue_square":{"id":"large_blue_square","name":"Blue Square","keywords":["large"],"skins":[{"unified":"1f7e6","native":"🟦"}],"version":12},"large_purple_square":{"id":"large_purple_square","name":"Purple Square","keywords":["large"],"skins":[{"unified":"1f7ea","native":"🟪"}],"version":12},"large_brown_square":{"id":"large_brown_square","name":"Brown Square","keywords":["large"],"skins":[{"unified":"1f7eb","native":"🟫"}],"version":12},"black_large_square":{"id":"black_large_square","name":"Black Large Square","keywords":["shape","icon","button"],"skins":[{"unified":"2b1b","native":"⬛"}],"version":1},"white_large_square":{"id":"white_large_square","name":"White Large Square","keywords":["shape","icon","stone","button"],"skins":[{"unified":"2b1c","native":"⬜"}],"version":1},"black_medium_square":{"id":"black_medium_square","name":"Black Medium Square","keywords":["shape","button","icon"],"skins":[{"unified":"25fc-fe0f","native":"â—¼ï¸"}],"version":1},"white_medium_square":{"id":"white_medium_square","name":"White Medium Square","keywords":["shape","stone","icon"],"skins":[{"unified":"25fb-fe0f","native":"â—»ï¸"}],"version":1},"black_medium_small_square":{"id":"black_medium_small_square","name":"Black Medium Small Square","keywords":["icon","shape","button"],"skins":[{"unified":"25fe","native":"â—¾"}],"version":1},"white_medium_small_square":{"id":"white_medium_small_square","name":"White Medium Small Square","keywords":["shape","stone","icon","button"],"skins":[{"unified":"25fd","native":"â—½"}],"version":1},"black_small_square":{"id":"black_small_square","name":"Black Small Square","keywords":["shape","icon"],"skins":[{"unified":"25aa-fe0f","native":"â–ªï¸"}],"version":1},"white_small_square":{"id":"white_small_square","name":"White Small Square","keywords":["shape","icon"],"skins":[{"unified":"25ab-fe0f","native":"â–«ï¸"}],"version":1},"large_orange_diamond":{"id":"large_orange_diamond","name":"Large Orange Diamond","keywords":["shape","jewel","gem"],"skins":[{"unified":"1f536","native":"🔶"}],"version":1},"large_blue_diamond":{"id":"large_blue_diamond","name":"Large Blue Diamond","keywords":["shape","jewel","gem"],"skins":[{"unified":"1f537","native":"🔷"}],"version":1},"small_orange_diamond":{"id":"small_orange_diamond","name":"Small Orange Diamond","keywords":["shape","jewel","gem"],"skins":[{"unified":"1f538","native":"🔸"}],"version":1},"small_blue_diamond":{"id":"small_blue_diamond","name":"Small Blue Diamond","keywords":["shape","jewel","gem"],"skins":[{"unified":"1f539","native":"🔹"}],"version":1},"small_red_triangle":{"id":"small_red_triangle","name":"Red Triangle Pointed Up","keywords":["small","shape","direction","top"],"skins":[{"unified":"1f53a","native":"🔺"}],"version":1},"small_red_triangle_down":{"id":"small_red_triangle_down","name":"Red Triangle Pointed Down","keywords":["small","shape","direction","bottom"],"skins":[{"unified":"1f53b","native":"🔻"}],"version":1},"diamond_shape_with_a_dot_inside":{"id":"diamond_shape_with_a_dot_inside","name":"Diamond with a Dot","keywords":["shape","inside","jewel","blue","gem","crystal","fancy"],"skins":[{"unified":"1f4a0","native":"💠"}],"version":1},"radio_button":{"id":"radio_button","name":"Radio Button","keywords":["input","old","music","circle"],"skins":[{"unified":"1f518","native":"🔘"}],"version":1},"white_square_button":{"id":"white_square_button","name":"White Square Button","keywords":["shape","input"],"skins":[{"unified":"1f533","native":"🔳"}],"version":1},"black_square_button":{"id":"black_square_button","name":"Black Square Button","keywords":["shape","input","frame"],"skins":[{"unified":"1f532","native":"🔲"}],"version":1},"checkered_flag":{"id":"checkered_flag","name":"Chequered Flag","keywords":["checkered","contest","finishline","race","gokart"],"skins":[{"unified":"1f3c1","native":"ðŸ"}],"version":1},"triangular_flag_on_post":{"id":"triangular_flag_on_post","name":"Triangular Flag","keywords":["on","post","mark","milestone","place"],"skins":[{"unified":"1f6a9","native":"🚩"}],"version":1},"crossed_flags":{"id":"crossed_flags","name":"Crossed Flags","keywords":["japanese","nation","country","border"],"skins":[{"unified":"1f38c","native":"🎌"}],"version":1},"waving_black_flag":{"id":"waving_black_flag","name":"Black Flag","keywords":["waving","pirate"],"skins":[{"unified":"1f3f4","native":"ðŸ´"}],"version":1},"waving_white_flag":{"id":"waving_white_flag","name":"White Flag","keywords":["waving","losing","loser","lost","surrender","give","up","fail"],"skins":[{"unified":"1f3f3-fe0f","native":"ðŸ³ï¸"}],"version":1},"rainbow-flag":{"id":"rainbow-flag","name":"Rainbow Flag","keywords":["pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"],"skins":[{"unified":"1f3f3-fe0f-200d-1f308","native":"ðŸ³ï¸â€ðŸŒˆ"}],"version":4},"transgender_flag":{"id":"transgender_flag","name":"Transgender Flag","keywords":["lgbtq"],"skins":[{"unified":"1f3f3-fe0f-200d-26a7-fe0f","native":"ðŸ³ï¸â€âš§ï¸"}],"version":13},"pirate_flag":{"id":"pirate_flag","name":"Pirate Flag","keywords":["skull","crossbones","banner"],"skins":[{"unified":"1f3f4-200d-2620-fe0f","native":"ðŸ´â€â˜ ï¸"}],"version":11},"flag-ac":{"id":"flag-ac","name":"Ascension Island Flag","keywords":["ac"],"skins":[{"unified":"1f1e6-1f1e8","native":"🇦🇨"}],"version":2},"flag-ad":{"id":"flag-ad","name":"Andorra Flag","keywords":["ad","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1e9","native":"🇦🇩"}],"version":2},"flag-ae":{"id":"flag-ae","name":"United Arab Emirates Flag","keywords":["ae","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1ea","native":"🇦🇪"}],"version":2},"flag-af":{"id":"flag-af","name":"Afghanistan Flag","keywords":["af","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1eb","native":"🇦🇫"}],"version":2},"flag-ag":{"id":"flag-ag","name":"Antigua & Barbuda Flag","keywords":["ag","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1ec","native":"🇦🇬"}],"version":2},"flag-ai":{"id":"flag-ai","name":"Anguilla Flag","keywords":["ai","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1ee","native":"🇦🇮"}],"version":2},"flag-al":{"id":"flag-al","name":"Albania Flag","keywords":["al","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f1","native":"🇦🇱"}],"version":2},"flag-am":{"id":"flag-am","name":"Armenia Flag","keywords":["am","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f2","native":"🇦🇲"}],"version":2},"flag-ao":{"id":"flag-ao","name":"Angola Flag","keywords":["ao","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f4","native":"🇦🇴"}],"version":2},"flag-aq":{"id":"flag-aq","name":"Antarctica Flag","keywords":["aq","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f6","native":"🇦🇶"}],"version":2},"flag-ar":{"id":"flag-ar","name":"Argentina Flag","keywords":["ar","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f7","native":"🇦🇷"}],"version":2},"flag-as":{"id":"flag-as","name":"American Samoa Flag","keywords":["as","ws","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f8","native":"🇦🇸"}],"version":2},"flag-at":{"id":"flag-at","name":"Austria Flag","keywords":["at","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1f9","native":"🇦🇹"}],"version":2},"flag-au":{"id":"flag-au","name":"Australia Flag","keywords":["au","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1fa","native":"🇦🇺"}],"version":2},"flag-aw":{"id":"flag-aw","name":"Aruba Flag","keywords":["aw","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1fc","native":"🇦🇼"}],"version":2},"flag-ax":{"id":"flag-ax","name":"Ã…land Islands Flag","keywords":["ax","aland","Aland","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1fd","native":"🇦🇽"}],"version":2},"flag-az":{"id":"flag-az","name":"Azerbaijan Flag","keywords":["az","nation","country","banner"],"skins":[{"unified":"1f1e6-1f1ff","native":"🇦🇿"}],"version":2},"flag-ba":{"id":"flag-ba","name":"Bosnia & Herzegovina Flag","keywords":["ba","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1e6","native":"🇧🇦"}],"version":2},"flag-bb":{"id":"flag-bb","name":"Barbados Flag","keywords":["bb","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1e7","native":"🇧🇧"}],"version":2},"flag-bd":{"id":"flag-bd","name":"Bangladesh Flag","keywords":["bd","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1e9","native":"🇧🇩"}],"version":2},"flag-be":{"id":"flag-be","name":"Belgium Flag","keywords":["be","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1ea","native":"🇧🇪"}],"version":2},"flag-bf":{"id":"flag-bf","name":"Burkina Faso Flag","keywords":["bf","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1eb","native":"🇧🇫"}],"version":2},"flag-bg":{"id":"flag-bg","name":"Bulgaria Flag","keywords":["bg","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1ec","native":"🇧🇬"}],"version":2},"flag-bh":{"id":"flag-bh","name":"Bahrain Flag","keywords":["bh","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1ed","native":"🇧ðŸ‡"}],"version":2},"flag-bi":{"id":"flag-bi","name":"Burundi Flag","keywords":["bi","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1ee","native":"🇧🇮"}],"version":2},"flag-bj":{"id":"flag-bj","name":"Benin Flag","keywords":["bj","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1ef","native":"🇧🇯"}],"version":2},"flag-bl":{"id":"flag-bl","name":"St. Barthélemy Flag","keywords":["bl","st","barthelemy","saint","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f1","native":"🇧🇱"}],"version":2},"flag-bm":{"id":"flag-bm","name":"Bermuda Flag","keywords":["bm","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f2","native":"🇧🇲"}],"version":2},"flag-bn":{"id":"flag-bn","name":"Brunei Flag","keywords":["bn","darussalam","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f3","native":"🇧🇳"}],"version":2},"flag-bo":{"id":"flag-bo","name":"Bolivia Flag","keywords":["bo","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f4","native":"🇧🇴"}],"version":2},"flag-bq":{"id":"flag-bq","name":"Caribbean Netherlands Flag","keywords":["bq","bonaire","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f6","native":"🇧🇶"}],"version":2},"flag-br":{"id":"flag-br","name":"Brazil Flag","keywords":["br","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f7","native":"🇧🇷"}],"version":2},"flag-bs":{"id":"flag-bs","name":"Bahamas Flag","keywords":["bs","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f8","native":"🇧🇸"}],"version":2},"flag-bt":{"id":"flag-bt","name":"Bhutan Flag","keywords":["bt","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1f9","native":"🇧🇹"}],"version":2},"flag-bv":{"id":"flag-bv","name":"Bouvet Island Flag","keywords":["bv","norway"],"skins":[{"unified":"1f1e7-1f1fb","native":"🇧🇻"}],"version":2},"flag-bw":{"id":"flag-bw","name":"Botswana Flag","keywords":["bw","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1fc","native":"🇧🇼"}],"version":2},"flag-by":{"id":"flag-by","name":"Belarus Flag","keywords":["by","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1fe","native":"🇧🇾"}],"version":2},"flag-bz":{"id":"flag-bz","name":"Belize Flag","keywords":["bz","nation","country","banner"],"skins":[{"unified":"1f1e7-1f1ff","native":"🇧🇿"}],"version":2},"flag-ca":{"id":"flag-ca","name":"Canada Flag","keywords":["ca","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1e6","native":"🇨🇦"}],"version":2},"flag-cc":{"id":"flag-cc","name":"Cocos (keeling) Islands Flag","keywords":["cc","keeling","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1e8","native":"🇨🇨"}],"version":2},"flag-cd":{"id":"flag-cd","name":"Congo - Kinshasa Flag","keywords":["cd","democratic","republic","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1e9","native":"🇨🇩"}],"version":2},"flag-cf":{"id":"flag-cf","name":"Central African Republic Flag","keywords":["cf","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1eb","native":"🇨🇫"}],"version":2},"flag-cg":{"id":"flag-cg","name":"Congo - Brazzaville Flag","keywords":["cg","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1ec","native":"🇨🇬"}],"version":2},"flag-ch":{"id":"flag-ch","name":"Switzerland Flag","keywords":["ch","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1ed","native":"🇨ðŸ‡"}],"version":2},"flag-ci":{"id":"flag-ci","name":"Côte D’ivoire Flag","keywords":["ci","cote","d","ivoire","ivory","coast","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1ee","native":"🇨🇮"}],"version":2},"flag-ck":{"id":"flag-ck","name":"Cook Islands Flag","keywords":["ck","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1f0","native":"🇨🇰"}],"version":2},"flag-cl":{"id":"flag-cl","name":"Chile Flag","keywords":["cl","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1f1","native":"🇨🇱"}],"version":2},"flag-cm":{"id":"flag-cm","name":"Cameroon Flag","keywords":["cm","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1f2","native":"🇨🇲"}],"version":2},"cn":{"id":"cn","name":"China Flag","keywords":["cn","chinese","prc","country","nation","banner"],"skins":[{"unified":"1f1e8-1f1f3","native":"🇨🇳"}],"version":1},"flag-co":{"id":"flag-co","name":"Colombia Flag","keywords":["co","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1f4","native":"🇨🇴"}],"version":2},"flag-cp":{"id":"flag-cp","name":"Clipperton Island Flag","keywords":["cp"],"skins":[{"unified":"1f1e8-1f1f5","native":"🇨🇵"}],"version":2},"flag-cr":{"id":"flag-cr","name":"Costa Rica Flag","keywords":["cr","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1f7","native":"🇨🇷"}],"version":2},"flag-cu":{"id":"flag-cu","name":"Cuba Flag","keywords":["cu","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1fa","native":"🇨🇺"}],"version":2},"flag-cv":{"id":"flag-cv","name":"Cape Verde Flag","keywords":["cv","cabo","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1fb","native":"🇨🇻"}],"version":2},"flag-cw":{"id":"flag-cw","name":"Curaçao Flag","keywords":["cw","curacao","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1fc","native":"🇨🇼"}],"version":2},"flag-cx":{"id":"flag-cx","name":"Christmas Island Flag","keywords":["cx","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1fd","native":"🇨🇽"}],"version":2},"flag-cy":{"id":"flag-cy","name":"Cyprus Flag","keywords":["cy","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1fe","native":"🇨🇾"}],"version":2},"flag-cz":{"id":"flag-cz","name":"Czechia Flag","keywords":["cz","nation","country","banner"],"skins":[{"unified":"1f1e8-1f1ff","native":"🇨🇿"}],"version":2},"de":{"id":"de","name":"Germany Flag","keywords":["de","german","nation","country","banner"],"skins":[{"unified":"1f1e9-1f1ea","native":"🇩🇪"}],"version":1},"flag-dg":{"id":"flag-dg","name":"Diego Garcia Flag","keywords":["dg"],"skins":[{"unified":"1f1e9-1f1ec","native":"🇩🇬"}],"version":2},"flag-dj":{"id":"flag-dj","name":"Djibouti Flag","keywords":["dj","nation","country","banner"],"skins":[{"unified":"1f1e9-1f1ef","native":"🇩🇯"}],"version":2},"flag-dk":{"id":"flag-dk","name":"Denmark Flag","keywords":["dk","nation","country","banner"],"skins":[{"unified":"1f1e9-1f1f0","native":"🇩🇰"}],"version":2},"flag-dm":{"id":"flag-dm","name":"Dominica Flag","keywords":["dm","nation","country","banner"],"skins":[{"unified":"1f1e9-1f1f2","native":"🇩🇲"}],"version":2},"flag-do":{"id":"flag-do","name":"Dominican Republic Flag","keywords":["do","nation","country","banner"],"skins":[{"unified":"1f1e9-1f1f4","native":"🇩🇴"}],"version":2},"flag-dz":{"id":"flag-dz","name":"Algeria Flag","keywords":["dz","nation","country","banner"],"skins":[{"unified":"1f1e9-1f1ff","native":"🇩🇿"}],"version":2},"flag-ea":{"id":"flag-ea","name":"Ceuta & Melilla Flag","keywords":["ea"],"skins":[{"unified":"1f1ea-1f1e6","native":"🇪🇦"}],"version":2},"flag-ec":{"id":"flag-ec","name":"Ecuador Flag","keywords":["ec","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1e8","native":"🇪🇨"}],"version":2},"flag-ee":{"id":"flag-ee","name":"Estonia Flag","keywords":["ee","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1ea","native":"🇪🇪"}],"version":2},"flag-eg":{"id":"flag-eg","name":"Egypt Flag","keywords":["eg","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1ec","native":"🇪🇬"}],"version":2},"flag-eh":{"id":"flag-eh","name":"Western Sahara Flag","keywords":["eh","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1ed","native":"🇪ðŸ‡"}],"version":2},"flag-er":{"id":"flag-er","name":"Eritrea Flag","keywords":["er","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1f7","native":"🇪🇷"}],"version":2},"es":{"id":"es","name":"Spain Flag","keywords":["es","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1f8","native":"🇪🇸"}],"version":1},"flag-et":{"id":"flag-et","name":"Ethiopia Flag","keywords":["et","nation","country","banner"],"skins":[{"unified":"1f1ea-1f1f9","native":"🇪🇹"}],"version":2},"flag-eu":{"id":"flag-eu","name":"European Union Flag","keywords":["eu","banner"],"skins":[{"unified":"1f1ea-1f1fa","native":"🇪🇺"}],"version":2},"flag-fi":{"id":"flag-fi","name":"Finland Flag","keywords":["fi","nation","country","banner"],"skins":[{"unified":"1f1eb-1f1ee","native":"🇫🇮"}],"version":2},"flag-fj":{"id":"flag-fj","name":"Fiji Flag","keywords":["fj","nation","country","banner"],"skins":[{"unified":"1f1eb-1f1ef","native":"🇫🇯"}],"version":2},"flag-fk":{"id":"flag-fk","name":"Falkland Islands Flag","keywords":["fk","malvinas","nation","country","banner"],"skins":[{"unified":"1f1eb-1f1f0","native":"🇫🇰"}],"version":2},"flag-fm":{"id":"flag-fm","name":"Micronesia Flag","keywords":["fm","federated","states","nation","country","banner"],"skins":[{"unified":"1f1eb-1f1f2","native":"🇫🇲"}],"version":2},"flag-fo":{"id":"flag-fo","name":"Faroe Islands Flag","keywords":["fo","nation","country","banner"],"skins":[{"unified":"1f1eb-1f1f4","native":"🇫🇴"}],"version":2},"fr":{"id":"fr","name":"France Flag","keywords":["fr","banner","nation","french","country"],"skins":[{"unified":"1f1eb-1f1f7","native":"🇫🇷"}],"version":1},"flag-ga":{"id":"flag-ga","name":"Gabon Flag","keywords":["ga","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1e6","native":"🇬🇦"}],"version":2},"gb":{"id":"gb","name":"United Kingdom Flag","keywords":["gb","uk","great","britain","northern","ireland","nation","country","banner","british","UK","english","england","union","jack"],"skins":[{"unified":"1f1ec-1f1e7","native":"🇬🇧"}],"version":1},"flag-gd":{"id":"flag-gd","name":"Grenada Flag","keywords":["gd","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1e9","native":"🇬🇩"}],"version":2},"flag-ge":{"id":"flag-ge","name":"Georgia Flag","keywords":["ge","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1ea","native":"🇬🇪"}],"version":2},"flag-gf":{"id":"flag-gf","name":"French Guiana Flag","keywords":["gf","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1eb","native":"🇬🇫"}],"version":2},"flag-gg":{"id":"flag-gg","name":"Guernsey Flag","keywords":["gg","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1ec","native":"🇬🇬"}],"version":2},"flag-gh":{"id":"flag-gh","name":"Ghana Flag","keywords":["gh","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1ed","native":"🇬ðŸ‡"}],"version":2},"flag-gi":{"id":"flag-gi","name":"Gibraltar Flag","keywords":["gi","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1ee","native":"🇬🇮"}],"version":2},"flag-gl":{"id":"flag-gl","name":"Greenland Flag","keywords":["gl","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f1","native":"🇬🇱"}],"version":2},"flag-gm":{"id":"flag-gm","name":"Gambia Flag","keywords":["gm","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f2","native":"🇬🇲"}],"version":2},"flag-gn":{"id":"flag-gn","name":"Guinea Flag","keywords":["gn","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f3","native":"🇬🇳"}],"version":2},"flag-gp":{"id":"flag-gp","name":"Guadeloupe Flag","keywords":["gp","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f5","native":"🇬🇵"}],"version":2},"flag-gq":{"id":"flag-gq","name":"Equatorial Guinea Flag","keywords":["gq","gn","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f6","native":"🇬🇶"}],"version":2},"flag-gr":{"id":"flag-gr","name":"Greece Flag","keywords":["gr","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f7","native":"🇬🇷"}],"version":2},"flag-gs":{"id":"flag-gs","name":"South Georgia & South Sandwich Islands Flag","keywords":["gs","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f8","native":"🇬🇸"}],"version":2},"flag-gt":{"id":"flag-gt","name":"Guatemala Flag","keywords":["gt","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1f9","native":"🇬🇹"}],"version":2},"flag-gu":{"id":"flag-gu","name":"Guam Flag","keywords":["gu","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1fa","native":"🇬🇺"}],"version":2},"flag-gw":{"id":"flag-gw","name":"Guinea-Bissau Flag","keywords":["gw","guinea","bissau","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1fc","native":"🇬🇼"}],"version":2},"flag-gy":{"id":"flag-gy","name":"Guyana Flag","keywords":["gy","nation","country","banner"],"skins":[{"unified":"1f1ec-1f1fe","native":"🇬🇾"}],"version":2},"flag-hk":{"id":"flag-hk","name":"Hong Kong Sar China Flag","keywords":["hk","nation","country","banner"],"skins":[{"unified":"1f1ed-1f1f0","native":"ðŸ‡ðŸ‡°"}],"version":2},"flag-hm":{"id":"flag-hm","name":"Heard & Mcdonald Islands Flag","keywords":["hm"],"skins":[{"unified":"1f1ed-1f1f2","native":"ðŸ‡ðŸ‡²"}],"version":2},"flag-hn":{"id":"flag-hn","name":"Honduras Flag","keywords":["hn","nation","country","banner"],"skins":[{"unified":"1f1ed-1f1f3","native":"ðŸ‡ðŸ‡³"}],"version":2},"flag-hr":{"id":"flag-hr","name":"Croatia Flag","keywords":["hr","nation","country","banner"],"skins":[{"unified":"1f1ed-1f1f7","native":"ðŸ‡ðŸ‡·"}],"version":2},"flag-ht":{"id":"flag-ht","name":"Haiti Flag","keywords":["ht","nation","country","banner"],"skins":[{"unified":"1f1ed-1f1f9","native":"ðŸ‡ðŸ‡¹"}],"version":2},"flag-hu":{"id":"flag-hu","name":"Hungary Flag","keywords":["hu","nation","country","banner"],"skins":[{"unified":"1f1ed-1f1fa","native":"ðŸ‡ðŸ‡º"}],"version":2},"flag-ic":{"id":"flag-ic","name":"Canary Islands Flag","keywords":["ic","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1e8","native":"🇮🇨"}],"version":2},"flag-id":{"id":"flag-id","name":"Indonesia Flag","keywords":["id","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1e9","native":"🇮🇩"}],"version":2},"flag-ie":{"id":"flag-ie","name":"Ireland Flag","keywords":["ie","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1ea","native":"🇮🇪"}],"version":2},"flag-il":{"id":"flag-il","name":"Israel Flag","keywords":["il","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f1","native":"🇮🇱"}],"version":2},"flag-im":{"id":"flag-im","name":"Isle of Man Flag","keywords":["im","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f2","native":"🇮🇲"}],"version":2},"flag-in":{"id":"flag-in","name":"India Flag","keywords":["in","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f3","native":"🇮🇳"}],"version":2},"flag-io":{"id":"flag-io","name":"British Indian Ocean Territory Flag","keywords":["io","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f4","native":"🇮🇴"}],"version":2},"flag-iq":{"id":"flag-iq","name":"Iraq Flag","keywords":["iq","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f6","native":"🇮🇶"}],"version":2},"flag-ir":{"id":"flag-ir","name":"Iran Flag","keywords":["ir","islamic","republic","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f7","native":"🇮🇷"}],"version":2},"flag-is":{"id":"flag-is","name":"Iceland Flag","keywords":["is","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f8","native":"🇮🇸"}],"version":2},"it":{"id":"it","name":"Italy Flag","keywords":["it","nation","country","banner"],"skins":[{"unified":"1f1ee-1f1f9","native":"🇮🇹"}],"version":1},"flag-je":{"id":"flag-je","name":"Jersey Flag","keywords":["je","nation","country","banner"],"skins":[{"unified":"1f1ef-1f1ea","native":"🇯🇪"}],"version":2},"flag-jm":{"id":"flag-jm","name":"Jamaica Flag","keywords":["jm","nation","country","banner"],"skins":[{"unified":"1f1ef-1f1f2","native":"🇯🇲"}],"version":2},"flag-jo":{"id":"flag-jo","name":"Jordan Flag","keywords":["jo","nation","country","banner"],"skins":[{"unified":"1f1ef-1f1f4","native":"🇯🇴"}],"version":2},"jp":{"id":"jp","name":"Japan Flag","keywords":["jp","japanese","nation","country","banner"],"skins":[{"unified":"1f1ef-1f1f5","native":"🇯🇵"}],"version":1},"flag-ke":{"id":"flag-ke","name":"Kenya Flag","keywords":["ke","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1ea","native":"🇰🇪"}],"version":2},"flag-kg":{"id":"flag-kg","name":"Kyrgyzstan Flag","keywords":["kg","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1ec","native":"🇰🇬"}],"version":2},"flag-kh":{"id":"flag-kh","name":"Cambodia Flag","keywords":["kh","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1ed","native":"🇰ðŸ‡"}],"version":2},"flag-ki":{"id":"flag-ki","name":"Kiribati Flag","keywords":["ki","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1ee","native":"🇰🇮"}],"version":2},"flag-km":{"id":"flag-km","name":"Comoros Flag","keywords":["km","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1f2","native":"🇰🇲"}],"version":2},"flag-kn":{"id":"flag-kn","name":"St. Kitts & Nevis Flag","keywords":["kn","st","saint","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1f3","native":"🇰🇳"}],"version":2},"flag-kp":{"id":"flag-kp","name":"North Korea Flag","keywords":["kp","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1f5","native":"🇰🇵"}],"version":2},"kr":{"id":"kr","name":"South Korea Flag","keywords":["kr","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1f7","native":"🇰🇷"}],"version":1},"flag-kw":{"id":"flag-kw","name":"Kuwait Flag","keywords":["kw","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1fc","native":"🇰🇼"}],"version":2},"flag-ky":{"id":"flag-ky","name":"Cayman Islands Flag","keywords":["ky","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1fe","native":"🇰🇾"}],"version":2},"flag-kz":{"id":"flag-kz","name":"Kazakhstan Flag","keywords":["kz","nation","country","banner"],"skins":[{"unified":"1f1f0-1f1ff","native":"🇰🇿"}],"version":2},"flag-la":{"id":"flag-la","name":"Laos Flag","keywords":["la","lao","democratic","republic","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1e6","native":"🇱🇦"}],"version":2},"flag-lb":{"id":"flag-lb","name":"Lebanon Flag","keywords":["lb","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1e7","native":"🇱🇧"}],"version":2},"flag-lc":{"id":"flag-lc","name":"St. Lucia Flag","keywords":["lc","st","saint","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1e8","native":"🇱🇨"}],"version":2},"flag-li":{"id":"flag-li","name":"Liechtenstein Flag","keywords":["li","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1ee","native":"🇱🇮"}],"version":2},"flag-lk":{"id":"flag-lk","name":"Sri Lanka Flag","keywords":["lk","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1f0","native":"🇱🇰"}],"version":2},"flag-lr":{"id":"flag-lr","name":"Liberia Flag","keywords":["lr","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1f7","native":"🇱🇷"}],"version":2},"flag-ls":{"id":"flag-ls","name":"Lesotho Flag","keywords":["ls","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1f8","native":"🇱🇸"}],"version":2},"flag-lt":{"id":"flag-lt","name":"Lithuania Flag","keywords":["lt","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1f9","native":"🇱🇹"}],"version":2},"flag-lu":{"id":"flag-lu","name":"Luxembourg Flag","keywords":["lu","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1fa","native":"🇱🇺"}],"version":2},"flag-lv":{"id":"flag-lv","name":"Latvia Flag","keywords":["lv","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1fb","native":"🇱🇻"}],"version":2},"flag-ly":{"id":"flag-ly","name":"Libya Flag","keywords":["ly","nation","country","banner"],"skins":[{"unified":"1f1f1-1f1fe","native":"🇱🇾"}],"version":2},"flag-ma":{"id":"flag-ma","name":"Morocco Flag","keywords":["ma","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1e6","native":"🇲🇦"}],"version":2},"flag-mc":{"id":"flag-mc","name":"Monaco Flag","keywords":["mc","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1e8","native":"🇲🇨"}],"version":2},"flag-md":{"id":"flag-md","name":"Moldova Flag","keywords":["md","republic","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1e9","native":"🇲🇩"}],"version":2},"flag-me":{"id":"flag-me","name":"Montenegro Flag","keywords":["me","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1ea","native":"🇲🇪"}],"version":2},"flag-mf":{"id":"flag-mf","name":"St. Martin Flag","keywords":["mf","st"],"skins":[{"unified":"1f1f2-1f1eb","native":"🇲🇫"}],"version":2},"flag-mg":{"id":"flag-mg","name":"Madagascar Flag","keywords":["mg","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1ec","native":"🇲🇬"}],"version":2},"flag-mh":{"id":"flag-mh","name":"Marshall Islands Flag","keywords":["mh","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1ed","native":"🇲ðŸ‡"}],"version":2},"flag-mk":{"id":"flag-mk","name":"North Macedonia Flag","keywords":["mk","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f0","native":"🇲🇰"}],"version":2},"flag-ml":{"id":"flag-ml","name":"Mali Flag","keywords":["ml","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f1","native":"🇲🇱"}],"version":2},"flag-mm":{"id":"flag-mm","name":"Myanmar (burma) Flag","keywords":["mm","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f2","native":"🇲🇲"}],"version":2},"flag-mn":{"id":"flag-mn","name":"Mongolia Flag","keywords":["mn","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f3","native":"🇲🇳"}],"version":2},"flag-mo":{"id":"flag-mo","name":"Macao Sar China Flag","keywords":["mo","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f4","native":"🇲🇴"}],"version":2},"flag-mp":{"id":"flag-mp","name":"Northern Mariana Islands Flag","keywords":["mp","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f5","native":"🇲🇵"}],"version":2},"flag-mq":{"id":"flag-mq","name":"Martinique Flag","keywords":["mq","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f6","native":"🇲🇶"}],"version":2},"flag-mr":{"id":"flag-mr","name":"Mauritania Flag","keywords":["mr","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f7","native":"🇲🇷"}],"version":2},"flag-ms":{"id":"flag-ms","name":"Montserrat Flag","keywords":["ms","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f8","native":"🇲🇸"}],"version":2},"flag-mt":{"id":"flag-mt","name":"Malta Flag","keywords":["mt","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1f9","native":"🇲🇹"}],"version":2},"flag-mu":{"id":"flag-mu","name":"Mauritius Flag","keywords":["mu","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1fa","native":"🇲🇺"}],"version":2},"flag-mv":{"id":"flag-mv","name":"Maldives Flag","keywords":["mv","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1fb","native":"🇲🇻"}],"version":2},"flag-mw":{"id":"flag-mw","name":"Malawi Flag","keywords":["mw","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1fc","native":"🇲🇼"}],"version":2},"flag-mx":{"id":"flag-mx","name":"Mexico Flag","keywords":["mx","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1fd","native":"🇲🇽"}],"version":2},"flag-my":{"id":"flag-my","name":"Malaysia Flag","keywords":["my","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1fe","native":"🇲🇾"}],"version":2},"flag-mz":{"id":"flag-mz","name":"Mozambique Flag","keywords":["mz","nation","country","banner"],"skins":[{"unified":"1f1f2-1f1ff","native":"🇲🇿"}],"version":2},"flag-na":{"id":"flag-na","name":"Namibia Flag","keywords":["na","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1e6","native":"🇳🇦"}],"version":2},"flag-nc":{"id":"flag-nc","name":"New Caledonia Flag","keywords":["nc","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1e8","native":"🇳🇨"}],"version":2},"flag-ne":{"id":"flag-ne","name":"Niger Flag","keywords":["ne","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1ea","native":"🇳🇪"}],"version":2},"flag-nf":{"id":"flag-nf","name":"Norfolk Island Flag","keywords":["nf","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1eb","native":"🇳🇫"}],"version":2},"flag-ng":{"id":"flag-ng","name":"Nigeria Flag","keywords":["ng","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1ec","native":"🇳🇬"}],"version":2},"flag-ni":{"id":"flag-ni","name":"Nicaragua Flag","keywords":["ni","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1ee","native":"🇳🇮"}],"version":2},"flag-nl":{"id":"flag-nl","name":"Netherlands Flag","keywords":["nl","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1f1","native":"🇳🇱"}],"version":2},"flag-no":{"id":"flag-no","name":"Norway Flag","keywords":["no","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1f4","native":"🇳🇴"}],"version":2},"flag-np":{"id":"flag-np","name":"Nepal Flag","keywords":["np","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1f5","native":"🇳🇵"}],"version":2},"flag-nr":{"id":"flag-nr","name":"Nauru Flag","keywords":["nr","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1f7","native":"🇳🇷"}],"version":2},"flag-nu":{"id":"flag-nu","name":"Niue Flag","keywords":["nu","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1fa","native":"🇳🇺"}],"version":2},"flag-nz":{"id":"flag-nz","name":"New Zealand Flag","keywords":["nz","nation","country","banner"],"skins":[{"unified":"1f1f3-1f1ff","native":"🇳🇿"}],"version":2},"flag-om":{"id":"flag-om","name":"Oman Flag","keywords":["om","symbol","nation","country","banner"],"skins":[{"unified":"1f1f4-1f1f2","native":"🇴🇲"}],"version":2},"flag-pa":{"id":"flag-pa","name":"Panama Flag","keywords":["pa","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1e6","native":"🇵🇦"}],"version":2},"flag-pe":{"id":"flag-pe","name":"Peru Flag","keywords":["pe","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1ea","native":"🇵🇪"}],"version":2},"flag-pf":{"id":"flag-pf","name":"French Polynesia Flag","keywords":["pf","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1eb","native":"🇵🇫"}],"version":2},"flag-pg":{"id":"flag-pg","name":"Papua New Guinea Flag","keywords":["pg","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1ec","native":"🇵🇬"}],"version":2},"flag-ph":{"id":"flag-ph","name":"Philippines Flag","keywords":["ph","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1ed","native":"🇵ðŸ‡"}],"version":2},"flag-pk":{"id":"flag-pk","name":"Pakistan Flag","keywords":["pk","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f0","native":"🇵🇰"}],"version":2},"flag-pl":{"id":"flag-pl","name":"Poland Flag","keywords":["pl","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f1","native":"🇵🇱"}],"version":2},"flag-pm":{"id":"flag-pm","name":"St. Pierre & Miquelon Flag","keywords":["pm","st","saint","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f2","native":"🇵🇲"}],"version":2},"flag-pn":{"id":"flag-pn","name":"Pitcairn Islands Flag","keywords":["pn","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f3","native":"🇵🇳"}],"version":2},"flag-pr":{"id":"flag-pr","name":"Puerto Rico Flag","keywords":["pr","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f7","native":"🇵🇷"}],"version":2},"flag-ps":{"id":"flag-ps","name":"Palestinian Territories Flag","keywords":["ps","palestine","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f8","native":"🇵🇸"}],"version":2},"flag-pt":{"id":"flag-pt","name":"Portugal Flag","keywords":["pt","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1f9","native":"🇵🇹"}],"version":2},"flag-pw":{"id":"flag-pw","name":"Palau Flag","keywords":["pw","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1fc","native":"🇵🇼"}],"version":2},"flag-py":{"id":"flag-py","name":"Paraguay Flag","keywords":["py","nation","country","banner"],"skins":[{"unified":"1f1f5-1f1fe","native":"🇵🇾"}],"version":2},"flag-qa":{"id":"flag-qa","name":"Qatar Flag","keywords":["qa","nation","country","banner"],"skins":[{"unified":"1f1f6-1f1e6","native":"🇶🇦"}],"version":2},"flag-re":{"id":"flag-re","name":"Réunion Flag","keywords":["re","reunion","nation","country","banner"],"skins":[{"unified":"1f1f7-1f1ea","native":"🇷🇪"}],"version":2},"flag-ro":{"id":"flag-ro","name":"Romania Flag","keywords":["ro","nation","country","banner"],"skins":[{"unified":"1f1f7-1f1f4","native":"🇷🇴"}],"version":2},"flag-rs":{"id":"flag-rs","name":"Serbia Flag","keywords":["rs","nation","country","banner"],"skins":[{"unified":"1f1f7-1f1f8","native":"🇷🇸"}],"version":2},"ru":{"id":"ru","name":"Russia Flag","keywords":["ru","russian","federation","nation","country","banner"],"skins":[{"unified":"1f1f7-1f1fa","native":"🇷🇺"}],"version":1},"flag-rw":{"id":"flag-rw","name":"Rwanda Flag","keywords":["rw","nation","country","banner"],"skins":[{"unified":"1f1f7-1f1fc","native":"🇷🇼"}],"version":2},"flag-sa":{"id":"flag-sa","name":"Saudi Arabia Flag","keywords":["sa","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1e6","native":"🇸🇦"}],"version":2},"flag-sb":{"id":"flag-sb","name":"Solomon Islands Flag","keywords":["sb","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1e7","native":"🇸🇧"}],"version":2},"flag-sc":{"id":"flag-sc","name":"Seychelles Flag","keywords":["sc","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1e8","native":"🇸🇨"}],"version":2},"flag-sd":{"id":"flag-sd","name":"Sudan Flag","keywords":["sd","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1e9","native":"🇸🇩"}],"version":2},"flag-se":{"id":"flag-se","name":"Sweden Flag","keywords":["se","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1ea","native":"🇸🇪"}],"version":2},"flag-sg":{"id":"flag-sg","name":"Singapore Flag","keywords":["sg","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1ec","native":"🇸🇬"}],"version":2},"flag-sh":{"id":"flag-sh","name":"St. Helena Flag","keywords":["sh","st","saint","ascension","tristan","cunha","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1ed","native":"🇸ðŸ‡"}],"version":2},"flag-si":{"id":"flag-si","name":"Slovenia Flag","keywords":["si","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1ee","native":"🇸🇮"}],"version":2},"flag-sj":{"id":"flag-sj","name":"Svalbard & Jan Mayen Flag","keywords":["sj"],"skins":[{"unified":"1f1f8-1f1ef","native":"🇸🇯"}],"version":2},"flag-sk":{"id":"flag-sk","name":"Slovakia Flag","keywords":["sk","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f0","native":"🇸🇰"}],"version":2},"flag-sl":{"id":"flag-sl","name":"Sierra Leone Flag","keywords":["sl","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f1","native":"🇸🇱"}],"version":2},"flag-sm":{"id":"flag-sm","name":"San Marino Flag","keywords":["sm","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f2","native":"🇸🇲"}],"version":2},"flag-sn":{"id":"flag-sn","name":"Senegal Flag","keywords":["sn","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f3","native":"🇸🇳"}],"version":2},"flag-so":{"id":"flag-so","name":"Somalia Flag","keywords":["so","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f4","native":"🇸🇴"}],"version":2},"flag-sr":{"id":"flag-sr","name":"Suriname Flag","keywords":["sr","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f7","native":"🇸🇷"}],"version":2},"flag-ss":{"id":"flag-ss","name":"South Sudan Flag","keywords":["ss","sd","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f8","native":"🇸🇸"}],"version":2},"flag-st":{"id":"flag-st","name":"São Tomé & PrÃncipe Flag","keywords":["st","sao","tome","principe","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1f9","native":"🇸🇹"}],"version":2},"flag-sv":{"id":"flag-sv","name":"El Salvador Flag","keywords":["sv","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1fb","native":"🇸🇻"}],"version":2},"flag-sx":{"id":"flag-sx","name":"Sint Maarten Flag","keywords":["sx","dutch","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1fd","native":"🇸🇽"}],"version":2},"flag-sy":{"id":"flag-sy","name":"Syria Flag","keywords":["sy","syrian","arab","republic","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1fe","native":"🇸🇾"}],"version":2},"flag-sz":{"id":"flag-sz","name":"Eswatini Flag","keywords":["sz","nation","country","banner"],"skins":[{"unified":"1f1f8-1f1ff","native":"🇸🇿"}],"version":2},"flag-ta":{"id":"flag-ta","name":"Tristan Da Cunha Flag","keywords":["ta"],"skins":[{"unified":"1f1f9-1f1e6","native":"🇹🇦"}],"version":2},"flag-tc":{"id":"flag-tc","name":"Turks & Caicos Islands Flag","keywords":["tc","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1e8","native":"🇹🇨"}],"version":2},"flag-td":{"id":"flag-td","name":"Chad Flag","keywords":["td","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1e9","native":"🇹🇩"}],"version":2},"flag-tf":{"id":"flag-tf","name":"French Southern Territories Flag","keywords":["tf","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1eb","native":"🇹🇫"}],"version":2},"flag-tg":{"id":"flag-tg","name":"Togo Flag","keywords":["tg","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1ec","native":"🇹🇬"}],"version":2},"flag-th":{"id":"flag-th","name":"Thailand Flag","keywords":["th","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1ed","native":"🇹ðŸ‡"}],"version":2},"flag-tj":{"id":"flag-tj","name":"Tajikistan Flag","keywords":["tj","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1ef","native":"🇹🇯"}],"version":2},"flag-tk":{"id":"flag-tk","name":"Tokelau Flag","keywords":["tk","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f0","native":"🇹🇰"}],"version":2},"flag-tl":{"id":"flag-tl","name":"Timor-Leste Flag","keywords":["tl","timor","leste","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f1","native":"🇹🇱"}],"version":2},"flag-tm":{"id":"flag-tm","name":"Turkmenistan Flag","keywords":["tm","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f2","native":"🇹🇲"}],"version":2},"flag-tn":{"id":"flag-tn","name":"Tunisia Flag","keywords":["tn","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f3","native":"🇹🇳"}],"version":2},"flag-to":{"id":"flag-to","name":"Tonga Flag","keywords":["to","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f4","native":"🇹🇴"}],"version":2},"flag-tr":{"id":"flag-tr","name":"Turkey Flag","keywords":["tr","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f7","native":"🇹🇷"}],"version":2},"flag-tt":{"id":"flag-tt","name":"Trinidad & Tobago Flag","keywords":["tt","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1f9","native":"🇹🇹"}],"version":2},"flag-tv":{"id":"flag-tv","name":"Tuvalu Flag","keywords":["tv","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1fb","native":"🇹🇻"}],"version":2},"flag-tw":{"id":"flag-tw","name":"Taiwan Flag","keywords":["tw","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1fc","native":"🇹🇼"}],"version":2},"flag-tz":{"id":"flag-tz","name":"Tanzania Flag","keywords":["tz","united","republic","nation","country","banner"],"skins":[{"unified":"1f1f9-1f1ff","native":"🇹🇿"}],"version":2},"flag-ua":{"id":"flag-ua","name":"Ukraine Flag","keywords":["ua","nation","country","banner"],"skins":[{"unified":"1f1fa-1f1e6","native":"🇺🇦"}],"version":2},"flag-ug":{"id":"flag-ug","name":"Uganda Flag","keywords":["ug","nation","country","banner"],"skins":[{"unified":"1f1fa-1f1ec","native":"🇺🇬"}],"version":2},"flag-um":{"id":"flag-um","name":"U.s. Outlying Islands Flag","keywords":["um","u","s"],"skins":[{"unified":"1f1fa-1f1f2","native":"🇺🇲"}],"version":2},"flag-un":{"id":"flag-un","name":"United Nations Flag","keywords":["un","banner"],"skins":[{"unified":"1f1fa-1f1f3","native":"🇺🇳"}],"version":4},"us":{"id":"us","name":"United States Flag","keywords":["us","america","nation","country","banner"],"skins":[{"unified":"1f1fa-1f1f8","native":"🇺🇸"}],"version":1},"flag-uy":{"id":"flag-uy","name":"Uruguay Flag","keywords":["uy","nation","country","banner"],"skins":[{"unified":"1f1fa-1f1fe","native":"🇺🇾"}],"version":2},"flag-uz":{"id":"flag-uz","name":"Uzbekistan Flag","keywords":["uz","nation","country","banner"],"skins":[{"unified":"1f1fa-1f1ff","native":"🇺🇿"}],"version":2},"flag-va":{"id":"flag-va","name":"Vatican City Flag","keywords":["va","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1e6","native":"🇻🇦"}],"version":2},"flag-vc":{"id":"flag-vc","name":"St. Vincent & Grenadines Flag","keywords":["vc","st","saint","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1e8","native":"🇻🇨"}],"version":2},"flag-ve":{"id":"flag-ve","name":"Venezuela Flag","keywords":["ve","bolivarian","republic","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1ea","native":"🇻🇪"}],"version":2},"flag-vg":{"id":"flag-vg","name":"British Virgin Islands Flag","keywords":["vg","bvi","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1ec","native":"🇻🇬"}],"version":2},"flag-vi":{"id":"flag-vi","name":"U.s. Virgin Islands Flag","keywords":["vi","u","s","us","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1ee","native":"🇻🇮"}],"version":2},"flag-vn":{"id":"flag-vn","name":"Vietnam Flag","keywords":["vn","viet","nam","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1f3","native":"🇻🇳"}],"version":2},"flag-vu":{"id":"flag-vu","name":"Vanuatu Flag","keywords":["vu","nation","country","banner"],"skins":[{"unified":"1f1fb-1f1fa","native":"🇻🇺"}],"version":2},"flag-wf":{"id":"flag-wf","name":"Wallis & Futuna Flag","keywords":["wf","nation","country","banner"],"skins":[{"unified":"1f1fc-1f1eb","native":"🇼🇫"}],"version":2},"flag-ws":{"id":"flag-ws","name":"Samoa Flag","keywords":["ws","nation","country","banner"],"skins":[{"unified":"1f1fc-1f1f8","native":"🇼🇸"}],"version":2},"flag-xk":{"id":"flag-xk","name":"Kosovo Flag","keywords":["xk","nation","country","banner"],"skins":[{"unified":"1f1fd-1f1f0","native":"🇽🇰"}],"version":2},"flag-ye":{"id":"flag-ye","name":"Yemen Flag","keywords":["ye","nation","country","banner"],"skins":[{"unified":"1f1fe-1f1ea","native":"🇾🇪"}],"version":2},"flag-yt":{"id":"flag-yt","name":"Mayotte Flag","keywords":["yt","nation","country","banner"],"skins":[{"unified":"1f1fe-1f1f9","native":"🇾🇹"}],"version":2},"flag-za":{"id":"flag-za","name":"South Africa Flag","keywords":["za","nation","country","banner"],"skins":[{"unified":"1f1ff-1f1e6","native":"🇿🇦"}],"version":2},"flag-zm":{"id":"flag-zm","name":"Zambia Flag","keywords":["zm","nation","country","banner"],"skins":[{"unified":"1f1ff-1f1f2","native":"🇿🇲"}],"version":2},"flag-zw":{"id":"flag-zw","name":"Zimbabwe Flag","keywords":["zw","nation","country","banner"],"skins":[{"unified":"1f1ff-1f1fc","native":"🇿🇼"}],"version":2},"flag-england":{"id":"flag-england","name":"England Flag","keywords":["english"],"skins":[{"unified":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f","native":"ðŸ´ó §ó ¢ó ¥ó ®ó §ó ¿"}],"version":5},"flag-scotland":{"id":"flag-scotland","name":"Scotland Flag","keywords":["scottish"],"skins":[{"unified":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f","native":"ðŸ´ó §ó ¢ó ³ó £ó ´ó ¿"}],"version":5},"flag-wales":{"id":"flag-wales","name":"Wales Flag","keywords":["welsh"],"skins":[{"unified":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f","native":"ðŸ´ó §ó ¢ó ·ó ¬ó ³ó ¿"}],"version":5}},"aliases":{"satisfied":"laughing","grinning_face_with_star_eyes":"star-struck","grinning_face_with_one_large_and_one_small_eye":"zany_face","smiling_face_with_smiling_eyes_and_hand_covering_mouth":"face_with_hand_over_mouth","face_with_finger_covering_closed_lips":"shushing_face","face_with_one_eyebrow_raised":"face_with_raised_eyebrow","face_with_open_mouth_vomiting":"face_vomiting","shocked_face_with_exploding_head":"exploding_head","serious_face_with_symbols_covering_mouth":"face_with_symbols_on_mouth","poop":"hankey","shit":"hankey","collision":"boom","raised_hand":"hand","hand_with_index_and_middle_fingers_crossed":"crossed_fingers","sign_of_the_horns":"the_horns","reversed_hand_with_middle_finger_extended":"middle_finger","thumbsup":"+1","thumbsdown":"-1","punch":"facepunch","mother_christmas":"mrs_claus","running":"runner","man-with-bunny-ears-partying":"men-with-bunny-ears-partying","woman-with-bunny-ears-partying":"women-with-bunny-ears-partying","women_holding_hands":"two_women_holding_hands","woman_and_man_holding_hands":"man_and_woman_holding_hands","couple":"man_and_woman_holding_hands","men_holding_hands":"two_men_holding_hands","paw_prints":"feet","flipper":"dolphin","honeybee":"bee","lady_beetle":"ladybug","cooking":"fried_egg","knife":"hocho","red_car":"car","sailboat":"boat","waxing_gibbous_moon":"moon","sun_small_cloud":"mostly_sunny","sun_behind_cloud":"barely_sunny","sun_behind_rain_cloud":"partly_sunny_rain","lightning_cloud":"lightning","tornado_cloud":"tornado","tshirt":"shirt","shoe":"mans_shoe","telephone":"phone","lantern":"izakaya_lantern","open_book":"book","envelope":"email","pencil":"memo","heavy_exclamation_mark":"exclamation","staff_of_aesculapius":"medical_symbol","flag-cn":"cn","flag-de":"de","flag-es":"es","flag-fr":"fr","uk":"gb","flag-gb":"gb","flag-it":"it","flag-jp":"jp","flag-kr":"kr","flag-ru":"ru","flag-us":"us"},"sheet":{"cols":61,"rows":61}} \ No newline at end of file diff --git a/emoji/emojiMart_test.go b/emoji/emojiMart_test.go deleted file mode 100644 index 0907d862d96d1139277ea979536632585525918e..0000000000000000000000000000000000000000 --- a/emoji/emojiMart_test.go +++ /dev/null @@ -1,85 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file // -//////////////////////////////////////////////////////////////////////////////// - -package main - -import ( - _ "embed" - "encoding/json" - "github.com/nsf/jsondiff" - "reflect" - "testing" -) - -//go:embed emojiMart.json -var emojiMartJson []byte - -// Tests that marshaling the emojiMartData object and unmarshalling that JSON -// data back into an object does not cause loss in data. -func Test_emojiMartData_JSON_Marshal_Unmarshal(t *testing.T) { - exampleData := emojiMartSet{ - Categories: []category{ - {ID: "100", Emojis: []emojiID{"100"}}, - {ID: "21"}, - {ID: "20"}, - }, - Emojis: map[emojiID]emoji{ - "100": { - ID: "100", - Name: "Hundred Points", - Keywords: []string{"hunna"}, - Skins: nil, - Version: 0, - }, - }, - Aliases: map[string]emojiID{ - "lady_beetle": "ladybug", - }, - Sheet: sheet{ - Cols: 5, - Rows: 12, - }, - } - - marshaled, err := json.Marshal(&exampleData) - if err != nil { - t.Fatalf("Failed to marshal: %+v", err) - } - - unmarshalData := emojiMartSet{} - err = json.Unmarshal(marshaled, &unmarshalData) - if err != nil { - t.Fatalf("Failed to unmarshal: %+v", err) - } - - if reflect.DeepEqual(unmarshalData, marshaled) { - t.Fatalf("Failed to unmarshal example and maintain original data."+ - "\nExpected: %+v"+ - "\nReceived: %+v", exampleData, unmarshalData) - } -} - -// Tests that the emoji-mart example JSON can be JSON unmarshalled and -// marshalled and that the result is semantically identical to the original. -func Test_emojiMartDataJSON_Example(t *testing.T) { - emojiMart := &emojiMartSet{} - err := json.Unmarshal(emojiMartJson, emojiMart) - if err != nil { - t.Fatalf("Failed to unamrshal: %+v", err) - } - - marshalled, err := json.Marshal(emojiMart) - if err != nil { - t.Fatalf("Failed to marshal: %+v", err) - } - - opts := jsondiff.DefaultConsoleOptions() - d, s := jsondiff.Compare(emojiMartJson, marshalled, &opts) - if d != jsondiff.FullMatch { - t.Errorf("Diff failed for marshalled JSON: %s\nDifferences: %s", d, s) - } -} diff --git a/emoji/emojiSet.go b/emoji/emojiSet.go deleted file mode 100644 index dbb2095ca4a25d62cbb8c7219e1f3aeaa57f4dbe..0000000000000000000000000000000000000000 --- a/emoji/emojiSet.go +++ /dev/null @@ -1,179 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file // -//////////////////////////////////////////////////////////////////////////////// - -package main - -import ( - "encoding/json" - "github.com/forPelevin/gomoji" - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - cEmoji "gitlab.com/elixxir/client/v4/emoji" - "strings" -) - -// Set contains the set of emoji's that the backend supports. This object will -// be used to remove all unsupported emojis in the given emoji-mart JSON set. -type Set struct { - // replacementMap contains a list of emoji code-points in the emoji-mart set - // that must be replaced to adhere to backend recognized code-points. - replacementMap map[codepoint]skin - - // supportedEmojis contains a list of all Unicode codepoints for the emojis - // that are supported. This allows for quick lookup when comparing against - // the emoji-mart of emojis. - supportedEmojis map[codepoint]struct{} -} - -// NewSet initialises a new Emoji Set with all supported backend emojis. -func NewSet() *Set { - return &Set{ - replacementMap: map[codepoint]skin{ - "2764-fe0f": { - Unified: "2764", // Has codepoint "2764-fe0f" in front-end - Native: "â¤", - }, - }, - supportedEmojis: emojiListToMap(cEmoji.SupportedEmojis()), - } -} - -// SanitizeEmojiMartSet removes all unsupported emojis from the emoji-mart set -// JSON. It also replaces any mismatched codepoints (where the same Emoji has -// two different codepoints). -func (s *Set) SanitizeEmojiMartSet(frontendEmojiSetJson []byte) ([]byte, error) { - - // Unmarshal the emoji-mart set JSON - var frontEndEmojiSet emojiMartSet - err := json.Unmarshal(frontendEmojiSetJson, &frontEndEmojiSet) - if err != nil { - return nil, errors.Errorf( - "failed to unmarshal emoji-mart set JSON: %+v", err) - } - - jww.DEBUG.Printf( - "Finding incompatible emojis and replacing mismatched codepoints.") - - // Find all incompatible emojis in the front end set - emojisToRemove := s.findIncompatibleEmojis(&frontEndEmojiSet) - - jww.DEBUG.Printf("Removing incompatible emojis.") - - // Remove all incompatible emojis from the set - removeIncompatibleEmojis(&frontEndEmojiSet, emojisToRemove) - - jww.INFO.Printf("Removed %d incompatible codepoints.", len(emojisToRemove)) - - return json.Marshal(frontEndEmojiSet) -} - -// findIncompatibleEmojis returns a list of emojis in the emojiMartSet that are -// not supported by the Set. Also, any emojiMartSet emoji codepoints that are -// incompatible and have replacements (as defined in Set) are replaced. -func (s *Set) findIncompatibleEmojis(set *emojiMartSet) (emojisToRemove []emojiID) { - // Iterate over all emojis in the emojiMartSet.Emojis list - for id, Emoji := range set.Emojis { - var newSkins []skin - for _, Skin := range Emoji.Skins { - // Determine if the emoji's codepoint should be replaced or removed - replacement, replace := s.replace(Skin.Unified) - if replace { - jww.TRACE.Printf("Replaced codepoint %q with %v for emoji %q", - Skin.Unified, replacement, id) - newSkins = append(newSkins, replacement) - } else if !s.remove(Skin.Unified) { - newSkins = append(newSkins, Skin) - } else { - jww.TRACE.Printf("Removed codepoint %q from emoji %q", - Skin.Unified, id) - } - } - - if len(newSkins) > 0 { - // Write to the set the possible edits (if emojis were replaced - // or removed) - Emoji.Skins = newSkins - set.Emojis[id] = Emoji - } else { - // If all skins have been removed, then mark the emoji for removal - emojisToRemove = append(emojisToRemove, id) - jww.DEBUG.Printf("All skins removed for emoji %q", id) - } - } - - return emojisToRemove -} - -// removeIncompatibleEmojis removes all the emojis in emojisToRemove from the -// emojiMartSet set. -func removeIncompatibleEmojis(set *emojiMartSet, emojisToRemove []emojiID) { - jww.DEBUG.Printf( - "Removing %d emojis: %s", len(emojisToRemove), emojisToRemove) - - // Remove all incompatible emojis from the emojiMartSet.Emojis list - for _, char := range emojisToRemove { - jww.TRACE.Printf("Removing %q from emojiMartSet.Emojis", char) - delete(set.Emojis, char) - } - - // Remove all incompatible emojis from the emojiMartSet.Categories list - for _, cat := range set.Categories { - // Iterate over the emoji list backwards to make removal of elements - // from the slice easier - for i := len(cat.Emojis) - 1; i >= 0; i-- { - for _, char := range emojisToRemove { - if cat.Emojis[i] == char { - cat.Emojis = append(cat.Emojis[:i], cat.Emojis[i+1:]...) - jww.TRACE.Printf( - "Removing %q from emojiMartSet.Categories", char) - } - } - } - } - - // Remove all incompatible emojis from the emojiMartSet.Aliases list - for alias, id := range set.Aliases { - for _, removedId := range emojisToRemove { - if id == removedId { - delete(set.Aliases, alias) - jww.TRACE.Printf( - "Removing %q from emojiMartSet.Aliases", alias) - } - } - } -} - -// replace returns whether the front end Unicode codepoint must be replaced. -// It will return a boolean on whether this codepoint needs to be replaced -// and what the codepoint must be replaced with. -func (s *Set) replace(code codepoint) (replacement skin, replace bool) { - replacement, replace = s.replacementMap[code] - return replacement, replace -} - -// remove returns true if the code point should be removed from the parent list. -func (s *Set) remove(code codepoint) bool { - _, exists := s.supportedEmojis[code] - return !exists -} - -// emojiListToMap constructs a map for simple lookup for gomoji.Emoji's -// Unicode codepoint. -func emojiListToMap(list []gomoji.Emoji) map[codepoint]struct{} { - emojiMap := make(map[codepoint]struct{}, len(list)) - for _, e := range list { - emojiMap[backToFrontCodePoint(e.CodePoint)] = struct{}{} - } - return emojiMap -} - -// backToFrontCodePoint converts Unicode codepoint format found in gomoji.Emoji -// to the one found in the emoji-mart JSON. The specific conversion is making it -// lowercase and replacing " " with "-". -func backToFrontCodePoint(code string) codepoint { - return codepoint(strings.ToLower(strings.ReplaceAll(code, " ", "-"))) -} diff --git a/emoji/emojiSet_test.go b/emoji/emojiSet_test.go deleted file mode 100644 index 9c1abfb44b956d8ec94c5573882cbd3cb4e58403..0000000000000000000000000000000000000000 --- a/emoji/emojiSet_test.go +++ /dev/null @@ -1,242 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file // -//////////////////////////////////////////////////////////////////////////////// - -package main - -import ( - "encoding/json" - "reflect" - "testing" -) - -// Benchmarks the entire code path for sanitizing front end's emoji file. -// This includes loading the file, parsing the data and sanitizing. -// NOTE: This does not include writing the file due to limitations of the -// embed.FS interface. -func BenchmarkSet_SanitizeFrontendEmojiList(b *testing.B) { - backendSet := NewSet() - - for i := 0; i < b.N; i++ { - - // Sanitize front end example - _, err := backendSet.SanitizeEmojiMartSet(emojiMartJson) - if err != nil { - b.Fatalf("Failed to Sanitize front end emojis: %+v", err) - } - } -} - -// Comprehensive test of Set.SanitizeEmojiMartSet using the front end example -// JSON file (emojiMart.json). -func TestSet_SanitizeFrontEndEmojis_FrontEndExample(t *testing.T) { - - backendSet := NewSet() - - // Sanitize front end example - sanitizedSetJson, err := backendSet.SanitizeEmojiMartSet(emojiMartJson) - if err != nil { - t.Fatalf("Failed to Sanitize front end emojis: %+v", err) - } - - // Unmarshal front end example - unsanitizedSet := &emojiMartSet{} - err = json.Unmarshal(emojiMartJson, unsanitizedSet) - if err != nil { - t.Fatalf("Failed to unmarshal unsanitized set: %+v", err) - } - - // Unmarshal sanitization of front end example - sanitizedSet := &emojiMartSet{} - err = json.Unmarshal(sanitizedSetJson, sanitizedSet) - if err != nil { - t.Fatalf("Failed to unmarshal sanitized set: +%v", err) - } - - // The front end example has known unsanitary emojis. - // Therefore, sanitization of the front end example - // should not contain the exact same data. - if reflect.DeepEqual(sanitizedSet, unsanitizedSet) { - t.Fatalf("No evidence of sanitization performed") - } - - // Check of replacement of the heart emoji (â¤ï¸). - // This is the known unsanitary emoji (it is not supported - // by backend's library). - heart, exists := sanitizedSet.Emojis["heart"] - if !exists { - t.Fatalf("Heart emoji was removed when it should have been replaced") - } - - expectedHeartReplacement := skin{ - Unified: "2764", - Native: "â¤", - } - - if !reflect.DeepEqual(expectedHeartReplacement, heart.Skins[0]) { - t.Fatalf("Heart emoji was not replaced as expected."+ - "\nExpected: %+v"+ - "\nReceived: %+v", expectedHeartReplacement, heart.Skins[0]) - } -} - -// Test of Set.findIncompatibleEmojis using the front end example -// JSON file (emojiMart.json). -func TestSet_findIncompatibleEmojis_FrontEndExample(t *testing.T) { - backendSet := NewSet() - - // Unmarshal front end example - unsanitizedSet := &emojiMartSet{} - err := json.Unmarshal(emojiMartJson, unsanitizedSet) - if err != nil { - t.Fatalf("Failed to unmarshal unsanitized set: %+v", err) - } - - emojisToRemove := backendSet.findIncompatibleEmojis(unsanitizedSet) - if len(emojisToRemove) != 0 { - t.Fatalf("Front end example should not contain any removable emojis.") - } -} - -// Tests Set.findIncompatibleEmojis using a custom emojiMartSet object. -// There is one ID with skins that should be marked as removable as all skins -// are invalid when compared against backend. There is another ID that should -// not be removed, and should not be returned by set.findIncompatibleEmojis. -func TestSet_findIncompatibleEmojis_RemovableExample(t *testing.T) { - backendSet := NewSet() - - // Construct custom emojiMartSet. - toBeRemovedId := emojiID("shouldBeRemoved") - emd := constructRemovableEmojiSet(toBeRemovedId) - - // Construct a removable list - removedEmojis := backendSet.findIncompatibleEmojis(emd) - - // Ensure only oen emoji is removed - if len(removedEmojis) != 1 { - t.Fatalf("findIncompatibleEmojis should only mark one emoji for removal.") - } - - // Ensure the single removed emoji is the expected emoji - if removedEmojis[0] != toBeRemovedId { - t.Fatalf("findIncompatibleEmojis should have found %s to be removable", - toBeRemovedId) - } -} - -// Tests that Set.removeIncompatibleEmojis will modify the passed in -// emojiMartSet according to the list of emojis to remove. -func TestSet_removeIncompatibleEmojis(t *testing.T) { - backendSet := NewSet() - - // Construct custom emojiMartSet. - toBeRemovedId := emojiID("shouldBeRemoved") - emd := constructRemovableEmojiSet(toBeRemovedId) - - // Construct a removable list - removedEmojis := backendSet.findIncompatibleEmojis(emd) - - removeIncompatibleEmojis(emd, removedEmojis) - - // Test that categories has been modified - for _, cat := range emd.Categories { - for _, e := range cat.Emojis { - if e == toBeRemovedId { - t.Fatalf("EmojiID %q was never removed from "+ - "emojiMartSet.Categories", toBeRemovedId) - } - } - } - - // Test that the emoji map has been modified - if _, exists := emd.Emojis[toBeRemovedId]; exists { - t.Fatalf("EmojiId %s was not removed from emojiMartSet.Emojis", - toBeRemovedId) - } - - // Test that the alias list has been modified - for _, id := range emd.Aliases { - if id == toBeRemovedId { - t.Fatalf("EmojiId %s twas not removed from emojiMartSet.Aliases", - toBeRemovedId) - } - } -} - -// Tests backToFrontCodePoint converts backend Unicode codepoints to their -// front end equivalent. -func Test_backToFrontCodePoint(t *testing.T) { - // Input for backend codepoints and their front end formatted pairings - tests := []struct { - input string - output codepoint - }{ - {"0023 FE0F 20E3", "0023-fe0f-20e3"}, // Single-rune emoji (\u1F44B) - {"002A FE0F 20E3", "002a-fe0f-20e3"}, // Duel-rune emoji with race modification (\u1F44B\u1F3FF) - {"00A9 FE0F", "00a9-fe0f"}, - {"1F481 1F3FC 200D 2640 FE0F", "1f481-1f3fc-200d-2640-fe0f"}, - {"1F481 1F3FE 200D 2642 FE0F", "1f481-1f3fe-200d-2642-fe0f"}, - {"1F481 1F3FF", "1f481-1f3ff"}, - {"1F481 1F3FF 200D 2642 FE0F", "1f481-1f3ff-200d-2642-fe0f"}, - {"1F469 1F3FB 200D 1F9B0", "1f469-1f3fb-200d-1f9b0"}, - {"1F378", "1f378"}, - {"1F377", "1f377"}, - {"1F376", "1f376"}, - } - - // Test that for all examples, all backend codepoints are converted - // to front end codepoints - for _, test := range tests { - received := backToFrontCodePoint(test.input) - if received != test.output { - t.Fatalf("Incorrect codepoint for %q.\nexpected: %s\nreceived: %s", - test.input, test.output, received) - } - } - -} - -// constructRemovableEmojiSet returns an emojiMartSet object used for testing. -// This object will contain data that should be marked as removable by -// Set.findIncompatibleEmojis. This removable data from -// Set.findIncompatibleEmojis can be passed to removeIncompatibleEmojis. -func constructRemovableEmojiSet(toBeRemovedId emojiID) *emojiMartSet { - return &emojiMartSet{ - Categories: []category{ - {Emojis: []emojiID{ - toBeRemovedId, - "1", - "two", - "tree", - "for", - "golden rings", - }}, - }, - Emojis: map[emojiID]emoji{ - toBeRemovedId: { - Skins: []skin{{ - Unified: "00A9 FE0F 20F3", - Native: "", - }, { - Unified: "AAAA FE0F 20F3", - Native: "", - }}, - }, - "shouldNotBeRemoved": { - Skins: []skin{{ - Unified: "1f9e1", - Native: "🧡", - }}, - }, - }, - Aliases: map[string]emojiID{ - "test": toBeRemovedId, - "tester": "safe", - "alias": "will_not", - "secret": "beRemoved", - }, - } -} diff --git a/emoji/main.go b/emoji/main.go deleted file mode 100644 index 7404448b68be319cc5db6715640ae9abfd323292..0000000000000000000000000000000000000000 --- a/emoji/main.go +++ /dev/null @@ -1,158 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -// package main is its own utility that is compiled separate from xxdk-WASM. It -// is used only to produce a compatible emoji file to be used by the frontend -// and is not a WASM module itself. - -package main - -import ( - "bytes" - "fmt" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/xx_network/primitives/utils" - "io" - "log" - "net/http" - "os" - "strconv" -) - -// emojiMartUrl is the URL pointing to the native.JSON from emoji-mart that is -// used by front end. -// -// NOTE: This points specifically to set version 14 of the emoji-mart data. This -// URL should be updated if new sets become available. -const emojiMartUrl = "https://raw.githubusercontent.com/missive/emoji-mart/main/packages/emoji-mart-data/sets/14/native.json" - -// Flag variables. -var ( - requestURL, outputPath, logFile string - logLevel int -) - -func main() { - if err := cmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -// Downloads the emoji file (from emoji-mart) and sanitizes that -// list. Sanitization removes all emojis not supported by the backend. The -// sanitized JSON is returned via a file specified by the user. Refer to the -// flags for details. -var cmd = &cobra.Command{ - Use: "sanitizeEmojis", - Short: "Downloads the emoji file (from emoji-mart) and sanitizes that " + - "list. Sanitization removes all emojis not supported by the backend. " + - "The sanitized JSON is returned via a file specified by the user." + - "Refer to the flags for details.", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - - // Initialize the logging - initLog(jww.Threshold(logLevel), logFile) - - // Retrieve emoji-mart file from URL - jww.INFO.Printf("Requesting file %s", requestURL) - resp, err := http.Get(requestURL) - if err != nil { - jww.FATAL.Panicf( - "Failed to retrieve emoji-mart JSON from URL: %+v", err) - } else if resp.StatusCode != http.StatusOK { - jww.FATAL.Panicf("Bad status: %s", resp.Status) - } - - jww.DEBUG.Printf("Received HTTP response: %+v", resp) - - // Read HTTP response into byte slice - var buf bytes.Buffer - _, err = buf.ReadFrom(resp.Body) - if err != nil { - jww.FATAL.Panicf("Failed to read from HTTP response: %+v", err) - } - if err = resp.Body.Close(); err != nil { - jww.FATAL.Panicf("Failed to close HTTP response: %+v", err) - } - emojiMartJson := buf.Bytes() - - jww.DEBUG.Printf("Read %d bytes of JSON file", len(emojiMartJson)) - - // Sanitize the JSON file - backendSet := NewSet() - sanitizedJSON, err := backendSet.SanitizeEmojiMartSet(emojiMartJson) - if err != nil { - jww.FATAL.Panicf("Failed to sanitize emoji-mart list: %+v", err) - } - - jww.DEBUG.Printf("Sanitised JSON file.") - - // Write sanitized JSON to file - err = utils.WriteFileDef(outputPath, sanitizedJSON) - if err != nil { - jww.FATAL.Panicf( - "Failed to write sanitized emojis to filepath %s: %+v", - outputPath, err) - } - - jww.INFO.Printf("Wrote sanitised JSON file to %s", outputPath) - }, -} - -// init is the initialization function for Cobra which defines flags. -func init() { - cmd.Flags().StringVarP(&requestURL, "url", "u", emojiMartUrl, - "URL to download emoji-mart JSON file.") - cmd.Flags().StringVarP(&outputPath, "output", "o", "output.json", - "Output JSON file path.") - cmd.Flags().StringVarP(&logFile, "log", "l", "-", - "Log output path. By default, logs are printed to stdout. "+ - "To disable logging, set this to empty (\"\").") - cmd.Flags().IntVarP(&logLevel, "logLevel", "v", 4, - "Verbosity level of logging. 0 = TRACE, 1 = DEBUG, 2 = INFO, "+ - "3 = WARN, 4 = ERROR, 5 = CRITICAL, 6 = FATAL") -} - -// initLog will enable JWW logging to the given log path with the given -// threshold. If log path is empty, then logging is not enabled. Panics if the -// log file cannot be opened or if the threshold is invalid. -func initLog(threshold jww.Threshold, logPath string) { - if logPath == "" { - // Do not enable logging if no log file is set - return - } else if logPath != "-" { - // Set the log file if stdout is not selected - - // Disable stdout output - jww.SetStdoutOutput(io.Discard) - - // Use log file - logOutput, err := - os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - panic(err) - } - jww.SetLogOutput(logOutput) - } - - if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - panic("Invalid log threshold: " + strconv.Itoa(int(threshold))) - } - - // Display microseconds if the threshold is set to TRACE or DEBUG - if threshold == jww.LevelTrace || threshold == jww.LevelDebug { - jww.SetFlags(log.LstdFlags | log.Lmicroseconds) - } - - // Enable logging - jww.SetStdoutThreshold(threshold) - jww.SetLogThreshold(threshold) - jww.INFO.Printf("Log level set to: %s", threshold) -} diff --git a/go.mod b/go.mod index 4c36aef04ba9f88e8164b9f5d2f71cdccc1cf8ee..b41a262ff540e218f685e01353622c2db8d45d38 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,11 @@ go 1.19 require ( github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 - github.com/forPelevin/gomoji v1.1.8 github.com/hack-pad/go-indexeddb v0.2.0 - github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/pkg/errors v0.9.1 - github.com/spf13/cobra v1.5.0 github.com/spf13/jwalterweatherman v1.1.0 - gitlab.com/elixxir/client/v4 v4.6.2-0.20230321163910-1bfc802768bd - gitlab.com/elixxir/crypto v0.0.7-0.20230214180106-72841fd1e426 + gitlab.com/elixxir/client/v4 v4.6.3 + gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32 gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4 @@ -27,34 +24,45 @@ require ( github.com/cloudflare/circl v1.2.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/gobwas/ws v1.1.0 // indirect github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-sqlite3 v1.14.15 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/pkg/profile v1.6.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/sethvargo/go-diceware v0.3.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.12.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/libphonenumber v1.2.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/blake3 v0.2.3 // indirect - gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f // indirect + gitlab.com/elixxir/bloomfilter v0.0.0-20230315224936-a4459418f300 // indirect gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b // indirect gitlab.com/elixxir/ekv v0.2.1 // indirect gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d // indirect @@ -70,6 +78,9 @@ require ( google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/sqlite v1.4.4 // indirect gorm.io/gorm v1.24.3 // indirect nhooyr.io/websocket v1.8.7 // indirect diff --git a/go.sum b/go.sum index f2a672c6f27595b828719df18feea34276488c1b..75aad62399baf6290ffb2634114aa930ac1ffd3a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,40 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -44,11 +79,16 @@ github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8 github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec= github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -74,18 +114,22 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/forPelevin/gomoji v1.1.8 h1:JElzDdt0TyiUlecy6PfITDL6eGvIaxqYH1V52zrd0qQ= -github.com/forPelevin/gomoji v1.1.8/go.mod h1:8+Z3KNGkdslmeGZBC3tCrwMrcPy5GRzAD+gL9NAwMXg= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -120,11 +164,20 @@ github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go. github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -144,12 +197,33 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -180,16 +254,22 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -206,6 +286,8 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -220,6 +302,7 @@ github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8 github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -232,6 +315,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -252,6 +337,8 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -272,8 +359,6 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= -github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 h1:lpwUgSIAfvJJ9sQ9BB//ZCjqzzFSuSg8mYOf+8L96+E= github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= @@ -296,6 +381,10 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= +github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -304,6 +393,9 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -330,8 +422,6 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= @@ -357,28 +447,42 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= @@ -393,20 +497,28 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40= -gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/client/v4 v4.6.2-0.20230321163910-1bfc802768bd h1:FixJkvw+IDL6aSIuuXGkzXokhaWKHsZq6dknVeG1vS4= -gitlab.com/elixxir/client/v4 v4.6.2-0.20230321163910-1bfc802768bd/go.mod h1:89NStRlnPDhYMRLXCX2dm4ZqsW+N2hYPNdTAqf3sFhE= +gitlab.com/elixxir/bloomfilter v0.0.0-20230315224936-a4459418f300 h1:oF3Pkf5NBb48KB89Q4sQXKQCIsWp1IVsqKWHWFsfBRc= +gitlab.com/elixxir/bloomfilter v0.0.0-20230315224936-a4459418f300/go.mod h1:1X8gRIAPDisS3W6Vtr/ymiUmZMJUIwDV1o5DEOo/pzw= +gitlab.com/elixxir/client/v4 v4.6.2-0.20230413171204-002612660098 h1:bdwXgEa0i9KpLiKQdhv6MEWAYLt3MsbNuIzFanVpWLY= +gitlab.com/elixxir/client/v4 v4.6.2-0.20230413171204-002612660098/go.mod h1:G+lN+LvQPGcm5BQnrhnqT1xiRIAzH3OffAM+5oI9SUg= +gitlab.com/elixxir/client/v4 v4.6.2-0.20230425190953-cd51598e9245 h1:pBwoSYD+BFIr5Wyc+PQhqm+fZGsRSNXCpF0z1cQQzK8= +gitlab.com/elixxir/client/v4 v4.6.2-0.20230425190953-cd51598e9245/go.mod h1:G+lN+LvQPGcm5BQnrhnqT1xiRIAzH3OffAM+5oI9SUg= +gitlab.com/elixxir/client/v4 v4.6.3 h1:oUsm5cn2Vnfqz+xwGYKrqFkPNN3sDAyp00EPGhUIA5E= +gitlab.com/elixxir/client/v4 v4.6.3/go.mod h1:G+lN+LvQPGcm5BQnrhnqT1xiRIAzH3OffAM+5oI9SUg= gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b h1:8AVK93UEs/aufoqtFgyMVt9gf0oJ8F4pA60ZvEVvG+s= gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q= -gitlab.com/elixxir/crypto v0.0.7-0.20230214180106-72841fd1e426 h1:O9Xz/ioc9NAj5k/QUsR0W4LCz2uVHawJF89yPTI7NXk= -gitlab.com/elixxir/crypto v0.0.7-0.20230214180106-72841fd1e426/go.mod h1:kv2nXmOnElsW8V3Yi1VqUUfaSv63mqp9w4ns3sxZO20= +gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32 h1:Had0F7rMPgJJ2BUZoFNgeJq33md9RpV15nvd08Uxdzc= +gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc= gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is= gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU= gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c h1:muG8ff95woeVVwQoJHCEclxBFB22lc7EixPylEkYDRU= @@ -429,7 +541,12 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -447,9 +564,12 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -457,6 +577,14 @@ golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -464,11 +592,23 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -481,25 +621,58 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -513,40 +686,69 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -554,30 +756,124 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -585,14 +881,23 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -617,6 +922,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= @@ -640,11 +947,18 @@ gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg= gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= src.agwa.name/tlshacks v0.0.0-20220518131152-d2c6f4e2b780 h1:iMW3HbLV3/OuK02FDW8qNC13i5o1uK079MGLH404rnQ= diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go index 68cc92d71f962ab4adc5d734f598561c95b006ea..205a5e0df78b3cb6125042a963e5de8026fd4e08 100644 --- a/indexedDb/impl/channels/callbacks.go +++ b/indexedDb/impl/channels/callbacks.go @@ -32,24 +32,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0} // manager handles the event model and the message callbacks, which is used to // send information between the event model and the main thread. type manager struct { - mh *worker.ThreadManager + wtm *worker.ThreadManager model channels.EventModel } // registerCallbacks registers all the reception callbacks to manage messages // from the main thread for the channels.EventModel. func (m *manager) registerCallbacks() { - m.mh.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB) - m.mh.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB) - m.mh.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB) - m.mh.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB) - m.mh.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB) - m.mh.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB) - m.mh.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB) - m.mh.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB) - m.mh.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB) - m.mh.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB) - m.mh.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB) + m.wtm.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB) + m.wtm.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB) + m.wtm.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB) + m.wtm.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB) + m.wtm.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB) + m.wtm.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB) + m.wtm.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB) + m.wtm.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB) + m.wtm.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB) + m.wtm.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB) + m.wtm.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB) } // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty @@ -71,12 +71,12 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { "failed to JSON unmarshal Cipher from main thread: %+v", err) } - m.model, err = NewWASMEventModel(msg.Path, encryption, - m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback, - m.storeDatabaseName, m.storeEncryptionStatus) + m.model, err = NewWASMEventModel(msg.DatabaseName, encryption, + m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback) if err != nil { return []byte(err.Error()), nil } + return []byte{}, nil } @@ -99,7 +99,7 @@ func (m *manager) messageReceivedCallback( } // Send it to the main thread - m.mh.SendMessage(wChannels.MessageReceivedCallbackTag, data) + m.wtm.SendMessage(wChannels.MessageReceivedCallbackTag, data) } // deletedMessageCallback sends calls to the channels.DeletedMessageCallback in @@ -107,7 +107,7 @@ func (m *manager) messageReceivedCallback( // // storeEncryptionStatus adhere to the channels.MessageReceivedCallback type. func (m *manager) deletedMessageCallback(messageID message.ID) { - m.mh.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal()) + m.wtm.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal()) } // mutedUserCallback sends calls to the channels.MutedUserCallback in the main @@ -129,89 +129,7 @@ func (m *manager) mutedUserCallback( } // Send it to the main thread - m.mh.SendMessage(wChannels.MutedUserCallbackTag, data) -} - -// storeDatabaseName sends the database name to the main thread and waits for -// the response. This function mocks the behavior of storage.StoreIndexedDb. -// -// storeDatabaseName adheres to the storeDatabaseNameFn type. -func (m *manager) storeDatabaseName(databaseName string) error { - // Register response callback with channel that will wait for the response - responseChan := make(chan []byte) - m.mh.RegisterCallback(wChannels.StoreDatabaseNameTag, - func(data []byte) ([]byte, error) { - responseChan <- data - return nil, nil - }) - - // Send encryption status to main thread - m.mh.SendMessage(wChannels.StoreDatabaseNameTag, []byte(databaseName)) - - // Wait for response - select { - case response := <-responseChan: - if len(response) > 0 { - return errors.New(string(response)) - } - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("[WW] Timed out after %s waiting for response "+ - "about storing the database name in local storage in the main "+ - "thread", worker.ResponseTimeout) - } - - return nil -} - -// storeEncryptionStatus sends the database name and encryption status to the -// main thread and waits for the response. If the value has not been previously -// saved, it returns the saves encryption status. This function mocks the -// behavior of storage.StoreIndexedDbEncryptionStatus. -// -// storeEncryptionStatus adheres to the storeEncryptionStatusFn type. -func (m *manager) storeEncryptionStatus( - databaseName string, encryption bool) (bool, error) { - // Package parameters for sending - msg := &wChannels.EncryptionStatusMessage{ - DatabaseName: databaseName, - EncryptionStatus: encryption, - } - data, err := json.Marshal(msg) - if err != nil { - return false, err - } - - // Register response callback with channel that will wait for the response - responseChan := make(chan []byte) - m.mh.RegisterCallback(wChannels.EncryptionStatusTag, - func(data []byte) ([]byte, error) { - responseChan <- data - return nil, nil - }) - - // Send encryption status to main thread - m.mh.SendMessage(wChannels.EncryptionStatusTag, data) - - // Wait for response - var response wChannels.EncryptionStatusReply - select { - case responseData := <-responseChan: - if err = json.Unmarshal(responseData, &response); err != nil { - return false, err - } - case <-time.After(worker.ResponseTimeout): - return false, errors.Errorf("timed out after %s waiting for "+ - "response about the database encryption status from local "+ - "storage in the main thread", worker.ResponseTimeout) - } - - // If the response contain an error, return it - if response.Error != "" { - return false, errors.New(response.Error) - } - - // Return the encryption status - return response.EncryptionStatus, nil + m.wtm.SendMessage(wChannels.MutedUserCallbackTag, data) } // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil; @@ -258,7 +176,7 @@ func (m *manager) receiveMessageCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil } @@ -280,7 +198,7 @@ func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil } @@ -302,7 +220,7 @@ func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil } @@ -340,8 +258,12 @@ func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) { status = &msg.Status } - m.model.UpdateFromUUID( + err = m.model.UpdateFromUUID( msg.UUID, messageID, timestamp, round, pinned, hidden, status) + if err != nil { + return []byte(err.Error()), nil + } + return nil, nil } @@ -374,15 +296,21 @@ func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) { status = &msg.Status } - uuid := m.model.UpdateFromMessageID( + var ue wChannels.UuidError + uuid, err := m.model.UpdateFromMessageID( msg.MessageID, timestamp, round, pinned, hidden, status) + if err != nil { + ue.Error = []byte(err.Error()) + } else { + ue.UUID = uuid + } - uuidData, err := json.Marshal(uuid) + data, err = json.Marshal(ue) if err != nil { - return nil, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return nil, errors.Errorf("failed to JSON marshal %T: %+v", ue, err) } - return uuidData, nil + return data, nil } // getMessageCB is the callback for wasmModel.GetMessage. Returns JSON diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js index 9e69bdd70eddebc9f82b23d04d823221ad3c1622..c109cba89735425f23c0d4d436532a3e3eb9a852 100644 --- a/indexedDb/impl/channels/channelsIndexedDbWorker.js +++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js @@ -7,11 +7,15 @@ importScripts('wasm_exec.js'); +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + const go = new Go(); const binPath = 'xxdk-channelsIndexedDkWorker.wasm' -WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); - LogLevel(1); + await isReady; }).catch((err) => { console.error(err); }); \ No newline at end of file diff --git a/indexedDb/impl/channels/fileTransferImpl.go b/indexedDb/impl/channels/fileTransferImpl.go new file mode 100644 index 0000000000000000000000000000000000000000..d75dc78b6dddd5825d7c1b157dddadd75d62698d --- /dev/null +++ b/indexedDb/impl/channels/fileTransferImpl.go @@ -0,0 +1,147 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package main + +import ( + "encoding/json" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/v4/channels" + cft "gitlab.com/elixxir/client/v4/channelsFileTransfer" + "gitlab.com/elixxir/crypto/fileTransfer" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "gitlab.com/elixxir/xxdk-wasm/utils" + "strings" + "time" +) + +// ReceiveFile is called when a file upload or download beings. +// +// fileLink and fileData are nillable and may be updated based +// upon the UUID or file ID later. +// +// fileID is always unique to the fileData. fileLink is the JSON of +// channelsFileTransfer.FileLink. +// +// Returns any fatal errors. +func (w *wasmModel) ReceiveFile(fileID fileTransfer.ID, fileLink, + fileData []byte, timestamp time.Time, status cft.Status) error { + + newFile := &File{ + Id: fileID.Marshal(), + Data: fileData, + Link: fileLink, + Timestamp: timestamp, + Status: uint8(status), + } + return w.upsertFile(newFile) +} + +// UpdateFile is called when a file upload or download completes or changes. +// +// fileLink, fileData, timestamp, and status are all nillable and may be +// updated based upon the file ID at a later date. If a nil value is passed, +// then make no update. +// +// Returns an error if the file cannot be updated. It must return +// channels.NoMessageErr if the file does not exist. +func (w *wasmModel) UpdateFile(fileID fileTransfer.ID, fileLink, + fileData []byte, timestamp *time.Time, status *cft.Status) error { + parentErr := "[Channels indexedDB] failed to UpdateFile" + + // Get the File as it currently exists in storage + fileObj, err := impl.Get(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal())) + if err != nil { + if strings.Contains(err.Error(), impl.ErrDoesNotExist) { + return errors.WithMessage(channels.NoMessageErr, parentErr) + } + return errors.WithMessage(err, parentErr) + } + currentFile, err := valueToFile(fileObj) + if err != nil { + return errors.WithMessage(err, parentErr) + } + + // Update the fields if specified + if status != nil { + currentFile.Status = uint8(*status) + } + if timestamp != nil { + currentFile.Timestamp = *timestamp + } + if fileData != nil { + currentFile.Data = fileData + } + if fileLink != nil { + currentFile.Link = fileLink + } + + return w.upsertFile(currentFile) +} + +// upsertFile is a helper function that will update an existing File +// if File.Id is specified. Otherwise, it will perform an insert. +func (w *wasmModel) upsertFile(newFile *File) error { + newFileJson, err := json.Marshal(&newFile) + if err != nil { + return err + } + fileObj, err := utils.JsonToJS(newFileJson) + if err != nil { + return err + } + + _, err = impl.Put(w.db, fileStoreName, fileObj) + return err +} + +// GetFile returns the ModelFile containing the file data and download link +// for the given file ID. +// +// Returns an error if the file cannot be retrieved. It must return +// channels.NoMessageErr if the file does not exist. +func (w *wasmModel) GetFile(fileID fileTransfer.ID) ( + cft.ModelFile, error) { + fileObj, err := impl.Get(w.db, fileStoreName, + impl.EncodeBytes(fileID.Marshal())) + if err != nil { + if strings.Contains(err.Error(), impl.ErrDoesNotExist) { + return cft.ModelFile{}, channels.NoMessageErr + } + return cft.ModelFile{}, err + } + + resultFile, err := valueToFile(fileObj) + if err != nil { + return cft.ModelFile{}, err + } + + result := cft.ModelFile{ + ID: fileTransfer.NewID(resultFile.Data), + Link: resultFile.Link, + Data: resultFile.Data, + Timestamp: resultFile.Timestamp, + Status: cft.Status(resultFile.Status), + } + return result, nil +} + +// DeleteFile deletes the file with the given file ID. +// +// Returns fatal errors. It must return channels.NoMessageErr if the file +// does not exist. +func (w *wasmModel) DeleteFile(fileID fileTransfer.ID) error { + err := impl.Delete(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal())) + if err != nil { + if strings.Contains(err.Error(), impl.ErrDoesNotExist) { + return channels.NoMessageErr + } + } + return err +} diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go index 627dd313a36974be2ff40ead5b00ee9f0635bfd3..0976d263b00011b7ec4a6a5d6bfce38c2780beb0 100644 --- a/indexedDb/impl/channels/implementation.go +++ b/indexedDb/impl/channels/implementation.go @@ -13,6 +13,7 @@ import ( "crypto/ed25519" "encoding/json" "strconv" + "strings" "syscall/js" "time" @@ -69,7 +70,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { return } - _, err = impl.Put(w.db, channelsStoreName, channelObj) + _, err = impl.Put(w.db, channelStoreName, channelObj) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, "Unable to put Channel: %+v", err)) @@ -81,7 +82,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) { parentErr := errors.New("failed to LeaveChannel") // Delete the channel from storage - err := impl.Delete(w.db, channelsStoreName, js.ValueOf(channelID.String())) + err := impl.Delete(w.db, channelStoreName, js.ValueOf(channelID.String())) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, "Unable to delete Channel: %+v", err)) @@ -120,19 +121,19 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { "Unable to get Index: %+v", err) } - // Perform the operation + // Set up the operation keyRange, err := idb.NewKeyRangeOnly(impl.EncodeBytes(channelID.Marshal())) cursorRequest, err := index.OpenCursorRange(keyRange, idb.CursorNext) if err != nil { return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - ctx, cancel := impl.NewContext() - err = cursorRequest.Iter(ctx, + + // Perform the operation + err = impl.SendCursorRequest(cursorRequest, func(cursor *idb.CursorWithValue) error { _, err := cursor.Delete() return err }) - cancel() if err != nil { return errors.WithMessagef(parentErr, "Unable to delete Message data: %+v", err) @@ -253,10 +254,13 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, // messageID, timestamp, round, pinned, and hidden are all nillable and may be // updated based upon the UUID at a later date. If a nil value is passed, then // make no update. +// +// Returns an error if the message cannot be updated. It must return +// channels.NoMessageErr if the message does not exist. func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, - status *channels.SentStatus) { - parentErr := errors.New("failed to UpdateFromUUID") + status *channels.SentStatus) error { + parentErr := "failed to UpdateFromUUID" // Convert messageID to the key generated by json.Marshal key := js.ValueOf(uuid) @@ -264,24 +268,24 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, // Use the key to get the existing Message msgObj, err := impl.Get(w.db, messageStoreName, key) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get message: %+v", err)) - return + if strings.Contains(err.Error(), impl.ErrDoesNotExist) { + return errors.WithMessage(channels.NoMessageErr, parentErr) + } + return errors.WithMessage(err, parentErr) } currentMsg, err := valueToMessage(msgObj) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Failed to marshal Message: %+v", err)) - return + return errors.WithMessagef(err, + "%s Failed to marshal Message", parentErr) } _, err = w.updateMessage(currentMsg, messageID, timestamp, round, pinned, hidden, status) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to updateMessage: %+v", err)) + return errors.WithMessage(err, parentErr) } + return nil } // UpdateFromMessageID is called whenever a message with the message ID is @@ -293,33 +297,35 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, // timestamp, round, pinned, and hidden are all nillable and may be updated // based upon the UUID at a later date. If a nil value is passed, then make // no update. +// +// Returns an error if the message cannot be updated. It must return +// channels.NoMessageErr if the message does not exist. func (w *wasmModel) UpdateFromMessageID(messageID message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, - status *channels.SentStatus) uint64 { - parentErr := errors.New("failed to UpdateFromMessageID") + status *channels.SentStatus) (uint64, error) { + parentErr := "failed to UpdateFromMessageID" msgObj, err := impl.GetIndex(w.db, messageStoreName, messageStoreMessageIndex, impl.EncodeBytes(messageID.Marshal())) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Failed to get message by index: %+v", err)) - return 0 + if strings.Contains(err.Error(), impl.ErrDoesNotExist) { + return 0, errors.WithMessage(channels.NoMessageErr, parentErr) + } + return 0, errors.WithMessage(err, parentErr) } currentMsg, err := valueToMessage(msgObj) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Failed to marshal Message: %+v", err)) - return 0 + return 0, errors.WithMessagef(err, + "%s Failed to marshal Message", parentErr) } uuid, err := w.updateMessage(currentMsg, &messageID, timestamp, round, pinned, hidden, status) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to updateMessage: %+v", err)) + return 0, errors.WithMessage(err, parentErr) } - return uuid + return uuid, nil } // buildMessage is a private helper that converts typical [channels.EventModel] @@ -409,7 +415,8 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) { // Store message to database msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj) if err != nil { - return 0, errors.Errorf("Unable to put Message: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v\n%s", + err, newMessageJson) } uuid := msgIdObj.Int() @@ -499,9 +506,11 @@ func (w *wasmModel) MuteUser( // valueToMessage is a helper for converting js.Value to Message. func valueToMessage(msgObj js.Value) (*Message, error) { resultMsg := &Message{} - err := json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg) - if err != nil { - return nil, err - } - return resultMsg, nil + return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg) +} + +// valueToFile is a helper for converting js.Value to File. +func valueToFile(fileObj js.Value) (*File, error) { + resultFile := &File{} + return resultFile, json.Unmarshal([]byte(utils.JsToJson(fileObj)), resultFile) } diff --git a/indexedDb/impl/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go index 67aaa6514777d2419414d800041bc84c6d6bec53..4b22b8300f5549a5dc2f3902f7ed95277777555f 100644 --- a/indexedDb/impl/channels/implementation_test.go +++ b/indexedDb/impl/channels/implementation_test.go @@ -10,9 +10,13 @@ package main import ( + "bytes" "crypto/ed25519" "encoding/json" + "errors" "fmt" + cft "gitlab.com/elixxir/client/v4/channelsFileTransfer" + "gitlab.com/elixxir/crypto/fileTransfer" "os" "strconv" "testing" @@ -41,9 +45,78 @@ func TestMain(m *testing.M) { func dummyReceivedMessageCB(uint64, *id.ID, bool) {} func dummyDeletedMessageCB(message.ID) {} func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {} -func dummyStoreDatabaseName(string) error { return nil } -func dummyStoreEncryptionStatus(_ string, encryptionStatus bool) (bool, error) { - return encryptionStatus, nil + +// Happy path test for receiving, updating, getting, and deleting a File. +func TestWasmModel_ReceiveFile(t *testing.T) { + testString := "TestWasmModel_ReceiveFile" + m, err := newWASMModel(testString, nil, + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + if err != nil { + t.Fatal(err) + } + + testTs := time.Now() + testBytes := []byte(testString) + testStatus := cft.Downloading + + // Insert a test row + fId := fileTransfer.NewID(testBytes) + err = m.ReceiveFile(fId, testBytes, testBytes, testTs, testStatus) + if err != nil { + t.Fatal(err) + } + + // Attempt to get stored row + storedFile, err := m.GetFile(fId) + if err != nil { + t.Fatal(err) + } + // Spot check stored attribute + if !bytes.Equal(storedFile.Link, testBytes) { + t.Fatalf("Got unequal FileLink values") + } + + // Attempt to updated stored row + newTs := time.Now() + newBytes := []byte("test") + newStatus := cft.Complete + err = m.UpdateFile(fId, nil, newBytes, &newTs, &newStatus) + if err != nil { + t.Fatal(err) + } + + // Check that the update took + updatedFile, err := m.GetFile(fId) + if err != nil { + t.Fatal(err) + } + // Link should not have changed + if !bytes.Equal(updatedFile.Link, testBytes) { + t.Fatalf("Link should not have changed") + } + // Other attributes should have changed + if !bytes.Equal(updatedFile.Data, newBytes) { + t.Fatalf("Data should have updated") + } + if !updatedFile.Timestamp.Equal(newTs) { + t.Fatalf("TS should have updated, expected %s got %s", + newTs, updatedFile.Timestamp) + } + if updatedFile.Status != newStatus { + t.Fatalf("Status should have updated") + } + + // Delete the row + err = m.DeleteFile(fId) + if err != nil { + t.Fatal(err) + } + + // Check that the delete operation took and get provides the expected error + _, err = m.GetFile(fId) + if err == nil || !errors.Is(channels.NoMessageErr, err) { + t.Fatal(err) + } } // Happy path, insert message and look it up @@ -58,14 +131,13 @@ func TestWasmModel_GetMessage(t *testing.T) { if c != nil { cs = "_withCipher" } - testString := "TestWasmModel_msgIDLookup" + cs + testString := "TestWasmModel_GetMessage" + cs t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB, - dummyStoreDatabaseName, dummyStoreEncryptionStatus) + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) if err != nil { t.Fatal(err) } @@ -96,8 +168,7 @@ func TestWasmModel_DeleteMessage(t *testing.T) { testString := "TestWasmModel_DeleteMessage" testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) eventModel, err := newWASMModel(testString, nil, dummyReceivedMessageCB, - dummyDeletedMessageCB, dummyMutedUserCB, dummyStoreDatabaseName, - dummyStoreEncryptionStatus) + dummyDeletedMessageCB, dummyMutedUserCB) if err != nil { t.Fatal(err) } @@ -154,8 +225,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { testMsgId := message.DeriveChannelMessageID( &id.ID{1}, 0, []byte(testString)) eventModel, err2 := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB, - dummyStoreDatabaseName, dummyStoreEncryptionStatus) + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) if err2 != nil { t.Fatal(err) } @@ -223,8 +293,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() eventModel, err2 := newWASMModel("test", c, dummyReceivedMessageCB, - dummyDeletedMessageCB, dummyMutedUserCB, dummyStoreDatabaseName, - dummyStoreEncryptionStatus) + dummyDeletedMessageCB, dummyMutedUserCB) if err2 != nil { t.Fatal(err2) } @@ -243,7 +312,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { } eventModel.JoinChannel(testChannel) eventModel.JoinChannel(testChannel2) - results, err2 := impl.Dump(eventModel.db, channelsStoreName) + results, err2 := impl.Dump(eventModel.db, channelStoreName) if err2 != nil { t.Fatal(err2) } @@ -251,7 +320,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { t.Fatalf("Expected 2 channels to exist") } eventModel.LeaveChannel(testChannel.ReceptionID) - results, err = impl.Dump(eventModel.db, channelsStoreName) + results, err = impl.Dump(eventModel.db, channelStoreName) if err != nil { t.Fatal(err) } @@ -278,8 +347,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) { storage.GetLocalStorage().Clear() testString := "testHello" + cs eventModel, err2 := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB, - dummyStoreDatabaseName, dummyStoreEncryptionStatus) + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) if err2 != nil { t.Fatal(err2) } @@ -326,8 +394,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB, - dummyStoreDatabaseName, dummyStoreEncryptionStatus) + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) if err != nil { t.Fatal(err) } @@ -376,8 +443,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { totalMessages := 10 expectedMessages := 5 eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB, - dummyStoreDatabaseName, dummyStoreEncryptionStatus) + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) if err != nil { t.Fatal(err) } @@ -448,8 +514,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { storage.GetLocalStorage().Clear() testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i) eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB, - dummyStoreDatabaseName, dummyStoreEncryptionStatus) + dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) if err != nil { t.Fatal(err) } diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go index 45107fa2c437b82bce04a50fe71eebeaac0625ee..ecc16da447b0db684ca55031a26e6a3d19206320 100644 --- a/indexedDb/impl/channels/init.go +++ b/indexedDb/impl/channels/init.go @@ -13,7 +13,6 @@ import ( "syscall/js" "github.com/hack-pad/go-indexeddb/idb" - "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/channels" @@ -22,45 +21,26 @@ import ( wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" ) -const ( - // databaseSuffix is the suffix to be appended to the name of - // the database. - databaseSuffix = "_speakeasy" - - // currentVersion is the current version of the IndexDb - // runtime. Used for migration purposes. - currentVersion uint = 1 -) - -// storeDatabaseNameFn matches storage.StoreIndexedDb so that the data can be -// sent between the worker and main thread. -type storeDatabaseNameFn func(databaseName string) error - -// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so -// that the data can be sent between the worker and main thread. -type storeEncryptionStatusFn func( - databaseName string, encryptionStatus bool) (bool, error) +// currentVersion is the current version of the IndexedDb runtime. Used for +// migration purposes. +const currentVersion uint = 2 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. -// The name should be a base64 encoding of the users public key. -func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, +// The name should be a base64 encoding of the users public key. Returns the +// EventModel based on IndexedDb and the database name as reported by IndexedDb. +func NewWASMEventModel(databaseName string, encryption cryptoChannel.Cipher, messageReceivedCB wChannels.MessageReceivedCallback, deletedMessageCB wChannels.DeletedMessageCallback, - mutedUserCB wChannels.MutedUserCallback, - storeDatabaseName storeDatabaseNameFn, - storeEncryptionStatus storeEncryptionStatusFn) (channels.EventModel, error) { - databaseName := path + databaseSuffix + mutedUserCB wChannels.MutedUserCallback) (channels.EventModel, error) { return newWASMModel(databaseName, encryption, messageReceivedCB, - deletedMessageCB, mutedUserCB, storeDatabaseName, storeEncryptionStatus) + deletedMessageCB, mutedUserCB) } // newWASMModel creates the given [idb.Database] and returns a wasmModel. func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, messageReceivedCB wChannels.MessageReceivedCallback, deletedMessageCB wChannels.DeletedMessageCallback, - mutedUserCB wChannels.MutedUserCallback, - storeDatabaseName storeDatabaseNameFn, - storeEncryptionStatus storeEncryptionStatusFn) (*wasmModel, error) { + mutedUserCB wChannels.MutedUserCallback) (*wasmModel, error) { // Attempt to open database object ctx, cancel := impl.NewContext() defer cancel() @@ -82,6 +62,14 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, oldVersion = 1 } + if oldVersion == 1 && newVersion >= 2 { + err := v2Upgrade(db) + if err != nil { + return err + } + oldVersion = 2 + } + // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } return nil }) @@ -93,29 +81,8 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, db, err := openRequest.Await(ctx) if err != nil { return nil, err - } - - // Get the database name and save it to storage - if dbName, err2 := db.Name(); err2 != nil { - return nil, err2 - } else if err = storeDatabaseName(dbName); err != nil { - return nil, err - } - - // Save the encryption status to storage - encryptionStatus := encryption != nil - loadedEncryptionStatus, err := - storeEncryptionStatus(databaseName, encryptionStatus) - if err != nil { - return nil, err - } - - // Verify encryption status does not change - if encryptionStatus != loadedEncryptionStatus { - return nil, errors.New( - "Cannot load database with different encryption status.") - } else if !encryptionStatus { - jww.WARN.Printf("IndexedDb encryption disabled!") + } else if ctx.Err() != nil { + return nil, ctx.Err() } wrapper := &wasmModel{ @@ -125,7 +92,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, deletedMessageCB: deletedMessageCB, mutedUserCB: mutedUserCB, } - return wrapper, nil } @@ -179,10 +145,22 @@ func v1Upgrade(db *idb.Database) error { } // Build Channel ObjectStore - _, err = db.CreateObjectStore(channelsStoreName, storeOpts) + _, err = db.CreateObjectStore(channelStoreName, storeOpts) if err != nil { return err } return nil } + +// v1Upgrade performs the v1 -> v2 database upgrade. +// +// This can never be changed without permanently breaking backwards +// compatibility. +func v2Upgrade(db *idb.Database) error { + _, err := db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(pkeyName), + AutoIncrement: false, + }) + return err +} diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go index 5290d0c89b6eedf92a1571aea04ff5b7fbfdc668..b84f13dc205ecd8bfe2466ea9ecb827770f31754 100644 --- a/indexedDb/impl/channels/main.go +++ b/indexedDb/impl/channels/main.go @@ -11,32 +11,70 @@ package main import ( "fmt" + "os" + "syscall/js" + + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" - "gitlab.com/elixxir/xxdk-wasm/wasm" "gitlab.com/elixxir/xxdk-wasm/worker" - "syscall/js" ) // SEMVER is the current semantic version of the xxDK channels web worker. const SEMVER = "0.1.0" -func init() { - // Set up Javascript console listener set at level INFO - ll := logging.NewJsConsoleLogListener(jww.LevelInfo) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + channelsCmd.SetArgs(os.Args) + + err := channelsCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } } -func main() { - jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") +var channelsCmd = &cobra.Command{ + Use: "channelsIndexedDbWorker", + Short: "IndexedDb database for channels.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } - js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) + jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) - m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)} - m.registerCallbacks() - m.mh.SignalReady() - <-make(chan bool) - fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") + m := &manager{ + wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true), + } + m.registerCallbacks() + m.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/channels/model.go b/indexedDb/impl/channels/model.go index d1dcee88b3efd5f4a877685366154caefaa46a82..e5d3e00aa5209985de60a77f5330a53208b1af88 100644 --- a/indexedDb/impl/channels/model.go +++ b/indexedDb/impl/channels/model.go @@ -18,8 +18,9 @@ const ( pkeyName = "id" // Text representation of the names of the various [idb.ObjectStore]. - messageStoreName = "messages" - channelsStoreName = "channels" + messageStoreName = "messages" + channelStoreName = "channels" + fileStoreName = "files" // Message index names. messageStoreMessageIndex = "message_id_index" @@ -73,3 +74,21 @@ type Channel struct { Name string `json:"name"` Description string `json:"description"` } + +// File defines the IndexedDb representation of a single File. +type File struct { + // Id is a unique identifier for a given File. + Id []byte `json:"id"` // Matches pkeyName + + // Data stores the actual contents of the File. + Data []byte `json:"data"` + + // Link contains all the information needed to download the file data. + Link []byte `json:"link"` + + // Timestamp is the last time the file data, link, or status was modified. + Timestamp time.Time `json:"timestamp"` + + // Status of the file in the event model. + Status uint8 `json:"status"` +} diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go index b477df50c01c0c9dda3195be7f9706d8070cd930..380f04d9953fdda65c4b0d80a66e7b40a7edc2b1 100644 --- a/indexedDb/impl/dm/callbacks.go +++ b/indexedDb/impl/dm/callbacks.go @@ -12,10 +12,10 @@ package main import ( "crypto/ed25519" "encoding/json" - "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/dm" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/fastRNG" @@ -29,24 +29,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0} // manager handles the event model and the message callbacks, which is used to // send information between the event model and the main thread. type manager struct { - mh *worker.ThreadManager + wtm *worker.ThreadManager model dm.EventModel } // registerCallbacks registers all the reception callbacks to manage messages // from the main thread for the channels.EventModel. func (m *manager) registerCallbacks() { - m.mh.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB) - m.mh.RegisterCallback(wDm.ReceiveTag, m.receiveCB) - m.mh.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB) - m.mh.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB) - m.mh.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB) - m.mh.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB) - - m.mh.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB) - m.mh.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB) - m.mh.RegisterCallback(wDm.GetConversationTag, m.getConversationCB) - m.mh.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB) + m.wtm.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB) + m.wtm.RegisterCallback(wDm.ReceiveTag, m.receiveCB) + m.wtm.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB) + m.wtm.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB) + m.wtm.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB) + m.wtm.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB) + + m.wtm.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB) + m.wtm.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB) + m.wtm.RegisterCallback(wDm.GetConversationTag, m.getConversationCB) + m.wtm.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB) } // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty @@ -68,11 +68,12 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { "cipher from main thread: %+v", err) } - m.model, err = NewWASMEventModel(msg.Path, encryption, - m.messageReceivedCallback, m.storeDatabaseName, m.storeEncryptionStatus) + m.model, err = NewWASMEventModel( + msg.DatabaseName, encryption, m.messageReceivedCallback) if err != nil { return []byte(err.Error()), nil } + return []byte{}, nil } @@ -97,89 +98,7 @@ func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey, } // Send it to the main thread - m.mh.SendMessage(wDm.MessageReceivedCallbackTag, data) -} - -// storeDatabaseName sends the database name to the main thread and waits for -// the response. This function mocks the behavior of storage.StoreIndexedDb. -// -// storeDatabaseName adheres to the storeDatabaseNameFn type. -func (m *manager) storeDatabaseName(databaseName string) error { - // Register response callback with channel that will wait for the response - responseChan := make(chan []byte) - m.mh.RegisterCallback(wDm.StoreDatabaseNameTag, - func(data []byte) ([]byte, error) { - responseChan <- data - return nil, nil - }) - - // Send encryption status to main thread - m.mh.SendMessage(wDm.StoreDatabaseNameTag, []byte(databaseName)) - - // Wait for response - select { - case response := <-responseChan: - if len(response) > 0 { - return errors.New(string(response)) - } - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("[WW] Timed out after %s waiting for response "+ - "about storing the database name in local storage in the main "+ - "thread", worker.ResponseTimeout) - } - - return nil -} - -// storeEncryptionStatus sends the database name and encryption status to the -// main thread and waits for the response. If the value has not been previously -// saved, it returns the saves encryption status. This function mocks the -// behavior of storage.StoreIndexedDbEncryptionStatus. -// -// storeEncryptionStatus adheres to the storeEncryptionStatusFn type. -func (m *manager) storeEncryptionStatus( - databaseName string, encryption bool) (bool, error) { - // Package parameters for sending - msg := &wDm.EncryptionStatusMessage{ - DatabaseName: databaseName, - EncryptionStatus: encryption, - } - data, err := json.Marshal(msg) - if err != nil { - return false, err - } - - // Register response callback with channel that will wait for the response - responseChan := make(chan []byte) - m.mh.RegisterCallback(wDm.EncryptionStatusTag, - func(data []byte) ([]byte, error) { - responseChan <- data - return nil, nil - }) - - // Send encryption status to main thread - m.mh.SendMessage(wDm.EncryptionStatusTag, data) - - // Wait for response - var response wDm.EncryptionStatusReply - select { - case responseData := <-responseChan: - if err = json.Unmarshal(responseData, &response); err != nil { - return false, err - } - case <-time.After(worker.ResponseTimeout): - return false, errors.Errorf("timed out after %s waiting for "+ - "response about the database encryption status from local "+ - "storage in the main thread", worker.ResponseTimeout) - } - - // If the response contain an error, return it - if response.Error != "" { - return false, errors.New(response.Error) - } - - // Return the encryption status - return response.EncryptionStatus, nil + m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data) } // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error @@ -198,7 +117,7 @@ func (m *manager) receiveCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil } @@ -219,7 +138,7 @@ func (m *manager) receiveTextCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return []byte{}, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return []byte{}, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil @@ -241,7 +160,7 @@ func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil @@ -263,7 +182,7 @@ func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { uuidData, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err) + return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) } return uuidData, nil diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js index e199a7bb812b9ff119b7f130f41d3bb555247302..8a5fdbf8ad9a02967b408985a0219647003eaf7e 100644 --- a/indexedDb/impl/dm/dmIndexedDbWorker.js +++ b/indexedDb/impl/dm/dmIndexedDbWorker.js @@ -7,11 +7,15 @@ importScripts('wasm_exec.js'); +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + const go = new Go(); const binPath = 'xxdk-dmIndexedDkWorker.wasm' -WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); - LogLevel(1); + await isReady; }).catch((err) => { console.error(err); }); \ No newline at end of file diff --git a/indexedDb/impl/dm/implementation.go b/indexedDb/impl/dm/implementation.go index e4c314a09b5c639a0012355c3acb1f3fc59ab6b7..1c8e18050d8c36c32ede354f9afaa9a6af1993db 100644 --- a/indexedDb/impl/dm/implementation.go +++ b/indexedDb/impl/dm/implementation.go @@ -30,8 +30,7 @@ import ( "gitlab.com/xx_network/primitives/id" ) -// wasmModel implements dm.EventModel interface, which uses the channels system -// passed an object that adheres to in order to get events on the channel. +// wasmModel implements dm.EventModel interface backed by IndexedDb. // NOTE: This model is NOT thread safe - it is the responsibility of the // caller to ensure that its methods are called sequentially. type wasmModel struct { @@ -42,14 +41,15 @@ type wasmModel struct { // upsertConversation is used for joining or updating a Conversation. func (w *wasmModel) upsertConversation(nickname string, - pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, blocked bool) error { + pubKey ed25519.PublicKey, partnerToken uint32, codeset uint8, + blocked bool) error { parentErr := errors.New("[DM indexedDB] failed to upsertConversation") // Build object newConvo := Conversation{ Pubkey: pubKey, Nickname: nickname, - Token: dmToken, + Token: partnerToken, CodesetVersion: codeset, Blocked: blocked, } @@ -214,11 +214,13 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, // receiveWrapper is a higher-level wrapper of upsertMessage. func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, nickname, - data string, partnerKey, senderKey ed25519.PublicKey, dmToken uint32, codeset uint8, + data string, partnerKey, senderKey ed25519.PublicKey, partnerToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, mType dm.MessageType, status dm.Status) (uint64, error) { - // Keep track of whether Conversation was altered - conversationUpdated := false + // Keep track of whether a Conversation was altered + var convoToUpdate *Conversation + + // Determine whether Conversation needs to be created result, err := w.getConversation(partnerKey) if err != nil { if !strings.Contains(err.Error(), impl.ErrDoesNotExist) { @@ -227,12 +229,14 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n // If there is no extant Conversation, create one. jww.DEBUG.Printf( "[DM indexedDB] Joining conversation with %s", nickname) - err = w.upsertConversation(nickname, partnerKey, dmToken, - codeset, false) - if err != nil { - return 0, err + + convoToUpdate = &Conversation{ + Pubkey: partnerKey, + Nickname: nickname, + Token: partnerToken, + CodesetVersion: codeset, + Blocked: false, } - conversationUpdated = true } } else { jww.DEBUG.Printf( @@ -245,12 +249,28 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n jww.DEBUG.Printf( "[DM indexedDB] Updating from nickname %s to %s", result.Nickname, nickname) - err = w.upsertConversation(nickname, result.Pubkey, result.Token, - result.CodesetVersion, result.Blocked) - if err != nil { - return 0, err - } - conversationUpdated = true + convoToUpdate = result + convoToUpdate.Nickname = nickname + } + + // Fix conversation if dmToken is altered + dmTokenChanged := result.Token != partnerToken + if isFromPartner && dmTokenChanged { + jww.WARN.Printf( + "[DM indexedDB] Updating from dmToken %d to %d", + result.Token, partnerToken) + convoToUpdate = result + convoToUpdate.Token = partnerToken + } + } + + // Update the conversation in storage, if needed + conversationUpdated := convoToUpdate != nil + if conversationUpdated { + err = w.upsertConversation(convoToUpdate.Nickname, convoToUpdate.Pubkey, + convoToUpdate.Token, convoToUpdate.CodesetVersion, convoToUpdate.Blocked) + if err != nil { + return 0, err } } @@ -298,7 +318,8 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) { // Store message to database msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj) if err != nil { - return 0, errors.Errorf("Unable to put Message: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v\n%s", + err, newMessageJson) } uuid := msgIdObj.Int() diff --git a/indexedDb/impl/dm/implementation_test.go b/indexedDb/impl/dm/implementation_test.go index 5936c83ba3b6537c350cec3a852f489a3911e913..8e05e6af5fff85af79ea0f15be9dc1e2f5a77c12 100644 --- a/indexedDb/impl/dm/implementation_test.go +++ b/indexedDb/impl/dm/implementation_test.go @@ -10,27 +10,123 @@ package main import ( + "bytes" "crypto/ed25519" - jww "github.com/spf13/jwalterweatherman" + "encoding/json" + "fmt" + "gitlab.com/elixxir/client/v4/cmix/rounds" + "gitlab.com/elixxir/client/v4/dm" + "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/xx_network/primitives/id" "os" + "syscall/js" "testing" + "time" + + jww "github.com/spf13/jwalterweatherman" ) func dummyReceivedMessageCB(uint64, ed25519.PublicKey, bool, bool) {} -func dummyStoreDatabaseName(string) error { return nil } -func dummyStoreEncryptionStatus(_ string, encryptionStatus bool) (bool, error) { - return encryptionStatus, nil -} func TestMain(m *testing.M) { jww.SetStdoutThreshold(jww.LevelDebug) os.Exit(m.Run()) } +// Test simple receive of a new message for a new conversation. +func TestImpl_Receive(t *testing.T) { + m, err := newWASMModel("TestImpl_Receive", nil, + dummyReceivedMessageCB) + if err != nil { + t.Fatal(err.Error()) + } + + testString := "test" + testBytes := []byte(testString) + partnerPubKey := ed25519.PublicKey(testBytes) + testRound := id.Round(10) + + // Can use ChannelMessageID for ease, doesn't matter here + testMsgId := message.DeriveChannelMessageID(&id.ID{1}, uint64(testRound), testBytes) + + // Receive a test message + uuid := m.Receive(testMsgId, testString, testBytes, + partnerPubKey, partnerPubKey, 0, 0, time.Now(), + rounds.Round{ID: testRound}, dm.TextType, dm.Received) + if uuid == 0 { + t.Fatalf("Expected non-zero message uuid") + } + jww.DEBUG.Printf("Received test message: %d", uuid) + + // First, we expect a conversation to be created + testConvo := m.GetConversation(partnerPubKey) + if testConvo == nil { + t.Fatalf("Expected conversation to be created") + } + // Spot check a conversation attribute + if testConvo.Nickname != testString { + t.Fatalf("Expected conversation nickname %s, got %s", + testString, testConvo.Nickname) + } + + // Next, we expect the message to be created + testMessageObj, err := impl.Get(m.db, messageStoreName, js.ValueOf(uuid)) + if err != nil { + t.Fatalf(err.Error()) + } + testMessage := &Message{} + err = json.Unmarshal([]byte(utils.JsToJson(testMessageObj)), testMessage) + if err != nil { + t.Fatalf(err.Error()) + } + // Spot check a message attribute + if !bytes.Equal(testMessage.SenderPubKey, partnerPubKey) { + t.Fatalf("Expected message attibutes to match, expected %v got %v", + partnerPubKey, testMessage.SenderPubKey) + } +} + +// Test happy path. Insert some conversations and check they exist. +func TestImpl_GetConversations(t *testing.T) { + m, err := newWASMModel("TestImpl_GetConversations", nil, + dummyReceivedMessageCB) + if err != nil { + t.Fatal(err.Error()) + } + numTestConvo := 10 + + // Insert a test convo + for i := 0; i < numTestConvo; i++ { + testBytes := []byte(fmt.Sprintf("%d", i)) + testPubKey := ed25519.PublicKey(testBytes) + err = m.upsertConversation("test", testPubKey, + uint32(i), uint8(i), false) + if err != nil { + t.Fatal(err.Error()) + } + } + + results := m.GetConversations() + if len(results) != numTestConvo { + t.Fatalf("Expected %d convos, got %d", numTestConvo, len(results)) + } + + for i, convo := range results { + if convo.Token != uint32(i) { + t.Fatalf("Expected %d convo token, got %d", i, convo.Token) + } + if convo.CodesetVersion != uint8(i) { + t.Fatalf("Expected %d convo codeset, got %d", + i, convo.CodesetVersion) + } + } +} + // Test happy path toggling between blocked/unblocked in a Conversation. func TestWasmModel_BlockSender(t *testing.T) { - m, err := newWASMModel("test", nil, - dummyReceivedMessageCB, dummyStoreDatabaseName, dummyStoreEncryptionStatus) + m, err := newWASMModel("TestWasmModel_BlockSender", nil, dummyReceivedMessageCB) if err != nil { t.Fatal(err.Error()) } diff --git a/indexedDb/impl/dm/init.go b/indexedDb/impl/dm/init.go index 98c9e85a8278e9ed67bce4a960df99431819648d..f99f4ef3e2d7f46f9a5672c8ea998c4afdbe1a77 100644 --- a/indexedDb/impl/dm/init.go +++ b/indexedDb/impl/dm/init.go @@ -11,25 +11,19 @@ package main import ( "crypto/ed25519" - "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" "syscall/js" "github.com/hack-pad/go-indexeddb/idb" - "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/dm" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" ) -const ( - // databaseSuffix is the suffix to be appended to the name of - // the database. - databaseSuffix = "_speakeasy_dm" - - // currentVersion is the current version of the IndexDb - // runtime. Used for migration purposes. - currentVersion uint = 1 -) +// currentVersion is the current version of the IndexedDb runtime. Used for +// migration purposes. +const currentVersion uint = 1 // MessageReceivedCallback is called any time a message is received or updated. // @@ -38,29 +32,17 @@ const ( type MessageReceivedCallback func( uuid uint64, pubKey ed25519.PublicKey, messageUpdate, conversationUpdate bool) -// storeDatabaseNameFn matches storage.StoreIndexedDb so that the data can be -// sent between the worker and main thread. -type storeDatabaseNameFn func(databaseName string) error - -// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so -// that the data can be sent between the worker and main thread. -type storeEncryptionStatusFn func( - databaseName string, encryptionStatus bool) (bool, error) - // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. -// The name should be a base64 encoding of the users public key. -func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback, storeDatabaseName storeDatabaseNameFn, - storeEncryptionStatus storeEncryptionStatusFn) (dm.EventModel, error) { - databaseName := path + databaseSuffix - return newWASMModel( - databaseName, encryption, cb, storeDatabaseName, storeEncryptionStatus) +// The name should be a base64 encoding of the users public key. Returns the +// EventModel based on IndexedDb and the database name as reported by IndexedDb. +func NewWASMEventModel(databaseName string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) (dm.EventModel, error) { + return newWASMModel(databaseName, encryption, cb) } // newWASMModel creates the given [idb.Database] and returns a wasmModel. func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback, storeDatabaseName storeDatabaseNameFn, - storeEncryptionStatus storeEncryptionStatusFn) (*wasmModel, error) { + cb MessageReceivedCallback) (*wasmModel, error) { // Attempt to open database object ctx, cancel := impl.NewContext() defer cancel() @@ -93,33 +75,11 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, db, err := openRequest.Await(ctx) if err != nil { return nil, err - } - - // Get the database name and save it to storage - if dbName, err2 := db.Name(); err2 != nil { - return nil, err2 - } else if err = storeDatabaseName(dbName); err != nil { - return nil, err - } - - // Save the encryption status to storage - encryptionStatus := encryption != nil - loadedEncryptionStatus, err := - storeEncryptionStatus(databaseName, encryptionStatus) - if err != nil { - return nil, err - } - - // Verify encryption status does not change - if encryptionStatus != loadedEncryptionStatus { - return nil, errors.New( - "Cannot load database with different encryption status.") - } else if !encryptionStatus { - jww.WARN.Printf("IndexedDb encryption disabled!") + } else if ctx.Err() != nil { + return nil, ctx.Err() } wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} - return wrapper, nil } diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go index 20b20a0856c78ac753798c9fd4a692e4a88e4852..96fae8e6fbdbce3767739891d4d7ea466e08149c 100644 --- a/indexedDb/impl/dm/main.go +++ b/indexedDb/impl/dm/main.go @@ -11,32 +11,71 @@ package main import ( "fmt" + "os" + "syscall/js" + + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" - "gitlab.com/elixxir/xxdk-wasm/wasm" "gitlab.com/elixxir/xxdk-wasm/worker" - "syscall/js" ) // SEMVER is the current semantic version of the xxDK DM web worker. const SEMVER = "0.1.0" -func init() { - // Set up Javascript console listener set at level INFO - ll := logging.NewJsConsoleLogListener(jww.LevelInfo) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + dmCmd.SetArgs(os.Args) + + err := dmCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } } -func main() { - jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") +var dmCmd = &cobra.Command{ + Use: "dmIndexedDbWorker", + Short: "IndexedDb database for DMs.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf( + "Failed to intialize logging in DM indexedDb worker: %+v", err) + os.Exit(1) + } - js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) + jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) - m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)} - m.registerCallbacks() - m.mh.SignalReady() - <-make(chan bool) - fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") + m := &manager{ + wtm: worker.NewThreadManager("DmIndexedDbWorker", true), + } + m.registerCallbacks() + m.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/utils.go b/indexedDb/impl/utils.go index 3765cfce5e01698ca625844b5fe495906985366a..b657240b7e7018a031365ffc8514c3fe54141471 100644 --- a/indexedDb/impl/utils.go +++ b/indexedDb/impl/utils.go @@ -30,9 +30,6 @@ const ( // ErrDoesNotExist is an error string for got undefined on Get operations. ErrDoesNotExist = "result is undefined" - - // ErrUniqueConstraint is an error string for failed uniqueness inserts. - ErrUniqueConstraint = "at least one key does not satisfy the uniqueness requirements" ) // NewContext builds a context for indexedDb operations. @@ -45,6 +42,31 @@ func EncodeBytes(input []byte) js.Value { return js.ValueOf(base64.StdEncoding.EncodeToString(input)) } +// SendRequest is a wrapper for the request.Await() method providing a timeout. +func SendRequest(request *idb.Request) (js.Value, error) { + ctx, cancel := NewContext() + defer cancel() + result, err := request.Await(ctx) + if err != nil { + return js.Undefined(), err + } else if ctx.Err() != nil { + return js.Undefined(), ctx.Err() + } + return result, nil +} + +// SendCursorRequest is a wrapper for the cursorRequest.Await() method providing a timeout. +func SendCursorRequest(cur *idb.CursorWithValueRequest, + iterFunc func(cursor *idb.CursorWithValue) error) error { + ctx, cancel := NewContext() + defer cancel() + err := cur.Iter(ctx, iterFunc) + if ctx.Err() != nil { + return ctx.Err() + } + return err +} + // Get is a generic helper for getting values from the given [idb.ObjectStore]. // Only usable by primary key. func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) { @@ -62,17 +84,15 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro "Unable to get ObjectStore: %+v", err) } - // Perform the operation + // Set up the operation getRequest, err := store.Get(key) if err != nil { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to Get from ObjectStore: %+v", err) } - // Wait for the operation to return - ctx, cancel := NewContext() - resultObj, err := getRequest.Await(ctx) - cancel() + // Perform the operation + resultObj, err := SendRequest(getRequest) if err != nil { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) @@ -103,14 +123,15 @@ func GetAll(db *idb.Database, objectStoreName string) ([]js.Value, error) { "Unable to get ObjectStore: %+v", err) } - // Perform the operation - result := make([]js.Value, 0) + // Set up the operation cursorRequest, err := store.OpenCursor(idb.CursorNext) if err != nil { return nil, errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - ctx, cancel := NewContext() - err = cursorRequest.Iter(ctx, + result := make([]js.Value, 0) + + // Perform the operation + err = SendCursorRequest(cursorRequest, func(cursor *idb.CursorWithValue) error { row, err := cursor.Value() if err != nil { @@ -119,7 +140,6 @@ func GetAll(db *idb.Database, objectStoreName string) ([]js.Value, error) { result = append(result, row) return nil }) - cancel() if err != nil { return nil, errors.WithMessagef(parentErr, err.Error()) } @@ -150,17 +170,15 @@ func GetIndex(db *idb.Database, objectStoreName, "Unable to get Index: %+v", err) } - // Perform the operation + // Set up the operation getRequest, err := idx.Get(key) if err != nil { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to Get from ObjectStore: %+v", err) } - // Wait for the operation to return - ctx, cancel := NewContext() - resultObj, err := getRequest.Await(ctx) - cancel() + // Perform the operation + resultObj, err := SendRequest(getRequest) if err != nil { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) @@ -189,23 +207,21 @@ func Put(db *idb.Database, objectStoreName string, value js.Value) (js.Value, er return js.Undefined(), errors.Errorf("Unable to get ObjectStore: %+v", err) } - // Perform the operation + // Set up the operation request, err := store.Put(value) if err != nil { return js.Undefined(), errors.Errorf("Unable to Put: %+v", err) } - // Wait for the operation to return - ctx, cancel := NewContext() - result, err := request.Await(ctx) - cancel() + // Perform the operation + resultObj, err := SendRequest(request) if err != nil { return js.Undefined(), errors.Errorf("Putting value failed: %+v\n%s", err, utils.JsToJson(value)) } jww.DEBUG.Printf("Successfully put value in %s: %s", objectStoreName, utils.JsToJson(value)) - return result, nil + return resultObj, nil } // Delete is a generic helper for removing values from the given @@ -226,16 +242,14 @@ func Delete(db *idb.Database, objectStoreName string, key js.Value) error { } // Perform the operation - _, err = store.Delete(key) + deleteRequest, err := store.Delete(key) if err != nil { return errors.WithMessagef(parentErr, "Unable to Delete from ObjectStore: %+v", err) } - // Wait for the operation to return - ctx, cancel := NewContext() - err = txn.Await(ctx) - cancel() + // Perform the operation + _, err = SendRequest(deleteRequest.Request) if err != nil { return errors.WithMessagef(parentErr, "Unable to Delete from ObjectStore: %+v", err) @@ -282,17 +296,18 @@ func Dump(db *idb.Database, objectStoreName string) ([]string, error) { return nil, errors.WithMessagef(parentErr, "Unable to get ObjectStore: %+v", err) } + + // Set up the operation cursorRequest, err := store.OpenCursor(idb.CursorNext) if err != nil { return nil, errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - - // Run the query jww.DEBUG.Printf("%s values:", objectStoreName) results := make([]string, 0) - ctx, cancel := NewContext() - err = cursorRequest.Iter(ctx, + + // Perform the operation + err = SendCursorRequest(cursorRequest, func(cursor *idb.CursorWithValue) error { value, err := cursor.Value() if err != nil { @@ -303,7 +318,6 @@ func Dump(db *idb.Database, objectStoreName string) ([]string, error) { jww.DEBUG.Printf("- %v", valueStr) return nil }) - cancel() if err != nil { return nil, errors.WithMessagef(parentErr, "Unable to dump ObjectStore: %+v", err) diff --git a/indexedDb/impl/utils_test.go b/indexedDb/impl/utils_test.go index 6b6da6ff36ee8be5412146443093ee7d85c07c4f..ba6700356fb5e434ab5e8850373c9bb6a2775116 100644 --- a/indexedDb/impl/utils_test.go +++ b/indexedDb/impl/utils_test.go @@ -11,9 +11,11 @@ package impl import ( "github.com/hack-pad/go-indexeddb/idb" + jww "github.com/spf13/jwalterweatherman" "strings" "syscall/js" "testing" + "time" ) // Error path: Tests that Get returns an error when trying to get a message that @@ -40,6 +42,20 @@ func TestGetIndex_NoMessageError(t *testing.T) { } } +// Test simple put on empty DB is successful +func TestPut(t *testing.T) { + objectStoreName := "messages" + db := newTestDB(objectStoreName, "index", t) + testValue := js.ValueOf(make(map[string]interface{})) + result, err := Put(db, objectStoreName, testValue) + if err != nil { + t.Fatalf(err.Error()) + } + if !result.Equal(js.ValueOf(1)) { + t.Fatalf("Failed to generate autoincremented key") + } +} + // newTestDB creates a new idb.Database for testing. func newTestDB(name, index string, t *testing.T) *idb.Database { // Attempt to open database object @@ -78,3 +94,52 @@ func newTestDB(name, index string, t *testing.T) *idb.Database { return db } + +// TestBenchmark ensures IndexedDb can take at least n operations per second. +func TestBenchmark(t *testing.T) { + jww.SetStdoutThreshold(jww.LevelInfo) + benchmarkDb(50, t) +} + +// benchmarkDb sends n operations to IndexedDb and prints errors. +func benchmarkDb(n int, t *testing.T) { + jww.INFO.Printf("Benchmarking IndexedDb: %d total.", n) + + objectStoreName := "test" + testValue := js.ValueOf(make(map[string]interface{})) + db := newTestDB(objectStoreName, "index", t) + + type metric struct { + didSucceed bool + duration time.Duration + } + done := make(chan metric) + + // Spawn n operations at the same time + startTime := time.Now() + for i := 0; i < n; i++ { + go func() { + opStart := time.Now() + _, err := Put(db, objectStoreName, testValue) + done <- metric{ + didSucceed: err == nil, + duration: time.Since(opStart), + } + }() + } + + // Wait for all to complete + didSucceed := true + for i := 0; i < n; i++ { + result := <-done + if !result.didSucceed { + didSucceed = false + } + jww.DEBUG.Printf("Operation time: %s", result.duration) + } + + timeElapsed := time.Since(startTime) + jww.INFO.Printf("Benchmarking complete. Succeeded: %t\n"+ + "Took %s, Average of %s.", + didSucceed, timeElapsed, timeElapsed/time.Duration(n)) +} diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go index 4d51b97e6a2941e07849c0e2e405539ccc86a8f7..9639bc185095e74cbf4b3e63256fead39bbcef82 100644 --- a/indexedDb/worker/channels/implementation.go +++ b/indexedDb/worker/channels/implementation.go @@ -263,9 +263,12 @@ type MessageUpdateInfo struct { // messageID, timestamp, round, pinned, and hidden are all nillable and may be // updated based upon the UUID at a later date. If a nil value is passed, then // make no update. +// +// Returns an error if the message cannot be updated. It must return +// [channels.NoMessageErr] if the message does not exist. func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, - status *channels.SentStatus) { + status *channels.SentStatus) error { msg := MessageUpdateInfo{UUID: uuid} if messageID != nil { msg.MessageID = *messageID @@ -294,12 +297,33 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, data, err := json.Marshal(msg) if err != nil { - jww.ERROR.Printf( - "[CH] Could not JSON marshal payload for UpdateFromUUID: %+v", err) - return + return errors.Errorf( + "could not JSON marshal payload for UpdateFromUUID: %+v", err) } - w.wm.SendMessage(UpdateFromUUIDTag, data, nil) + errChan := make(chan error) + w.wm.SendMessage(UpdateFromUUIDTag, data, func(data []byte) { + if data != nil { + errChan <- errors.New(string(data)) + } else { + errChan <- nil + } + }) + + select { + case err = <-errChan: + return err + case <-time.After(worker.ResponseTimeout): + return errors.Errorf("timed out after %s waiting for response from "+ + "the worker about UpdateFromUUID", worker.ResponseTimeout) + } +} + +// UuidError is JSON marshalled and sent to the worker for +// [wasmModel.UpdateFromMessageID]. +type UuidError struct { + UUID uint64 `json:"uuid"` + Error []byte `json:"error"` } // UpdateFromMessageID is called whenever a message with the message ID is @@ -313,7 +337,7 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, // no update. func (w *wasmModel) UpdateFromMessageID(messageID message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, - status *channels.SentStatus) uint64 { + status *channels.SentStatus) (uint64, error) { msg := MessageUpdateInfo{MessageID: messageID, MessageIDSet: true} if timestamp != nil { @@ -339,33 +363,34 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID, data, err := json.Marshal(msg) if err != nil { - jww.ERROR.Printf("[CH] Could not JSON marshal payload for "+ + return 0, errors.Errorf("could not JSON marshal payload for "+ "UpdateFromMessageID: %+v", err) - return 0 } uuidChan := make(chan uint64) + errChan := make(chan error) w.wm.SendMessage(UpdateFromMessageIDTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "UpdateFromMessageID: %+v", err) - uuidChan <- 0 + var ue UuidError + if err = json.Unmarshal(data, &ue); err != nil { + errChan <- errors.Errorf("could not JSON unmarshal response "+ + "to UpdateFromMessageID: %+v", err) + } else if ue.Error != nil { + errChan <- errors.New(string(ue.Error)) + } else { + uuidChan <- ue.UUID } - uuidChan <- uuid }) select { case uuid := <-uuidChan: - return uuid + return uuid, nil + case err = <-errChan: + return 0, err case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ + return 0, errors.Errorf("timed out after %s waiting for response from "+ "the worker about UpdateFromMessageID", worker.ResponseTimeout) } - - return 0 } // GetMessageMessage is JSON marshalled and sent to the worker for diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go index 04b7a8f4392ac78051bd9cce001be45a98477dca..2ee630caf43177460a19f14d2b953d3a03b9c687 100644 --- a/indexedDb/worker/channels/init.go +++ b/indexedDb/worker/channels/init.go @@ -25,6 +25,9 @@ import ( "gitlab.com/xx_network/primitives/id" ) +// databaseSuffix is the suffix to be appended to the name of the database. +const databaseSuffix = "_speakeasy" + // MessageReceivedCallback is called any time a message is received or updated. // // update is true if the row is old and was edited. @@ -55,7 +58,7 @@ func NewWASMEventModelBuilder(wasmJsPath string, // NewWASMEventModelMessage is JSON marshalled and sent to the worker for // [NewWASMEventModel]. type NewWASMEventModelMessage struct { - Path string `json:"path"` + DatabaseName string `json:"databaseName"` EncryptionJSON string `json:"encryptionJSON"` } @@ -65,6 +68,7 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, messageReceivedCB MessageReceivedCallback, deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback) ( channels.EventModel, error) { + databaseName := path + databaseSuffix wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb", true) if err != nil { @@ -83,11 +87,18 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, wm.RegisterCallback(MutedUserCallbackTag, mutedUserCallbackHandler(mutedUserCB)) - // Register handler to manage checking encryption status from local storage - wm.RegisterCallback(EncryptionStatusTag, checkDbEncryptionStatusHandler(wm)) + // Store the database name + err = storage.StoreIndexedDb(databaseName) + if err != nil { + return nil, err + } - // Register handler to manage the storage of the database name - wm.RegisterCallback(StoreDatabaseNameTag, storeDatabaseNameHandler(wm)) + // Check that the encryption status + encryptionStatus := encryption != nil + err = checkDbEncryptionStatus(databaseName, encryptionStatus) + if err != nil { + return nil, err + } encryptionJSON, err := json.Marshal(encryption) if err != nil { @@ -95,7 +106,7 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, } msg := NewWASMEventModelMessage{ - Path: path, + DatabaseName: databaseName, EncryptionJSON: string(encryptionJSON), } @@ -104,14 +115,14 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, return nil, err } - errChan := make(chan string) + dataChan := make(chan []byte) wm.SendMessage(NewWASMEventModelTag, payload, - func(data []byte) { errChan <- string(data) }) + func(data []byte) { dataChan <- data }) select { - case workerErr := <-errChan: - if workerErr != "" { - return nil, errors.New(workerErr) + case data := <-dataChan: + if len(data) > 0 { + return nil, errors.New(string(data)) } case <-time.After(worker.ResponseTimeout): return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ @@ -189,54 +200,24 @@ type EncryptionStatusReply struct { Error string `json:"error"` } -// checkDbEncryptionStatusHandler returns a handler to manage checking -// encryption status from local storage. -func checkDbEncryptionStatusHandler( - wh *worker.Manager) func(data []byte) { - return func(data []byte) { - // Unmarshal received message - var msg EncryptionStatusMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "EncryptionStatusMessage message from worker: %+v", err) - return - } +// checkDbEncryptionStatus returns an error if the encryption status provided +// does not match the stored status for this database name. +func checkDbEncryptionStatus(databaseName string, encryptionStatus bool) error { - // Pass message values to storage - loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( - msg.DatabaseName, msg.EncryptionStatus) - var reply EncryptionStatusReply - if err != nil { - reply.Error = err.Error() - } else { - reply.EncryptionStatus = loadedEncryptionStatus - } - - // Return response - statusData, err := json.Marshal(reply) - if err != nil { - jww.ERROR.Printf( - "Failed to JSON marshal EncryptionStatusReply: %+v", err) - return - } - - wh.SendMessage(EncryptionStatusTag, statusData, nil) + // Pass message values to storage + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + databaseName, encryptionStatus) + if err != nil { + return err } -} - -// storeDatabaseNameHandler returns a handler that stores the database name to -// storage when it is received from the worker. -func storeDatabaseNameHandler( - wh *worker.Manager) func(data []byte) { - return func(data []byte) { - var returnData []byte - // Get the database name and save it to storage - if err := storage.StoreIndexedDb(string(data)); err != nil { - returnData = []byte(err.Error()) - } - - wh.SendMessage(StoreDatabaseNameTag, returnData, nil) + // Verify encryption status does not change + if encryptionStatus != loadedEncryptionStatus { + return errors.New( + "cannot load database with different encryption status") + } else if !encryptionStatus { + jww.WARN.Printf("IndexedDb encryption disabled!") } + + return nil } diff --git a/indexedDb/worker/channels/tags.go b/indexedDb/worker/channels/tags.go index d2ae61a9f6b65a9bf2a02483806e89fbc14edb9d..d3555e549163c18b772cc86f965a2f7aeca7a827 100644 --- a/indexedDb/worker/channels/tags.go +++ b/indexedDb/worker/channels/tags.go @@ -18,8 +18,6 @@ const ( MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback" DeletedMessageCallbackTag worker.Tag = "DeletedMessageCallback" MutedUserCallbackTag worker.Tag = "MutedUserCallback" - EncryptionStatusTag worker.Tag = "EncryptionStatus" - StoreDatabaseNameTag worker.Tag = "StoreDatabaseName" JoinChannelTag worker.Tag = "JoinChannel" LeaveChannelTag worker.Tag = "LeaveChannel" diff --git a/indexedDb/worker/dm/init.go b/indexedDb/worker/dm/init.go index 4bf441a73448d45404be8a371855b63817f21de3..3fd1cd13897bdc8684e2efc13750e1fed20ff000 100644 --- a/indexedDb/worker/dm/init.go +++ b/indexedDb/worker/dm/init.go @@ -23,6 +23,9 @@ import ( "gitlab.com/elixxir/xxdk-wasm/worker" ) +// databaseSuffix is the suffix to be appended to the name of the database. +const databaseSuffix = "_speakeasy_dm" + // MessageReceivedCallback is called any time a message is received or updated. // // messageUpdate is true if the Message already exists and was edited. @@ -33,7 +36,7 @@ type MessageReceivedCallback func(uuid uint64, pubKey ed25519.PublicKey, // NewWASMEventModelMessage is JSON marshalled and sent to the worker for // [NewWASMEventModel]. type NewWASMEventModelMessage struct { - Path string `json:"path"` + DatabaseName string `json:"databaseName"` EncryptionJSON string `json:"encryptionJSON"` } @@ -41,6 +44,7 @@ type NewWASMEventModelMessage struct { // The name should be a base64 encoding of the users public key. func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, cb MessageReceivedCallback) (dm.EventModel, error) { + databaseName := path + databaseSuffix wh, err := worker.NewManager(wasmJsPath, "dmIndexedDb", true) if err != nil { @@ -51,11 +55,18 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, wh.RegisterCallback( MessageReceivedCallbackTag, messageReceivedCallbackHandler(cb)) - // Register handler to manage checking encryption status from local storage - wh.RegisterCallback(EncryptionStatusTag, checkDbEncryptionStatusHandler(wh)) + // Store the database name + err = storage.StoreIndexedDb(databaseName) + if err != nil { + return nil, err + } - // Register handler to manage the storage of the database name - wh.RegisterCallback(StoreDatabaseNameTag, storeDatabaseNameHandler(wh)) + // Check that the encryption status + encryptionStatus := encryption != nil + err = checkDbEncryptionStatus(databaseName, encryptionStatus) + if err != nil { + return nil, err + } encryptionJSON, err := json.Marshal(encryption) if err != nil { @@ -63,7 +74,7 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, } msg := NewWASMEventModelMessage{ - Path: path, + DatabaseName: databaseName, EncryptionJSON: string(encryptionJSON), } @@ -72,14 +83,14 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, return nil, err } - errChan := make(chan string) + dataChan := make(chan []byte) wh.SendMessage(NewWASMEventModelTag, payload, - func(data []byte) { errChan <- string(data) }) + func(data []byte) { dataChan <- data }) select { - case workerErr := <-errChan: - if workerErr != "" { - return nil, errors.New(workerErr) + case data := <-dataChan: + if len(data) > 0 { + return nil, errors.New(string(data)) } case <-time.After(worker.ResponseTimeout): return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ @@ -113,66 +124,23 @@ func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte } } -// EncryptionStatusMessage is JSON marshalled and received from the worker when -// the database checks if it is encrypted. -type EncryptionStatusMessage struct { - DatabaseName string `json:"databaseName"` - EncryptionStatus bool `json:"encryptionStatus"` -} - -// EncryptionStatusReply is JSON marshalled and sent to the worker is response -// to the [EncryptionStatusMessage]. -type EncryptionStatusReply struct { - EncryptionStatus bool `json:"encryptionStatus"` - Error string `json:"error"` -} - -// checkDbEncryptionStatusHandler returns a handler to manage checking -// encryption status from local storage. -func checkDbEncryptionStatusHandler(wh *worker.Manager) func(data []byte) { - return func(data []byte) { - // Unmarshal received message - var msg EncryptionStatusMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "EncryptionStatusMessage message from worker: %+v", err) - return - } - - // Pass message values to storage - loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( - msg.DatabaseName, msg.EncryptionStatus) - var reply EncryptionStatusReply - if err != nil { - reply.Error = err.Error() - } else { - reply.EncryptionStatus = loadedEncryptionStatus - } - - // Return response - statusData, err := json.Marshal(reply) - if err != nil { - jww.ERROR.Printf( - "Failed to JSON marshal EncryptionStatusReply: %+v", err) - return - } - - wh.SendMessage(EncryptionStatusTag, statusData, nil) +// checkDbEncryptionStatus returns an error if the encryption status provided +// does not match the stored status for this database name. +func checkDbEncryptionStatus(databaseName string, encryptionStatus bool) error { + // Pass message values to storage + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + databaseName, encryptionStatus) + if err != nil { + return err } -} - -// storeDatabaseNameHandler returns a handler that stores the database name to -// storage when it is received from the worker. -func storeDatabaseNameHandler(wh *worker.Manager) func(data []byte) { - return func(data []byte) { - var returnData []byte - - // Get the database name and save it to storage - if err := storage.StoreIndexedDb(string(data)); err != nil { - returnData = []byte(err.Error()) - } - wh.SendMessage(StoreDatabaseNameTag, returnData, nil) + // Verify encryption status does not change + if encryptionStatus != loadedEncryptionStatus { + return errors.New( + "cannot load database with different encryption status") + } else if !encryptionStatus { + jww.WARN.Printf("IndexedDb encryption disabled!") } + + return nil } diff --git a/indexedDb/worker/dm/tags.go b/indexedDb/worker/dm/tags.go index 9a3e710b870f5f2747fd0b7baeb428c826b9f2d1..b71762421a15003123071954312e290e5677a24e 100644 --- a/indexedDb/worker/dm/tags.go +++ b/indexedDb/worker/dm/tags.go @@ -16,8 +16,6 @@ import "gitlab.com/elixxir/xxdk-wasm/worker" const ( NewWASMEventModelTag worker.Tag = "NewWASMEventModel" MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback" - EncryptionStatusTag worker.Tag = "EncryptionStatus" - StoreDatabaseNameTag worker.Tag = "StoreDatabaseName" ReceiveReplyTag worker.Tag = "ReceiveReply" ReceiveReactionTag worker.Tag = "ReceiveReaction" diff --git a/logging/fileLogger.go b/logging/fileLogger.go new file mode 100644 index 0000000000000000000000000000000000000000..831511a81e069b1899b081d40268cf38042cf0b7 --- /dev/null +++ b/logging/fileLogger.go @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "io" + "math" + + "github.com/armon/circbuf" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// fileLogger manages the recording of jwalterweatherman logs to the local +// in-memory file buffer. +type fileLogger struct { + threshold jww.Threshold + cb *circbuf.Buffer +} + +// newFileLogger starts logging to a local, in-memory log file at the specified +// threshold. Returns a [fileLogger] that can be used to get the log file. +func newFileLogger(threshold jww.Threshold, maxLogFileSize int) (*fileLogger, error) { + b, err := circbuf.NewBuffer(int64(maxLogFileSize)) + if err != nil { + return nil, errors.Wrap(err, "could not create new circular buffer") + } + + fl := &fileLogger{ + threshold: threshold, + cb: b, + } + + jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level %s", + b.Size(), fl.threshold) + + logger = fl + return fl, nil +} + +// Write adheres to the io.Writer interface and writes log entries to the +// buffer. +func (fl *fileLogger) Write(p []byte) (n int, err error) { + return fl.cb.Write(p) +} + +// Listen adheres to the [jwalterweatherman.LogListener] type and returns the +// log writer when the threshold is within the set threshold limit. +func (fl *fileLogger) Listen(threshold jww.Threshold) io.Writer { + if threshold < fl.threshold { + return nil + } + return fl +} + +// StopLogging stops log message writes. Once logging is stopped, it cannot be +// resumed and the log file cannot be recovered. +func (fl *fileLogger) StopLogging() { + fl.threshold = math.MaxInt + fl.cb.Reset() +} + +// GetFile returns the entire log file. +func (fl *fileLogger) GetFile() []byte { + return fl.cb.Bytes() +} + +// Threshold returns the log level threshold used in the file. +func (fl *fileLogger) Threshold() jww.Threshold { + return fl.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (fl *fileLogger) MaxSize() int { + return int(fl.cb.Size()) +} + +// Size returns the current size, in bytes, written to the log file. +func (fl *fileLogger) Size() int { + return int(fl.cb.TotalWritten()) +} + +// Worker returns nil. +func (fl *fileLogger) Worker() *worker.Manager { + return nil +} diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..317f69544a927280827aa4f3739c7f1e39d30ac4 --- /dev/null +++ b/logging/fileLogger_test.go @@ -0,0 +1,228 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "bytes" + "github.com/armon/circbuf" + jww "github.com/spf13/jwalterweatherman" + "math/rand" + "reflect" + "testing" +) + +func Test_newFileLogger(t *testing.T) { + expected := &fileLogger{ + threshold: jww.LevelError, + } + expected.cb, _ = circbuf.NewBuffer(512) + fl, err := newFileLogger(expected.threshold, int(expected.cb.Size())) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + if !reflect.DeepEqual(expected, fl) { + t.Errorf("Unexpected new fileLogger.\nexpected: %+v\nreceived: %+v", + expected, fl) + } + if !reflect.DeepEqual(logger, fl) { + t.Errorf("Failed to set logger.\nexpected: %+v\nreceived: %+v", + logger, fl) + } + +} + +// Tests that fileLogger.Write writes the expected data to the buffer and that +// when the max file size is reached, old data is replaced. +func Test_fileLogger_Write(t *testing.T) { + rng := rand.New(rand.NewSource(3424)) + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + expected := make([]byte, fl.MaxSize()) + rng.Read(expected) + n, err := fl.Write(expected) + if err != nil { + t.Fatalf("Failed to write: %+v", err) + } else if n != len(expected) { + t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d", + len(expected), n) + } + + if !bytes.Equal(fl.cb.Bytes(), expected) { + t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v", + expected, fl.cb.Bytes()) + } + + // Check that the data is overwritten + rng.Read(expected) + n, err = fl.Write(expected) + if err != nil { + t.Fatalf("Failed to write: %+v", err) + } else if n != len(expected) { + t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d", + len(expected), n) + } + + if !bytes.Equal(fl.cb.Bytes(), expected) { + t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v", + expected, fl.cb.Bytes()) + } +} + +// Tests that fileLogger.Listen only returns an io.Writer for valid thresholds. +func Test_fileLogger_Listen(t *testing.T) { + th := jww.LevelError + fl, err := newFileLogger(th, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug, + jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical, + jww.LevelFatal} + + for _, threshold := range thresholds { + w := fl.Listen(threshold) + if threshold < th { + if w != nil { + t.Errorf("Did not receive nil io.Writer for level %s: %+v", + threshold, w) + } + } else if w == nil { + t.Errorf("Received nil io.Writer for level %s", threshold) + } + } +} + +// Tests that fileLogger.Listen always returns nil after fileLogger.StopLogging +// is called. +func Test_fileLogger_StopLogging(t *testing.T) { + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + fl.StopLogging() + + if w := fl.Listen(jww.LevelFatal); w != nil { + t.Errorf("Listen returned non-nil io.Writer when logging should have "+ + "been stopped: %+v", w) + } + + file := fl.GetFile() + if !bytes.Equal([]byte{}, file) { + t.Errorf("Did not receice empty file: %+v", file) + } +} + +// Tests that fileLogger.GetFile returns the expected file. +func Test_fileLogger_GetFile(t *testing.T) { + rng := rand.New(rand.NewSource(9863)) + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + var expected []byte + for i := 0; i < 5; i++ { + p := make([]byte, rng.Intn(64)) + rng.Read(p) + expected = append(expected, p...) + + if _, err = fl.Write(p); err != nil { + t.Errorf("Write %d failed: %+v", i, err) + } + } + + file := fl.GetFile() + if !bytes.Equal(expected, file) { + t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file) + } +} + +// Tests that fileLogger.Threshold returns the expected threshold. +func Test_fileLogger_Threshold(t *testing.T) { + thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug, + jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical, + jww.LevelFatal} + + for _, threshold := range thresholds { + fl, err := newFileLogger(threshold, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + if fl.Threshold() != threshold { + t.Errorf("Incorrect threshold.\nexpected: %s (%d)\nreceived: %s (%d)", + threshold, threshold, fl.Threshold(), fl.Threshold()) + } + } +} + +// Unit test of fileLogger.MaxSize. +func Test_fileLogger_MaxSize(t *testing.T) { + maxSize := 512 + fl, err := newFileLogger(jww.LevelError, maxSize) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + if fl.MaxSize() != maxSize { + t.Errorf("Incorrect max size.\nexpected: %d\nreceived: %d", + maxSize, fl.MaxSize()) + } +} + +// Unit test of fileLogger.Size. +func Test_fileLogger_Size(t *testing.T) { + rng := rand.New(rand.NewSource(9863)) + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + var expected []byte + for i := 0; i < 5; i++ { + p := make([]byte, rng.Intn(64)) + rng.Read(p) + expected = append(expected, p...) + + if _, err = fl.Write(p); err != nil { + t.Errorf("Write %d failed: %+v", i, err) + } + + size := fl.Size() + if size != len(expected) { + t.Errorf("Incorrect size (%d).\nexpected: %d\nreceived: %d", + i, len(expected), size) + } + } + + file := fl.GetFile() + if !bytes.Equal(expected, file) { + t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file) + } +} + +// Tests that fileLogger.Worker always returns nil. +func Test_fileLogger_Worker(t *testing.T) { + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + w := fl.Worker() + if w != nil { + t.Errorf("Did not get nil worker: %+v", w) + } +} diff --git a/logging/logLevel.go b/logging/logLevel.go deleted file mode 100644 index 895857475c4c647d7625b43644e6c4f32be98155..0000000000000000000000000000000000000000 --- a/logging/logLevel.go +++ /dev/null @@ -1,89 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -//go:build js && wasm - -package logging - -import ( - "fmt" - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" - "log" - "syscall/js" -) - -// LogLevel sets level of logging. All logs at the set level and below will be -// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL -// messages will be printed). -// -// The default log level without updates is INFO. -func LogLevel(threshold jww.Threshold) error { - if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - return errors.Errorf("log level is not valid: log level: %d", threshold) - } - - jww.SetLogThreshold(threshold) - jww.SetFlags(log.LstdFlags | log.Lmicroseconds) - - ll := NewJsConsoleLogListener(threshold) - AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - - msg := fmt.Sprintf("Log level set to: %s", threshold) - switch threshold { - case jww.LevelTrace: - fallthrough - case jww.LevelDebug: - fallthrough - case jww.LevelInfo: - jww.INFO.Print(msg) - case jww.LevelWarn: - jww.WARN.Print(msg) - case jww.LevelError: - jww.ERROR.Print(msg) - case jww.LevelCritical: - jww.CRITICAL.Print(msg) - case jww.LevelFatal: - jww.FATAL.Print(msg) - } - - return nil -} - -// LogLevelJS sets level of logging. All logs at the set level and below will be -// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL -// messages will be printed). -// -// Log level options: -// -// TRACE - 0 -// DEBUG - 1 -// INFO - 2 -// WARN - 3 -// ERROR - 4 -// CRITICAL - 5 -// FATAL - 6 -// -// The default log level without updates is INFO. -// -// Parameters: -// - args[0] - Log level (int). -// -// Returns: -// - Throws TypeError if the log level is invalid. -func LogLevelJS(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - err := LogLevel(threshold) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return nil -} diff --git a/logging/logger.go b/logging/logger.go index b3e5f728d803e113c1952bae62384d668f7752fc..3064a00d551a464a486c8f8a23925cf74cea7998 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -10,29 +10,13 @@ package logging import ( - "encoding/binary" - "encoding/json" - "fmt" - "github.com/armon/circbuf" "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" - "gitlab.com/elixxir/xxdk-wasm/worker" - "io" - "strconv" - "sync/atomic" "syscall/js" - "time" -) -const ( - // DefaultInitThreshold is the log threshold used for the initial log before - // any logging options is set. - DefaultInitThreshold = jww.LevelTrace + jww "github.com/spf13/jwalterweatherman" - // logListenerChanSize is the size of the listener channel that stores log - // messages before they are written. - logListenerChanSize = 3000 + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/xxdk-wasm/worker" ) // List of tags that can be used when sending a message or registering a handler @@ -46,346 +30,79 @@ const ( ) // logger is the global that all jwalterweatherman logging is sent to. -var logger *Logger - -// Logger manages the recording of jwalterweatherman logs. It can write logs to -// a local, in-memory buffer or to an external worker. -type Logger struct { - threshold jww.Threshold - maxLogFileSize int - logListenerID uint64 - - listenChan chan []byte - mode atomic.Uint32 - processQuit chan struct{} - - cb *circbuf.Buffer - wm *worker.Manager -} - -// InitLogger initializes the logger. Include this in the init function in main. -func InitLogger() *Logger { - logger = NewLogger() - return logger -} +var logger Logger // GetLogger returns the Logger object, used to manager where logging is // recorded. -func GetLogger() *Logger { +func GetLogger() Logger { return logger } -// NewLogger creates a new Logger that begins storing the first -// DefaultInitThreshold log entries. If either the log file or log worker is -// enabled, then these logs are redirected to the set destination. If the -// channel fills up with no log recorder enabled, then the listener is disabled. -func NewLogger() *Logger { - lf := newLogger() - - // Add the log listener - lf.logListenerID = AddLogListener(lf.Listen) +type Logger interface { + // StopLogging stops log message writes. Once logging is stopped, it cannot + // be resumed and the log file cannot be recovered. + StopLogging() - jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+ - "at threshold %s that can store %d entries", - lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan)) - - return lf -} + // GetFile returns the entire log file. + GetFile() []byte -// newLogger initialises a Logger without adding it as a log listener. -func newLogger() *Logger { - lf := &Logger{ - threshold: DefaultInitThreshold, - listenChan: make(chan []byte, logListenerChanSize), - mode: atomic.Uint32{}, - processQuit: make(chan struct{}), - } - lf.setMode(initMode) - - return lf -} + // Threshold returns the log level threshold used in the file. + Threshold() jww.Threshold -// LogToFile starts logging to a local, in-memory log file. -func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error { - err := l.prepare(threshold, maxLogFileSize, fileMode) - if err != nil { - return err - } + // MaxSize returns the maximum size, in bytes, of the log file before it + // rolls over and starts overwriting the oldest entries + MaxSize() int - b, err := circbuf.NewBuffer(int64(maxLogFileSize)) - if err != nil { - return err - } - l.cb = b - - sendLog := func(p []byte) { - if n, err2 := l.cb.Write(p); err2 != nil { - jww.ERROR.Printf( - "[LOG] Error writing log to circular buffer: %+v", err2) - } else if n != len(p) { - jww.ERROR.Printf( - "[LOG] Wrote %d bytes when %d bytes expected", n, len(p)) - } - } - go l.processLog(workerMode, sendLog, l.processQuit) + // Size returns the number of bytes written to the log file. + Size() int - return nil + // Worker returns the manager for the Javascript Worker object. If the + // worker has not been initialized, it returns nil. + Worker() *worker.Manager } -// LogToFileWorker starts a new worker that begins listening for logs and -// writing them to file. This function blocks until the worker has started. -func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int, - wasmJsPath, workerName string) error { - err := l.prepare(threshold, maxLogFileSize, workerMode) - if err != nil { - return err - } +// EnableLogging enables logging to the Javascript console and to a local or +// worker file buffer. This must be called only once at initialisation. +func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, + workerScriptURL, workerName string) error { - // Create new worker manager, which will start the worker and wait until - // communication has been established - wm, err := worker.NewManager(wasmJsPath, workerName, false) - if err != nil { - return err - } - l.wm = wm - - // Register the callback used by the Javascript to request the log file. - // This prevents an error print when GetFileExtTag is not registered. - l.wm.RegisterCallback(GetFileExtTag, func([]byte) { - jww.DEBUG.Print("[LOG] Received file requested from external " + - "Javascript. Ignoring file.") - }) - - data, err := json.Marshal(l.maxLogFileSize) - if err != nil { - return err + var listeners []jww.LogListener + if logLevel > -1 { + // Overwrites setting the log level to INFO done in bindings so that the + // Javascript console can be used + ll := NewJsConsoleLogListener(logLevel) + listeners = append(listeners, ll.Listen) + jww.SetStdoutThreshold(jww.LevelFatal + 1) + jww.FEEDBACK.Printf("[LOG] Log level for console set to %s", logLevel) + } else { + jww.FEEDBACK.Print("[LOG] Disabling logging to console.") } - // Send message to initialize the log file listener - errChan := make(chan error) - l.wm.SendMessage(NewLogFileTag, data, func(data []byte) { - if len(data) > 0 { - errChan <- errors.New(string(data)) + if fileLogLevel > -1 { + maxLogFileSize := maxLogFileSizeMB * 1_000_000 + if workerScriptURL == "" { + fl, err := newFileLogger(fileLogLevel, maxLogFileSize) + if err != nil { + return errors.Wrap(err, "could not initialize logging to file") + } + listeners = append(listeners, fl.Listen) } else { - errChan <- nil - } - }) - - // Wait for worker to respond - select { - case err = <-errChan: - if err != nil { - return err - } - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for new log "+ - "file in worker to initialize", worker.ResponseTimeout) - } - - jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName) - - sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) } - go l.processLog(workerMode, sendLog, l.processQuit) - - return nil -} + wl, err := newWorkerLogger( + fileLogLevel, maxLogFileSize, workerScriptURL, workerName) + if err != nil { + return errors.Wrap(err, "could not initialize logging to worker file") + } -// processLog processes the log messages sent to the listener channel and sends -// them to the appropriate recorder. -func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) { - jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m) - - for { - select { - case <-quit: - jww.INFO.Printf("[LOG] Stopping log file processing thread.") - return - case p := <-l.listenChan: - go sendLog(p) + listeners = append(listeners, wl.Listen) } - } -} -// prepare sets the threshold, maxLogFileSize, and mode of the logger and -// prints a log message indicating this information. -func (l *Logger) prepare( - threshold jww.Threshold, maxLogFileSize int, m mode) error { - if m := l.getMode(); m != initMode { - return errors.Errorf("log already set to %s", m) - } else if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - return errors.Errorf("log level of %d is invalid", threshold) - } - - l.threshold = threshold - l.maxLogFileSize = maxLogFileSize - l.setMode(m) - - msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+ - "with level %s", m, l.MaxSize(), l.Threshold()) - switch l.Threshold() { - case jww.LevelTrace: - fallthrough - case jww.LevelDebug: - fallthrough - case jww.LevelInfo: - jww.INFO.Print(msg) - case jww.LevelWarn: - jww.WARN.Print(msg) - case jww.LevelError: - jww.ERROR.Print(msg) - case jww.LevelCritical: - jww.CRITICAL.Print(msg) - case jww.LevelFatal: - jww.FATAL.Print(msg) + js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS)) } + jww.SetLogListeners(listeners...) return nil } -// StopLogging stops the logging of log messages and disables the log listener. -// If the log worker is running, it is terminated. Once logging is stopped, it -// cannot be resumed the log file cannot be recovered. -func (l *Logger) StopLogging() { - jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID) - RemoveLogListener(l.logListenerID) - - switch l.getMode() { - case workerMode: - go l.wm.Terminate() - jww.DEBUG.Printf("[LOG] Terminated log worker.") - case fileMode: - jww.DEBUG.Printf("[LOG] Reset circular buffer.") - l.cb.Reset() - } - - select { - case l.processQuit <- struct{}{}: - jww.DEBUG.Printf("[LOG] Sent quit channel to log process.") - default: - jww.DEBUG.Printf("[LOG] Failed to stop log processes.") - } -} - -// GetFile returns the entire log file. -// -// If the log file is listening locally, it returns it from the local buffer. If -// it is listening from the worker, it blocks until the file is returned. -func (l *Logger) GetFile() []byte { - switch l.getMode() { - case fileMode: - return l.cb.Bytes() - case workerMode: - fileChan := make(chan []byte) - l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) - - select { - case file := <-fileChan: - return file - case <-time.After(worker.ResponseTimeout): - jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ - "file from worker", worker.ResponseTimeout) - return nil - } - default: - return nil - } -} - -// Threshold returns the log level threshold used in the file. -func (l *Logger) Threshold() jww.Threshold { - return l.threshold -} - -// MaxSize returns the max size, in bytes, that the log file is allowed to be. -func (l *Logger) MaxSize() int { - return l.maxLogFileSize -} - -// Size returns the current size, in bytes, written to the log file. -// -// If the log file is listening locally, it returns it from the local buffer. If -// it is listening from the worker, it blocks until the size is returned. -func (l *Logger) Size() int { - switch l.getMode() { - case fileMode: - return int(l.cb.Size()) - case workerMode: - sizeChan := make(chan []byte) - l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) - - select { - case data := <-sizeChan: - return int(jww.Threshold(binary.LittleEndian.Uint64(data))) - case <-time.After(worker.ResponseTimeout): - jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ - "file size from worker", worker.ResponseTimeout) - return 0 - } - default: - return 0 - } -} - -//////////////////////////////////////////////////////////////////////////////// -// JWW Listener // -//////////////////////////////////////////////////////////////////////////////// - -// Listen is called for every logging event. This function adheres to the -// [jwalterweatherman.LogListener] type. -func (l *Logger) Listen(t jww.Threshold) io.Writer { - if t < l.threshold { - return nil - } - - return l -} - -// Write sends the bytes to the listener channel. It always returns the length -// of p and a nil error. This function adheres to the io.Writer interface. -func (l *Logger) Write(p []byte) (n int, err error) { - select { - case l.listenChan <- append([]byte{}, p...): - default: - jww.ERROR.Printf( - "[LOG] Logger channel filled. Log file recording stopping.") - l.StopLogging() - return 0, errors.Errorf( - "Logger channel filled. Log file recording stopping.") - } - return len(p), nil -} - -//////////////////////////////////////////////////////////////////////////////// -// Log File Mode // -//////////////////////////////////////////////////////////////////////////////// - -// mode represents the state of the Logger. -type mode uint32 - -const ( - initMode mode = iota - fileMode - workerMode -) - -func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) } -func (l *Logger) getMode() mode { return mode(l.mode.Load()) } - -// String returns a human-readable representation of the mode for logging and -// debugging. This function adheres to the fmt.Stringer interface. -func (m mode) String() string { - switch m { - case initMode: - return "uninitialized mode" - case fileMode: - return "file mode" - case workerMode: - return "worker mode" - default: - return "invalid mode: " + strconv.Itoa(int(m)) - } -} - //////////////////////////////////////////////////////////////////////////////// // Javascript Bindings // //////////////////////////////////////////////////////////////////////////////// @@ -396,142 +113,98 @@ func (m mode) String() string { // Returns: // - A Javascript representation of the [Logger] object. func GetLoggerJS(js.Value, []js.Value) any { - return newLoggerJS(GetLogger()) + // l := GetLogger() + // if l != nil { + // return newLoggerJS(LoggerJS{GetLogger()}) + // } + // return js.Null() + return newLoggerJS(LoggerJS{GetLogger()}) +} + +type LoggerJS struct { + api Logger } // newLoggerJS creates a new Javascript compatible object (map[string]any) that // matches the [Logger] structure. -func newLoggerJS(lfw *Logger) map[string]any { +func newLoggerJS(l LoggerJS) map[string]any { logFileWorker := map[string]any{ - "LogToFile": js.FuncOf(lfw.LogToFileJS), - "LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS), - "StopLogging": js.FuncOf(lfw.StopLoggingJS), - "GetFile": js.FuncOf(lfw.GetFileJS), - "Threshold": js.FuncOf(lfw.ThresholdJS), - "MaxSize": js.FuncOf(lfw.MaxSizeJS), - "Size": js.FuncOf(lfw.SizeJS), - "Worker": js.FuncOf(lfw.WorkerJS), + "StopLogging": js.FuncOf(l.StopLogging), + "GetFile": js.FuncOf(l.GetFile), + "Threshold": js.FuncOf(l.Threshold), + "MaxSize": js.FuncOf(l.MaxSize), + "Size": js.FuncOf(l.Size), + "Worker": js.FuncOf(l.Worker), } return logFileWorker } -// LogToFileJS starts logging to a local, in-memory log file. -// -// Parameters: -// - args[0] - Log level (int). -// - args[1] - Max log file size, in bytes (int). -// -// Returns: -// - Throws a TypeError if starting the log file fails. -func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - maxLogFileSize := args[1].Int() - - err := l.LogToFile(threshold, maxLogFileSize) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return nil -} - -// LogToFileWorkerJS starts a new worker that begins listening for logs and -// writing them to file. This function blocks until the worker has started. -// -// Parameters: -// - args[0] - Log level (int). -// - args[1] - Max log file size, in bytes (int). -// - args[2] - Path to Javascript start file for the worker WASM (string). -// - args[3] - Name of the worker (used in logs) (string). -// -// Returns a promise: -// - Resolves to nothing on success (void). -// - Rejected with an error if starting the worker fails. -func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - maxLogFileSize := args[1].Int() - wasmJsPath := args[2].String() - workerName := args[3].String() - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - err := l.LogToFileWorker( - threshold, maxLogFileSize, wasmJsPath, workerName) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve() - } - } - - return utils.CreatePromise(promiseFn) -} - -// StopLoggingJS stops the logging of log messages and disables the log +// StopLogging stops the logging of log messages and disables the log // listener. If the log worker is running, it is terminated. Once logging is // stopped, it cannot be resumed the log file cannot be recovered. -func (l *Logger) StopLoggingJS(js.Value, []js.Value) any { - l.StopLogging() +func (l *LoggerJS) StopLogging(js.Value, []js.Value) any { + l.api.StopLogging() return nil } -// GetFileJS returns the entire log file. +// GetFile returns the entire log file. // // If the log file is listening locally, it returns it from the local buffer. If // it is listening from the worker, it blocks until the file is returned. // // Returns a promise: // - Resolves to the log file contents (string). -func (l *Logger) GetFileJS(js.Value, []js.Value) any { +func (l *LoggerJS) GetFile(js.Value, []js.Value) any { promiseFn := func(resolve, _ func(args ...any) js.Value) { - resolve(string(l.GetFile())) + resolve(string(l.api.GetFile())) } return utils.CreatePromise(promiseFn) } -// ThresholdJS returns the log level threshold used in the file. +// Threshold returns the log level threshold used in the file. // // Returns: // - Log level (int). -func (l *Logger) ThresholdJS(js.Value, []js.Value) any { - return int(l.Threshold()) +func (l *LoggerJS) Threshold(js.Value, []js.Value) any { + return int(l.api.Threshold()) } -// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be. +// MaxSize returns the max size, in bytes, that the log file is allowed to be. // // Returns: // - Max file size (int). -func (l *Logger) MaxSizeJS(js.Value, []js.Value) any { - return l.MaxSize() +func (l *LoggerJS) MaxSize(js.Value, []js.Value) any { + return l.api.MaxSize() } -// SizeJS returns the current size, in bytes, written to the log file. +// Size returns the current size, in bytes, written to the log file. // // If the log file is listening locally, it returns it from the local buffer. If // it is listening from the worker, it blocks until the size is returned. // // Returns a promise: // - Resolves to the current file size (int). -func (l *Logger) SizeJS(js.Value, []js.Value) any { +func (l *LoggerJS) Size(js.Value, []js.Value) any { promiseFn := func(resolve, _ func(args ...any) js.Value) { - resolve(l.Size()) + resolve(l.api.Size()) } return utils.CreatePromise(promiseFn) } -// WorkerJS returns the web worker object. +// Worker returns the web worker object. // // Returns: // - Javascript worker object. If the worker has not been initialized, it // returns null. -func (l *Logger) WorkerJS(js.Value, []js.Value) any { - if l.getMode() == workerMode { - return l.wm.GetWorker() +func (l *LoggerJS) Worker(js.Value, []js.Value) any { + wm := l.api.Worker() + if wm == nil { + return js.Null() } - return js.Null() + return wm.GetWorker() } diff --git a/logging/logger_test.go b/logging/logger_test.go deleted file mode 100644 index 0b5267be9cabaf995ddc8fcdba4b5d3ec8eea133..0000000000000000000000000000000000000000 --- a/logging/logger_test.go +++ /dev/null @@ -1,172 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -//go:build js && wasm - -package logging - -import ( - "bytes" - "fmt" - jww "github.com/spf13/jwalterweatherman" - "testing" -) - -// Tests InitLogger -func TestInitLogger(t *testing.T) { -} - -// Tests GetLogger -func TestGetLogger(t *testing.T) { -} - -// Tests NewLogger -func TestNewLogger(t *testing.T) { -} - -// Tests Logger.LogToFile -func TestLogger_LogToFile(t *testing.T) { - jww.SetStdoutThreshold(jww.LevelTrace) - l := NewLogger() - - err := l.LogToFile(jww.LevelTrace, 50000000) - if err != nil { - t.Fatalf("Failed to LogToFile: %+v", err) - } - - jww.INFO.Printf("test") - - file := l.cb.Bytes() - fmt.Printf("file:----------------------------\n%s\n---------------------------------\n", file) -} - -// Tests Logger.LogToFileWorker -func TestLogger_LogToFileWorker(t *testing.T) { -} - -// Tests Logger.processLog -func TestLogger_processLog(t *testing.T) { -} - -// Tests Logger.prepare -func TestLogger_prepare(t *testing.T) { -} - -// Tests Logger.StopLogging -func TestLogger_StopLogging(t *testing.T) { -} - -// Tests Logger.GetFile -func TestLogger_GetFile(t *testing.T) { -} - -// Tests Logger.Threshold -func TestLogger_Threshold(t *testing.T) { -} - -// Tests Logger.MaxSize -func TestLogger_MaxSize(t *testing.T) { -} - -// Tests Logger.Size -func TestLogger_Size(t *testing.T) { -} - -// Tests Logger.Listen -func TestLogger_Listen(t *testing.T) { - - // l := newLogger() - -} - -// Tests that Logger.Write can fill the listenChan channel completely and that -// all messages are received in the order they were added. -func TestLogger_Write(t *testing.T) { - l := newLogger() - expectedLogs := make([][]byte, logListenerChanSize) - - for i := range expectedLogs { - p := []byte( - fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize)) - expectedLogs[i] = p - n, err := l.Listen(jww.LevelError).Write(p) - if err != nil { - t.Errorf("Received impossible error (%d): %+v", i, err) - } else if n != len(p) { - t.Errorf("Received incorrect bytes written (%d)."+ - "\nexpected: %d\nreceived: %d", i, len(p), n) - } - } - - for i, expected := range expectedLogs { - select { - case received := <-l.listenChan: - if !bytes.Equal(expected, received) { - t.Errorf("Received unexpected meessage (%d)."+ - "\nexpected: %q\nreceived: %q", i, expected, received) - } - default: - t.Errorf("Failed to read from channel.") - } - } -} - -// Error path: Tests that Logger.Write returns an error when the listener -// channel is full. -func TestLogger_Write_ChannelFilledError(t *testing.T) { - l := newLogger() - expectedLogs := make([][]byte, logListenerChanSize) - - for i := range expectedLogs { - p := []byte( - fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize)) - expectedLogs[i] = p - n, err := l.Listen(jww.LevelError).Write(p) - if err != nil { - t.Errorf("Received impossible error (%d): %+v", i, err) - } else if n != len(p) { - t.Errorf("Received incorrect bytes written (%d)."+ - "\nexpected: %d\nreceived: %d", i, len(p), n) - } - } - - _, err := l.Write([]byte("test")) - if err == nil { - t.Error("Failed to receive error when the chanel should be full.") - } -} - -// Tests that Logger.getMode gets the same value set with Logger.setMode. -func TestLogger_setMode_getMode(t *testing.T) { - l := newLogger() - - for i, m := range []mode{initMode, fileMode, workerMode, 12} { - l.setMode(m) - received := l.getMode() - if m != received { - t.Errorf("Received wrong mode (%d).\nexpected: %s\nreceived: %s", - i, m, received) - } - } - -} - -// Unit test of mode.String. -func Test_mode_String(t *testing.T) { - for m, expected := range map[mode]string{ - initMode: "uninitialized mode", - fileMode: "file mode", - workerMode: "worker mode", - 12: "invalid mode: 12", - } { - s := m.String() - if s != expected { - t.Errorf("Wrong string for mode %d.\nexpected: %s\nreceived: %s", - m, expected, s) - } - } -} diff --git a/logging/workerLogger.go b/logging/workerLogger.go new file mode 100644 index 0000000000000000000000000000000000000000..bfac4703943c94956a8dc6501b722e7d0e5cad62 --- /dev/null +++ b/logging/workerLogger.go @@ -0,0 +1,162 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "encoding/binary" + "encoding/json" + "io" + "math" + "time" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// TODO: add ability to import worker so that multiple threads can send logs: https://stackoverflow.com/questions/8343781/how-to-do-worker-to-worker-communication + +// workerLogger manages the recording of jwalterweatherman logs to the in-memory +// file buffer in a remote Worker thread. +type workerLogger struct { + threshold jww.Threshold + maxLogFileSize int + wm *worker.Manager +} + +// newWorkerLogger starts logging to an in-memory log file in a remote Worker +// at the specified threshold. Returns a [workerLogger] that can be used to get +// the log file. +func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, + wasmJsPath, workerName string) (*workerLogger, error) { + // Create new worker manager, which will start the worker and wait until + // communication has been established + wm, err := worker.NewManager(wasmJsPath, workerName, false) + if err != nil { + return nil, err + } + + wl := &workerLogger{ + threshold: threshold, + maxLogFileSize: maxLogFileSize, + wm: wm, + } + + // Register the callback used by the Javascript to request the log file. + // This prevents an error print when GetFileExtTag is not registered. + wl.wm.RegisterCallback(GetFileExtTag, func([]byte) { + jww.DEBUG.Print("[LOG] Received file requested from external " + + "Javascript. Ignoring file.") + }) + + data, err := json.Marshal(wl.maxLogFileSize) + if err != nil { + return nil, err + } + + // Send message to initialize the log file listener + errChan := make(chan error) + wl.wm.SendMessage(NewLogFileTag, data, func(data []byte) { + if len(data) > 0 { + errChan <- errors.New(string(data)) + } else { + errChan <- nil + } + }) + + // Wait for worker to respond + select { + case err = <-errChan: + if err != nil { + return nil, err + } + case <-time.After(worker.ResponseTimeout): + return nil, errors.Errorf("timed out after %s waiting for new log "+ + "file in worker to initialize", worker.ResponseTimeout) + } + + jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+ + "%s using web worker %s", wl.maxLogFileSize, wl.threshold, workerName) + + logger = wl + return wl, nil +} + +// Write adheres to the io.Writer interface and sends the log entries to the +// worker to be added to the file buffer. Always returns the length of p and +// nil. All errors are printed to the log. +func (wl *workerLogger) Write(p []byte) (n int, err error) { + wl.wm.SendMessage(WriteLogTag, p, nil) + return len(p), nil +} + +// Listen adheres to the [jwalterweatherman.LogListener] type and returns the +// log writer when the threshold is within the set threshold limit. +func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer { + if threshold < wl.threshold { + return nil + } + return wl +} + +// StopLogging stops log message writes and terminates the worker. Once logging +// is stopped, it cannot be resumed and the log file cannot be recovered. +func (wl *workerLogger) StopLogging() { + wl.threshold = math.MaxInt + + wl.wm.Stop() + jww.DEBUG.Printf("[LOG] Terminated log worker.") +} + +// GetFile returns the entire log file. +func (wl *workerLogger) GetFile() []byte { + fileChan := make(chan []byte) + wl.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) + + select { + case file := <-fileChan: + return file + case <-time.After(worker.ResponseTimeout): + jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ + "file from worker", worker.ResponseTimeout) + return nil + } +} + +// Threshold returns the log level threshold used in the file. +func (wl *workerLogger) Threshold() jww.Threshold { + return wl.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (wl *workerLogger) MaxSize() int { + return wl.maxLogFileSize +} + +// Size returns the number of bytes written to the log file. +func (wl *workerLogger) Size() int { + sizeChan := make(chan []byte) + wl.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) + + select { + case data := <-sizeChan: + return int(binary.LittleEndian.Uint64(data)) + case <-time.After(worker.ResponseTimeout): + jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ + "file size from worker", worker.ResponseTimeout) + return 0 + } +} + +// Worker returns the manager for the Javascript Worker object. +func (wl *workerLogger) Worker() *worker.Manager { + return wl.wm +} diff --git a/logging/workerThread/logFileWorker.js b/logging/workerThread/logFileWorker.js index 159bfaa0d919a4f0cb2758af48d80c65891e7820..ed246f62563f89645f95fe13c715a81b60aa756b 100644 --- a/logging/workerThread/logFileWorker.js +++ b/logging/workerThread/logFileWorker.js @@ -7,11 +7,15 @@ importScripts('wasm_exec.js'); +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + const go = new Go(); const binPath = 'xxdk-logFileWorker.wasm' -WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); - LogLevel(1); + await isReady; }).catch((err) => { console.error(err); }); \ No newline at end of file diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go index 91b059f3da1195b0ff244027608a0f8a98482fed..1a9a31a0ca39ee684b4427eef03fc89f4a407b8c 100644 --- a/logging/workerThread/main.go +++ b/logging/workerThread/main.go @@ -13,25 +13,21 @@ import ( "encoding/binary" "encoding/json" "fmt" + "os" + "syscall/js" + "github.com/armon/circbuf" "github.com/pkg/errors" + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" - "syscall/js" ) // SEMVER is the current semantic version of the xxDK Logger web worker. const SEMVER = "0.1.0" -func init() { - // Set up Javascript console listener set at level INFO - ll := logging.NewJsConsoleLogListener(jww.LevelDebug) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER) -} - // workerLogFile manages communication with the main thread and writing incoming // logging messages to the log file. type workerLogFile struct { @@ -40,17 +36,60 @@ type workerLogFile struct { } func main() { - jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + LoggerCmd.SetArgs(os.Args) + + err := LoggerCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +var LoggerCmd = &cobra.Command{ + Use: "Logger", + Short: "Web worker buffer file logger", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf( + "Failed to intialize logging in logging worker: %+v", err) + os.Exit(1) + } - js.Global().Set("LogLevel", js.FuncOf(logging.LogLevelJS)) + jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER) - wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} + jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") - wlf.registerCallbacks() + wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} - wlf.wtm.SignalReady() - <-make(chan bool) - fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.") + wlf.registerCallbacks() + + wlf.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + LoggerCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } // registerCallbacks registers all the necessary callbacks for the main thread diff --git a/main.go b/main.go index b88042e2c8e27c2c16aae2f3f9ec80c554c8506c..8ee45ddf9f458caa6f888d05887d4f5268ea1e64 100644 --- a/main.go +++ b/main.go @@ -10,38 +10,75 @@ package main import ( - "gitlab.com/elixxir/xxdk-wasm/logging" + "fmt" + "github.com/spf13/cobra" "os" "syscall/js" jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/elixxir/xxdk-wasm/wasm" ) -func init() { - // Start logger first to capture all logging events - logging.InitLogger() - - // Overwrites setting the log level to INFO done in bindings so that the - // Javascript console can be used - ll := logging.NewJsConsoleLogListener(jww.LevelInfo) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + wasmCmd.SetArgs(os.Args) - // Check that the WASM binary version is correct - err := storage.CheckAndStoreVersions() + err := wasmCmd.Execute() if err != nil { - jww.FATAL.Panicf("WASM binary version error: %+v", err) + fmt.Println(err) + os.Exit(1) } } -func main() { - jww.INFO.Printf("Starting xxDK WebAssembly bindings.") +var wasmCmd = &cobra.Command{ + Use: "xxdk-wasm", + Short: "WebAssembly bindings for xxDK.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, fileLogLevel, maxLogFileSizeMB, + workerScriptURL, workerName) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + // Check that the WASM binary version is correct + err = storage.CheckAndStoreVersions() + if err != nil { + jww.FATAL.Panicf("WASM binary version error: %+v", err) + } + + // Enable all top level bindings functions + setGlobals() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller, as shown below: + // + // let isReady = new Promise((resolve) => { + // window.onWasmInitialized = resolve; + // }); + // + // const go = new Go(); + // go.run(result.instance); + // await isReady; + // + // Source: https://github.com/golang/go/issues/49710#issuecomment-986484758 + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + os.Exit(0) + }, +} - // logging/worker.go - js.Global().Set("GetLogger", js.FuncOf(logging.GetLoggerJS)) +// setGlobals enables all global functions to be accessible to Javascript. +func setGlobals() { + jww.INFO.Printf("Starting xxDK WebAssembly bindings.") // storage/password.go js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword)) @@ -89,9 +126,15 @@ func main() { js.Global().Set("GetShareUrlType", js.FuncOf(wasm.GetShareUrlType)) js.Global().Set("ValidForever", js.FuncOf(wasm.ValidForever)) js.Global().Set("IsNicknameValid", js.FuncOf(wasm.IsNicknameValid)) + js.Global().Set("GetNoMessageErr", js.FuncOf(wasm.GetNoMessageErr)) + js.Global().Set("CheckNoMessageErr", js.FuncOf(wasm.CheckNoMessageErr)) js.Global().Set("NewChannelsDatabaseCipher", js.FuncOf(wasm.NewChannelsDatabaseCipher)) + // wasm/dm.go + js.Global().Set("InitChannelsFileTransfer", + js.FuncOf(wasm.InitChannelsFileTransfer)) + // wasm/dm.go js.Global().Set("NewDMClient", js.FuncOf(wasm.NewDMClient)) js.Global().Set("NewDMClientWithIndexedDb", @@ -118,6 +161,7 @@ func main() { // wasm/emoji.go js.Global().Set("SupportedEmojis", js.FuncOf(wasm.SupportedEmojis)) + js.Global().Set("SupportedEmojisMap", js.FuncOf(wasm.SupportedEmojisMap)) js.Global().Set("ValidateReaction", js.FuncOf(wasm.ValidateReaction)) // wasm/errors.go @@ -150,7 +194,6 @@ func main() { js.FuncOf(wasm.GetFactsFromContact)) // wasm/logging.go - js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter)) js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs)) @@ -211,7 +254,32 @@ func main() { js.Global().Set("GetClientDependencies", js.FuncOf(wasm.GetClientDependencies)) js.Global().Set("GetWasmSemanticVersion", js.FuncOf(wasm.GetWasmSemanticVersion)) js.Global().Set("GetXXDKSemanticVersion", js.FuncOf(wasm.GetXXDKSemanticVersion)) +} + +var ( + logLevel, fileLogLevel jww.Threshold + maxLogFileSizeMB int + workerScriptURL, workerName string +) - <-make(chan bool) - os.Exit(0) +func init() { + // Initialize all startup flags + wasmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + wasmCmd.Flags().IntVarP((*int)(&fileLogLevel), "fileLogLevel", "m", -1, + "The log level when outputting to the file buffer. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + wasmCmd.Flags().IntVarP(&maxLogFileSizeMB, "maxLogFileSize", "s", 5, + "Max file size, in MB, for the file buffer before it rolls over "+ + "and starts overwriting the oldest entries.") + wasmCmd.Flags().StringVarP(&workerScriptURL, "workerScriptURL", "w", "", + "URL to the script that executes the worker. If set, it enables the "+ + "saving of log file to buffer in Worker instead of in the local "+ + "thread. This allows logging to be available after the main WASM "+ + "thread crashes.") + wasmCmd.Flags().StringVar(&workerName, "workerName", "xxdkLogFileWorker", + "Name of the logger worker.") } diff --git a/storage/version.go b/storage/version.go index 0406927a3a4598f3bb3c79ae069af0c8b68e0b01..b0f192ab24f8ad80d1fb9a9a5c96177f67545440 100644 --- a/storage/version.go +++ b/storage/version.go @@ -20,7 +20,7 @@ import ( ) // SEMVER is the current semantic version of xxDK WASM. -const SEMVER = "0.3.1" +const SEMVER = "0.3.3" // Storage keys. const ( diff --git a/utils/utils.go b/utils/utils.go index 3e4408019c48b6e0a7af1640d82b4968b668ab63..28ea6102a7e8239813cb88fb3028ab7378d182dc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -61,10 +61,12 @@ type PromiseFn func(resolve, reject func(args ...any) js.Value) // Go function to Javascript. func CreatePromise(f PromiseFn) any { // Create handler for promise (this will be a Javascript function) - handler := js.FuncOf(func(this js.Value, args []js.Value) any { + var handler js.Func + handler = js.FuncOf(func(this js.Value, args []js.Value) any { // Spawn a new go routine to perform the blocking function go func(resolve, reject js.Value) { f(resolve.Invoke, reject.Invoke) + go func() { handler.Release() }() }(args[0], args[1]) return nil diff --git a/wasm/channels.go b/wasm/channels.go index a775928eb820439bdb1a4739d8011b1b952cf7de..307fe5cee304ff580ba1726b8036ce81268cc991 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -256,7 +256,11 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any { // using [Cmix.GetID]. // - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is // generated by [GenerateChannelIdentity] (Uint8Array). -// - args[2] - A function that initialises and returns a Javascript object +// - args[2] - JSON of an array of integers of [channels.ExtensionBuilder] +// IDs. The ID can be retrieved from an object with an extension builder +// (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not +// using extension builders. Example: `[2,11,5]` (Uint8Array). +// - args[3] - A function that initialises and returns a Javascript object // that matches the [bindings.EventModel] interface. The function must match // the Build function in [bindings.EventModelBuilder]. // @@ -264,10 +268,13 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any { // - Javascript representation of the [ChannelsManager] object. // - Throws a TypeError if creating the manager fails. func NewChannelsManager(_ js.Value, args []js.Value) any { + cmixId := args[0].Int() privateIdentity := utils.CopyBytesToGo(args[1]) - em := newEventModelBuilder(args[2]) + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[2]) + em := newEventModelBuilder(args[3]) - cm, err := bindings.NewChannelsManager(args[0].Int(), privateIdentity, em) + cm, err := bindings.NewChannelsManager( + cmixId, privateIdentity, extensionBuilderIDsJSON, em) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -324,7 +331,11 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any { // - args[1] - Path to Javascript file that starts the worker (string). // - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is // generated by [GenerateChannelIdentity] (Uint8Array). -// - args[3] - The received message callback, which is called everytime a +// - args[3] - JSON of an array of integers of [channels.ExtensionBuilder] +// IDs. The ID can be retrieved from an object with an extension builder +// (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not +// using extension builders. Example: `[2,11,5]` (Uint8Array). +// - args[4] - The received message callback, which is called everytime a // message is added or changed in the database. It is a function that takes // in the same parameters as [channels.MessageReceivedCallback]. On the // Javascript side, the UUID is returned as an int and the channelID as a @@ -332,15 +343,15 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any { // the UUID. The channel ID is provided so that the recipient can filter if // they want to the processes the update now or not. An "update" bool is // present which tells you if the row is new or if it is an edited old row. -// - args[4] - The deleted message callback, which is called everytime a +// - args[5] - The deleted message callback, which is called everytime a // message is deleted from the database. It is a function that takes in the // same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript // side, the message ID is returned as a Uint8Array. -// - args[5] - The muted user callback, which is called everytime a user is +// - args[6] - The muted user callback, which is called everytime a user is // muted or unmuted. It is a function that takes in the same parameters as // [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and // user public key are returned as Uint8Array. -// - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this +// - args[7] - ID of [ChannelDbCipher] object in tracker (int). Create this // object with [NewChannelsDatabaseCipher] and get its id with // [ChannelDbCipher.GetID]. // @@ -352,10 +363,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { cmixID := args[0].Int() wasmJsPath := args[1].String() privateIdentity := utils.CopyBytesToGo(args[2]) - messageReceivedCB := args[3] - deletedMessageCB := args[4] - mutedUserCB := args[5] - cipherID := args[6].Int() + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + messageReceivedCB := args[4] + deletedMessageCB := args[5] + mutedUserCB := args[6] + cipherID := args[7].Int() cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID) if err != nil { @@ -363,7 +375,8 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { } return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity, - messageReceivedCB, deletedMessageCB, mutedUserCB, cipher) + extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB, + mutedUserCB, cipher) } // NewChannelsManagerWithIndexedDbUnsafe creates a new [ChannelsManager] from a @@ -384,7 +397,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { // - args[1] - Path to Javascript file that starts the worker (string). // - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is // generated by [GenerateChannelIdentity] (Uint8Array). -// - args[3] - The received message callback, which is called everytime a +// - args[3] - JSON of an array of integers of [channels.ExtensionBuilder] +// IDs. The ID can be retrieved from an object with an extension builder +// (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not +// using extension builders. Example: `[2,11,5]` (Uint8Array). +// - args[4] - The received message callback, which is called everytime a // message is added or changed in the database. It is a function that takes // in the same parameters as [indexedDb.MessageReceivedCallback]. On the // Javascript side, the UUID is returned as an int and the channelID as a @@ -392,11 +409,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { // the UUID. The channel ID is provided so that the recipient can filter if // they want to the processes the update now or not. An "update" bool is // present which tells you if the row is new or if it is an edited old row. -// - args[4] - The deleted message callback, which is called everytime a +// - args[5] - The deleted message callback, which is called everytime a // message is deleted from the database. It is a function that takes in the // same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript // side, the message ID is returned as a Uint8Array. -// - args[5] - The muted user callback, which is called everytime a user is +// - args[6] - The muted user callback, which is called everytime a user is // muted or unmuted. It is a function that takes in the same parameters as // [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and // user public key are returned as Uint8Array. @@ -410,17 +427,19 @@ func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any { cmixID := args[0].Int() wasmJsPath := args[1].String() privateIdentity := utils.CopyBytesToGo(args[2]) - messageReceivedCB := args[3] - deletedMessageCB := args[4] - mutedUserCB := args[5] + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + messageReceivedCB := args[4] + deletedMessageCB := args[5] + mutedUserCB := args[6] return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity, - messageReceivedCB, deletedMessageCB, mutedUserCB, nil) + extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB, + mutedUserCB, nil) } func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string, - privateIdentity []byte, messageReceivedCB, deletedMessageCB, mutedUserCB js.Value, - cipher *bindings.ChannelDbCipher) any { + privateIdentity, extensionBuilderIDsJSON []byte, messageReceivedCB, + deletedMessageCB, mutedUserCB js.Value, cipher *bindings.ChannelDbCipher) any { messageReceived := func(uuid uint64, channelID *id.ID, update bool) { messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) @@ -440,7 +459,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string, promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.NewChannelsManagerGoEventModel( - cmixID, privateIdentity, model) + cmixID, privateIdentity, extensionBuilderIDsJSON, model) if err != nil { reject(utils.JsTrace(err)) } else { @@ -577,7 +596,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string, promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.LoadChannelsManagerGoEventModel( - cmixID, storageTag, model) + cmixID, storageTag, model, nil) if err != nil { reject(utils.JsTrace(err)) } else { @@ -1747,6 +1766,30 @@ func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) a // Event Model Logic // //////////////////////////////////////////////////////////////////////////////// +// GetNoMessageErr returns the error channels.NoMessageErr, which must be +// returned by EventModel methods (such as EventModel.UpdateFromUUID, +// EventModel.UpdateFromMessageID, and EventModel.GetMessage) when the message +// cannot be found. +// +// Returns: +// - channels.NoMessageErr error message (string). +func GetNoMessageErr(js.Value, []js.Value) any { + return bindings.GetNoMessageErr() +} + +// CheckNoMessageErr determines if the error returned by an EventModel function +// indicates that the message or item does not exist. It returns true if the +// error contains channels.NoMessageErr. +// +// Parameters: +// - args[0] - Error to check (Error). +// +// Returns +// - True if the error contains channels.NoMessageErr (boolean). +func CheckNoMessageErr(_ js.Value, args []js.Value) any { + return bindings.CheckNoMessageErr(utils.JsErrorToJson(args[0])) +} + // eventModelBuilder adheres to the [bindings.EventModelBuilder] interface. type eventModelBuilder struct { build func(args ...any) js.Value @@ -1944,13 +1987,22 @@ func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte, // - uuid - The unique identifier of the message in the database (int). // - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo] // (Uint8Array). -func (em *eventModel) UpdateFromUUID(uuid int64, messageUpdateInfoJSON []byte) { - em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON)) +// +// Returns: +// - Returns an error if the message cannot be updated. It must return the +// error from [GetNoMessageErr] if the message does not exist. +func (em *eventModel) UpdateFromUUID( + uuid int64, messageUpdateInfoJSON []byte) error { + err := em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON)) + return js.Error{Value: err} } // UpdateFromMessageID is called whenever a message with the message ID is // modified. // +// Note for developers: The internal Javascript function must return JSON of +// [UuidAndError], which includes the returned UUID or an error. +// // Parameters: // - messageID - The bytes of the [channel.MessageID] of the received message // (Uint8Array). @@ -1960,16 +2012,31 @@ func (em *eventModel) UpdateFromUUID(uuid int64, messageUpdateInfoJSON []byte) { // Returns: // - A non-negative unique uuid for the modified message by which it can be // referenced later with [EventModel.UpdateFromUUID] int). +// - Returns an error if the message cannot be updated. It must return the +// error from [GetNoMessageErr] if the message does not exist. func (em *eventModel) UpdateFromMessageID( - messageID []byte, messageUpdateInfoJSON []byte) int64 { - return int64(em.updateFromMessageID(utils.CopyBytesToJS(messageID), - utils.CopyBytesToJS(messageUpdateInfoJSON)).Int()) + messageID []byte, messageUpdateInfoJSON []byte) (int64, error) { + uuidAndErrorBytes := utils.CopyBytesToGo(em.updateFromMessageID( + utils.CopyBytesToJS(messageID), + utils.CopyBytesToJS(messageUpdateInfoJSON))) + + var uae UuidAndError + err := json.Unmarshal(uuidAndErrorBytes, &uae) + if err != nil { + return 0, err + } + + if uae.Error != "" { + return 0, errors.New(uae.Error) + } + + return uae.UUID, nil } // GetMessage returns the message with the given [channel.MessageID]. // // Note for developers: The internal Javascript function must return JSON of -// MessageAndError, which includes the returned [channels.ModelMessage] or any +// [MessageAndError], which includes the returned [channels.ModelMessage] or any // error that occurs during lookup. // // Parameters: @@ -2020,6 +2087,21 @@ func (em *eventModel) MuteUser(channelID, pubkey []byte, unmute bool) { utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(pubkey), unmute) } +// UuidAndError contains a UUID returned by an eventModel method or any possible +// error that occurs. Only one field should be present at a time. +// +// Example JSON: +// +// { "uuid": 5, } +// +// Or: +// +// { "error": "An error occurred." } +type UuidAndError struct { + UUID int64 `json:"uuid,omitempty"` + Error string `json:"error,omitempty"` +} + // MessageAndError contains a message returned by eventModel.GetMessage or any // possible error that occurs during lookup. Only one field should be present at // a time; if an error occurs, ModelMessage should be empty. diff --git a/wasm/channelsFileTransfer.go b/wasm/channelsFileTransfer.go new file mode 100644 index 0000000000000000000000000000000000000000..5155e4ad4668bef0f3ad52f52efec748e7dbb0ff --- /dev/null +++ b/wasm/channelsFileTransfer.go @@ -0,0 +1,569 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package wasm + +import ( + "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/xxdk-wasm/utils" + "syscall/js" +) + +// ChannelsFileTransfer wraps the [bindings.ChannelsFileTransfer] object so its +// methods can be wrapped to be Javascript compatible. +type ChannelsFileTransfer struct { + api *bindings.ChannelsFileTransfer +} + +// newChannelsFileTransferJS creates a new Javascript compatible object +// (map[string]any) that matches the [ChannelsFileTransfer] structure. +func newChannelsFileTransferJS(api *bindings.ChannelsFileTransfer) map[string]any { + cft := ChannelsFileTransfer{api} + channelsFileTransferMap := map[string]any{ + "GetExtensionBuilderID": js.FuncOf(cft.GetExtensionBuilderID), + "MaxFileNameLen": js.FuncOf(cft.MaxFileNameLen), + "MaxFileTypeLen": js.FuncOf(cft.MaxFileTypeLen), + "MaxFileSize": js.FuncOf(cft.MaxFileSize), + "MaxPreviewSize": js.FuncOf(cft.MaxPreviewSize), + + // Uploading/Sending + "Upload": js.FuncOf(cft.Upload), + "Send": js.FuncOf(cft.Send), + "RegisterSentProgressCallback": js.FuncOf(cft.RegisterSentProgressCallback), + "RetryUpload": js.FuncOf(cft.RetryUpload), + "CloseSend": js.FuncOf(cft.CloseSend), + + // Downloading + "Download": js.FuncOf(cft.Download), + "RegisterReceivedProgressCallback": js.FuncOf(cft.RegisterReceivedProgressCallback), + } + + return channelsFileTransferMap +} + +// InitChannelsFileTransfer creates a file transfer manager for channels. +// +// Parameters: +// - args[0] - ID of [E2e] object in tracker (int). +// - args[1] - JSON of [channelsFileTransfer.Params] (Uint8Array). +// +// Returns: +// - New [ChannelsFileTransfer] object. +// +// Returns a promise: +// - Resolves to a Javascript representation of the [ChannelsFileTransfer] +// object. +// - Rejected with an error if creating the file transfer object fails. +func InitChannelsFileTransfer(_ js.Value, args []js.Value) any { + e2eID := args[0].Int() + paramsJson := utils.CopyBytesToGo(args[1]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + cft, err := bindings.InitChannelsFileTransfer(e2eID, paramsJson) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(newChannelsFileTransferJS(cft)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// GetExtensionBuilderID returns the ID of the extension builder in the tracker. +// Pass this ID into the channel manager creator to use file transfer manager in +// conjunction with channels. +// +// Returns: +// - Extension builder ID (int). +func (cft *ChannelsFileTransfer) GetExtensionBuilderID(js.Value, []js.Value) any { + return cft.api.GetExtensionBuilderID() +} + +// MaxFileNameLen returns the max number of bytes allowed for a file name. +// +// Returns: +// - Max number of bytes (int). +func (cft *ChannelsFileTransfer) MaxFileNameLen(js.Value, []js.Value) any { + return cft.api.MaxFileNameLen() +} + +// MaxFileTypeLen returns the max number of bytes allowed for a file type. +// +// Returns: +// - Max number of bytes (int). +func (cft *ChannelsFileTransfer) MaxFileTypeLen(js.Value, []js.Value) any { + return cft.api.MaxFileNameLen() +} + +// MaxFileSize returns the max number of bytes allowed for a file. +// +// Returns: +// - Max number of bytes (int). +func (cft *ChannelsFileTransfer) MaxFileSize(js.Value, []js.Value) any { + return cft.api.MaxFileSize() +} + +// MaxPreviewSize returns the max number of bytes allowed for a file preview. +// +// Returns: +// - Max number of bytes (int). +func (cft *ChannelsFileTransfer) MaxPreviewSize(js.Value, []js.Value) any { + return cft.api.MaxFileSize() +} + +//////////////////////////////////////////////////////////////////////////////// +// Uploading/Sending // +//////////////////////////////////////////////////////////////////////////////// + +// Upload starts uploading the file to a new ID that can be sent to the +// specified channel when complete. To get progress information about the +// upload, a [bindings.FtSentProgressCallback] must be registered. All errors +// returned on the callback are fatal and the user must take action to either +// [ChannelsFileTransfer.RetryUpload] or [ChannelsFileTransfer.CloseSend]. +// +// The file is added to the event model at the returned file ID with the status +// [channelsFileTransfer.Uploading]. Once the upload is complete, the file link +// is added to the event model with the status [channelsFileTransfer.Complete]. +// +// The [bindings.FtSentProgressCallback] only indicates the progress of the file +// upload, not the status of the file in the event model. You must rely on +// updates from the event model to know when it can be retrieved. +// +// Parameters: +// - args[0] - File contents. Max size defined by +// [ChannelsFileTransfer.MaxFileSize] (Uint8Array). +// - args[1] - The number of sending retries allowed on send failure (e.g. a +// retry of 2.0 with 6 parts means 12 total possible sends) (float). +// - args[2] - The progress callback, which is a callback that reports the +// progress of the file upload. The callback is called once on +// initialization, on every progress update (or less if restricted by the +// period), or on fatal error. It must be a Javascript object that +// implements the [bindings.FtSentProgressCallback] interface. +// - args[3] - Progress callback period. A progress callback will be limited +// from triggering only once per period, in milliseconds (int). +// +// Returns a promise: +// - Resolves to the marshalled bytes of [fileTransfer.ID] that uniquely +// identifies the file (Uint8Array). +// - Rejected with an error if initiating the upload fails. +func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any { + var ( + fileData = utils.CopyBytesToGo(args[0]) + retry = float32(args[1].Float()) + progressCB = &ftSentCallback{utils.WrapCB(args[2], "Callback")} + period = args[3].Int() + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + fileID, err := cft.api.Upload(fileData, retry, progressCB, period) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(fileID)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// Send sends the specified file info to the channel. Once a file is uploaded +// via [ChannelsFileTransfer.Upload], its file info (found in the event model) +// can be sent to any channel. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel's [id.ID] to send the file to +// (Uint8Array). +// - args[1] - JSON of [channelsFileTransfer.FileLink] stored in the event +// model (Uint8Array). +// - args[2] - Human-readable file name. Max length defined by +// [ChannelsFileTransfer.MaxFileNameLen] (string). +// - args[3] - Shorthand that identifies the type of file. Max length defined +// by [ChannelsFileTransfer.MaxFileTypeLen] (string). +// - args[4] - A preview of the file data (e.g. a thumbnail). Max size defined +// by [ChannelsFileTransfer.MaxPreviewSize] (Uint8Array). +// - args[5] - The duration, in milliseconds, that the file is available in +// the channel (int). For the maximum amount of time, use [ValidForever]. +// - args[6] - JSON of [xxdk.CMIXParams] (Uint8Array). If left empty, +// [GetDefaultCMixParams] will be used internally. +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any { + var ( + channelIdBytes = utils.CopyBytesToGo(args[0]) + fileLinkJSON = utils.CopyBytesToGo(args[1]) + fileName = args[2].String() + fileType = args[3].String() + preview = utils.CopyBytesToGo(args[4]) + validUntilMS = args[5].Int() + cmixParamsJSON = utils.CopyBytesToGo(args[6]) + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, fileName, + fileType, preview, validUntilMS, cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(fileID)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// RegisterSentProgressCallback allows for the registration of a callback to +// track the progress of an individual file upload. A +// [bindings.FtSentProgressCallback] is auto-registered on +// [ChannelsFileTransfer.Send]; this function should be called when resuming +// clients or registering extra callbacks. +// +// The callback will be called immediately when added to report the current +// progress of the transfer. It will then call every time a file part arrives, +// the transfer completes, or a fatal error occurs. It is called at most once +// every period regardless of the number of progress updates. +// +// In the event that the client is closed and resumed, this function must be +// used to re-register any callbacks previously registered with this function or +// [ChannelsFileTransfer.Send]. +// +// The [bindings.FtSentProgressCallback] only indicates the progress of the file +// upload, not the status of the file in the event model. You must rely on +// updates from the event model to know when it can be retrieved. +// +// Parameters: +// - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array). +// - args[1] - The progress callback, which is a callback that reports the +// progress of the file upload. The callback is called once on +// initialization, on every progress update (or less if restricted by the +// period), or on fatal error. It must be a Javascript object that +// implements the [bindings.FtSentProgressCallback] interface. +// - args[2] - Progress callback period. A progress callback will be limited +// from triggering only once per period, in milliseconds (int). +// +// Returns a promise: +// - Resolves on success (void). +// - Rejected with an error if registering the callback fails. +func (cft *ChannelsFileTransfer) RegisterSentProgressCallback( + _ js.Value, args []js.Value) any { + var ( + fileIDBytes = utils.CopyBytesToGo(args[0]) + progressCB = &ftSentCallback{utils.WrapCB(args[1], "Callback")} + periodMS = args[2].Int() + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := cft.api.RegisterSentProgressCallback( + fileIDBytes, progressCB, periodMS) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// RetryUpload retries uploading a failed file upload. Returns an error if the +// transfer has not failed. +// +// This function should be called once a transfer errors out (as reported by the +// progress callback). +// +// A new progress callback must be registered on retry. Any previously +// registered callbacks are defunct when the upload fails. +// +// Parameters: +// - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array). +// - args[1] - The progress callback, which is a callback that reports the +// progress of the file upload. The callback is called once on +// initialization, on every progress update (or less if restricted by the +// period), or on fatal error. It must be a Javascript object that +// implements the [bindings.FtSentProgressCallback] interface. +// - args[2] - Progress callback period. A progress callback will be limited +// from triggering only once per period, in milliseconds (int). +// +// Returns a promise: +// - Resolves on success (void). +// - Rejected with an error if registering retrying the upload fails. +func (cft *ChannelsFileTransfer) RetryUpload(_ js.Value, args []js.Value) any { + var ( + fileIDBytes = utils.CopyBytesToGo(args[0]) + progressCB = &ftSentCallback{utils.WrapCB(args[1], "Callback")} + periodMS = args[2].Int() + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := cft.api.RetryUpload(fileIDBytes, progressCB, periodMS) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// CloseSend deletes a file from the internal storage once a transfer has +// completed or reached the retry limit. If neither of those condition are met, +// an error is returned. +// +// This function should be called once a transfer completes or errors out (as +// reported by the progress callback). +// +// Parameters: +// - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array). +// +// Returns a promise: +// - Resolves on success (void). +// - Rejected with an error if the file has not failed or completed or if +// closing failed. +func (cft *ChannelsFileTransfer) CloseSend(_ js.Value, args []js.Value) any { + fileIDBytes := utils.CopyBytesToGo(args[0]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := cft.api.CloseSend(fileIDBytes) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +//////////////////////////////////////////////////////////////////////////////// +// Download // +//////////////////////////////////////////////////////////////////////////////// + +// Download begins the download of the file described in the marshalled +// [channelsFileTransfer.FileInfo]. The progress of the download is reported on +// the [bindings.FtReceivedProgressCallback]. +// +// Once the download completes, the file will be stored in the event model with +// the given file ID and with the status [channels.ReceptionProcessingComplete]. +// +// The [bindings.FtReceivedProgressCallback] only indicates the progress of the +// file download, not the status of the file in the event model. You must rely +// on updates from the event model to know when it can be retrieved. +// +// Parameters: +// - args[0] - The JSON of [channelsFileTransfer.FileInfo] received on a +// channel (Uint8Array). +// - args[1] - The progress callback, which is a callback that reports the +// progress of the file download. The callback is called once on +// initialization, on every progress update (or less if restricted by the +// period), or on fatal error. It must be a Javascript object that +// implements the [bindings.FtReceivedProgressCallback] interface. +// - args[2] - Progress callback period. A progress callback will be limited +// from triggering only once per period, in milliseconds (int). +// +// Returns: +// - Marshalled bytes of [fileTransfer.ID] that uniquely identifies the file. +// +// Returns a promise: +// - Resolves to the marshalled bytes of [fileTransfer.ID] that uniquely +// identifies the file. (Uint8Array). +// - Rejected with an error if downloading fails. +func (cft *ChannelsFileTransfer) Download(_ js.Value, args []js.Value) any { + var ( + fileInfoJSON = utils.CopyBytesToGo(args[0]) + progressCB = &ftReceivedCallback{utils.WrapCB(args[1], "Callback")} + periodMS = args[2].Int() + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + fileID, err := cft.api.Download(fileInfoJSON, progressCB, periodMS) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(fileID)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// RegisterReceivedProgressCallback allows for the registration of a callback to +// track the progress of an individual file download. +// +// The callback will be called immediately when added to report the current +// progress of the transfer. It will then call every time a file part is +// received, the transfer completes, or a fatal error occurs. It is called at +// most once every period regardless of the number of progress updates. +// +// In the event that the client is closed and resumed, this function must be +// used to re-register any callbacks previously registered. +// +// Once the download completes, the file will be stored in the event model with +// the given file ID and with the status [channelsFileTransfer.Complete]. +// +// The [bindings.FtReceivedProgressCallback] only indicates the progress of the +// file download, not the status of the file in the event model. You must rely +// on updates from the event model to know when it can be retrieved. +// +// Parameters: +// - args[0] - Marshalled bytes of the file's [fileTransfer.ID] (Uint8Array). +// - args[1] - The progress callback, which is a callback that reports the +// progress of the file download. The callback is called once on +// initialization, on every progress update (or less if restricted by the +// period), or on fatal error. It must be a Javascript object that +// implements the [bindings.FtReceivedProgressCallback] interface. +// - args[2] - Progress callback period. A progress callback will be limited +// from triggering only once per period, in milliseconds (int). +// +// Returns a promise: +// - Resolves on success (void). +// - Rejected with an error if registering the callback fails. +func (cft *ChannelsFileTransfer) RegisterReceivedProgressCallback( + _ js.Value, args []js.Value) any { + var ( + fileIDBytes = utils.CopyBytesToGo(args[0]) + progressCB = &ftReceivedCallback{utils.WrapCB(args[1], "Callback")} + periodMS = args[2].Int() + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := cft.api.RegisterReceivedProgressCallback( + fileIDBytes, progressCB, periodMS) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +//////////////////////////////////////////////////////////////////////////////// +// Callbacks // +//////////////////////////////////////////////////////////////////////////////// + +// ftSentCallback wraps Javascript callbacks to adhere to the +// [bindings.FtSentProgressCallback] interface. +type ftSentCallback struct { + callback func(args ...any) js.Value +} + +// Callback is called when the status of the sent file changes. +// +// Parameters: +// - payload - Returns the contents of the message. JSON of +// [bindings.Progress] (Uint8Array). +// - t - Returns a tracker that allows the lookup of the status of any file +// part. It is a Javascript object that matches the functions on +// [FilePartTracker]. +// - err - Returns an error on failure (Error). + +// Callback is called when the progress on a sent file changes or an error +// occurs in the transfer. +// +// The [ChFilePartTracker] can be used to look up the status of individual file +// parts. Note, when completed == true, the [ChFilePartTracker] may be nil. +// +// Any error returned is fatal and the file must either be retried with +// [ChannelsFileTransfer.RetryUpload] or canceled with +// [ChannelsFileTransfer.CloseSend]. +// +// This callback only indicates the status of the file transfer, not the status +// of the file in the event model. Do NOT use this callback as an indicator of +// when the file is available in the event model. +// +// Parameters: +// - payload - JSON of [bindings.FtSentProgress], which describes the progress +// of the current sent transfer. +// - fpt - File part tracker that allows the lookup of the status of +// individual file parts. +// - err - Fatal errors during sending. +func (fsc *ftSentCallback) Callback( + payload []byte, t *bindings.ChFilePartTracker, err error) { + fsc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t), + utils.JsTrace(err)) +} + +// ftReceivedCallback wraps Javascript callbacks to adhere to the +// [bindings.FtReceivedProgressCallback] interface. +type ftReceivedCallback struct { + callback func(args ...any) js.Value +} + +// Callback is called when +// the progress on a received file changes or an error occurs in the transfer. +// +// The [ChFilePartTracker] can be used to look up the status of individual file +// parts. Note, when completed == true, the [ChFilePartTracker] may be nil. +// +// This callback only indicates the status of the file transfer, not the status +// of the file in the event model. Do NOT use this callback as an indicator of +// when the file is available in the event model. +// +// Parameters: +// - payload - JSON of [bindings.FtReceivedProgress], which describes the +// progress of the current received transfer. +// - fpt - File part tracker that allows the lookup of the status of +// individual file parts. +// - err - Fatal errors during receiving. +func (frc *ftReceivedCallback) Callback( + payload []byte, t *bindings.ChFilePartTracker, err error) { + frc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t), + utils.JsTrace(err)) +} + +//////////////////////////////////////////////////////////////////////////////// +// File Part Tracker // +//////////////////////////////////////////////////////////////////////////////// + +// ChFilePartTracker wraps the [bindings.ChFilePartTracker] object so its +// methods can be wrapped to be Javascript compatible. +type ChFilePartTracker struct { + api *bindings.ChFilePartTracker +} + +// newChFilePartTrackerJS creates a new Javascript compatible object +// (map[string]any) that matches the [FilePartTracker] structure. +func newChFilePartTrackerJS(api *bindings.ChFilePartTracker) map[string]any { + fpt := ChFilePartTracker{api} + ftMap := map[string]any{ + "GetPartStatus": js.FuncOf(fpt.GetPartStatus), + "GetNumParts": js.FuncOf(fpt.GetNumParts), + } + + return ftMap +} + +// GetPartStatus returns the status of the file part with the given part number. +// +// The possible values for the status are: +// - 0 < Part does not exist +// - 0 = unsent +// - 1 = arrived (sender has sent a part, and it has arrived) +// - 2 = received (receiver has received a part) +// +// Parameters: +// - args[0] - Index of part (int). +// +// Returns: +// - Part status (int). +func (fpt *ChFilePartTracker) GetPartStatus(_ js.Value, args []js.Value) any { + return fpt.api.GetPartStatus(args[0].Int()) +} + +// GetNumParts returns the total number of file parts in the transfer. +// +// Returns: +// - Number of parts (int). +func (fpt *ChFilePartTracker) GetNumParts(js.Value, []js.Value) any { + return fpt.api.GetNumParts() +} diff --git a/wasm/channelsFileTransfer_test.go b/wasm/channelsFileTransfer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d697de63b3bb23049b7e933355a09eeb749955c1 --- /dev/null +++ b/wasm/channelsFileTransfer_test.go @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package wasm + +import ( + "gitlab.com/elixxir/client/v4/bindings" + "reflect" + "testing" +) + +// Tests that the map representing ChannelsFileTransfer returned by +// newChannelsFileTransferJS contains all of the methods on ChannelsFileTransfer. +func Test_newChannelsFileTransferJS(t *testing.T) { + cftType := reflect.TypeOf(&ChannelsFileTransfer{}) + + ft := newChannelsFileTransferJS(&bindings.ChannelsFileTransfer{}) + if len(ft) != cftType.NumMethod() { + t.Errorf("ChannelsFileTransfer JS object does not have all methods."+ + "\nexpected: %d\nreceived: %d", cftType.NumMethod(), len(ft)) + } + + for i := 0; i < cftType.NumMethod(); i++ { + method := cftType.Method(i) + + if _, exists := ft[method.Name]; !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} + +// Tests that ChannelsFileTransfer has all the methods that +// [bindings.ChannelsFileTransfer] has. +func Test_ChannelsFileTransferMethods(t *testing.T) { + cftType := reflect.TypeOf(&ChannelsFileTransfer{}) + binCftType := reflect.TypeOf(&bindings.ChannelsFileTransfer{}) + + if binCftType.NumMethod() != cftType.NumMethod() { + t.Errorf("WASM ChannelsFileTransfer object does not have all methods "+ + "from bindings.\nexpected: %d\nreceived: %d", + binCftType.NumMethod(), cftType.NumMethod()) + } + + for i := 0; i < binCftType.NumMethod(); i++ { + method := binCftType.Method(i) + + if _, exists := cftType.MethodByName(method.Name); !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} + +// Tests that the map representing ChFilePartTracker returned by +// newChFilePartTrackerJS contains all of the methods on ChFilePartTracker. +func Test_newChFilePartTrackerJS(t *testing.T) { + fptType := reflect.TypeOf(&FilePartTracker{}) + + fpt := newChFilePartTrackerJS(&bindings.ChFilePartTracker{}) + if len(fpt) != fptType.NumMethod() { + t.Errorf("ChFilePartTracker JS object does not have all methods."+ + "\nexpected: %d\nreceived: %d", fptType.NumMethod(), len(fpt)) + } + + for i := 0; i < fptType.NumMethod(); i++ { + method := fptType.Method(i) + + if _, exists := fpt[method.Name]; !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} + +// Tests that ChFilePartTracker has all the methods that +// [bindings.ChFilePartTracker] has. +func Test_ChFilePartTrackerMethods(t *testing.T) { + fptType := reflect.TypeOf(&ChFilePartTracker{}) + binFptType := reflect.TypeOf(&bindings.ChFilePartTracker{}) + + if binFptType.NumMethod() != fptType.NumMethod() { + t.Errorf("WASM ChFilePartTracker object does not have all methods from "+ + "bindings.\nexpected: %d\nreceived: %d", + binFptType.NumMethod(), fptType.NumMethod()) + } + + for i := 0; i < binFptType.NumMethod(); i++ { + method := binFptType.Method(i) + + if _, exists := fptType.MethodByName(method.Name); !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} diff --git a/wasm/cmix.go b/wasm/cmix.go index 88c9853db7654fb065ab91a6fa9b0dc1fcbddd54..e3430f4cf4b489ad7480ab53522fac2631c7cbce 100644 --- a/wasm/cmix.go +++ b/wasm/cmix.go @@ -27,7 +27,10 @@ func newCmixJS(api *bindings.Cmix) map[string]any { c := Cmix{api} cmix := map[string]any{ // cmix.go - "GetID": js.FuncOf(c.GetID), + "GetID": js.FuncOf(c.GetID), + "GetReceptionID": js.FuncOf(c.GetReceptionID), + "EKVGet": js.FuncOf(c.EKVGet), + "EKVSet": js.FuncOf(c.EKVSet), // identity.go "MakeReceptionIdentity": js.FuncOf( @@ -146,3 +149,59 @@ func LoadCmix(_ js.Value, args []js.Value) any { func (c *Cmix) GetID(js.Value, []js.Value) any { return c.api.GetID() } + +// GetReceptionID returns the default reception identity for this cMix instance. +// +// Returns: +// - Marshalled bytes of [id.ID] (Uint8Array). +func (c *Cmix) GetReceptionID(js.Value, []js.Value) any { + return utils.CopyBytesToJS(c.api.GetReceptionID()) +} + +// EKVGet allows access to a value inside the secure encrypted key value store. +// +// Parameters: +// - args[0] - Key (string). +// +// Returns a promise: +// - Resolves to the value (Uint8Array) +// - Rejected with an error if accessing the KV fails. +func (c *Cmix) EKVGet(_ js.Value, args []js.Value) any { + key := args[0].String() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + val, err := c.api.EKVGet(key) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(val)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// EKVSet sets a value inside the secure encrypted key value store. +// +// Parameters: +// - args[0] - Key (string). +// - args[1] - Value (Uint8Array). +// +// Returns a promise: +// - Resolves on a successful save (void). +// - Rejected with an error if saving fails. +func (c *Cmix) EKVSet(_ js.Value, args []js.Value) any { + key := args[0].String() + val := utils.CopyBytesToGo(args[1]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := c.api.EKVSet(key, val) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(nil) + } + } + + return utils.CreatePromise(promiseFn) +} diff --git a/wasm/dm.go b/wasm/dm.go index e3f71e289e9d7153123317fccc9b5b0240676fbd..50aebb08a1ac9bdbce7ba5d7e53c2f0591a24577 100644 --- a/wasm/dm.go +++ b/wasm/dm.go @@ -53,6 +53,9 @@ func newDMClientJS(api *bindings.DMClient) map[string]any { "GetBlockedSenders": js.FuncOf(cm.GetBlockedSenders), "GetDatabaseName": js.FuncOf(cm.GetDatabaseName), + // Share URL + "GetShareURL": js.FuncOf(cm.GetShareURL), + // DM Sending Methods and Reports "SendText": js.FuncOf(cm.SendText), "SendReply": js.FuncOf(cm.SendReply), @@ -481,7 +484,7 @@ func (dmc *DMClient) SendReaction(_ js.Value, args []js.Value) any { // The meaning of leaseTimeMS depends on the use case. // // Parameters: -// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[0] - Marshalled bytes of the partner pubkyey (Uint8Array). // - args[1] - The token used to derive the reception ID for the partner // (int). // - args[2] - The message type of the message. This will be a valid @@ -531,6 +534,77 @@ func (dmc *DMClient) GetDatabaseName(js.Value, []js.Value) any { "_speakeasy_dm" } +//////////////////////////////////////////////////////////////////////////////// +// DM Share URL // +//////////////////////////////////////////////////////////////////////////////// + +// DMShareURL is returned from [DMClient.GetShareURL]. It includes the +// user's share URL. +// +// JSON example for a user: +// +// { +// "url": "https://internet.speakeasy.tech/?l=32&m=5&p=EfDzQDa4fQ5BoqNIMbECFDY9ckRr_fadd8F1jE49qJc%3D&t=4231817746&v=1", +// "password": "hunter2", +// } +type DMShareURL struct { + URL string `json:"url"` + Password string `json:"password"` +} + +// DMUser is returned from [DecodeDMShareURL]. It includes the token +// and public key of the user who created the URL. +// +// JSON example for a user: +// +// { +// "token": 4231817746, +// "publicKey": "EfDzQDa4fQ5BoqNIMbECFDY9ckRr/fadd8F1jE49qJc=" +// } +type DMUser struct { + Token int32 `json:"token"` + PublicKey []byte `json:"publicKey"` +} + +// GetShareURL generates a URL that can be used to share a URL to initiate a +// direct messages with this user. +// +// Parameters: +// - args[0] - The URL to append the DM info to (string). +// +// Returns: +// - JSON of [DMShareURL] (Uint8Array). +func (dmc *DMClient) GetShareURL(_ js.Value, args []js.Value) any { + host := args[0].String() + urlReport, err := dmc.api.GetShareURL(host) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(urlReport) +} + +// DecodeDMShareURL decodes the user's URL into a [DMUser]. +// +// Parameters: +// - args[0] - The user's share URL. Should be received from another user or +// generated via [DMClient.GetShareURL] (string). +// +// Returns: +// - JSON of [DMUser] (Uint8Array). +func DecodeDMShareURL(_ js.Value, args []js.Value) any { + + url := args[0].String() + report, err := bindings.DecodeDMShareURL(url) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(report) +} + //////////////////////////////////////////////////////////////////////////////// // Channel Receiving Logic and Callback Registration // //////////////////////////////////////////////////////////////////////////////// diff --git a/wasm/docs.go b/wasm/docs.go index b2d6f4894014b15873b2649118e2af09a8cd7091..5b8e7a6dced810bb861c14c45bc41e55fc2385ae 100644 --- a/wasm/docs.go +++ b/wasm/docs.go @@ -15,6 +15,7 @@ import ( "gitlab.com/elixxir/client/v4/auth" "gitlab.com/elixxir/client/v4/catalog" "gitlab.com/elixxir/client/v4/channels" + "gitlab.com/elixxir/client/v4/channelsFileTransfer" "gitlab.com/elixxir/client/v4/cmix" "gitlab.com/elixxir/client/v4/cmix/message" "gitlab.com/elixxir/client/v4/connect" @@ -69,4 +70,6 @@ var ( _ = broadcast.Channel{} _ = netTime.Now _ = ed25519.PublicKey{} + _ = channelsFileTransfer.Params{} + _ = fileTransfer.ID{} ) diff --git a/wasm/emoji.go b/wasm/emoji.go index 498cdb31f7eb7195d9913f9d0a986a8482c5955c..fbde2b5717a4c745fc7f7339d96f157703138cad 100644 --- a/wasm/emoji.go +++ b/wasm/emoji.go @@ -10,49 +10,96 @@ package wasm import ( - "encoding/json" + "syscall/js" + "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/client/v4/emoji" "gitlab.com/elixxir/xxdk-wasm/utils" - "syscall/js" ) // SupportedEmojis returns a list of emojis that are supported by the backend. +// The list includes all emojis described in [UTS #51 section A.1: Data Files]. // // Returns: -// - JSON of an array of gomoji.Emoji (Uint8Array). +// - JSON of an array of emoji.Emoji (Uint8Array). // - Throws a TypeError if marshalling the JSON fails. // // Example JSON: // -// [ -// { -// "slug": "smiling-face", -// "character": "☺ï¸", -// "unicode_name": "E0.6 smiling face", -// "code_point": "263A FE0F", -// "group": "Smileys \u0026 Emotion", -// "sub_group": "face-affection" -// }, -// { -// "slug": "frowning-face", -// "character": "☹ï¸", -// "unicode_name": "E0.7 frowning face", -// "code_point": "2639 FE0F", -// "group": "Smileys \u0026 Emotion", -// "sub_group": "face-concerned" -// }, -// { -// "slug": "banana", -// "character": "�", -// "unicode_name": "E0.6 banana", -// "code_point": "1F34C", -// "group": "Food \u0026 Drink", -// "sub_group": "food-fruit" -// } -// ] +// [ +// { +// "character": "☹ï¸", +// "name": "frowning face", +// "comment": "E0.7", +// "codePoint": "2639 FE0F", +// "group": "Smileys \u0026 Emotion", +// "subgroup": "face-concerned" +// }, +// { +// "character": "☺ï¸", +// "name": "smiling face", +// "comment": "E0.6", +// "codePoint": "263A FE0F", +// "group": "Smileys \u0026 Emotion", +// "subgroup": "face-affection" +// }, +// { +// "character": "☢ï¸", +// "name": "radioactive", +// "comment": "E1.0", +// "codePoint": "2622 FE0F", +// "group": "Symbols", +// "subgroup": "warning" +// } +// ] +// +// [UTS #51 section A.1: Data Files]: https://www.unicode.org/reports/tr51/#Data_Files func SupportedEmojis(js.Value, []js.Value) any { - data, err := json.Marshal(emoji.SupportedEmojis()) + data, err := bindings.SupportedEmojis() + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(data) +} + +// SupportedEmojisMap returns a map of emojis that are supported by the backend +// as described by [SupportedEmojis]. +// +// Returns: +// - JSON of a map of emoji.Emoji (Uint8Array). +// - Throws a TypeError if marshalling the JSON fails. +// +// Example JSON: +// +// [ +// { +// "character": "☹ï¸", +// "name": "frowning face", +// "comment": "E0.7", +// "codePoint": "2639 FE0F", +// "group": "Smileys \u0026 Emotion", +// "subgroup": "face-concerned" +// }, +// { +// "character": "☺ï¸", +// "name": "smiling face", +// "comment": "E0.6", +// "codePoint": "263A FE0F", +// "group": "Smileys \u0026 Emotion", +// "subgroup": "face-affection" +// }, +// { +// "character": "☢ï¸", +// "name": "radioactive", +// "comment": "E1.0", +// "codePoint": "2622 FE0F", +// "group": "Symbols", +// "subgroup": "warning" +// } +// ] +func SupportedEmojisMap(js.Value, []js.Value) any { + data, err := bindings.SupportedEmojisMap() if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -61,10 +108,11 @@ func SupportedEmojis(js.Value, []js.Value) any { return utils.CopyBytesToJS(data) } -// ValidateReaction checks that the reaction only contains a single emoji. +// ValidateReaction checks that the reaction only contains a single grapheme +// (one or more codepoints that appear as a single character to the user). // // Parameters: -// - args[0] - The reaction emoji to validate (string). +// - args[0] - The reaction to validate (string). // // Returns: // - If the reaction is valid, returns null. diff --git a/wasm/follow.go b/wasm/follow.go index 7f573609e037aa3a7c78f286b3cb3e51de016bc5..dbff9722411ddac19999b28b137739ec31279a45 100644 --- a/wasm/follow.go +++ b/wasm/follow.go @@ -87,6 +87,21 @@ func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) any { // SetTrackNetworkPeriod allows changing the frequency that follower threads // are started. // +// Note that the frequency of the follower threads affect the power usage +// of the device following the network. +// - Low period -> Higher frequency of polling -> Higher battery usage +// - High period -> Lower frequency of polling -> Lower battery usage +// +// This may be used to enable a low power (or battery optimization) mode +// for the end user. +// +// Suggested values are provided, however there are no guarantees that these +// values will perfectly fit what the end user's device would require to match +// the user's expectations: +// - Low Power Usage: 5000 milliseconds +// - High Power Usage: 1000 milliseconds (default, see +// [cmix.DefaultFollowPeriod] +// // Parameters: // - args[0] - The duration of the period, in milliseconds (int). func (c *Cmix) SetTrackNetworkPeriod(_ js.Value, args []js.Value) any { diff --git a/wasm/logging.go b/wasm/logging.go index 8199edc02a829a8e8b81b59dbfc73cddb4d46857..1ae9bb7127447f1a786833788eb34250ed783979 100644 --- a/wasm/logging.go +++ b/wasm/logging.go @@ -10,35 +10,10 @@ package wasm import ( - "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/logging" "syscall/js" -) -// LogLevel sets level of logging. All logs at the set level and below will be -// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL -// messages will be printed). -// -// Log level options: -// -// TRACE - 0 -// DEBUG - 1 -// INFO - 2 -// WARN - 3 -// ERROR - 4 -// CRITICAL - 5 -// FATAL - 6 -// -// The default log level without updates is INFO. -// -// Parameters: -// - args[0] - Log level (int). -// -// Returns: -// - Throws TypeError if the log level is invalid. -func LogLevel(this js.Value, args []js.Value) any { - return logging.LogLevelJS(this, args) -} + "gitlab.com/elixxir/client/v4/bindings" +) // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter] // interface. diff --git a/wasm_test.go b/wasm_test.go index 3a4b0dbacbeb1223ab063fb4e9001f6254e996dd..59c4485c516894972b0cbcfa74b49218f28299f8 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -58,6 +58,14 @@ func TestPublicFunctions(t *testing.T) { // Mobile-specific bindings not supported by the browser "NewChannelsManagerMobile": {}, "LoadChannelsManagerMobile": {}, + "NewDmManagerMobile": {}, + + // C-Library specific bindings not needed by the browser + "GetDMInstance": {}, + "GetCMixInstance": {}, + + // Logging has been moved to startup flags + "LogLevel": {}, } wasmFuncs := getPublicFunctions("wasm", t) bindingsFuncs := getPublicFunctions( diff --git a/worker/manager.go b/worker/manager.go index 1a28ac2f99e803b902cdac1e1696ec031d42ca48..3a5831db7c07a54927429b8d818dd66db7b9fc7a 100644 --- a/worker/manager.go +++ b/worker/manager.go @@ -11,12 +11,14 @@ package worker import ( "encoding/json" - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" "sync" "syscall/js" "time" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/utils" ) // initID is the ID for the first item in the callback list. If the list only @@ -35,7 +37,11 @@ const ( ResponseTimeout = 30 * time.Second ) -// ReceptionCallback is the function that handles incoming data from the worker. +// receiveQueueChanSize is the size of the channel that received messages are +// put on. +const receiveQueueChanSize = 100 + +// ReceptionCallback is called with a message received from the worker. type ReceptionCallback func(data []byte) // Manager manages the handling of messages received from the worker. @@ -56,6 +62,13 @@ type Manager struct { // original message sent by the main thread. responseIDs map[Tag]uint64 + // receiveQueue is the channel that all received messages are queued on + // while they wait to be processed. + receiveQueue chan js.Value + + // quit, when triggered, stops the thread that processes received messages. + quit chan struct{} + // name describes the worker. It is used for debugging and logging purposes. name string @@ -76,10 +89,15 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { worker: js.Global().Get("Worker").New(aURL, opts), callbacks: make(map[Tag]map[uint64]ReceptionCallback), responseIDs: make(map[Tag]uint64), + receiveQueue: make(chan js.Value, receiveQueueChanSize), + quit: make(chan struct{}), name: name, messageLogging: messageLogging, } + // Start thread to process responses from worker + go m.processThread() + // Register listeners on the Javascript worker object that receive messages // and errors from the worker m.addEventListeners() @@ -101,6 +119,48 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { return m, nil } +// Stop closes the worker manager and terminates the worker. +func (m *Manager) Stop() { + // Stop processThread + select { + case m.quit <- struct{}{}: + } + + // Terminate the worker + go m.terminate() +} + +// processThread processes received messages sequentially. +func (m *Manager) processThread() { + jww.INFO.Printf("[WW] [%s] Starting process thread.", m.name) + for { + select { + case <-m.quit: + jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name) + return + case msgData := <-m.receiveQueue: + + switch msgData.Type() { + case js.TypeObject: + if msgData.Get("constructor").Equal(utils.Uint8Array) { + err := m.processReceivedMessage(utils.CopyBytesToGo(msgData)) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process received "+ + "message from worker: %+v", m.name, err) + } + break + } + fallthrough + + default: + jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ + "from worker: %s", m.name, msgData.Type(), + utils.JsToJson(msgData)) + } + } + } +} + // SendMessage sends a message to the worker with the given tag. If a reception // callback is specified, then the message is given a unique ID to handle the // reply. Set receptionCB to nil if no reply is expected. @@ -127,12 +187,19 @@ func (m *Manager) SendMessage( "ID %d going to worker: %+v", m.name, msg, tag, id, err) } - go m.postMessage(string(payload)) + go m.postMessage(payload) } // receiveMessage is registered with the Javascript event listener and is called // every time a new message from the worker is received. -func (m *Manager) receiveMessage(data []byte) error { +func (m *Manager) receiveMessage(data js.Value) { + m.receiveQueue <- data +} + +// processReceivedMessage processes the message received from the worker and +// calls the associated callback. This functions blocks until the callback +// returns. +func (m *Manager) processReceivedMessage(data []byte) error { var msg Message err := json.Unmarshal(data, &msg) if err != nil { @@ -149,7 +216,7 @@ func (m *Manager) receiveMessage(data []byte) error { return err } - go callback(msg.Data) + callback(msg.Data) return nil } @@ -249,11 +316,7 @@ func (m *Manager) addEventListeners() { // occurs when a message is received from the worker. // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - err := m.receiveMessage([]byte(args[0].Get("data").String())) - if err != nil { - jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+ - "worker: %+v", m.name, err) - } + m.receiveMessage(args[0].Get("data")) return nil }) @@ -262,8 +325,8 @@ func (m *Manager) addEventListeners() { // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { event := args[0] - jww.ERROR.Printf("[WW] [%s] Main received error event: %s", - m.name, utils.JsErrorToJson(event)) + jww.FATAL.Panicf("[WW] [%s] Main received error event: %+v", + m.name, js.Error{Value: event}) return nil }) @@ -272,8 +335,8 @@ func (m *Manager) addEventListeners() { // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { event := args[0] - jww.ERROR.Printf("[WW] [%s] Main received message error event: %s", - m.name, utils.JsErrorToJson(event)) + jww.ERROR.Printf("[WW] [%s] Main received message error event: %+v", + m.name, js.Error{Value: event}) return nil }) @@ -286,27 +349,26 @@ func (m *Manager) addEventListeners() { // postMessage sends a message to the worker. // -// message is the object to deliver to the worker; this will be in the data -// field in the event delivered to the worker. It must be a js.Value or a -// primitive type that can be converted via js.ValueOf. The Javascript object -// must be "any value or JavaScript object handled by the structured clone -// algorithm, which includes cyclical references.". See the doc for more -// information. +// msg is the object to deliver to the worker; this will be in the data +// field in the event delivered to the worker. It must be a transferable object +// because this function transfers ownership of the message instead of copying +// it for better performance. See the doc for more information. // // If the message parameter is not provided, a SyntaxError will be thrown by the // parser. If the data to be passed to the worker is unimportant, js.Null or // js.Undefined can be passed explicitly. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage -func (m *Manager) postMessage(msg any) { - m.worker.Call("postMessage", msg) +func (m *Manager) postMessage(msg []byte) { + buffer := utils.CopyBytesToJS(msg) + m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")}) } -// Terminate immediately terminates the Worker. This does not offer the worker +// terminate immediately terminates the Worker. This does not offer the worker // an opportunity to finish its operations; it is stopped at once. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate -func (m *Manager) Terminate() { +func (m *Manager) terminate() { m.worker.Call("terminate") } diff --git a/worker/manager_test.go b/worker/manager_test.go index 2eadaff2edeaf66a4071f387f608a602c6967b8d..49a395a9c45a33f25892cc1efc86e263ccf06063 100644 --- a/worker/manager_test.go +++ b/worker/manager_test.go @@ -16,8 +16,8 @@ import ( "time" ) -// Tests Manager.receiveMessage calls the expected callback. -func TestManager_receiveMessage(t *testing.T) { +// Tests Manager.processReceivedMessage calls the expected callback. +func TestManager_processReceivedMessage(t *testing.T) { m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} msg := Message{Tag: readyTag, ID: 5} @@ -38,7 +38,7 @@ func TestManager_receiveMessage(t *testing.T) { } }() - err = m.receiveMessage(data) + err = m.processReceivedMessage(data) if err != nil { t.Errorf("Failed to receive message: %+v", err) } @@ -92,7 +92,7 @@ func TestManager_getCallback(t *testing.T) { } // Tests that Manager.RegisterCallback registers a callback that is then called -// by Manager.receiveMessage. +// by Manager.processReceivedMessage. func TestManager_RegisterCallback(t *testing.T) { m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} @@ -114,14 +114,14 @@ func TestManager_RegisterCallback(t *testing.T) { } }() - err = m.receiveMessage(data) + err = m.processReceivedMessage(data) if err != nil { t.Errorf("Failed to receive message: %+v", err) } } // Tests that Manager.registerReplyCallback registers a callback that is then -// called by Manager.receiveMessage. +// called by Manager.processReceivedMessage. func TestManager_registerReplyCallback(t *testing.T) { m := &Manager{ callbacks: make(map[Tag]map[uint64]ReceptionCallback), @@ -147,7 +147,7 @@ func TestManager_registerReplyCallback(t *testing.T) { } }() - err = m.receiveMessage(data) + err = m.processReceivedMessage(data) if err != nil { t.Errorf("Failed to receive message: %+v", err) } diff --git a/worker/thread.go b/worker/thread.go index df824d045f4337c6384eac389f692b15bec200a8..203b00302a97c22f4e44501e2c721b52312c14ab 100644 --- a/worker/thread.go +++ b/worker/thread.go @@ -11,15 +11,18 @@ package worker import ( "encoding/json" + "sync" + "syscall/js" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/utils" - "sync" - "syscall/js" ) -// ThreadReceptionCallback is the function that handles incoming data from the -// main thread. +// ThreadReceptionCallback is called with a message received from the main +// thread. Any bytes returned are sent as a response back to the main thread. +// Any returned errors are printed to the log. type ThreadReceptionCallback func(data []byte) ([]byte, error) // ThreadManager queues incoming messages from the main thread and handles them @@ -32,6 +35,13 @@ type ThreadManager struct { // main thread keyed on the callback tag. callbacks map[Tag]ThreadReceptionCallback + // receiveQueue is the channel that all received MessageEvent.data are + // queued on while they wait to be processed. + receiveQueue chan js.Value + + // quit, when triggered, stops the thread that processes received messages. + quit chan struct{} + // name describes the worker. It is used for debugging and logging purposes. name string @@ -44,16 +54,62 @@ type ThreadManager struct { // NewThreadManager initialises a new ThreadManager. func NewThreadManager(name string, messageLogging bool) *ThreadManager { - mh := &ThreadManager{ + tm := &ThreadManager{ messages: make(chan js.Value, 100), callbacks: make(map[Tag]ThreadReceptionCallback), + receiveQueue: make(chan js.Value, receiveQueueChanSize), + quit: make(chan struct{}), name: name, messageLogging: messageLogging, } + // Start thread to process messages from the main thread + go tm.processThread() - mh.addEventListeners() + tm.addEventListeners() - return mh + return tm +} + +// Stop closes the thread manager and stops the worker. +func (tm *ThreadManager) Stop() { + // Stop processThread + select { + case tm.quit <- struct{}{}: + } + + // Terminate the worker + go tm.close() +} + +// processThread processes received messages sequentially. +func (tm *ThreadManager) processThread() { + jww.INFO.Printf("[WW] [%s] Starting worker process thread.", tm.name) + for { + select { + case <-tm.quit: + jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name) + return + case msgData := <-tm.receiveQueue: + + switch msgData.Type() { + case js.TypeObject: + if msgData.Get("constructor").Equal(utils.Uint8Array) { + err := tm.processReceivedMessage(utils.CopyBytesToGo(msgData)) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process message "+ + "received from main thread: %+v", tm.name, err) + } + break + } + fallthrough + + default: + jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ + "from main thread: %s", + tm.name, msgData.Type(), utils.JsToJson(msgData)) + } + } + } } // SignalReady sends a signal to the main thread indicating that the worker is @@ -83,12 +139,11 @@ func (tm *ThreadManager) SendMessage(tag Tag, data []byte) { "to main: %+v", tm.name, msg, tag, err) } - go tm.postMessage(string(payload)) + go tm.postMessage(payload) } // sendResponse sends a reply to the main thread with the given tag and ID. -func (tm *ThreadManager) sendResponse( - tag Tag, id uint64, data []byte) { +func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error { msg := Message{ Tag: tag, ID: id, @@ -103,17 +158,26 @@ func (tm *ThreadManager) sendResponse( payload, err := json.Marshal(msg) if err != nil { - jww.FATAL.Panicf("[WW] [%s] Worker failed to marshal %T for %q and ID "+ - "%d going to main: %+v", tm.name, msg, tag, id, err) + return errors.Errorf("worker failed to marshal %T for %q and ID "+ + "%d going to main: %+v", msg, tag, id, err) } - go tm.postMessage(string(payload)) + go tm.postMessage(payload) + + return nil } // receiveMessage is registered with the Javascript event listener and is called -// everytime a message from the main thread is received. If the registered -// callback returns a response, it is sent to the main thread. -func (tm *ThreadManager) receiveMessage(data []byte) error { +// every time a new message from the main thread is received. +func (tm *ThreadManager) receiveMessage(data js.Value) { + tm.receiveQueue <- data +} + +// processReceivedMessage processes the message received from the main thread +// and calls the associated callback. If the registered callback returns a +// response, it is sent to the main thread. This functions blocks until the +// callback returns. +func (tm *ThreadManager) processReceivedMessage(data []byte) error { var msg Message err := json.Unmarshal(data, &msg) if err != nil { @@ -133,16 +197,14 @@ func (tm *ThreadManager) receiveMessage(data []byte) error { } // Call callback and register response with its return - go func() { - response, err2 := callback(msg.Data) - if err2 != nil { - jww.ERROR.Printf("[WW] [%s] Callback for for %q and ID %d "+ - "returned an error: %+v", tm.name, msg.Tag, msg.ID, err) - } - if response != nil { - tm.sendResponse(msg.Tag, msg.ID, response) - } - }() + response, err := callback(msg.Data) + if err != nil { + return errors.Errorf("callback for %q and ID %d returned an error: %+v", + msg.Tag, msg.ID, err) + } + if response != nil { + return tm.sendResponse(msg.Tag, msg.ID, response) + } return nil } @@ -173,11 +235,7 @@ func (tm *ThreadManager) addEventListeners() { // occurs when a message is received from the main thread. // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - err := tm.receiveMessage([]byte(args[0].Get("data").String())) - if err != nil { - jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+ - "main thread: %+v", tm.name, err) - } + tm.receiveMessage(args[0].Get("data")) return nil }) @@ -213,8 +271,26 @@ func (tm *ThreadManager) addEventListeners() { // aMessage must be a js.Value or a primitive type that can be converted via // js.ValueOf. The Javascript object must be "any value or JavaScript object // handled by the structured clone algorithm". See the doc for more information. + +// aMessage is the object to deliver to the main thread; this will be in the +// data field in the event delivered to the thread. It must be a transferable +// object because this function transfers ownership of the message instead of +// copying it for better performance. See the doc for more information. // // Doc: https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/postMessage -func (tm *ThreadManager) postMessage(aMessage any) { - js.Global().Call("postMessage", aMessage) +func (tm *ThreadManager) postMessage(aMessage []byte) { + buffer := utils.CopyBytesToJS(aMessage) + js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")}) +} + +// close discards any tasks queued in the worker's event loop, effectively +// closing this particular scope. +// +// aMessage must be a js.Value or a primitive type that can be converted via +// js.ValueOf. The Javascript object must be "any value or JavaScript object +// handled by the structured clone algorithm". See the doc for more information. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/close +func (tm *ThreadManager) close() { + js.Global().Call("close") } diff --git a/worker/thread_test.go b/worker/thread_test.go index 8f7d09cd931f5e28002ea9cb22b764df834c8062..ada6de8fc00916699b45eaee1483830a277b9fc9 100644 --- a/worker/thread_test.go +++ b/worker/thread_test.go @@ -15,8 +15,8 @@ import ( "time" ) -// Tests that ThreadManager.receiveMessage calls the expected callback. -func TestThreadManager_receiveMessage(t *testing.T) { +// Tests that ThreadManager.processReceivedMessage calls the expected callback. +func TestThreadManager_processReceivedMessage(t *testing.T) { tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} msg := Message{Tag: readyTag, ID: 5} @@ -37,14 +37,14 @@ func TestThreadManager_receiveMessage(t *testing.T) { } }() - err = tm.receiveMessage(data) + err = tm.processReceivedMessage(data) if err != nil { t.Errorf("Failed to receive message: %+v", err) } } // Tests that ThreadManager.RegisterCallback registers a callback that is then -// called by ThreadManager.receiveMessage. +// called by ThreadManager.processReceivedMessage. func TestThreadManager_RegisterCallback(t *testing.T) { tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} @@ -66,7 +66,7 @@ func TestThreadManager_RegisterCallback(t *testing.T) { } }() - err = tm.receiveMessage(data) + err = tm.processReceivedMessage(data) if err != nil { t.Errorf("Failed to receive message: %+v", err) }