diff -uNr akris-desktop/LICENSE.TXT akris-desktop-genesis/LICENSE.TXT --- akris-desktop/LICENSE.TXT false +++ akris-desktop-genesis/LICENSE.TXT 6aee15c0990c8c92448c42753a109379a87ffc2047876105974414ea870bf4e7a9e869074e47ea2c1de8ac0b31b53212bf5ab59208dba3660054516062464aed @@ -0,0 +1,24 @@ +(C) 2023 Adam Thorsen ( alethepedia.com ) + +This product is distributed under "The V Public License" ("VPL"; version 0xFF.) + +This means that it is a document with a cryptographically-verifiable history of attributable authorship, rather than one of the many vile agglomerations of faecal droppings from anonymous vermin typically met with in public digital cesspools. + +The V Public License mandates that all redistribution of this product is to take the canonical form of a VTree, which consists of: a Genesis VPatch; zero or more revision VPatches; and one or more VSeals which authenticate each such VPatch, including the Genesis. + +A VPatch is defined as the output of "VDiff" on a revision against its predecessor: this is presently equivalent to standard Unix "diff -uNr PREDECESSOR SUCCESSOR", where the timestamp normally found in every delta is replaced with a Keccak hash of the original or transformed file. A Genesis VPatch is a VDiff of a "revision" constituting the initial public release of the product against a null predecessor (i.e. an empty directory). + +A VSeal is a public key signature (typically generated via GPG or a compatible program) of a particular VPatch, by an author, coauthor, or reviewer who wishes to be known as a signatory. If the creator of a VSeal has a public presence, e.g. a WWW site, the public key against which the VSeal validates must be publicly accessible and publicly associated with said presence (e.g. offered for download on the creator's WWW site.) + +This product may be used for ANY PURPOSE WHATSOEVER, and is supplied with NO WARRANTY OF ANY KIND, unless otherwise specified by written and signed agreements with all of its VSeal signatories. + +However, if you choose to redistribute the product and/or any changes you have made to it, you must do so in VTree form, and the latter must include all VPatches and VSeals you had originally received. The VTree you redistribute must be fully-compatible in every respect with the format of the one you had received. + +You may offer copies of a VPL-licensed product for download to third parties in variant (i.e. non-VTree) forms, so long as the canonical VTree form at all times exactly corresponding to every such variant is made readily and conspicuously available. Concretely, this means that if you intend to distribute a binary build of a VPL-licensed computer program, you must not only distribute its VTree with it, but must also include any and all components and instructions required to reproduce a bitwise-identical binary build. + +Similarly, if you distribute a VPL-licensed product as a source archive, via a publicly-facing version control interface, as a printed book, inside a physical device, or in any form whatsoever other than a VTree, then a VTree corresponding exactly to the item you are distributing, retaining all of the history which you had originally received, must be distributed along with every such variant copy. + +The only permitted exception to the three paragraphs above is that a VPL-licensed product MAY be incorporated whole into a new (i.e. with a novel Genesis) VTree, so long as the latter contains a statement of this fact, along with any information required to locate and retrieve a copy of the original product's canonical VTree. All provisions of the VPL shall apply to the new VTree. + +The text of this notice is itself licensed under the VPL, and is to be retained where found. + diff -uNr akris-desktop/Makefile akris-desktop-genesis/Makefile --- akris-desktop/Makefile false +++ akris-desktop-genesis/Makefile 93c5c775d1883602a1796e54d5267b87703d642760f069ee13b0a44ff1d561c65ccd14bc4d85787b736edebfe208513fa94edc9894780bc52816d92f5f8a89b3 @@ -0,0 +1,34 @@ +.PHONY: all test + +LATEST_VERSION := $(shell cat akris_desktop/VERSION) + +all: + @echo "Building akris-desktop..." + python3 setup.py build + +dist: + pyinstaller --onefile \ + --name akris-desktop \ + --add-data ../akris/akris/migrations/*:./akris/migrations\ + --add-data ../akris/akris/VERSION:./akris/\ + --add-data ./akris_desktop/images/*:./akris_desktop/images/ \ + --add-binary ../akris/_serpent_cffi.abi3.so:./ \ + --clean bin/main.py --hidden-import='PIL._tkinter_finder' + staticx dist/akris-desktop dist/akris-desktop-$(LATEST_VERSION).linux.x86_64.bin + +dist-osx: + pyinstaller --onefile --add-data ../akris/akris/migrations/*:./akris/migrations\ + --add-data ../akris/akris/VERSION:./akris/\ + --add-data ./akris_desktop/images/*:./akris_desktop/images/ \ + --add-binary ../akris/_serpent_cffi.abi3.so:./ \ + --clean bin/main.py --hidden-import='PIL._tkinter_finder' + mkdir -p dist/Akris.app/Contents/MacOs + mkdir -p dist/Akris.app/Contents/Resources + cp dist/main dist/Akris.app/Contents/MacOs/Akris + cp osx/Info.plist dist/Akris.app/Contents/ + chmod +x dist/Akris.app/Contents/MacOs/Akris + cp -r osx/locust_icon.icns dist/Akris.app/Contents/Resources/Akris.icns + +clean: + @echo "Cleaning akris..." + rm -rf build/ dist/ .eggs/ akris.egg-info/ v-build \ No newline at end of file diff -uNr akris-desktop/README.md akris-desktop-genesis/README.md --- akris-desktop/README.md false +++ akris-desktop-genesis/README.md b0e462d7de8e2b58ea2eaf81f4344a0488f99e63d0e2d5dc98a0afc402346668ed10a8462a23cde2793ebe0b7e6ea569dc3e88a2cc0592b8eca74c7cb47b9842 @@ -0,0 +1,129 @@ +# Akris Desktop +## Description +Akris Desktop is a pest station graphical client implemented using the Akris Pest station library. + +## Getting Akris Desktop + +This guide assumes a working [pentacle](http://problematic.site/src/pentacle/) chroot environment + +### Using [V](https://archive.ph/pRfAz) +Download all patches and seals from [v.alethepedia.com/akris_desktop](http://v.alethepedia.com/akris_desktop): + + mkdir -p patches + mkdir -p seals + curl -o patches/akris-desktop-genesis-VERSION.vpatch http://v.alethepedia.com/akris/akris-desktop-genesis-VERSION.vpatch + curl -o seals/akris-desktop-genesis-VERSION.vpatch.thimbronion.sig http://v.alethepedia.com/akris/akris-desktop-genesis-VERSION.vpatch.thimbronion.sig + +Pentacle v expects gpg public keys to be found in ~/wot. + +Using the bash implementation of v provided in [pentacle](http://problematic.site/src/pentacle/), +press Akris Desktop with the leaf you wish to target (usually the patch with the lowest version number): + + v press akris-desktop patches/akris-desktop-genesis-99999.vpatch + +### Download and run the signed binary + + curl -o akris-desktop-VERSION.linux.x86_64.bin https://v.alethepedia.com/akris_desktop/akris-desktop-VERSION.linux.x86_64.bin + curl -o akris-desktop-VERSION.linux.x86_64.bin.thimbronion.sig https://v.alethepedia.com/akris_desktop/akris-desktop-VERSION.linux.x86_64.bin.thimbronion.sig + +#### Run Akris Desktop + + ./akris-desktop-VERSION.linux.x86_64.bin + +### From PyPI [WIP] + + pip install akris-desktop + +### From a signed tarball [WIP] +#### Download and unzip Akris Desktop +#### Download and unzip Akris + +## Build and Install Akris Desktop from source + +### Pre-requisites +- Python >= 3.11 +- tk +- python3.11-dev + +### Dependency installation +#### Debian/Ubuntu + # These steps may or may not be necessary depending on the state of your system + sudo apt-get install python3.11-dev + sudo apt-get install python3-tk + sudo apt-get install python3-pil python3-pil.imagetk + +##### Gentoo [WIP] + +### Create a python virtual environment. +This is optional but is recommended to avoid cluttering up your system python site-packages. + +#### Create the virtual environment + cd akris-desktop + python -m venv venv + source venv/bin/activate + +### Install tk in the virtual environment + + pip install tk + +### Install Akris into the virtual environment from a local copy of the source code + + pip install -e file:../akris + +### Install Akris Desktop into the virtual environment + + pip install -e . + +### Run Akris Desktop + + python3 bin/main.py + +### Configure Akris Desktop +Akris desktop is inert on initial startup. To connect to a configure your station via the console tab: +1. Set your handle (this is the name others must use for you when peering with your station): + + `> handle cortes` + +2. Enable presence reporting (this will update akris desktop when peers come online or go offline): + + `> knob presence.report_interval 1` + +3. Set the address cast interval (this is how often your station will broadcast its address to peers): + + `> knob address.cast_interval 1` + +4. Add a peer: + + `> peer pizarro` + +5. Generate a symmetric key to share with the peer: + + `> genkey` + +6. Set the peer's key: + + `> key pizarro ` + +7. Set the peer's address: + + `> at pizarro W.X.Y.Z:12345` + +If this pestnet has been around a while, you should start to see messages syncing in the +'broadcast messages' tab momentarily. If this is a new pestnet, feel free to send the +first message. + +## Development +### Install development dependencies + pip install .[dev] + +### Build a binary distribution +#### Linux + make dist +#### OSX + make dist-osx + +### Database Management +#### Run new migrations if any have been added + caribou upgrade akris.db migrations + +### Client API Documentation [WIP] diff -uNr akris-desktop/akris_desktop/VERSION akris-desktop-genesis/akris_desktop/VERSION --- akris-desktop/akris_desktop/VERSION false +++ akris-desktop-genesis/akris_desktop/VERSION 14d7328d4d921e50e9c92013a2013ade9daafd0401c780f62dad415d07a934ced36a5b0d023b083260788b256f7675c6787fc5d8aa1c22fb1c409748a21b170c @@ -0,0 +1 @@ +99999 \ No newline at end of file diff -uNr akris-desktop/akris_desktop/__init__.py akris-desktop-genesis/akris_desktop/__init__.py --- akris-desktop/akris_desktop/__init__.py false +++ akris-desktop-genesis/akris_desktop/__init__.py 26691c382525798d231437a34db006989c1884f59c1767858cdb4bee12bfcf9734b3105d5622e4cc5d1bf1e91765248656fd832e07a67056249c7a89703ddef8 @@ -0,0 +1 @@ +# empty diff -uNr akris-desktop/akris_desktop/app.py akris-desktop-genesis/akris_desktop/app.py --- akris-desktop/akris_desktop/app.py false +++ akris-desktop-genesis/akris_desktop/app.py 6b50d248c0ce30939fc2c35002e4a9d7d808ecabe801477590193ae7dee43ba94d3d02dc581119c29962c96691a8da65d3c21a04a719a618495b833abee2368d @@ -0,0 +1,156 @@ +import importlib +import logging +import os +import tkinter as tk +from tkinter import ttk +from PIL import Image, ImageTk + +from akris_desktop.broadcast_tab import BroadcastTab +from akris_desktop.direct_tab import DirectTab +from akris_desktop.ref_link_tab import RefLinkTab +from akris_desktop.console import Console +from akris_desktop.message_stats_listener import MessageStatsListener +from akris_desktop.new_handle_listener import NewHandleListener +from akris_desktop.new_ref_link_listener import NewRefLinkListener +from akris_desktop.handle_set_listener import HandleSetListener +from akris_desktop.timeline_view import DuplicateMessageException + +logger = logging.getLogger("akris_desktop") + + +class App(tk.Frame): + def __init__(self, root=None, api_client=None, options=None): + super().__init__(root) + try: + with importlib.resources.path("akris_desktop", "images") as resource_path: + ico = Image.open(os.path.join(resource_path, "locust_icon.png")) + photo = ImageTk.PhotoImage(ico) + root.wm_iconphoto(False, photo) + except FileNotFoundError: + logger.exception("icon file missing.") + self.disable_multipart = options.disable_multipart + self.remote_station = options.remote_station + self.handle = None + self.root = root + self.root.title("Akris") + self.root.configure(bg="black") + self.configure(bg="black") + self.api_client = api_client + self.message_detail = None + self.new_handle_listener = NewHandleListener(self) + self.new_ref_link_listener = NewRefLinkListener(self) + self.handle_set_listener = HandleSetListener(self) + self.message_listeners = [] + self.direct_tabs = [] + self.ref_link_tabs = [] + self.message_stats = None + self.pack(fill=tk.BOTH, expand=True) + root.protocol("WM_DELETE_WINDOW", self.on_close) + + # create notebook + style = ttk.Style() + style.configure("TNotebook", background="#333232", bordercolor="black") + style.configure("TNotebook.Tab", background="#2C2A2A", foreground="darkgray") + style.map( + "TNotebook.Tab", + background=[("selected", "#333232")], + foreground=[("selected", "white")], + ) + self.notebook = ttk.Notebook(self, style="TNotebook") + self.notebook.pack(fill=tk.BOTH, expand=True) + + # create main tab + self.broadcast_tab = BroadcastTab(self.root, self) + self.broadcast_tab.configure(bg="black") + self.notebook.add(self.broadcast_tab, text="broadcast messages") + self.console_tab = tk.Frame(self.root) + self.console_tab.configure(bg="black") + self.notebook.add(self.console_tab, text="console") + + # setup console tab + self.console_frame = tk.Frame(self.console_tab) + self.console_frame.pack(fill=tk.BOTH, expand=True) + self.console = Console(self.console_frame, self.api_client) + + # register message listeners + self.register_message_listener(self.console) + self.register_message_listener(self.new_handle_listener) + self.register_message_listener(self.new_ref_link_listener) + self.register_message_listener(self.handle_set_listener) + self.register_message_listener(MessageStatsListener(self)) + self.api_client.send_command({"command": "message_stats", "args": []}) + self.api_client.send_command({"command": "version_info", "args": []}) + self.api_client.send_command({"command": "knob", "args": ["handle"]}) + self.api_client.send_command({"command": "report_presence", "args": []}) + + def add_direct_message_tab(self, handle): + # check if tab already exists + for tab in self.direct_tabs: + if tab.handle == handle: + self.notebook.select(tab) + return + dm_tab = DirectTab(self.root, handle, self) + self.direct_tabs.append(dm_tab) + self.notebook.insert(1, dm_tab, text=handle) + return dm_tab + + def add_ref_link_tab(self, message_hash): + # check if tab already exists + for tab in self.ref_link_tabs: + if tab.message_hash == message_hash: + self.notebook.select(tab) + return + ref_link_tab = RefLinkTab(self.root, message_hash, self) + self.ref_link_tabs.append(ref_link_tab) + self.notebook.insert(1, ref_link_tab, text=ref_link_tab.title()) + self.notebook.select(ref_link_tab) + return ref_link_tab + + def open_direct_message_tab(self, handle): + tab = self.add_direct_message_tab(handle) + self.notebook.select(tab) + + def close_ref_link_tab(self, tab): + self.ref_link_tabs.remove(tab) + self.notebook.forget(tab) + self.unregister_message_listener(tab.message_table) + self.notebook.select(self.broadcast_tab) + + def close_tab(self, tab): + self.direct_tabs.remove(tab) + self.notebook.forget(tab) + self.unregister_message_listener(tab.message_table) + self.notebook.select(self.broadcast_tab) + + def register_message_listener(self, listener): + self.message_listeners.append(listener) + + def unregister_message_listener(self, listener): + self.message_listeners.remove(listener) + + def check_message_queue(self): + # check the queue and perform actions if needed + if not self.api_client.message_queue.empty(): + while not self.api_client.message_queue.empty(): + message = self.api_client.message_queue.get() + for listener in self.message_listeners: + try: + listener.render_message(message) + except DuplicateMessageException: + pass + + def run(self): + self.check_message_queue() + self.root.after( + 100, self.run + ) # schedule the function to be called again in 1 second + + def on_close(self): + logger.info("closing") + self.root.destroy() + self.root.quit() + if not self.remote_station: + self.api_client.shutdown_station() + return + + self.api_client.disconnect() diff -uNr akris-desktop/akris_desktop/broadcast_message_entry.py akris-desktop-genesis/akris_desktop/broadcast_message_entry.py --- akris-desktop/akris_desktop/broadcast_message_entry.py false +++ akris-desktop-genesis/akris_desktop/broadcast_message_entry.py 861a6c044d983e011486f661ec61d040c27f2298cfc5e18dd4e0313c51e8531954dbce87d719a6401f0e09be49751eb849dbd5685fbaaa8d2a4b9e963a906280 @@ -0,0 +1,32 @@ +import tkinter as tk +from .message_entry import MessageEntry, count_bytes + + +class BroadcastMessageEntry(MessageEntry): + def __init__(self, root, app): + super().__init__(root, app) + + def handle_slash_command(self, message): + pass + + def send_message(self): + # Get the text from the text entry widget + message = self.text.get("1.0", tk.END).rstrip("\n") + if message[0] == "/": + message = self.encode_action(message) + + # Clear the text entry widget + self.text.delete("1.0", tk.END) + + if not self.app.disable_multipart: + # count the number of bytes in the message body + # if necessary send a multipart message + if count_bytes(message) > 324: + self.app.api_client.send_command( + {"command": "broadcast_text_m", "args": [message]} + ) + return + + self.app.api_client.send_command( + {"command": "broadcast_text", "args": [message]} + ) diff -uNr akris-desktop/akris_desktop/broadcast_tab.py akris-desktop-genesis/akris_desktop/broadcast_tab.py --- akris-desktop/akris_desktop/broadcast_tab.py false +++ akris-desktop-genesis/akris_desktop/broadcast_tab.py fc28b0fba234d17ffef8be760695d94ba9cc1d423b95d7ee2d071485755cd1eca93a3e335eb9ca5ef1381018289f70e37094ec770c89fc1753410fe6f79cfcdd @@ -0,0 +1,30 @@ +import tkinter as tk + +from .broadcast_message_entry import BroadcastMessageEntry +from .peers import Peers +from .broadcast_view import BroadcastView + + +class BroadcastTab(tk.Frame): + def __init__(self, root, app): + super().__init__(root) + self.app = app + self.toolbar_frame = tk.Frame(self, background="#333232") + self.toolbar_frame.pack(side=tk.TOP, fill=tk.X) + self.messages_and_peers_frame = tk.Frame(self) + self.messages_and_peers_frame.pack(fill=tk.BOTH, expand=True) + self.peers = Peers(self.messages_and_peers_frame, app) + self.message_table = BroadcastView(self.messages_and_peers_frame, app, self) + self.more_link = tk.Label( + self.toolbar_frame, + text=self.message_table.page_monitor.button_label(), + fg="#00a1ff", + cursor="hand2", + background="#333232", + ) + self.message_table.await_message_stats() + self.more_link.pack(side=tk.LEFT) + self.more_link.bind("", lambda e: self.message_table.load_previous()) + self.message_entry = BroadcastMessageEntry(self, app) + self.app.register_message_listener(self.message_table) + self.app.register_message_listener(self.peers) diff -uNr akris-desktop/akris_desktop/broadcast_view.py akris-desktop-genesis/akris_desktop/broadcast_view.py --- akris-desktop/akris_desktop/broadcast_view.py false +++ akris-desktop-genesis/akris_desktop/broadcast_view.py 3325b1f572ece8cb696f34b9712ca2000e51a0637a47d0879ff030d6fde49cb1da699159467669feda72b0c1dc2c9f0d860290baec96a6d7be26cfb57cb480d6 @@ -0,0 +1,60 @@ +from akris.log_config import LogConfig + +logger = LogConfig.get_instance().get_logger("akris_desktop.broadcast_view") + +from .timeline_view import TimelineView, MultipartMessage + + +class BroadcastView(TimelineView): + def __init__(self, root, app, tab): + super().__init__(root, app) + self.tab = tab + self.root = root + + def render_message(self, message): + super().render_message(message) + + if message.get("ref_link_page_response"): + return + + if message.get("command") == "message_update": + # if we already have this message we need to remove it and update it when the page refresh arrives + if self.page_monitor.in_window(message): + if message.get("message_hash") in self.filter: + self.remove_message(message) + del self.filter[message.get("message_hash")] + self.should_refresh_window = True + + if message["command"] not in ["broadcast_text", "broadcast_text_m"]: + return + + # if this is a realtime message or get_data response, check if we should refresh the window + if not message.get("page_response") and not message.get( + "ref_link_page_response" + ): + if self.page_monitor.in_window(message): + self.should_refresh_window = True + return + + if message.get("command") == "broadcast_text_m": + self.filter[message.get("message_hash")] = True + if not message.get("text_hash") in self.multipart_staging: + self.multipart_staging[message.get("text_hash")] = MultipartMessage( + message + ) + else: + self.multipart_staging[message.get("text_hash")].add_part(message) + + multipart_message = self.multipart_staging[message.get("text_hash")] + if multipart_message.is_complete(): + message["body"] = multipart_message.assembled_body() + del self.multipart_staging[message.get("text_hash")] + else: + return + + if self.is_hearsay(message): + message["hearsay"] = True + + # track only messages that are page responses + self.filter[message.get("message_hash")] = True + self.add_message(message) diff -uNr akris-desktop/akris_desktop/chain.py akris-desktop-genesis/akris_desktop/chain.py --- akris-desktop/akris_desktop/chain.py false +++ akris-desktop-genesis/akris_desktop/chain.py c3c0c3aec605d13406d69efe0b11ed4fbdec4e5729894f26ac4ae5239e785f3896f3afe9556c116da736dfa2d94cfa6b91fe7d0cb6e786052d97d12b4dd4a921 @@ -0,0 +1,63 @@ +from treelib import Node, Tree + + +class Chain: + def __init__(self): + self.chain = Tree() + self.chain.create_node("root", "root") + + def add_message(self, message): + # if the tree is empty, add the message as a child of root + if self.chain.children("root") == []: + self.chain.create_node( + message["message_hash"], + message["message_hash"], + parent="root", + data=message, + ) + return + + # if the message is an antecedent (message_hash == net_chain), insert it at the top and reassign the children + # search for descendants of the message + def filter_function(node): + if node.data: + return node.data["net_chain"] == message["message_hash"] + else: + return False + + descendants = list(self.chain.filter_nodes(filter_function)) + if descendants: + # insert the message at the top of the chain + self.chain.create_node( + message["message_hash"], + message["message_hash"], + parent="root", + data=message, + ) + # reassign the children + for node in descendants: + self.chain.move_node(node.identifier, message["message_hash"]) + + # if the message is a descendant of a preexisting message, add it to the descendants + else: + self.chain.create_node( + message["message_hash"], + message["message_hash"], + parent=message["net_chain"], + data=message, + ) + + def index(self, message_hash): + expansion = self.chain.expand_tree( + mode=Tree.WIDTH, sorting=True, key=self.sort_by_timestamp + ) + for index, id in enumerate(expansion): + if id == message_hash: + return self.indexize(index) + + def sort_by_timestamp(self, node): + timestamp = node.data["timestamp"] + return timestamp + + def indexize(self, index): + return str(float(index)) diff -uNr akris-desktop/akris_desktop/console.py akris-desktop-genesis/akris_desktop/console.py --- akris-desktop/akris_desktop/console.py false +++ akris-desktop-genesis/akris_desktop/console.py 9692e5f15617533e9558500e0c7360b963daa28fb5bdaced4ed59aa9ea84267bc7a31cb4dc1f62395f27d00e7f1111f5d9ad637a851d8dc04006d2743a6b9c9d @@ -0,0 +1,190 @@ +import importlib +import os +import tkinter as tk +from tkinter import ttk + +from PIL import Image, ImageTk +from akris_desktop.console_renderers.at import At +from akris_desktop.console_renderers.default import Default +from akris_desktop.console_renderers.error import Error +from akris_desktop.console_renderers.help import Help +from akris_desktop.console_renderers.knob import Knob +from akris_desktop.console_renderers.search import Search +from akris_desktop.console_renderers.version_info import VersionInfo +from akris_desktop.console_renderers.wot import Wot + + +class Console(tk.Frame): + def __init__(self, root, api_client): + super().__init__(root) + self.root = root + self.history = [] + self.history_index = 0 + self.frame = tk.Frame(root) + self.frame.pack(fill=tk.BOTH, expand=True) + self.api_client = api_client + self.output = tk.Text( + self.frame, wrap=tk.WORD, highlightthickness=0, padx=10, pady=10 + ) + self.configure_scrollbar_style() + self.scrollbar = ttk.Scrollbar(self.frame) + self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.output.configure(yscrollcommand=self.scrollbar.set) + self.scrollbar.configure(command=self.output.yview) + self.output.character_insertion_index = 0 + self.output.configure(state="disabled") + self.output.configure(background="black", foreground="green") + self.output.pack(fill=tk.BOTH, expand=True) + + # Setup the context menus + self.output_context_menu = tk.Menu(self.output, tearoff=0) + self.configure_output_context_menu() + + self.entry = tk.Entry(self.frame, highlightthickness=0) + self.entry.configure( + insertbackground="green", background="black", foreground="green" + ) + self.entry.pack(fill=tk.X) + + # Setup the entry context menus + self.entry_context_menu = tk.Menu(self.output, tearoff=0) + self.configure_entry_context_menu() + + + # Bind events to the widget + self.entry.bind("", self.handle_enter) + self.entry.bind("", self.handle_up_arrow) + self.entry.bind("", self.handle_down_arrow) + + # Setup tags: + self.output.tag_configure("error", foreground="red") + self.output.tag_configure("search_term", font=tk.font.Font(size=11, weight="bold")) + lmargin2 = tk.font.Font(size=11).measure("nGsDUOHFRCmP0mG1h/KCDOSLM3hY4KBxthI2XF6B7D4= awt | ") + self.output.tag_configure("body", lmargin2=lmargin2) + + try: + with importlib.resources.path("akris_desktop", "images") as images_path: + self.insert_image(os.path.join(images_path, "pixelated_green_flag.png")) + self.insert_message({"body": "\n\n"}) + except FileNotFoundError: + pass + + # Setup renderers: + self.renderers = { + "at": At(self.output), + "error": Error(self.output), + "help": Help(self.output), + "knob": Knob(self.output), + "search": Search(self.output, self.api_client), + "version_info": VersionInfo(self.output), + "wot": Wot(self.output), + } + + def render_message(self, message): + if message["command"] == "console_response": + self.renderers.get(message.get("type"), Default(self.output)).render( + message + ) + self.output.see(tk.END) + + def configure_entry_context_menu(self): + self.entry_context_menu.add_command( + label="Paste", command=lambda: self.handle_entry_context_menu_paste() + ) + self.entry_context_menu.bind("", lambda e: self.entry_context_menu.unpost()) + self.entry.bind("", self.show_entry_context_menu) + + def show_entry_context_menu(self, event): + self.entry_context_menu.post(event.x_root, event.y_root) + + def handle_entry_context_menu_paste(self): + content = self.root.clipboard_get() + self.entry.insert(tk.INSERT, content) + + def configure_output_context_menu(self): + self.output_context_menu.add_command( + label="Copy", command=lambda: self.copy_output_selected_text() + ) + self.output_context_menu.bind("", lambda e: self.output_context_menu.unpost()) + self.output.bind("", self.show_output_context_menu) + + def copy_output_selected_text(self): + # Get the selected text + selected_text = self.output.get("sel.first", "sel.last") + + # Copy the selected text to the clipboard + self.output.clipboard_clear() + self.output.clipboard_append(selected_text) + + def show_output_context_menu(self, event): + self.output_context_menu.post(event.x_root, event.y_root) + + def configure_scrollbar_style(self): + style = ttk.Style() + style.theme_use("default") + style.configure( + "TScrollbar", + foreground="darkgray", + troughcolor="black", + background="darkgray", + arrowcolor="white", + bordercolor="black", + ) + def insert_message(self, message): + self.output.configure(state="normal") + self.output.insert(tk.END, message["body"]) + self.output.configure(state="disabled") + + def insert_image(self, image_path): + image = Image.open(image_path) + photo = ImageTk.PhotoImage(image) + self.output.image_create("insert", image=photo) + self.output.image = photo + + + def handle_enter(self, event): + if event.state == 0: + # Handle Enter key (send message) + self.execute_command() + return "break" + + def handle_up_arrow(self, event): + # if the index is 0, do nothing + # Get the clipboard content and insert it into the Text widget + if len(self.history) > 0: + self.entry.delete("0", tk.END) + self.entry.insert("0", self.history[self.history_index]) + if self.history_index > 0: + self.history_index -= 1 + + def handle_down_arrow(self, event): + # Get the clipboard content and insert it into the Text widget + self.entry.delete("0", tk.END) + if self.history_index < len(self.history) - 1: + self.history_index += 1 + self.entry.insert("0", self.history[self.history_index]) + + def execute_command(self): + # Get the text from the text entry widget + command_string = self.entry.get() + args = command_string.split(" ")[1:] + command = command_string.split(" ")[0] + if command == "search": + args = [" ".join(args)] + if command == "searchs": + args = command_string.split(" ")[2:] + speaker = command_string.split(" ")[1] + args = [speaker, " ".join(args)] + + # Log the command to the command history + self.history.append(command_string) + self.history_index = len(self.history) - 1 + + # Clear the text entry widget + self.entry.delete("0", tk.END) + + self.insert_message({"body": "> " + command_string + "\n"}) + self.api_client.send_command({"command": command, "args": args}) + + def configure_scrollbar_style(self): + pass diff -uNr akris-desktop/akris_desktop/console_renderers/__init__.py akris-desktop-genesis/akris_desktop/console_renderers/__init__.py --- akris-desktop/akris_desktop/console_renderers/__init__.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/__init__.py 26691c382525798d231437a34db006989c1884f59c1767858cdb4bee12bfcf9734b3105d5622e4cc5d1bf1e91765248656fd832e07a67056249c7a89703ddef8 @@ -0,0 +1 @@ +# empty diff -uNr akris-desktop/akris_desktop/console_renderers/at.py akris-desktop-genesis/akris_desktop/console_renderers/at.py --- akris-desktop/akris_desktop/console_renderers/at.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/at.py 479596b71b3d6bdafc1ef2df242e1e5356d4f753f60872b11492ed97008c33741f100acb0f7d19de8be738949c1a3cbcab4a44947126c24287a4b5ec0a4ddf62 @@ -0,0 +1,29 @@ +import tkinter as tk + + +class At: + def __init__(self, output): + self.output = output + + def render(self, response): + peers = response.get("body") + if len(peers) == 0: + self.render_no_peers() + else: + for peer in response.get("body"): + self.render_peer(peer) + + def render_peer(self, peer): + self.output.configure(state="normal") + self.output.insert( + tk.END, + "{}\t{}\t{}\n".format( + peer.get("handle"), peer.get("address"), peer.get("active_at") + ), + ) + self.output.configure(state="disabled") + + def render_no_peers(self): + self.output.configure(state="normal") + self.output.insert(tk.END, "No peers found.\n") + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/console_renderers/default.py akris-desktop-genesis/akris_desktop/console_renderers/default.py --- akris-desktop/akris_desktop/console_renderers/default.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/default.py 5dc9df490e852777bf67932f6afb56430080569436f47003a10b0afbf49bcbd59695d4ee81a42bdd4cca330b8fb289d0181a0e432a9c4b897eaa4bdd247d5b0c @@ -0,0 +1,13 @@ +import tkinter as tk + + +class Default: + def __init__(self, output): + self.output = output + pass + + def render(self, response): + if response.get("body"): + self.output.configure(state="normal") + self.output.insert(tk.END, "{}\n".format(response.get("body"))) + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/console_renderers/error.py akris-desktop-genesis/akris_desktop/console_renderers/error.py --- akris-desktop/akris_desktop/console_renderers/error.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/error.py e21b56af73ff253b6994b529f3cafaef9bb89f1f7fcbca017a457775bca648d30b36a989c306b9058a5f40f4699e0baff6693ff567b2536ad1701c3648d29c45 @@ -0,0 +1,20 @@ +import tkinter as tk + + +class Error: + def __init__(self, output): + self.output = output + + def render(self, response): + error = response.get("body") + self.render_error(error) + + def render_error(self, error): + self.output.configure(state="normal") + self.output.tag_remove("sel", "1.0", tk.END) + self.output.mark_set(tk.INSERT, tk.END) + start = self.output.index(tk.INSERT) + self.output.insert(tk.END, "{}\n".format(error)) + end = "{}.end".format(int(float(start))) + self.output.tag_add("error", start, end) + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/console_renderers/help.py akris-desktop-genesis/akris_desktop/console_renderers/help.py --- akris-desktop/akris_desktop/console_renderers/help.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/help.py 7f3c78cd8c90b84aaddf87d0fc294d0a5738c038a3e5361f2545dab48867ab3facdf513ea7d28940aaaa4aad6601fbfd071d9ad67218851d79b85297b5c35180 @@ -0,0 +1,16 @@ +import tkinter as tk + + +class Help: + def __init__(self, output): + self.output = output + + def render(self, response): + help_dict = response.get("body") + for key, value in help_dict.items(): + self.render_help(value["help"]) + + def render_help(self, error): + self.output.configure(state="normal") + self.output.insert(tk.END, "{}\n".format(error)) + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/console_renderers/knob.py akris-desktop-genesis/akris_desktop/console_renderers/knob.py --- akris-desktop/akris_desktop/console_renderers/knob.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/knob.py 589306c4b4c775a92139ffd6052277324ab59a5ac9e1e38022baf098c213072d7398cfb99a65ea110a860dc396c649f8d4e270ba65190ffbc46ba1797faab411 @@ -0,0 +1,16 @@ +import tkinter as tk + + +class Knob: + def __init__(self, output): + self.output = output + + def render(self, response): + knobs = response.get("body") + for key, value in knobs.items(): + self.render_knob("{}:\t{}".format(key, value)) + + def render_knob(self, knob): + self.output.configure(state="normal") + self.output.insert(tk.END, "{}\n".format(knob)) + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/console_renderers/search.py akris-desktop-genesis/akris_desktop/console_renderers/search.py --- akris-desktop/akris_desktop/console_renderers/search.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/search.py e2f841719995b53bf6092c20f15d0b4787fbd789382cec653b68f7be54857be1603b9f970e4fa1ecb3c873d0a6087fbbf86158dff56502a1613a26ceb4c4474c @@ -0,0 +1,116 @@ +import re +import tkinter as tk +from functools import partial +from ..timeline_view import remove_unicode_surrogates + +class SearchResult: + def __init__(self, record): + ( + self.speaker, + self.recipient_handle, + self.body, + self.base64_message_hash + ) = record + +class Search: + def __init__(self, output, api_client): + self.output = output + self.search_results = {} + self.api_client = api_client + self.output.tag_bind("message_hash", "", partial(self.on_message_hash_click, "message_hash")) + self.output.tag_bind( + "message_hash", "", (lambda e: self.output.config(cursor="hand2")) + ) + self.output.tag_bind( + "message_hash", "", (lambda e: self.output.config(cursor="xterm")) + ) + + def render(self, search_results): + if len(search_results.get("body")) == 0: + self.insert_text("No results\n") + + for search_result in search_results.get("body"): + self.render_search_result(SearchResult(search_result)) + + def on_message_hash_click(self, tag, event): + tag_range = event.widget.tag_prevrange(tag, "current + 1 char") + assert tag_range + start, end = tag_range + message_hash = event.widget.get(start, end).rstrip() + search_result = self.search_results[message_hash] + if search_result.recipient_handle: + args = [message_hash, search_result.recipient_handle] + else: + args = [message_hash] + self.api_client.send_command( + {"command": "page_around", "args": args} + ) + + def render_search_result(self, search_result): + self.search_results[search_result.base64_message_hash] = search_result + self.output.character_insertion_index = 0 + self.render_message_hash(search_result.base64_message_hash) + self.render_speaker(search_result.speaker) + self.render_body(search_result.body) + # line_index = int(self.output.index(tk.END).split(".")[0]) + # self.output.tag_add("error", f"{int(line_index)-2}.0", self.output.index(tk.END)) + + def render_message_hash(self, message_hash): + self.insert_text( + f"{message_hash}", + ("message_hash",), + ) + + def render_speaker(self, speaker): + justified_speaker = f"{speaker:>{20}}" + self.insert_text( + f"{justified_speaker} | ", + ) + + def render_body(self, body): + body = remove_unicode_surrogates(body) + positions = self.replace_search_terms(body) + self.insert_text( + f"{positions.get('body')}\n", + ("body",), + ) + for position in positions["positions"]: + self.tag_search_terms(position) + + def tag_search_terms(self, position): + self.output.tag_add( + "search_term", + position["match_start"], + position["match_end"], + ) + + def replace_search_terms(self, body): + line_index = int(self.output.index(tk.END).split(".")[0]) - 1 + positions = [] + pattern = r'<<([^<>]+)>>' + match = re.search(pattern, body) + while match: + body = re.sub(pattern, r'\1', body, count=1) + positions.append({ + "match_start": f"{line_index}.{self.output.character_insertion_index + match.start()}", + "match_end": f"{line_index}.{self.output.character_insertion_index + match.start() + len(match.group(1))}", + }) + match = re.search(pattern, body) + return { + "body": body, + "positions": positions, + } + + def insert_text(self, text, tags=()): + self.output.configure(state="normal") + line_index = self.output.index(tk.END).split(".")[0] + self.output.insert( + self.get_insertion_index(line_index), + text, + tags, + ) + self.output.configure(state="disabled") + self.output.character_insertion_index += len(text) + + def get_insertion_index(self, line_index): + return "{}.{}".format(line_index, self.output.character_insertion_index) diff -uNr akris-desktop/akris_desktop/console_renderers/version_info.py akris-desktop-genesis/akris_desktop/console_renderers/version_info.py --- akris-desktop/akris_desktop/console_renderers/version_info.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/version_info.py 4521a2c9a8b1197d64677d0cba9ae5b570a8dd8b743283751816534ae6eed9acbcf45d0e81e8e4b261720646a78f347f7336449e018a13ec1ebd73939dcfa2c7 @@ -0,0 +1,24 @@ +import tkinter as tk + + +class VersionInfo: + def __init__(self, output): + self.output = output + + def render(self, response): + akris_version = response.get("version") + pest_version = response.get("pest_version") + earliest_supported_pest_version = response.get( + "earliest_supported_pest_version" + ) + body = ( + "Welcome to the Akris Desktop Pest console!\n\n" + f"Akris station version: {akris_version}\n" + f"Pest version: {hex(pest_version)}\n" + f"Minimum pest version: {hex(earliest_supported_pest_version)}\n\n" + f"Enter a pest command below or type help for a list of supported commands.\n\n" + ) + + self.output.configure(state="normal") + self.output.insert(tk.END, body) + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/console_renderers/wot.py akris-desktop-genesis/akris_desktop/console_renderers/wot.py --- akris-desktop/akris_desktop/console_renderers/wot.py false +++ akris-desktop-genesis/akris_desktop/console_renderers/wot.py 99c2205fdeb68283c183aeea8ee033e7261429ef37c0704aa146f85fecb24fc0c417d02de15a8c3810ea7d438b6d79337923e6a7c723ffd9936053e3d621186e @@ -0,0 +1,33 @@ +import tkinter as tk + + +class Wot: + def __init__(self, output): + self.output = output + + def render(self, response): + peers = response.get("body") + if len(peers) == 0: + self.render_no_peers() + else: + for peer in response.get("body"): + self.render_peer(peer) + + def render_peer(self, peer): + if peer.get("address") and peer.get("port"): + address = "{}:{}".format(peer.get("address"), peer.get("port")) + else: + address = "
" + self.output.configure(state="normal") + self.output.insert( + tk.END, "{}\t{}\n".format(" ".join(peer.get("handles")), address) + ) + if len(peer.get("keys")) > 0: + for key in peer.get("keys"): + self.output.insert(tk.END, "\t{}\n".format(key)) + self.output.configure(state="disabled") + + def render_no_peers(self): + self.output.configure(state="normal") + self.output.insert(tk.END, "No peers found.\n") + self.output.configure(state="disabled") diff -uNr akris-desktop/akris_desktop/direct_message_entry.py akris-desktop-genesis/akris_desktop/direct_message_entry.py --- akris-desktop/akris_desktop/direct_message_entry.py false +++ akris-desktop-genesis/akris_desktop/direct_message_entry.py ab01186bad4320eb0c56506861af19f2a50d64e98719b41d0428e09c6c7b3de033164cf6af47b336e06a03c63b6bd0621d6f61cdb1e4cfe0a6a83c3a612a3bf1 @@ -0,0 +1,36 @@ +import tkinter as tk +from .message_entry import MessageEntry, count_bytes + + +class DirectMessageEntry(MessageEntry): + def __init__(self, root, app, handle): + super().__init__(root, app) + self.handle = handle + + def handle_slash_command(self, message): + if message.startswith("/close"): + self.app.close_tab(self.root) + # Clear the text entry widget + self.text.delete("1.0", tk.END) + + def send_message(self): + # Get the text from the text entry widget + message = self.text.get("1.0", tk.END).rstrip("\n") + if message[0] == "/": + message = self.encode_action(message) + + # Clear the text entry widget + self.text.delete("1.0", tk.END) + + if not self.app.disable_multipart: + # count the number of bytes in the message body + # if necessary send a multipart message + if count_bytes(message) > 324: + self.app.api_client.send_command( + {"command": "direct_text_m", "args": [self.handle, message]} + ) + return + + self.app.api_client.send_command( + {"command": "direct_text", "args": [self.handle, message]} + ) diff -uNr akris-desktop/akris_desktop/direct_tab.py akris-desktop-genesis/akris_desktop/direct_tab.py --- akris-desktop/akris_desktop/direct_tab.py false +++ akris-desktop-genesis/akris_desktop/direct_tab.py d20bfabdd0d03d3be254e33d8e8ad302007c69cb0f2fea036e5bdcc15d6386a96c129a8ecdd0f3cbaca4f46b26213152073db35cebfa6b4d5f93117e1f28266f @@ -0,0 +1,39 @@ +import tkinter as tk + +from .direct_message_entry import DirectMessageEntry +from .direct_view import DirectView + + +class DirectTab(tk.Frame): + def __init__(self, root, handle, app): + super().__init__(root) + self.handle = handle + self.app = app + self.toolbar_frame = tk.Frame(self, background="#333232") + self.toolbar_frame.pack(side=tk.TOP, fill=tk.X) + self.close_link = tk.Label( + self.toolbar_frame, + text="๐Ÿ—™", + fg="white", + cursor="hand2", + background="red", + ) + self.close_link.pack(side=tk.RIGHT) + self.close_link.bind("", lambda e: self.app.close_tab(self)) + self.messages_and_peers_frame = tk.Frame(self) + self.messages_and_peers_frame.pack(fill=tk.BOTH, expand=True) + self.message_table = DirectView( + self.messages_and_peers_frame, handle, app, self + ) + self.more_link = tk.Label( + self.toolbar_frame, + text=self.message_table.page_monitor.button_label(), + fg="#00a1ff", + cursor="hand2", + background="#333232", + ) + self.message_table.await_message_stats() + self.more_link.pack(side=tk.LEFT) + self.more_link.bind("", lambda e: self.message_table.load_previous()) + self.message_entry = DirectMessageEntry(self, app, handle) + self.app.register_message_listener(self.message_table) diff -uNr akris-desktop/akris_desktop/direct_view.py akris-desktop-genesis/akris_desktop/direct_view.py --- akris-desktop/akris_desktop/direct_view.py false +++ akris-desktop-genesis/akris_desktop/direct_view.py f4a98d391364aa5dd2a3ac93e89b68bc328ede37be51c0e788282eef128348fac971397085b289d6c850d711552f8e62fcc8049b11dd6ad9096d24835a6b91d4 @@ -0,0 +1,64 @@ +import datetime +import time + +from .timeline_view import TimelineView + + +class DirectView(TimelineView): + def __init__(self, root, handle, app, tab): + self.handle = handle + self.tab = tab + super().__init__(root, app) + + def minimum_days_before(self): + latest_ts = self.app.message_stats.get("direct_message_timestamps", {}).get( + self.handle + ) + if not latest_ts: + return 1 + latest_message_date = datetime.datetime.fromtimestamp(latest_ts) + now = datetime.datetime.now() + return (now - latest_message_date).days + 1 + + def render_message(self, message): + # check for duplicates + super().render_message(message) + + if message.get("ref_link_page_response"): + return + + # is this a direct_text? + if message.get("command") not in ["direct_text", "direct_text_m"]: + return + + # is this the right dm tab? + if not ( + message.get("handle") == self.handle + or message.get("speaker") == self.handle + ): + return + + # if this is a realtime message or get_data response, update the window if it's in range + self.update_window(message) + + # is this a realtime message or get_data response? + if not message.get("page_response"): + return + + # add message to filter and render + self.filter[message.get("message_hash")] = True + self.add_message(message) + + def load_page(self, command): + if command == "page_up": + self.app.api_client.send_command( + { + "command": command, + "args": [time.time(), self.page_monitor.days_before, self.handle], + } + ) + + def update_window(self, message): + if not message.get("page_response"): + if self.page_monitor.in_window(message): + self.should_refresh_window = True diff -uNr akris-desktop/akris_desktop/emojis/__init__.py akris-desktop-genesis/akris_desktop/emojis/__init__.py --- akris-desktop/akris_desktop/emojis/__init__.py false +++ akris-desktop-genesis/akris_desktop/emojis/__init__.py 4f3df6c590c4c7dd2ec0f3ccce58663730ddc3b8d182bc7f38f2ad5cc48385b1f69ae8a351368cbb24fe34a0d06facd57f81c6292da031cad8869b0e0af43b38 @@ -0,0 +1,7 @@ +""" +Emojis for Python ๐Ÿ +""" + +__all__ = ["encode", "decode", "get", "count", "iter"] + +from .emojis import encode, decode, get, count, iter diff -uNr akris-desktop/akris_desktop/emojis/db/__init__.py akris-desktop-genesis/akris_desktop/emojis/db/__init__.py --- akris-desktop/akris_desktop/emojis/db/__init__.py false +++ akris-desktop-genesis/akris_desktop/emojis/db/__init__.py 60fbdf457de2851a3147562b6eef39985c674e59da9c285a2d3378f4135d5e3dccf791a4a5b5c7e6f4d0716ac5b9f7131af2971ebceff31c6dbfb1f8924350d6 @@ -0,0 +1,25 @@ +""" +Emoji database. +""" + +__all__ = [ + "Emoji", + "get_emoji_aliases", + "get_emoji_by_code", + "get_emoji_by_alias", + "get_emojis_by_tag", + "get_emojis_by_category", + "get_tags", + "get_categories", +] + +from .db import Emoji +from .utils import ( + get_emoji_aliases, + get_emoji_by_code, + get_emoji_by_alias, + get_emojis_by_tag, + get_emojis_by_category, + get_tags, + get_categories, +) diff -uNr akris-desktop/akris_desktop/emojis/db/db.py akris-desktop-genesis/akris_desktop/emojis/db/db.py --- akris-desktop/akris_desktop/emojis/db/db.py false +++ akris-desktop-genesis/akris_desktop/emojis/db/db.py 288626e8e6a0b2ce5513f929bb9b3dbf1abd69c7ad8a85459e91b563b635c989619a63d20d05150ce44dae56fdbf20fd7d8972f530be72be309683c7519d1407 @@ -0,0 +1,2016 @@ +### This is a generated file. +### Do not edit this file. +### Date: 2022-12-01T12:27:54 +### This file is based on v4.0.1. + +from collections import namedtuple + +Emoji = namedtuple("Emoji", ["aliases", "emoji", "tags", "category", "unicode_version"]) + +EMOJI_DB = [ + Emoji(["grinning"], "๐Ÿ˜€", ["smile", "happy"], "Smileys & Emotion", "6.1"), + Emoji(["smiley"], "๐Ÿ˜ƒ", ["happy", "joy", "haha"], "Smileys & Emotion", "6.0"), + Emoji( + ["smile"], "๐Ÿ˜„", ["happy", "joy", "laugh", "pleased"], "Smileys & Emotion", "6.0" + ), + Emoji(["grin"], "๐Ÿ˜", [], "Smileys & Emotion", "6.0"), + Emoji( + ["laughing", "satisfied"], "๐Ÿ˜†", ["happy", "haha"], "Smileys & Emotion", "6.0" + ), + Emoji(["sweat_smile"], "๐Ÿ˜…", ["hot"], "Smileys & Emotion", "6.0"), + Emoji(["rofl"], "๐Ÿคฃ", ["lol", "laughing"], "Smileys & Emotion", "9.0"), + Emoji(["joy"], "๐Ÿ˜‚", ["tears"], "Smileys & Emotion", "6.0"), + Emoji(["slightly_smiling_face"], "๐Ÿ™‚", [], "Smileys & Emotion", "7.0"), + Emoji(["upside_down_face"], "๐Ÿ™ƒ", [], "Smileys & Emotion", "8.0"), + Emoji(["melting_face"], "๐Ÿซ ", ["sarcasm", "dread"], "Smileys & Emotion", "14.0"), + Emoji(["wink"], "๐Ÿ˜‰", ["flirt"], "Smileys & Emotion", "6.0"), + Emoji(["blush"], "๐Ÿ˜Š", ["proud"], "Smileys & Emotion", "6.0"), + Emoji(["innocent"], "๐Ÿ˜‡", ["angel"], "Smileys & Emotion", "6.0"), + Emoji( + ["smiling_face_with_three_hearts"], "๐Ÿฅฐ", ["love"], "Smileys & Emotion", "11.0" + ), + Emoji(["heart_eyes"], "๐Ÿ˜", ["love", "crush"], "Smileys & Emotion", "6.0"), + Emoji(["star_struck"], "๐Ÿคฉ", ["eyes"], "Smileys & Emotion", "11.0"), + Emoji(["kissing_heart"], "๐Ÿ˜˜", ["flirt"], "Smileys & Emotion", "6.0"), + Emoji(["kissing"], "๐Ÿ˜—", [], "Smileys & Emotion", "6.1"), + Emoji(["relaxed"], "โ˜บ๏ธ", ["blush", "pleased"], "Smileys & Emotion", ""), + Emoji(["kissing_closed_eyes"], "๐Ÿ˜š", [], "Smileys & Emotion", "6.0"), + Emoji(["kissing_smiling_eyes"], "๐Ÿ˜™", [], "Smileys & Emotion", "6.1"), + Emoji(["smiling_face_with_tear"], "๐Ÿฅฒ", [], "Smileys & Emotion", "13.0"), + Emoji(["yum"], "๐Ÿ˜‹", ["tongue", "lick"], "Smileys & Emotion", "6.0"), + Emoji(["stuck_out_tongue"], "๐Ÿ˜›", [], "Smileys & Emotion", "6.1"), + Emoji( + ["stuck_out_tongue_winking_eye"], + "๐Ÿ˜œ", + ["prank", "silly"], + "Smileys & Emotion", + "6.0", + ), + Emoji(["zany_face"], "๐Ÿคช", ["goofy", "wacky"], "Smileys & Emotion", "11.0"), + Emoji(["stuck_out_tongue_closed_eyes"], "๐Ÿ˜", ["prank"], "Smileys & Emotion", "6.0"), + Emoji(["money_mouth_face"], "๐Ÿค‘", ["rich"], "Smileys & Emotion", "8.0"), + Emoji(["hugs"], "๐Ÿค—", [], "Smileys & Emotion", "8.0"), + Emoji(["hand_over_mouth"], "๐Ÿคญ", ["quiet", "whoops"], "Smileys & Emotion", "11.0"), + Emoji( + ["face_with_open_eyes_and_hand_over_mouth"], + "๐Ÿซข", + ["gasp", "shock"], + "Smileys & Emotion", + "14.0", + ), + Emoji(["face_with_peeking_eye"], "๐Ÿซฃ", [], "Smileys & Emotion", "14.0"), + Emoji(["shushing_face"], "๐Ÿคซ", ["silence", "quiet"], "Smileys & Emotion", "11.0"), + Emoji(["thinking"], "๐Ÿค”", [], "Smileys & Emotion", "8.0"), + Emoji(["saluting_face"], "๐Ÿซก", ["respect"], "Smileys & Emotion", "14.0"), + Emoji(["zipper_mouth_face"], "๐Ÿค", ["silence", "hush"], "Smileys & Emotion", "8.0"), + Emoji(["raised_eyebrow"], "๐Ÿคจ", ["suspicious"], "Smileys & Emotion", "11.0"), + Emoji(["neutral_face"], "๐Ÿ˜", ["meh"], "Smileys & Emotion", "6.0"), + Emoji(["expressionless"], "๐Ÿ˜‘", [], "Smileys & Emotion", "6.1"), + Emoji(["no_mouth"], "๐Ÿ˜ถ", ["mute", "silence"], "Smileys & Emotion", "6.0"), + Emoji(["dotted_line_face"], "๐Ÿซฅ", ["invisible"], "Smileys & Emotion", "14.0"), + Emoji(["face_in_clouds"], "๐Ÿ˜ถโ€๐ŸŒซ๏ธ", [], "Smileys & Emotion", "13.1"), + Emoji(["smirk"], "๐Ÿ˜", ["smug"], "Smileys & Emotion", "6.0"), + Emoji(["unamused"], "๐Ÿ˜’", ["meh"], "Smileys & Emotion", "6.0"), + Emoji(["roll_eyes"], "๐Ÿ™„", [], "Smileys & Emotion", "8.0"), + Emoji(["grimacing"], "๐Ÿ˜ฌ", [], "Smileys & Emotion", "6.1"), + Emoji(["face_exhaling"], "๐Ÿ˜ฎโ€๐Ÿ’จ", [], "Smileys & Emotion", "13.1"), + Emoji(["lying_face"], "๐Ÿคฅ", ["liar"], "Smileys & Emotion", "9.0"), + Emoji(["relieved"], "๐Ÿ˜Œ", ["whew"], "Smileys & Emotion", "6.0"), + Emoji(["pensive"], "๐Ÿ˜”", [], "Smileys & Emotion", "6.0"), + Emoji(["sleepy"], "๐Ÿ˜ช", ["tired"], "Smileys & Emotion", "6.0"), + Emoji(["drooling_face"], "๐Ÿคค", [], "Smileys & Emotion", "9.0"), + Emoji(["sleeping"], "๐Ÿ˜ด", ["zzz"], "Smileys & Emotion", "6.1"), + Emoji(["mask"], "๐Ÿ˜ท", ["sick", "ill"], "Smileys & Emotion", "6.0"), + Emoji(["face_with_thermometer"], "๐Ÿค’", ["sick"], "Smileys & Emotion", "8.0"), + Emoji(["face_with_head_bandage"], "๐Ÿค•", ["hurt"], "Smileys & Emotion", "8.0"), + Emoji( + ["nauseated_face"], + "๐Ÿคข", + ["sick", "barf", "disgusted"], + "Smileys & Emotion", + "9.0", + ), + Emoji(["vomiting_face"], "๐Ÿคฎ", ["barf", "sick"], "Smileys & Emotion", "11.0"), + Emoji(["sneezing_face"], "๐Ÿคง", ["achoo", "sick"], "Smileys & Emotion", "9.0"), + Emoji(["hot_face"], "๐Ÿฅต", ["heat", "sweating"], "Smileys & Emotion", "11.0"), + Emoji(["cold_face"], "๐Ÿฅถ", ["freezing", "ice"], "Smileys & Emotion", "11.0"), + Emoji(["woozy_face"], "๐Ÿฅด", ["groggy"], "Smileys & Emotion", "11.0"), + Emoji(["dizzy_face"], "๐Ÿ˜ต", [], "Smileys & Emotion", "6.0"), + Emoji(["face_with_spiral_eyes"], "๐Ÿ˜ตโ€๐Ÿ’ซ", [], "Smileys & Emotion", "13.1"), + Emoji(["exploding_head"], "๐Ÿคฏ", ["mind", "blown"], "Smileys & Emotion", "11.0"), + Emoji(["cowboy_hat_face"], "๐Ÿค ", [], "Smileys & Emotion", "9.0"), + Emoji( + ["partying_face"], "๐Ÿฅณ", ["celebration", "birthday"], "Smileys & Emotion", "11.0" + ), + Emoji(["disguised_face"], "๐Ÿฅธ", [], "Smileys & Emotion", "13.0"), + Emoji(["sunglasses"], "๐Ÿ˜Ž", ["cool"], "Smileys & Emotion", "6.0"), + Emoji(["nerd_face"], "๐Ÿค“", ["geek", "glasses"], "Smileys & Emotion", "8.0"), + Emoji(["monocle_face"], "๐Ÿง", [], "Smileys & Emotion", "11.0"), + Emoji(["confused"], "๐Ÿ˜•", [], "Smileys & Emotion", "6.1"), + Emoji(["face_with_diagonal_mouth"], "๐Ÿซค", ["confused"], "Smileys & Emotion", "14.0"), + Emoji(["worried"], "๐Ÿ˜Ÿ", ["nervous"], "Smileys & Emotion", "6.1"), + Emoji(["slightly_frowning_face"], "๐Ÿ™", [], "Smileys & Emotion", "7.0"), + Emoji(["frowning_face"], "โ˜น๏ธ", [], "Smileys & Emotion", ""), + Emoji( + ["open_mouth"], + "๐Ÿ˜ฎ", + ["surprise", "impressed", "wow"], + "Smileys & Emotion", + "6.1", + ), + Emoji(["hushed"], "๐Ÿ˜ฏ", ["silence", "speechless"], "Smileys & Emotion", "6.1"), + Emoji(["astonished"], "๐Ÿ˜ฒ", ["amazed", "gasp"], "Smileys & Emotion", "6.0"), + Emoji(["flushed"], "๐Ÿ˜ณ", [], "Smileys & Emotion", "6.0"), + Emoji(["pleading_face"], "๐Ÿฅบ", ["puppy", "eyes"], "Smileys & Emotion", "11.0"), + Emoji( + ["face_holding_back_tears"], + "๐Ÿฅน", + ["tears", "gratitude"], + "Smileys & Emotion", + "14.0", + ), + Emoji(["frowning"], "๐Ÿ˜ฆ", [], "Smileys & Emotion", "6.1"), + Emoji(["anguished"], "๐Ÿ˜ง", ["stunned"], "Smileys & Emotion", "6.1"), + Emoji(["fearful"], "๐Ÿ˜จ", ["scared", "shocked", "oops"], "Smileys & Emotion", "6.0"), + Emoji(["cold_sweat"], "๐Ÿ˜ฐ", ["nervous"], "Smileys & Emotion", "6.0"), + Emoji( + ["disappointed_relieved"], + "๐Ÿ˜ฅ", + ["phew", "sweat", "nervous"], + "Smileys & Emotion", + "6.0", + ), + Emoji(["cry"], "๐Ÿ˜ข", ["sad", "tear"], "Smileys & Emotion", "6.0"), + Emoji(["sob"], "๐Ÿ˜ญ", ["sad", "cry", "bawling"], "Smileys & Emotion", "6.0"), + Emoji(["scream"], "๐Ÿ˜ฑ", ["horror", "shocked"], "Smileys & Emotion", "6.0"), + Emoji(["confounded"], "๐Ÿ˜–", [], "Smileys & Emotion", "6.0"), + Emoji(["persevere"], "๐Ÿ˜ฃ", ["struggling"], "Smileys & Emotion", "6.0"), + Emoji(["disappointed"], "๐Ÿ˜ž", ["sad"], "Smileys & Emotion", "6.0"), + Emoji(["sweat"], "๐Ÿ˜“", [], "Smileys & Emotion", "6.0"), + Emoji(["weary"], "๐Ÿ˜ฉ", ["tired"], "Smileys & Emotion", "6.0"), + Emoji(["tired_face"], "๐Ÿ˜ซ", ["upset", "whine"], "Smileys & Emotion", "6.0"), + Emoji(["yawning_face"], "๐Ÿฅฑ", [], "Smileys & Emotion", "12.0"), + Emoji(["triumph"], "๐Ÿ˜ค", ["smug"], "Smileys & Emotion", "6.0"), + Emoji(["rage", "pout"], "๐Ÿ˜ก", ["angry"], "Smileys & Emotion", "6.0"), + Emoji(["angry"], "๐Ÿ˜ ", ["mad", "annoyed"], "Smileys & Emotion", "6.0"), + Emoji(["cursing_face"], "๐Ÿคฌ", ["foul"], "Smileys & Emotion", "11.0"), + Emoji(["smiling_imp"], "๐Ÿ˜ˆ", ["devil", "evil", "horns"], "Smileys & Emotion", "6.0"), + Emoji( + ["imp"], "๐Ÿ‘ฟ", ["angry", "devil", "evil", "horns"], "Smileys & Emotion", "6.0" + ), + Emoji(["skull"], "๐Ÿ’€", ["dead", "danger", "poison"], "Smileys & Emotion", "6.0"), + Emoji( + ["skull_and_crossbones"], "โ˜ ๏ธ", ["danger", "pirate"], "Smileys & Emotion", "" + ), + Emoji(["hankey", "poop", "shit"], "๐Ÿ’ฉ", ["crap"], "Smileys & Emotion", "6.0"), + Emoji(["clown_face"], "๐Ÿคก", [], "Smileys & Emotion", "9.0"), + Emoji(["japanese_ogre"], "๐Ÿ‘น", ["monster"], "Smileys & Emotion", "6.0"), + Emoji(["japanese_goblin"], "๐Ÿ‘บ", [], "Smileys & Emotion", "6.0"), + Emoji(["ghost"], "๐Ÿ‘ป", ["halloween"], "Smileys & Emotion", "6.0"), + Emoji(["alien"], "๐Ÿ‘ฝ", ["ufo"], "Smileys & Emotion", "6.0"), + Emoji(["space_invader"], "๐Ÿ‘พ", ["game", "retro"], "Smileys & Emotion", "6.0"), + Emoji(["robot"], "๐Ÿค–", [], "Smileys & Emotion", "8.0"), + Emoji(["smiley_cat"], "๐Ÿ˜บ", [], "Smileys & Emotion", "6.0"), + Emoji(["smile_cat"], "๐Ÿ˜ธ", [], "Smileys & Emotion", "6.0"), + Emoji(["joy_cat"], "๐Ÿ˜น", [], "Smileys & Emotion", "6.0"), + Emoji(["heart_eyes_cat"], "๐Ÿ˜ป", [], "Smileys & Emotion", "6.0"), + Emoji(["smirk_cat"], "๐Ÿ˜ผ", [], "Smileys & Emotion", "6.0"), + Emoji(["kissing_cat"], "๐Ÿ˜ฝ", [], "Smileys & Emotion", "6.0"), + Emoji(["scream_cat"], "๐Ÿ™€", ["horror"], "Smileys & Emotion", "6.0"), + Emoji(["crying_cat_face"], "๐Ÿ˜ฟ", ["sad", "tear"], "Smileys & Emotion", "6.0"), + Emoji(["pouting_cat"], "๐Ÿ˜พ", [], "Smileys & Emotion", "6.0"), + Emoji( + ["see_no_evil"], "๐Ÿ™ˆ", ["monkey", "blind", "ignore"], "Smileys & Emotion", "6.0" + ), + Emoji(["hear_no_evil"], "๐Ÿ™‰", ["monkey", "deaf"], "Smileys & Emotion", "6.0"), + Emoji( + ["speak_no_evil"], "๐Ÿ™Š", ["monkey", "mute", "hush"], "Smileys & Emotion", "6.0" + ), + Emoji(["kiss"], "๐Ÿ’‹", ["lipstick"], "Smileys & Emotion", "6.0"), + Emoji(["love_letter"], "๐Ÿ’Œ", ["email", "envelope"], "Smileys & Emotion", "6.0"), + Emoji(["cupid"], "๐Ÿ’˜", ["love", "heart"], "Smileys & Emotion", "6.0"), + Emoji(["gift_heart"], "๐Ÿ’", ["chocolates"], "Smileys & Emotion", "6.0"), + Emoji(["sparkling_heart"], "๐Ÿ’–", [], "Smileys & Emotion", "6.0"), + Emoji(["heartpulse"], "๐Ÿ’—", [], "Smileys & Emotion", "6.0"), + Emoji(["heartbeat"], "๐Ÿ’“", [], "Smileys & Emotion", "6.0"), + Emoji(["revolving_hearts"], "๐Ÿ’ž", [], "Smileys & Emotion", "6.0"), + Emoji(["two_hearts"], "๐Ÿ’•", [], "Smileys & Emotion", "6.0"), + Emoji(["heart_decoration"], "๐Ÿ’Ÿ", [], "Smileys & Emotion", "6.0"), + Emoji(["heavy_heart_exclamation"], "โฃ๏ธ", [], "Smileys & Emotion", ""), + Emoji(["broken_heart"], "๐Ÿ’”", [], "Smileys & Emotion", "6.0"), + Emoji(["heart_on_fire"], "โค๏ธโ€๐Ÿ”ฅ", [], "Smileys & Emotion", "13.1"), + Emoji(["mending_heart"], "โค๏ธโ€๐Ÿฉน", [], "Smileys & Emotion", "13.1"), + Emoji(["heart"], "โค๏ธ", ["love"], "Smileys & Emotion", ""), + Emoji(["orange_heart"], "๐Ÿงก", [], "Smileys & Emotion", "11.0"), + Emoji(["yellow_heart"], "๐Ÿ’›", [], "Smileys & Emotion", "6.0"), + Emoji(["green_heart"], "๐Ÿ’š", [], "Smileys & Emotion", "6.0"), + Emoji(["blue_heart"], "๐Ÿ’™", [], "Smileys & Emotion", "6.0"), + Emoji(["purple_heart"], "๐Ÿ’œ", [], "Smileys & Emotion", "6.0"), + Emoji(["brown_heart"], "๐ŸคŽ", [], "Smileys & Emotion", "12.0"), + Emoji(["black_heart"], "๐Ÿ–ค", [], "Smileys & Emotion", "9.0"), + Emoji(["white_heart"], "๐Ÿค", [], "Smileys & Emotion", "12.0"), + Emoji(["100"], "๐Ÿ’ฏ", ["score", "perfect"], "Smileys & Emotion", "6.0"), + Emoji(["anger"], "๐Ÿ’ข", ["angry"], "Smileys & Emotion", "6.0"), + Emoji(["boom", "collision"], "๐Ÿ’ฅ", ["explode"], "Smileys & Emotion", "6.0"), + Emoji(["dizzy"], "๐Ÿ’ซ", ["star"], "Smileys & Emotion", "6.0"), + Emoji(["sweat_drops"], "๐Ÿ’ฆ", ["water", "workout"], "Smileys & Emotion", "6.0"), + Emoji(["dash"], "๐Ÿ’จ", ["wind", "blow", "fast"], "Smileys & Emotion", "6.0"), + Emoji(["hole"], "๐Ÿ•ณ๏ธ", [], "Smileys & Emotion", "7.0"), + Emoji(["bomb"], "๐Ÿ’ฃ", ["boom"], "Smileys & Emotion", "6.0"), + Emoji(["speech_balloon"], "๐Ÿ’ฌ", ["comment"], "Smileys & Emotion", "6.0"), + Emoji(["eye_speech_bubble"], "๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ", [], "Smileys & Emotion", "11.0"), + Emoji(["left_speech_bubble"], "๐Ÿ—จ๏ธ", [], "Smileys & Emotion", "11.0"), + Emoji(["right_anger_bubble"], "๐Ÿ—ฏ๏ธ", [], "Smileys & Emotion", "7.0"), + Emoji(["thought_balloon"], "๐Ÿ’ญ", ["thinking"], "Smileys & Emotion", "6.0"), + Emoji(["zzz"], "๐Ÿ’ค", ["sleeping"], "Smileys & Emotion", "6.0"), + Emoji(["wave"], "๐Ÿ‘‹", ["goodbye"], "People & Body", "6.0"), + Emoji(["raised_back_of_hand"], "๐Ÿคš", [], "People & Body", "9.0"), + Emoji(["raised_hand_with_fingers_splayed"], "๐Ÿ–๏ธ", [], "People & Body", "7.0"), + Emoji(["hand", "raised_hand"], "โœ‹", ["highfive", "stop"], "People & Body", "6.0"), + Emoji(["vulcan_salute"], "๐Ÿ––", ["prosper", "spock"], "People & Body", "7.0"), + Emoji(["rightwards_hand"], "๐Ÿซฑ", [], "People & Body", "14.0"), + Emoji(["leftwards_hand"], "๐Ÿซฒ", [], "People & Body", "14.0"), + Emoji(["palm_down_hand"], "๐Ÿซณ", [], "People & Body", "14.0"), + Emoji(["palm_up_hand"], "๐Ÿซด", [], "People & Body", "14.0"), + Emoji(["ok_hand"], "๐Ÿ‘Œ", [], "People & Body", "6.0"), + Emoji(["pinched_fingers"], "๐ŸคŒ", [], "People & Body", "13.0"), + Emoji(["pinching_hand"], "๐Ÿค", [], "People & Body", "12.0"), + Emoji(["v"], "โœŒ๏ธ", ["victory", "peace"], "People & Body", ""), + Emoji(["crossed_fingers"], "๐Ÿคž", ["luck", "hopeful"], "People & Body", "9.0"), + Emoji( + ["hand_with_index_finger_and_thumb_crossed"], "๐Ÿซฐ", [], "People & Body", "14.0" + ), + Emoji(["love_you_gesture"], "๐ŸคŸ", [], "People & Body", "11.0"), + Emoji(["metal"], "๐Ÿค˜", [], "People & Body", "8.0"), + Emoji(["call_me_hand"], "๐Ÿค™", [], "People & Body", "9.0"), + Emoji(["point_left"], "๐Ÿ‘ˆ", [], "People & Body", "6.0"), + Emoji(["point_right"], "๐Ÿ‘‰", [], "People & Body", "6.0"), + Emoji(["point_up_2"], "๐Ÿ‘†", [], "People & Body", "6.0"), + Emoji(["middle_finger", "fu"], "๐Ÿ–•", [], "People & Body", "7.0"), + Emoji(["point_down"], "๐Ÿ‘‡", [], "People & Body", "6.0"), + Emoji(["point_up"], "โ˜๏ธ", [], "People & Body", ""), + Emoji(["index_pointing_at_the_viewer"], "๐Ÿซต", [], "People & Body", "14.0"), + Emoji(["+1", "thumbsup"], "๐Ÿ‘", ["approve", "ok"], "People & Body", "6.0"), + Emoji(["-1", "thumbsdown"], "๐Ÿ‘Ž", ["disapprove", "bury"], "People & Body", "6.0"), + Emoji(["fist_raised", "fist"], "โœŠ", ["power"], "People & Body", "6.0"), + Emoji( + ["fist_oncoming", "facepunch", "punch"], "๐Ÿ‘Š", ["attack"], "People & Body", "6.0" + ), + Emoji(["fist_left"], "๐Ÿค›", [], "People & Body", "9.0"), + Emoji(["fist_right"], "๐Ÿคœ", [], "People & Body", "9.0"), + Emoji(["clap"], "๐Ÿ‘", ["praise", "applause"], "People & Body", "6.0"), + Emoji(["raised_hands"], "๐Ÿ™Œ", ["hooray"], "People & Body", "6.0"), + Emoji(["heart_hands"], "๐Ÿซถ", ["love"], "People & Body", "14.0"), + Emoji(["open_hands"], "๐Ÿ‘", [], "People & Body", "6.0"), + Emoji(["palms_up_together"], "๐Ÿคฒ", [], "People & Body", "11.0"), + Emoji(["handshake"], "๐Ÿค", ["deal"], "People & Body", "9.0"), + Emoji(["pray"], "๐Ÿ™", ["please", "hope", "wish"], "People & Body", "6.0"), + Emoji(["writing_hand"], "โœ๏ธ", [], "People & Body", ""), + Emoji(["nail_care"], "๐Ÿ’…", ["beauty", "manicure"], "People & Body", "6.0"), + Emoji(["selfie"], "๐Ÿคณ", [], "People & Body", "9.0"), + Emoji( + ["muscle"], "๐Ÿ’ช", ["flex", "bicep", "strong", "workout"], "People & Body", "6.0" + ), + Emoji(["mechanical_arm"], "๐Ÿฆพ", [], "People & Body", "12.0"), + Emoji(["mechanical_leg"], "๐Ÿฆฟ", [], "People & Body", "12.0"), + Emoji(["leg"], "๐Ÿฆต", [], "People & Body", "11.0"), + Emoji(["foot"], "๐Ÿฆถ", [], "People & Body", "11.0"), + Emoji(["ear"], "๐Ÿ‘‚", ["hear", "sound", "listen"], "People & Body", "6.0"), + Emoji(["ear_with_hearing_aid"], "๐Ÿฆป", [], "People & Body", "12.0"), + Emoji(["nose"], "๐Ÿ‘ƒ", ["smell"], "People & Body", "6.0"), + Emoji(["brain"], "๐Ÿง ", [], "People & Body", "11.0"), + Emoji(["anatomical_heart"], "๐Ÿซ€", [], "People & Body", "13.0"), + Emoji(["lungs"], "๐Ÿซ", [], "People & Body", "13.0"), + Emoji(["tooth"], "๐Ÿฆท", [], "People & Body", "11.0"), + Emoji(["bone"], "๐Ÿฆด", [], "People & Body", "11.0"), + Emoji(["eyes"], "๐Ÿ‘€", ["look", "see", "watch"], "People & Body", "6.0"), + Emoji(["eye"], "๐Ÿ‘๏ธ", [], "People & Body", "7.0"), + Emoji(["tongue"], "๐Ÿ‘…", ["taste"], "People & Body", "6.0"), + Emoji(["lips"], "๐Ÿ‘„", ["kiss"], "People & Body", "6.0"), + Emoji(["biting_lip"], "๐Ÿซฆ", [], "People & Body", "14.0"), + Emoji(["baby"], "๐Ÿ‘ถ", ["child", "newborn"], "People & Body", "6.0"), + Emoji(["child"], "๐Ÿง’", [], "People & Body", "11.0"), + Emoji(["boy"], "๐Ÿ‘ฆ", ["child"], "People & Body", "6.0"), + Emoji(["girl"], "๐Ÿ‘ง", ["child"], "People & Body", "6.0"), + Emoji(["adult"], "๐Ÿง‘", [], "People & Body", "11.0"), + Emoji(["blond_haired_person"], "๐Ÿ‘ฑ", [], "People & Body", "6.0"), + Emoji(["man"], "๐Ÿ‘จ", ["mustache", "father", "dad"], "People & Body", "6.0"), + Emoji(["bearded_person"], "๐Ÿง”", [], "People & Body", "11.0"), + Emoji(["man_beard"], "๐Ÿง”โ€โ™‚๏ธ", [], "People & Body", "13.1"), + Emoji(["woman_beard"], "๐Ÿง”โ€โ™€๏ธ", [], "People & Body", "13.1"), + Emoji(["red_haired_man"], "๐Ÿ‘จโ€๐Ÿฆฐ", [], "People & Body", "11.0"), + Emoji(["curly_haired_man"], "๐Ÿ‘จโ€๐Ÿฆฑ", [], "People & Body", "11.0"), + Emoji(["white_haired_man"], "๐Ÿ‘จโ€๐Ÿฆณ", [], "People & Body", "11.0"), + Emoji(["bald_man"], "๐Ÿ‘จโ€๐Ÿฆฒ", [], "People & Body", "11.0"), + Emoji(["woman"], "๐Ÿ‘ฉ", ["girls"], "People & Body", "6.0"), + Emoji(["red_haired_woman"], "๐Ÿ‘ฉโ€๐Ÿฆฐ", [], "People & Body", "11.0"), + Emoji(["person_red_hair"], "๐Ÿง‘โ€๐Ÿฆฐ", [], "People & Body", "12.1"), + Emoji(["curly_haired_woman"], "๐Ÿ‘ฉโ€๐Ÿฆฑ", [], "People & Body", "11.0"), + Emoji(["person_curly_hair"], "๐Ÿง‘โ€๐Ÿฆฑ", [], "People & Body", "12.1"), + Emoji(["white_haired_woman"], "๐Ÿ‘ฉโ€๐Ÿฆณ", [], "People & Body", "11.0"), + Emoji(["person_white_hair"], "๐Ÿง‘โ€๐Ÿฆณ", [], "People & Body", "12.1"), + Emoji(["bald_woman"], "๐Ÿ‘ฉโ€๐Ÿฆฒ", [], "People & Body", "11.0"), + Emoji(["person_bald"], "๐Ÿง‘โ€๐Ÿฆฒ", [], "People & Body", "12.1"), + Emoji(["blond_haired_woman", "blonde_woman"], "๐Ÿ‘ฑโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["blond_haired_man"], "๐Ÿ‘ฑโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["older_adult"], "๐Ÿง“", [], "People & Body", "11.0"), + Emoji(["older_man"], "๐Ÿ‘ด", [], "People & Body", "6.0"), + Emoji(["older_woman"], "๐Ÿ‘ต", [], "People & Body", "6.0"), + Emoji(["frowning_person"], "๐Ÿ™", [], "People & Body", "6.0"), + Emoji(["frowning_man"], "๐Ÿ™โ€โ™‚๏ธ", [], "People & Body", "6.0"), + Emoji(["frowning_woman"], "๐Ÿ™โ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["pouting_face"], "๐Ÿ™Ž", [], "People & Body", "6.0"), + Emoji(["pouting_man"], "๐Ÿ™Žโ€โ™‚๏ธ", [], "People & Body", "6.0"), + Emoji(["pouting_woman"], "๐Ÿ™Žโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["no_good"], "๐Ÿ™…", ["stop", "halt", "denied"], "People & Body", "6.0"), + Emoji( + ["no_good_man", "ng_man"], + "๐Ÿ™…โ€โ™‚๏ธ", + ["stop", "halt", "denied"], + "People & Body", + "6.0", + ), + Emoji( + ["no_good_woman", "ng_woman"], + "๐Ÿ™…โ€โ™€๏ธ", + ["stop", "halt", "denied"], + "People & Body", + "11.0", + ), + Emoji(["ok_person"], "๐Ÿ™†", [], "People & Body", "6.0"), + Emoji(["ok_man"], "๐Ÿ™†โ€โ™‚๏ธ", [], "People & Body", "6.0"), + Emoji(["ok_woman"], "๐Ÿ™†โ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji( + ["tipping_hand_person", "information_desk_person"], + "๐Ÿ’", + [], + "People & Body", + "6.0", + ), + Emoji( + ["tipping_hand_man", "sassy_man"], + "๐Ÿ’โ€โ™‚๏ธ", + ["information"], + "People & Body", + "6.0", + ), + Emoji( + ["tipping_hand_woman", "sassy_woman"], + "๐Ÿ’โ€โ™€๏ธ", + ["information"], + "People & Body", + "11.0", + ), + Emoji(["raising_hand"], "๐Ÿ™‹", [], "People & Body", "6.0"), + Emoji(["raising_hand_man"], "๐Ÿ™‹โ€โ™‚๏ธ", [], "People & Body", "6.0"), + Emoji(["raising_hand_woman"], "๐Ÿ™‹โ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["deaf_person"], "๐Ÿง", [], "People & Body", "12.0"), + Emoji(["deaf_man"], "๐Ÿงโ€โ™‚๏ธ", [], "People & Body", "12.0"), + Emoji(["deaf_woman"], "๐Ÿงโ€โ™€๏ธ", [], "People & Body", "12.0"), + Emoji(["bow"], "๐Ÿ™‡", ["respect", "thanks"], "People & Body", "6.0"), + Emoji(["bowing_man"], "๐Ÿ™‡โ€โ™‚๏ธ", ["respect", "thanks"], "People & Body", "11.0"), + Emoji(["bowing_woman"], "๐Ÿ™‡โ€โ™€๏ธ", ["respect", "thanks"], "People & Body", "6.0"), + Emoji(["facepalm"], "๐Ÿคฆ", [], "People & Body", "11.0"), + Emoji(["man_facepalming"], "๐Ÿคฆโ€โ™‚๏ธ", [], "People & Body", "9.0"), + Emoji(["woman_facepalming"], "๐Ÿคฆโ€โ™€๏ธ", [], "People & Body", "9.0"), + Emoji(["shrug"], "๐Ÿคท", [], "People & Body", "11.0"), + Emoji(["man_shrugging"], "๐Ÿคทโ€โ™‚๏ธ", [], "People & Body", "9.0"), + Emoji(["woman_shrugging"], "๐Ÿคทโ€โ™€๏ธ", [], "People & Body", "9.0"), + Emoji(["health_worker"], "๐Ÿง‘โ€โš•๏ธ", [], "People & Body", "12.1"), + Emoji(["man_health_worker"], "๐Ÿ‘จโ€โš•๏ธ", ["doctor", "nurse"], "People & Body", ""), + Emoji(["woman_health_worker"], "๐Ÿ‘ฉโ€โš•๏ธ", ["doctor", "nurse"], "People & Body", ""), + Emoji(["student"], "๐Ÿง‘โ€๐ŸŽ“", [], "People & Body", "12.1"), + Emoji(["man_student"], "๐Ÿ‘จโ€๐ŸŽ“", ["graduation"], "People & Body", ""), + Emoji(["woman_student"], "๐Ÿ‘ฉโ€๐ŸŽ“", ["graduation"], "People & Body", ""), + Emoji(["teacher"], "๐Ÿง‘โ€๐Ÿซ", [], "People & Body", "12.1"), + Emoji(["man_teacher"], "๐Ÿ‘จโ€๐Ÿซ", ["school", "professor"], "People & Body", ""), + Emoji(["woman_teacher"], "๐Ÿ‘ฉโ€๐Ÿซ", ["school", "professor"], "People & Body", ""), + Emoji(["judge"], "๐Ÿง‘โ€โš–๏ธ", [], "People & Body", "12.1"), + Emoji(["man_judge"], "๐Ÿ‘จโ€โš–๏ธ", ["justice"], "People & Body", ""), + Emoji(["woman_judge"], "๐Ÿ‘ฉโ€โš–๏ธ", ["justice"], "People & Body", ""), + Emoji(["farmer"], "๐Ÿง‘โ€๐ŸŒพ", [], "People & Body", "12.1"), + Emoji(["man_farmer"], "๐Ÿ‘จโ€๐ŸŒพ", [], "People & Body", ""), + Emoji(["woman_farmer"], "๐Ÿ‘ฉโ€๐ŸŒพ", [], "People & Body", ""), + Emoji(["cook"], "๐Ÿง‘โ€๐Ÿณ", [], "People & Body", "12.1"), + Emoji(["man_cook"], "๐Ÿ‘จโ€๐Ÿณ", ["chef"], "People & Body", ""), + Emoji(["woman_cook"], "๐Ÿ‘ฉโ€๐Ÿณ", ["chef"], "People & Body", ""), + Emoji(["mechanic"], "๐Ÿง‘โ€๐Ÿ”ง", [], "People & Body", "12.1"), + Emoji(["man_mechanic"], "๐Ÿ‘จโ€๐Ÿ”ง", [], "People & Body", ""), + Emoji(["woman_mechanic"], "๐Ÿ‘ฉโ€๐Ÿ”ง", [], "People & Body", ""), + Emoji(["factory_worker"], "๐Ÿง‘โ€๐Ÿญ", [], "People & Body", "12.1"), + Emoji(["man_factory_worker"], "๐Ÿ‘จโ€๐Ÿญ", [], "People & Body", ""), + Emoji(["woman_factory_worker"], "๐Ÿ‘ฉโ€๐Ÿญ", [], "People & Body", ""), + Emoji(["office_worker"], "๐Ÿง‘โ€๐Ÿ’ผ", [], "People & Body", "12.1"), + Emoji(["man_office_worker"], "๐Ÿ‘จโ€๐Ÿ’ผ", ["business"], "People & Body", ""), + Emoji(["woman_office_worker"], "๐Ÿ‘ฉโ€๐Ÿ’ผ", ["business"], "People & Body", ""), + Emoji(["scientist"], "๐Ÿง‘โ€๐Ÿ”ฌ", [], "People & Body", "12.1"), + Emoji(["man_scientist"], "๐Ÿ‘จโ€๐Ÿ”ฌ", ["research"], "People & Body", ""), + Emoji(["woman_scientist"], "๐Ÿ‘ฉโ€๐Ÿ”ฌ", ["research"], "People & Body", ""), + Emoji(["technologist"], "๐Ÿง‘โ€๐Ÿ’ป", [], "People & Body", "12.1"), + Emoji(["man_technologist"], "๐Ÿ‘จโ€๐Ÿ’ป", ["coder"], "People & Body", ""), + Emoji(["woman_technologist"], "๐Ÿ‘ฉโ€๐Ÿ’ป", ["coder"], "People & Body", ""), + Emoji(["singer"], "๐Ÿง‘โ€๐ŸŽค", [], "People & Body", "12.1"), + Emoji(["man_singer"], "๐Ÿ‘จโ€๐ŸŽค", ["rockstar"], "People & Body", ""), + Emoji(["woman_singer"], "๐Ÿ‘ฉโ€๐ŸŽค", ["rockstar"], "People & Body", ""), + Emoji(["artist"], "๐Ÿง‘โ€๐ŸŽจ", [], "People & Body", "12.1"), + Emoji(["man_artist"], "๐Ÿ‘จโ€๐ŸŽจ", ["painter"], "People & Body", ""), + Emoji(["woman_artist"], "๐Ÿ‘ฉโ€๐ŸŽจ", ["painter"], "People & Body", ""), + Emoji(["pilot"], "๐Ÿง‘โ€โœˆ๏ธ", [], "People & Body", "12.1"), + Emoji(["man_pilot"], "๐Ÿ‘จโ€โœˆ๏ธ", [], "People & Body", ""), + Emoji(["woman_pilot"], "๐Ÿ‘ฉโ€โœˆ๏ธ", [], "People & Body", ""), + Emoji(["astronaut"], "๐Ÿง‘โ€๐Ÿš€", [], "People & Body", "12.1"), + Emoji(["man_astronaut"], "๐Ÿ‘จโ€๐Ÿš€", ["space"], "People & Body", ""), + Emoji(["woman_astronaut"], "๐Ÿ‘ฉโ€๐Ÿš€", ["space"], "People & Body", ""), + Emoji(["firefighter"], "๐Ÿง‘โ€๐Ÿš’", [], "People & Body", "12.1"), + Emoji(["man_firefighter"], "๐Ÿ‘จโ€๐Ÿš’", [], "People & Body", ""), + Emoji(["woman_firefighter"], "๐Ÿ‘ฉโ€๐Ÿš’", [], "People & Body", ""), + Emoji(["police_officer", "cop"], "๐Ÿ‘ฎ", ["law"], "People & Body", "6.0"), + Emoji(["policeman"], "๐Ÿ‘ฎโ€โ™‚๏ธ", ["law", "cop"], "People & Body", "11.0"), + Emoji(["policewoman"], "๐Ÿ‘ฎโ€โ™€๏ธ", ["law", "cop"], "People & Body", "6.0"), + Emoji(["detective"], "๐Ÿ•ต๏ธ", ["sleuth"], "People & Body", "7.0"), + Emoji(["male_detective"], "๐Ÿ•ต๏ธโ€โ™‚๏ธ", ["sleuth"], "People & Body", "11.0"), + Emoji(["female_detective"], "๐Ÿ•ต๏ธโ€โ™€๏ธ", ["sleuth"], "People & Body", "6.0"), + Emoji(["guard"], "๐Ÿ’‚", [], "People & Body", "6.0"), + Emoji(["guardsman"], "๐Ÿ’‚โ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["guardswoman"], "๐Ÿ’‚โ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["ninja"], "๐Ÿฅท", [], "People & Body", "13.0"), + Emoji(["construction_worker"], "๐Ÿ‘ท", ["helmet"], "People & Body", "6.0"), + Emoji(["construction_worker_man"], "๐Ÿ‘ทโ€โ™‚๏ธ", ["helmet"], "People & Body", "11.0"), + Emoji(["construction_worker_woman"], "๐Ÿ‘ทโ€โ™€๏ธ", ["helmet"], "People & Body", "6.0"), + Emoji(["person_with_crown"], "๐Ÿซ…", [], "People & Body", "14.0"), + Emoji(["prince"], "๐Ÿคด", ["crown", "royal"], "People & Body", "9.0"), + Emoji(["princess"], "๐Ÿ‘ธ", ["crown", "royal"], "People & Body", "6.0"), + Emoji(["person_with_turban"], "๐Ÿ‘ณ", [], "People & Body", "6.0"), + Emoji(["man_with_turban"], "๐Ÿ‘ณโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["woman_with_turban"], "๐Ÿ‘ณโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["man_with_gua_pi_mao"], "๐Ÿ‘ฒ", [], "People & Body", "6.0"), + Emoji(["woman_with_headscarf"], "๐Ÿง•", ["hijab"], "People & Body", "11.0"), + Emoji( + ["person_in_tuxedo"], + "๐Ÿคต", + ["groom", "marriage", "wedding"], + "People & Body", + "9.0", + ), + Emoji(["man_in_tuxedo"], "๐Ÿคตโ€โ™‚๏ธ", [], "People & Body", "13.0"), + Emoji(["woman_in_tuxedo"], "๐Ÿคตโ€โ™€๏ธ", [], "People & Body", "13.0"), + Emoji(["person_with_veil"], "๐Ÿ‘ฐ", ["marriage", "wedding"], "People & Body", "6.0"), + Emoji(["man_with_veil"], "๐Ÿ‘ฐโ€โ™‚๏ธ", [], "People & Body", "13.0"), + Emoji(["woman_with_veil", "bride_with_veil"], "๐Ÿ‘ฐโ€โ™€๏ธ", [], "People & Body", "13.0"), + Emoji(["pregnant_woman"], "๐Ÿคฐ", [], "People & Body", "9.0"), + Emoji(["pregnant_man"], "๐Ÿซƒ", [], "People & Body", "14.0"), + Emoji(["pregnant_person"], "๐Ÿซ„", [], "People & Body", "14.0"), + Emoji(["breast_feeding"], "๐Ÿคฑ", ["nursing"], "People & Body", "11.0"), + Emoji(["woman_feeding_baby"], "๐Ÿ‘ฉโ€๐Ÿผ", [], "People & Body", "13.0"), + Emoji(["man_feeding_baby"], "๐Ÿ‘จโ€๐Ÿผ", [], "People & Body", "13.0"), + Emoji(["person_feeding_baby"], "๐Ÿง‘โ€๐Ÿผ", [], "People & Body", "13.0"), + Emoji(["angel"], "๐Ÿ‘ผ", [], "People & Body", "6.0"), + Emoji(["santa"], "๐ŸŽ…", ["christmas"], "People & Body", "6.0"), + Emoji(["mrs_claus"], "๐Ÿคถ", ["santa"], "People & Body", "9.0"), + Emoji(["mx_claus"], "๐Ÿง‘โ€๐ŸŽ„", [], "People & Body", "13.0"), + Emoji(["superhero"], "๐Ÿฆธ", [], "People & Body", "11.0"), + Emoji(["superhero_man"], "๐Ÿฆธโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["superhero_woman"], "๐Ÿฆธโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["supervillain"], "๐Ÿฆน", [], "People & Body", "11.0"), + Emoji(["supervillain_man"], "๐Ÿฆนโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["supervillain_woman"], "๐Ÿฆนโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["mage"], "๐Ÿง™", ["wizard"], "People & Body", "11.0"), + Emoji(["mage_man"], "๐Ÿง™โ€โ™‚๏ธ", ["wizard"], "People & Body", "11.0"), + Emoji(["mage_woman"], "๐Ÿง™โ€โ™€๏ธ", ["wizard"], "People & Body", "11.0"), + Emoji(["fairy"], "๐Ÿงš", [], "People & Body", "11.0"), + Emoji(["fairy_man"], "๐Ÿงšโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["fairy_woman"], "๐Ÿงšโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["vampire"], "๐Ÿง›", [], "People & Body", "11.0"), + Emoji(["vampire_man"], "๐Ÿง›โ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["vampire_woman"], "๐Ÿง›โ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["merperson"], "๐Ÿงœ", [], "People & Body", "11.0"), + Emoji(["merman"], "๐Ÿงœโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["mermaid"], "๐Ÿงœโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["elf"], "๐Ÿง", [], "People & Body", "11.0"), + Emoji(["elf_man"], "๐Ÿงโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["elf_woman"], "๐Ÿงโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["genie"], "๐Ÿงž", [], "People & Body", "11.0"), + Emoji(["genie_man"], "๐Ÿงžโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["genie_woman"], "๐Ÿงžโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["zombie"], "๐ŸงŸ", [], "People & Body", "11.0"), + Emoji(["zombie_man"], "๐ŸงŸโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["zombie_woman"], "๐ŸงŸโ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["troll"], "๐ŸงŒ", [], "People & Body", "14.0"), + Emoji(["massage"], "๐Ÿ’†", ["spa"], "People & Body", "6.0"), + Emoji(["massage_man"], "๐Ÿ’†โ€โ™‚๏ธ", ["spa"], "People & Body", "6.0"), + Emoji(["massage_woman"], "๐Ÿ’†โ€โ™€๏ธ", ["spa"], "People & Body", "11.0"), + Emoji(["haircut"], "๐Ÿ’‡", ["beauty"], "People & Body", "6.0"), + Emoji(["haircut_man"], "๐Ÿ’‡โ€โ™‚๏ธ", [], "People & Body", "6.0"), + Emoji(["haircut_woman"], "๐Ÿ’‡โ€โ™€๏ธ", [], "People & Body", "11.0"), + Emoji(["walking"], "๐Ÿšถ", [], "People & Body", "6.0"), + Emoji(["walking_man"], "๐Ÿšถโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["walking_woman"], "๐Ÿšถโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["standing_person"], "๐Ÿง", [], "People & Body", "12.0"), + Emoji(["standing_man"], "๐Ÿงโ€โ™‚๏ธ", [], "People & Body", "12.0"), + Emoji(["standing_woman"], "๐Ÿงโ€โ™€๏ธ", [], "People & Body", "12.0"), + Emoji(["kneeling_person"], "๐ŸงŽ", [], "People & Body", "12.0"), + Emoji(["kneeling_man"], "๐ŸงŽโ€โ™‚๏ธ", [], "People & Body", "12.0"), + Emoji(["kneeling_woman"], "๐ŸงŽโ€โ™€๏ธ", [], "People & Body", "12.0"), + Emoji(["person_with_probing_cane"], "๐Ÿง‘โ€๐Ÿฆฏ", [], "People & Body", "12.1"), + Emoji(["man_with_probing_cane"], "๐Ÿ‘จโ€๐Ÿฆฏ", [], "People & Body", "12.0"), + Emoji(["woman_with_probing_cane"], "๐Ÿ‘ฉโ€๐Ÿฆฏ", [], "People & Body", "12.0"), + Emoji(["person_in_motorized_wheelchair"], "๐Ÿง‘โ€๐Ÿฆผ", [], "People & Body", "12.1"), + Emoji(["man_in_motorized_wheelchair"], "๐Ÿ‘จโ€๐Ÿฆผ", [], "People & Body", "12.0"), + Emoji(["woman_in_motorized_wheelchair"], "๐Ÿ‘ฉโ€๐Ÿฆผ", [], "People & Body", "12.0"), + Emoji(["person_in_manual_wheelchair"], "๐Ÿง‘โ€๐Ÿฆฝ", [], "People & Body", "12.1"), + Emoji(["man_in_manual_wheelchair"], "๐Ÿ‘จโ€๐Ÿฆฝ", [], "People & Body", "12.0"), + Emoji(["woman_in_manual_wheelchair"], "๐Ÿ‘ฉโ€๐Ÿฆฝ", [], "People & Body", "12.0"), + Emoji( + ["runner", "running"], + "๐Ÿƒ", + ["exercise", "workout", "marathon"], + "People & Body", + "6.0", + ), + Emoji( + ["running_man"], + "๐Ÿƒโ€โ™‚๏ธ", + ["exercise", "workout", "marathon"], + "People & Body", + "11.0", + ), + Emoji( + ["running_woman"], + "๐Ÿƒโ€โ™€๏ธ", + ["exercise", "workout", "marathon"], + "People & Body", + "6.0", + ), + Emoji(["woman_dancing", "dancer"], "๐Ÿ’ƒ", ["dress"], "People & Body", "6.0"), + Emoji(["man_dancing"], "๐Ÿ•บ", ["dancer"], "People & Body", "9.0"), + Emoji(["business_suit_levitating"], "๐Ÿ•ด๏ธ", [], "People & Body", "7.0"), + Emoji(["dancers"], "๐Ÿ‘ฏ", ["bunny"], "People & Body", "6.0"), + Emoji(["dancing_men"], "๐Ÿ‘ฏโ€โ™‚๏ธ", ["bunny"], "People & Body", "6.0"), + Emoji(["dancing_women"], "๐Ÿ‘ฏโ€โ™€๏ธ", ["bunny"], "People & Body", "11.0"), + Emoji(["sauna_person"], "๐Ÿง–", ["steamy"], "People & Body", "11.0"), + Emoji(["sauna_man"], "๐Ÿง–โ€โ™‚๏ธ", ["steamy"], "People & Body", "11.0"), + Emoji(["sauna_woman"], "๐Ÿง–โ€โ™€๏ธ", ["steamy"], "People & Body", "11.0"), + Emoji(["climbing"], "๐Ÿง—", ["bouldering"], "People & Body", "11.0"), + Emoji(["climbing_man"], "๐Ÿง—โ€โ™‚๏ธ", ["bouldering"], "People & Body", "11.0"), + Emoji(["climbing_woman"], "๐Ÿง—โ€โ™€๏ธ", ["bouldering"], "People & Body", "11.0"), + Emoji(["person_fencing"], "๐Ÿคบ", [], "People & Body", "9.0"), + Emoji(["horse_racing"], "๐Ÿ‡", [], "People & Body", "6.0"), + Emoji(["skier"], "โ›ท๏ธ", [], "People & Body", "5.2"), + Emoji(["snowboarder"], "๐Ÿ‚", [], "People & Body", "6.0"), + Emoji(["golfing"], "๐ŸŒ๏ธ", [], "People & Body", "7.0"), + Emoji(["golfing_man"], "๐ŸŒ๏ธโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["golfing_woman"], "๐ŸŒ๏ธโ€โ™€๏ธ", [], "People & Body", ""), + Emoji(["surfer"], "๐Ÿ„", [], "People & Body", "6.0"), + Emoji(["surfing_man"], "๐Ÿ„โ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["surfing_woman"], "๐Ÿ„โ€โ™€๏ธ", [], "People & Body", "7.0"), + Emoji(["rowboat"], "๐Ÿšฃ", [], "People & Body", "6.0"), + Emoji(["rowing_man"], "๐Ÿšฃโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["rowing_woman"], "๐Ÿšฃโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["swimmer"], "๐ŸŠ", [], "People & Body", "6.0"), + Emoji(["swimming_man"], "๐ŸŠโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["swimming_woman"], "๐ŸŠโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["bouncing_ball_person"], "โ›น๏ธ", ["basketball"], "People & Body", "5.2"), + Emoji( + ["bouncing_ball_man", "basketball_man"], "โ›น๏ธโ€โ™‚๏ธ", [], "People & Body", "11.0" + ), + Emoji( + ["bouncing_ball_woman", "basketball_woman"], "โ›น๏ธโ€โ™€๏ธ", [], "People & Body", "7.0" + ), + Emoji(["weight_lifting"], "๐Ÿ‹๏ธ", ["gym", "workout"], "People & Body", "7.0"), + Emoji(["weight_lifting_man"], "๐Ÿ‹๏ธโ€โ™‚๏ธ", ["gym", "workout"], "People & Body", "11.0"), + Emoji( + ["weight_lifting_woman"], "๐Ÿ‹๏ธโ€โ™€๏ธ", ["gym", "workout"], "People & Body", "6.0" + ), + Emoji(["bicyclist"], "๐Ÿšด", [], "People & Body", "6.0"), + Emoji(["biking_man"], "๐Ÿšดโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["biking_woman"], "๐Ÿšดโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["mountain_bicyclist"], "๐Ÿšต", [], "People & Body", "6.0"), + Emoji(["mountain_biking_man"], "๐Ÿšตโ€โ™‚๏ธ", [], "People & Body", "11.0"), + Emoji(["mountain_biking_woman"], "๐Ÿšตโ€โ™€๏ธ", [], "People & Body", "6.0"), + Emoji(["cartwheeling"], "๐Ÿคธ", [], "People & Body", "11.0"), + Emoji(["man_cartwheeling"], "๐Ÿคธโ€โ™‚๏ธ", [], "People & Body", ""), + Emoji(["woman_cartwheeling"], "๐Ÿคธโ€โ™€๏ธ", [], "People & Body", ""), + Emoji(["wrestling"], "๐Ÿคผ", [], "People & Body", "11.0"), + Emoji(["men_wrestling"], "๐Ÿคผโ€โ™‚๏ธ", [], "People & Body", "9.0"), + Emoji(["women_wrestling"], "๐Ÿคผโ€โ™€๏ธ", [], "People & Body", "9.0"), + Emoji(["water_polo"], "๐Ÿคฝ", [], "People & Body", "11.0"), + Emoji(["man_playing_water_polo"], "๐Ÿคฝโ€โ™‚๏ธ", [], "People & Body", "9.0"), + Emoji(["woman_playing_water_polo"], "๐Ÿคฝโ€โ™€๏ธ", [], "People & Body", "9.0"), + Emoji(["handball_person"], "๐Ÿคพ", [], "People & Body", "11.0"), + Emoji(["man_playing_handball"], "๐Ÿคพโ€โ™‚๏ธ", [], "People & Body", "9.0"), + Emoji(["woman_playing_handball"], "๐Ÿคพโ€โ™€๏ธ", [], "People & Body", "9.0"), + Emoji(["juggling_person"], "๐Ÿคน", [], "People & Body", "11.0"), + Emoji(["man_juggling"], "๐Ÿคนโ€โ™‚๏ธ", [], "People & Body", "9.0"), + Emoji(["woman_juggling"], "๐Ÿคนโ€โ™€๏ธ", [], "People & Body", "9.0"), + Emoji(["lotus_position"], "๐Ÿง˜", ["meditation"], "People & Body", "11.0"), + Emoji(["lotus_position_man"], "๐Ÿง˜โ€โ™‚๏ธ", ["meditation"], "People & Body", "11.0"), + Emoji(["lotus_position_woman"], "๐Ÿง˜โ€โ™€๏ธ", ["meditation"], "People & Body", "11.0"), + Emoji(["bath"], "๐Ÿ›€", ["shower"], "People & Body", "6.0"), + Emoji(["sleeping_bed"], "๐Ÿ›Œ", [], "People & Body", "7.0"), + Emoji( + ["people_holding_hands"], "๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘", ["couple", "date"], "People & Body", "12.0" + ), + Emoji(["two_women_holding_hands"], "๐Ÿ‘ญ", ["couple", "date"], "People & Body", "6.0"), + Emoji(["couple"], "๐Ÿ‘ซ", ["date"], "People & Body", "6.0"), + Emoji(["two_men_holding_hands"], "๐Ÿ‘ฌ", ["couple", "date"], "People & Body", "6.0"), + Emoji(["couplekiss"], "๐Ÿ’", [], "People & Body", "6.0"), + Emoji(["couplekiss_man_woman"], "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ", [], "People & Body", "11.0"), + Emoji(["couplekiss_man_man"], "๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ", [], "People & Body", "6.0"), + Emoji(["couplekiss_woman_woman"], "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ", [], "People & Body", "6.0"), + Emoji(["couple_with_heart"], "๐Ÿ’‘", [], "People & Body", "6.0"), + Emoji(["couple_with_heart_woman_man"], "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ", [], "People & Body", "11.0"), + Emoji(["couple_with_heart_man_man"], "๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ", [], "People & Body", "6.0"), + Emoji(["couple_with_heart_woman_woman"], "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ", [], "People & Body", "6.0"), + Emoji(["family"], "๐Ÿ‘ช", ["home", "parents", "child"], "People & Body", "6.0"), + Emoji(["family_man_woman_boy"], "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ", [], "People & Body", "11.0"), + Emoji(["family_man_woman_girl"], "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_man_woman_girl_boy"], "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_woman_boy_boy"], "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_woman_girl_girl"], "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_man_man_boy"], "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_man_girl"], "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_man_man_girl_boy"], "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_man_boy_boy"], "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_man_girl_girl"], "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_woman_woman_boy"], "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_woman_woman_girl"], "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_woman_woman_girl_boy"], "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_woman_woman_boy_boy"], "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_woman_woman_girl_girl"], "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_man_boy"], "๐Ÿ‘จโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_boy_boy"], "๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_girl"], "๐Ÿ‘จโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_man_girl_boy"], "๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_man_girl_girl"], "๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_woman_boy"], "๐Ÿ‘ฉโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_woman_boy_boy"], "๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_woman_girl"], "๐Ÿ‘ฉโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["family_woman_girl_boy"], "๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", [], "People & Body", "6.0"), + Emoji(["family_woman_girl_girl"], "๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", [], "People & Body", "6.0"), + Emoji(["speaking_head"], "๐Ÿ—ฃ๏ธ", [], "People & Body", "7.0"), + Emoji(["bust_in_silhouette"], "๐Ÿ‘ค", ["user"], "People & Body", "6.0"), + Emoji( + ["busts_in_silhouette"], "๐Ÿ‘ฅ", ["users", "group", "team"], "People & Body", "6.0" + ), + Emoji(["people_hugging"], "๐Ÿซ‚", [], "People & Body", "13.0"), + Emoji(["footprints"], "๐Ÿ‘ฃ", ["feet", "tracks"], "People & Body", "6.0"), + Emoji(["monkey_face"], "๐Ÿต", [], "Animals & Nature", "6.0"), + Emoji(["monkey"], "๐Ÿ’", [], "Animals & Nature", "6.0"), + Emoji(["gorilla"], "๐Ÿฆ", [], "Animals & Nature", "9.0"), + Emoji(["orangutan"], "๐Ÿฆง", [], "Animals & Nature", "12.0"), + Emoji(["dog"], "๐Ÿถ", ["pet"], "Animals & Nature", "6.0"), + Emoji(["dog2"], "๐Ÿ•", [], "Animals & Nature", "6.0"), + Emoji(["guide_dog"], "๐Ÿฆฎ", [], "Animals & Nature", "12.0"), + Emoji(["service_dog"], "๐Ÿ•โ€๐Ÿฆบ", [], "Animals & Nature", "12.0"), + Emoji(["poodle"], "๐Ÿฉ", ["dog"], "Animals & Nature", "6.0"), + Emoji(["wolf"], "๐Ÿบ", [], "Animals & Nature", "6.0"), + Emoji(["fox_face"], "๐ŸฆŠ", [], "Animals & Nature", "9.0"), + Emoji(["raccoon"], "๐Ÿฆ", [], "Animals & Nature", "11.0"), + Emoji(["cat"], "๐Ÿฑ", ["pet"], "Animals & Nature", "6.0"), + Emoji(["cat2"], "๐Ÿˆ", [], "Animals & Nature", "6.0"), + Emoji(["black_cat"], "๐Ÿˆโ€โฌ›", [], "Animals & Nature", "13.0"), + Emoji(["lion"], "๐Ÿฆ", [], "Animals & Nature", "8.0"), + Emoji(["tiger"], "๐Ÿฏ", [], "Animals & Nature", "6.0"), + Emoji(["tiger2"], "๐Ÿ…", [], "Animals & Nature", "6.0"), + Emoji(["leopard"], "๐Ÿ†", [], "Animals & Nature", "6.0"), + Emoji(["horse"], "๐Ÿด", [], "Animals & Nature", "6.0"), + Emoji(["racehorse"], "๐ŸŽ", ["speed"], "Animals & Nature", "6.0"), + Emoji(["unicorn"], "๐Ÿฆ„", [], "Animals & Nature", "8.0"), + Emoji(["zebra"], "๐Ÿฆ“", [], "Animals & Nature", "11.0"), + Emoji(["deer"], "๐ŸฆŒ", [], "Animals & Nature", "9.0"), + Emoji(["bison"], "๐Ÿฆฌ", [], "Animals & Nature", "13.0"), + Emoji(["cow"], "๐Ÿฎ", [], "Animals & Nature", "6.0"), + Emoji(["ox"], "๐Ÿ‚", [], "Animals & Nature", "6.0"), + Emoji(["water_buffalo"], "๐Ÿƒ", [], "Animals & Nature", "6.0"), + Emoji(["cow2"], "๐Ÿ„", [], "Animals & Nature", "6.0"), + Emoji(["pig"], "๐Ÿท", [], "Animals & Nature", "6.0"), + Emoji(["pig2"], "๐Ÿ–", [], "Animals & Nature", "6.0"), + Emoji(["boar"], "๐Ÿ—", [], "Animals & Nature", "6.0"), + Emoji(["pig_nose"], "๐Ÿฝ", [], "Animals & Nature", "6.0"), + Emoji(["ram"], "๐Ÿ", [], "Animals & Nature", "6.0"), + Emoji(["sheep"], "๐Ÿ‘", [], "Animals & Nature", "6.0"), + Emoji(["goat"], "๐Ÿ", [], "Animals & Nature", "6.0"), + Emoji(["dromedary_camel"], "๐Ÿช", ["desert"], "Animals & Nature", "6.0"), + Emoji(["camel"], "๐Ÿซ", [], "Animals & Nature", "6.0"), + Emoji(["llama"], "๐Ÿฆ™", [], "Animals & Nature", "11.0"), + Emoji(["giraffe"], "๐Ÿฆ’", [], "Animals & Nature", "11.0"), + Emoji(["elephant"], "๐Ÿ˜", [], "Animals & Nature", "6.0"), + Emoji(["mammoth"], "๐Ÿฆฃ", [], "Animals & Nature", "13.0"), + Emoji(["rhinoceros"], "๐Ÿฆ", [], "Animals & Nature", "9.0"), + Emoji(["hippopotamus"], "๐Ÿฆ›", [], "Animals & Nature", "11.0"), + Emoji(["mouse"], "๐Ÿญ", [], "Animals & Nature", "6.0"), + Emoji(["mouse2"], "๐Ÿ", [], "Animals & Nature", "6.0"), + Emoji(["rat"], "๐Ÿ€", [], "Animals & Nature", "6.0"), + Emoji(["hamster"], "๐Ÿน", ["pet"], "Animals & Nature", "6.0"), + Emoji(["rabbit"], "๐Ÿฐ", ["bunny"], "Animals & Nature", "6.0"), + Emoji(["rabbit2"], "๐Ÿ‡", [], "Animals & Nature", "6.0"), + Emoji(["chipmunk"], "๐Ÿฟ๏ธ", [], "Animals & Nature", "7.0"), + Emoji(["beaver"], "๐Ÿฆซ", [], "Animals & Nature", "13.0"), + Emoji(["hedgehog"], "๐Ÿฆ”", [], "Animals & Nature", "11.0"), + Emoji(["bat"], "๐Ÿฆ‡", [], "Animals & Nature", "9.0"), + Emoji(["bear"], "๐Ÿป", [], "Animals & Nature", "6.0"), + Emoji(["polar_bear"], "๐Ÿปโ€โ„๏ธ", [], "Animals & Nature", "13.0"), + Emoji(["koala"], "๐Ÿจ", [], "Animals & Nature", "6.0"), + Emoji(["panda_face"], "๐Ÿผ", [], "Animals & Nature", "6.0"), + Emoji(["sloth"], "๐Ÿฆฅ", [], "Animals & Nature", "12.0"), + Emoji(["otter"], "๐Ÿฆฆ", [], "Animals & Nature", "12.0"), + Emoji(["skunk"], "๐Ÿฆจ", [], "Animals & Nature", "12.0"), + Emoji(["kangaroo"], "๐Ÿฆ˜", [], "Animals & Nature", "11.0"), + Emoji(["badger"], "๐Ÿฆก", [], "Animals & Nature", "11.0"), + Emoji(["feet", "paw_prints"], "๐Ÿพ", [], "Animals & Nature", "6.0"), + Emoji(["turkey"], "๐Ÿฆƒ", ["thanksgiving"], "Animals & Nature", "8.0"), + Emoji(["chicken"], "๐Ÿ”", [], "Animals & Nature", "6.0"), + Emoji(["rooster"], "๐Ÿ“", [], "Animals & Nature", "6.0"), + Emoji(["hatching_chick"], "๐Ÿฃ", [], "Animals & Nature", "6.0"), + Emoji(["baby_chick"], "๐Ÿค", [], "Animals & Nature", "6.0"), + Emoji(["hatched_chick"], "๐Ÿฅ", [], "Animals & Nature", "6.0"), + Emoji(["bird"], "๐Ÿฆ", [], "Animals & Nature", "6.0"), + Emoji(["penguin"], "๐Ÿง", [], "Animals & Nature", "6.0"), + Emoji(["dove"], "๐Ÿ•Š๏ธ", ["peace"], "Animals & Nature", "7.0"), + Emoji(["eagle"], "๐Ÿฆ…", [], "Animals & Nature", "9.0"), + Emoji(["duck"], "๐Ÿฆ†", [], "Animals & Nature", "9.0"), + Emoji(["swan"], "๐Ÿฆข", [], "Animals & Nature", "11.0"), + Emoji(["owl"], "๐Ÿฆ‰", [], "Animals & Nature", "9.0"), + Emoji(["dodo"], "๐Ÿฆค", [], "Animals & Nature", "13.0"), + Emoji(["feather"], "๐Ÿชถ", [], "Animals & Nature", "13.0"), + Emoji(["flamingo"], "๐Ÿฆฉ", [], "Animals & Nature", "12.0"), + Emoji(["peacock"], "๐Ÿฆš", [], "Animals & Nature", "11.0"), + Emoji(["parrot"], "๐Ÿฆœ", [], "Animals & Nature", "11.0"), + Emoji(["frog"], "๐Ÿธ", [], "Animals & Nature", "6.0"), + Emoji(["crocodile"], "๐ŸŠ", [], "Animals & Nature", "6.0"), + Emoji(["turtle"], "๐Ÿข", ["slow"], "Animals & Nature", "6.0"), + Emoji(["lizard"], "๐ŸฆŽ", [], "Animals & Nature", "9.0"), + Emoji(["snake"], "๐Ÿ", [], "Animals & Nature", "6.0"), + Emoji(["dragon_face"], "๐Ÿฒ", [], "Animals & Nature", "6.0"), + Emoji(["dragon"], "๐Ÿ‰", [], "Animals & Nature", "6.0"), + Emoji(["sauropod"], "๐Ÿฆ•", ["dinosaur"], "Animals & Nature", "11.0"), + Emoji(["t-rex"], "๐Ÿฆ–", ["dinosaur"], "Animals & Nature", "11.0"), + Emoji(["whale"], "๐Ÿณ", ["sea"], "Animals & Nature", "6.0"), + Emoji(["whale2"], "๐Ÿ‹", [], "Animals & Nature", "6.0"), + Emoji(["dolphin", "flipper"], "๐Ÿฌ", [], "Animals & Nature", "6.0"), + Emoji(["seal"], "๐Ÿฆญ", [], "Animals & Nature", "13.0"), + Emoji(["fish"], "๐ŸŸ", [], "Animals & Nature", "6.0"), + Emoji(["tropical_fish"], "๐Ÿ ", [], "Animals & Nature", "6.0"), + Emoji(["blowfish"], "๐Ÿก", [], "Animals & Nature", "6.0"), + Emoji(["shark"], "๐Ÿฆˆ", [], "Animals & Nature", "9.0"), + Emoji(["octopus"], "๐Ÿ™", [], "Animals & Nature", "6.0"), + Emoji(["shell"], "๐Ÿš", ["sea", "beach"], "Animals & Nature", "6.0"), + Emoji(["coral"], "๐Ÿชธ", [], "Animals & Nature", "14.0"), + Emoji(["snail"], "๐ŸŒ", ["slow"], "Animals & Nature", "6.0"), + Emoji(["butterfly"], "๐Ÿฆ‹", [], "Animals & Nature", "9.0"), + Emoji(["bug"], "๐Ÿ›", [], "Animals & Nature", "6.0"), + Emoji(["ant"], "๐Ÿœ", [], "Animals & Nature", "6.0"), + Emoji(["bee", "honeybee"], "๐Ÿ", [], "Animals & Nature", "6.0"), + Emoji(["beetle"], "๐Ÿชฒ", [], "Animals & Nature", "13.0"), + Emoji(["lady_beetle"], "๐Ÿž", ["bug"], "Animals & Nature", "6.0"), + Emoji(["cricket"], "๐Ÿฆ—", [], "Animals & Nature", "11.0"), + Emoji(["cockroach"], "๐Ÿชณ", [], "Animals & Nature", "13.0"), + Emoji(["spider"], "๐Ÿ•ท๏ธ", [], "Animals & Nature", "7.0"), + Emoji(["spider_web"], "๐Ÿ•ธ๏ธ", [], "Animals & Nature", "7.0"), + Emoji(["scorpion"], "๐Ÿฆ‚", [], "Animals & Nature", "8.0"), + Emoji(["mosquito"], "๐ŸฆŸ", [], "Animals & Nature", "11.0"), + Emoji(["fly"], "๐Ÿชฐ", [], "Animals & Nature", "13.0"), + Emoji(["worm"], "๐Ÿชฑ", [], "Animals & Nature", "13.0"), + Emoji(["microbe"], "๐Ÿฆ ", ["germ"], "Animals & Nature", "11.0"), + Emoji(["bouquet"], "๐Ÿ’", ["flowers"], "Animals & Nature", "6.0"), + Emoji(["cherry_blossom"], "๐ŸŒธ", ["flower", "spring"], "Animals & Nature", "6.0"), + Emoji(["white_flower"], "๐Ÿ’ฎ", [], "Animals & Nature", "6.0"), + Emoji(["lotus"], "๐Ÿชท", [], "Animals & Nature", "14.0"), + Emoji(["rosette"], "๐Ÿต๏ธ", [], "Animals & Nature", "7.0"), + Emoji(["rose"], "๐ŸŒน", ["flower"], "Animals & Nature", "6.0"), + Emoji(["wilted_flower"], "๐Ÿฅ€", [], "Animals & Nature", "9.0"), + Emoji(["hibiscus"], "๐ŸŒบ", [], "Animals & Nature", "6.0"), + Emoji(["sunflower"], "๐ŸŒป", [], "Animals & Nature", "6.0"), + Emoji(["blossom"], "๐ŸŒผ", [], "Animals & Nature", "6.0"), + Emoji(["tulip"], "๐ŸŒท", ["flower"], "Animals & Nature", "6.0"), + Emoji(["seedling"], "๐ŸŒฑ", ["plant"], "Animals & Nature", "6.0"), + Emoji(["potted_plant"], "๐Ÿชด", [], "Animals & Nature", "13.0"), + Emoji(["evergreen_tree"], "๐ŸŒฒ", ["wood"], "Animals & Nature", "6.0"), + Emoji(["deciduous_tree"], "๐ŸŒณ", ["wood"], "Animals & Nature", "6.0"), + Emoji(["palm_tree"], "๐ŸŒด", [], "Animals & Nature", "6.0"), + Emoji(["cactus"], "๐ŸŒต", [], "Animals & Nature", "6.0"), + Emoji(["ear_of_rice"], "๐ŸŒพ", [], "Animals & Nature", "6.0"), + Emoji(["herb"], "๐ŸŒฟ", [], "Animals & Nature", "6.0"), + Emoji(["shamrock"], "โ˜˜๏ธ", [], "Animals & Nature", "4.1"), + Emoji(["four_leaf_clover"], "๐Ÿ€", ["luck"], "Animals & Nature", "6.0"), + Emoji(["maple_leaf"], "๐Ÿ", ["canada"], "Animals & Nature", "6.0"), + Emoji(["fallen_leaf"], "๐Ÿ‚", ["autumn"], "Animals & Nature", "6.0"), + Emoji(["leaves"], "๐Ÿƒ", ["leaf"], "Animals & Nature", "6.0"), + Emoji(["empty_nest"], "๐Ÿชน", [], "Animals & Nature", "14.0"), + Emoji(["nest_with_eggs"], "๐Ÿชบ", [], "Animals & Nature", "14.0"), + Emoji(["grapes"], "๐Ÿ‡", [], "Food & Drink", "6.0"), + Emoji(["melon"], "๐Ÿˆ", [], "Food & Drink", "6.0"), + Emoji(["watermelon"], "๐Ÿ‰", [], "Food & Drink", "6.0"), + Emoji(["tangerine", "orange", "mandarin"], "๐ŸŠ", [], "Food & Drink", "6.0"), + Emoji(["lemon"], "๐Ÿ‹", [], "Food & Drink", "6.0"), + Emoji(["banana"], "๐ŸŒ", ["fruit"], "Food & Drink", "6.0"), + Emoji(["pineapple"], "๐Ÿ", [], "Food & Drink", "6.0"), + Emoji(["mango"], "๐Ÿฅญ", [], "Food & Drink", "11.0"), + Emoji(["apple"], "๐ŸŽ", [], "Food & Drink", "6.0"), + Emoji(["green_apple"], "๐Ÿ", ["fruit"], "Food & Drink", "6.0"), + Emoji(["pear"], "๐Ÿ", [], "Food & Drink", "6.0"), + Emoji(["peach"], "๐Ÿ‘", [], "Food & Drink", "6.0"), + Emoji(["cherries"], "๐Ÿ’", ["fruit"], "Food & Drink", "6.0"), + Emoji(["strawberry"], "๐Ÿ“", ["fruit"], "Food & Drink", "6.0"), + Emoji(["blueberries"], "๐Ÿซ", [], "Food & Drink", "13.0"), + Emoji(["kiwi_fruit"], "๐Ÿฅ", [], "Food & Drink", "9.0"), + Emoji(["tomato"], "๐Ÿ…", [], "Food & Drink", "6.0"), + Emoji(["olive"], "๐Ÿซ’", [], "Food & Drink", "13.0"), + Emoji(["coconut"], "๐Ÿฅฅ", [], "Food & Drink", "11.0"), + Emoji(["avocado"], "๐Ÿฅ‘", [], "Food & Drink", "9.0"), + Emoji(["eggplant"], "๐Ÿ†", ["aubergine"], "Food & Drink", "6.0"), + Emoji(["potato"], "๐Ÿฅ”", [], "Food & Drink", "9.0"), + Emoji(["carrot"], "๐Ÿฅ•", [], "Food & Drink", "9.0"), + Emoji(["corn"], "๐ŸŒฝ", [], "Food & Drink", "6.0"), + Emoji(["hot_pepper"], "๐ŸŒถ๏ธ", ["spicy"], "Food & Drink", "7.0"), + Emoji(["bell_pepper"], "๐Ÿซ‘", [], "Food & Drink", "13.0"), + Emoji(["cucumber"], "๐Ÿฅ’", [], "Food & Drink", "9.0"), + Emoji(["leafy_green"], "๐Ÿฅฌ", [], "Food & Drink", "11.0"), + Emoji(["broccoli"], "๐Ÿฅฆ", [], "Food & Drink", "11.0"), + Emoji(["garlic"], "๐Ÿง„", [], "Food & Drink", "12.0"), + Emoji(["onion"], "๐Ÿง…", [], "Food & Drink", "12.0"), + Emoji(["mushroom"], "๐Ÿ„", [], "Food & Drink", "6.0"), + Emoji(["peanuts"], "๐Ÿฅœ", [], "Food & Drink", "9.0"), + Emoji(["beans"], "๐Ÿซ˜", [], "Food & Drink", "14.0"), + Emoji(["chestnut"], "๐ŸŒฐ", [], "Food & Drink", "6.0"), + Emoji(["bread"], "๐Ÿž", ["toast"], "Food & Drink", "6.0"), + Emoji(["croissant"], "๐Ÿฅ", [], "Food & Drink", "9.0"), + Emoji(["baguette_bread"], "๐Ÿฅ–", [], "Food & Drink", "9.0"), + Emoji(["flatbread"], "๐Ÿซ“", [], "Food & Drink", "13.0"), + Emoji(["pretzel"], "๐Ÿฅจ", [], "Food & Drink", "11.0"), + Emoji(["bagel"], "๐Ÿฅฏ", [], "Food & Drink", "11.0"), + Emoji(["pancakes"], "๐Ÿฅž", [], "Food & Drink", "9.0"), + Emoji(["waffle"], "๐Ÿง‡", [], "Food & Drink", "12.0"), + Emoji(["cheese"], "๐Ÿง€", [], "Food & Drink", "8.0"), + Emoji(["meat_on_bone"], "๐Ÿ–", [], "Food & Drink", "6.0"), + Emoji(["poultry_leg"], "๐Ÿ—", ["meat", "chicken"], "Food & Drink", "6.0"), + Emoji(["cut_of_meat"], "๐Ÿฅฉ", [], "Food & Drink", "11.0"), + Emoji(["bacon"], "๐Ÿฅ“", [], "Food & Drink", "9.0"), + Emoji(["hamburger"], "๐Ÿ”", ["burger"], "Food & Drink", "6.0"), + Emoji(["fries"], "๐ŸŸ", [], "Food & Drink", "6.0"), + Emoji(["pizza"], "๐Ÿ•", [], "Food & Drink", "6.0"), + Emoji(["hotdog"], "๐ŸŒญ", [], "Food & Drink", "8.0"), + Emoji(["sandwich"], "๐Ÿฅช", [], "Food & Drink", "11.0"), + Emoji(["taco"], "๐ŸŒฎ", [], "Food & Drink", "8.0"), + Emoji(["burrito"], "๐ŸŒฏ", [], "Food & Drink", "8.0"), + Emoji(["tamale"], "๐Ÿซ”", [], "Food & Drink", "13.0"), + Emoji(["stuffed_flatbread"], "๐Ÿฅ™", [], "Food & Drink", "9.0"), + Emoji(["falafel"], "๐Ÿง†", [], "Food & Drink", "12.0"), + Emoji(["egg"], "๐Ÿฅš", [], "Food & Drink", "9.0"), + Emoji(["fried_egg"], "๐Ÿณ", ["breakfast"], "Food & Drink", "6.0"), + Emoji(["shallow_pan_of_food"], "๐Ÿฅ˜", ["paella", "curry"], "Food & Drink", ""), + Emoji(["stew"], "๐Ÿฒ", [], "Food & Drink", "6.0"), + Emoji(["fondue"], "๐Ÿซ•", [], "Food & Drink", "13.0"), + Emoji(["bowl_with_spoon"], "๐Ÿฅฃ", [], "Food & Drink", "11.0"), + Emoji(["green_salad"], "๐Ÿฅ—", [], "Food & Drink", "9.0"), + Emoji(["popcorn"], "๐Ÿฟ", [], "Food & Drink", "8.0"), + Emoji(["butter"], "๐Ÿงˆ", [], "Food & Drink", "12.0"), + Emoji(["salt"], "๐Ÿง‚", [], "Food & Drink", "11.0"), + Emoji(["canned_food"], "๐Ÿฅซ", [], "Food & Drink", "11.0"), + Emoji(["bento"], "๐Ÿฑ", [], "Food & Drink", "6.0"), + Emoji(["rice_cracker"], "๐Ÿ˜", [], "Food & Drink", "6.0"), + Emoji(["rice_ball"], "๐Ÿ™", [], "Food & Drink", "6.0"), + Emoji(["rice"], "๐Ÿš", [], "Food & Drink", "6.0"), + Emoji(["curry"], "๐Ÿ›", [], "Food & Drink", "6.0"), + Emoji(["ramen"], "๐Ÿœ", ["noodle"], "Food & Drink", "6.0"), + Emoji(["spaghetti"], "๐Ÿ", ["pasta"], "Food & Drink", "6.0"), + Emoji(["sweet_potato"], "๐Ÿ ", [], "Food & Drink", "6.0"), + Emoji(["oden"], "๐Ÿข", [], "Food & Drink", "6.0"), + Emoji(["sushi"], "๐Ÿฃ", [], "Food & Drink", "6.0"), + Emoji(["fried_shrimp"], "๐Ÿค", ["tempura"], "Food & Drink", "6.0"), + Emoji(["fish_cake"], "๐Ÿฅ", [], "Food & Drink", "6.0"), + Emoji(["moon_cake"], "๐Ÿฅฎ", [], "Food & Drink", "11.0"), + Emoji(["dango"], "๐Ÿก", [], "Food & Drink", "6.0"), + Emoji(["dumpling"], "๐ŸฅŸ", [], "Food & Drink", "11.0"), + Emoji(["fortune_cookie"], "๐Ÿฅ ", [], "Food & Drink", "11.0"), + Emoji(["takeout_box"], "๐Ÿฅก", [], "Food & Drink", "11.0"), + Emoji(["crab"], "๐Ÿฆ€", [], "Food & Drink", "8.0"), + Emoji(["lobster"], "๐Ÿฆž", [], "Food & Drink", "11.0"), + Emoji(["shrimp"], "๐Ÿฆ", [], "Food & Drink", "9.0"), + Emoji(["squid"], "๐Ÿฆ‘", [], "Food & Drink", "9.0"), + Emoji(["oyster"], "๐Ÿฆช", [], "Food & Drink", "12.0"), + Emoji(["icecream"], "๐Ÿฆ", [], "Food & Drink", "6.0"), + Emoji(["shaved_ice"], "๐Ÿง", [], "Food & Drink", "6.0"), + Emoji(["ice_cream"], "๐Ÿจ", [], "Food & Drink", "6.0"), + Emoji(["doughnut"], "๐Ÿฉ", [], "Food & Drink", "6.0"), + Emoji(["cookie"], "๐Ÿช", [], "Food & Drink", "6.0"), + Emoji(["birthday"], "๐ŸŽ‚", ["party"], "Food & Drink", "6.0"), + Emoji(["cake"], "๐Ÿฐ", ["dessert"], "Food & Drink", "6.0"), + Emoji(["cupcake"], "๐Ÿง", [], "Food & Drink", "11.0"), + Emoji(["pie"], "๐Ÿฅง", [], "Food & Drink", "11.0"), + Emoji(["chocolate_bar"], "๐Ÿซ", [], "Food & Drink", "6.0"), + Emoji(["candy"], "๐Ÿฌ", ["sweet"], "Food & Drink", "6.0"), + Emoji(["lollipop"], "๐Ÿญ", [], "Food & Drink", "6.0"), + Emoji(["custard"], "๐Ÿฎ", [], "Food & Drink", "6.0"), + Emoji(["honey_pot"], "๐Ÿฏ", [], "Food & Drink", "6.0"), + Emoji(["baby_bottle"], "๐Ÿผ", ["milk"], "Food & Drink", "6.0"), + Emoji(["milk_glass"], "๐Ÿฅ›", [], "Food & Drink", "9.0"), + Emoji(["coffee"], "โ˜•", ["cafe", "espresso"], "Food & Drink", "4.0"), + Emoji(["teapot"], "๐Ÿซ–", [], "Food & Drink", "13.0"), + Emoji(["tea"], "๐Ÿต", ["green", "breakfast"], "Food & Drink", "6.0"), + Emoji(["sake"], "๐Ÿถ", [], "Food & Drink", "6.0"), + Emoji( + ["champagne"], "๐Ÿพ", ["bottle", "bubbly", "celebration"], "Food & Drink", "8.0" + ), + Emoji(["wine_glass"], "๐Ÿท", [], "Food & Drink", "6.0"), + Emoji(["cocktail"], "๐Ÿธ", ["drink"], "Food & Drink", "6.0"), + Emoji(["tropical_drink"], "๐Ÿน", ["summer", "vacation"], "Food & Drink", "6.0"), + Emoji(["beer"], "๐Ÿบ", ["drink"], "Food & Drink", "6.0"), + Emoji(["beers"], "๐Ÿป", ["drinks"], "Food & Drink", "6.0"), + Emoji(["clinking_glasses"], "๐Ÿฅ‚", ["cheers", "toast"], "Food & Drink", "9.0"), + Emoji(["tumbler_glass"], "๐Ÿฅƒ", ["whisky"], "Food & Drink", "9.0"), + Emoji(["pouring_liquid"], "๐Ÿซ—", [], "Food & Drink", "14.0"), + Emoji(["cup_with_straw"], "๐Ÿฅค", [], "Food & Drink", "11.0"), + Emoji(["bubble_tea"], "๐Ÿง‹", [], "Food & Drink", "13.0"), + Emoji(["beverage_box"], "๐Ÿงƒ", [], "Food & Drink", "12.0"), + Emoji(["mate"], "๐Ÿง‰", [], "Food & Drink", "12.0"), + Emoji(["ice_cube"], "๐ŸงŠ", [], "Food & Drink", "12.0"), + Emoji(["chopsticks"], "๐Ÿฅข", [], "Food & Drink", "11.0"), + Emoji(["plate_with_cutlery"], "๐Ÿฝ๏ธ", ["dining", "dinner"], "Food & Drink", "7.0"), + Emoji(["fork_and_knife"], "๐Ÿด", ["cutlery"], "Food & Drink", "6.0"), + Emoji(["spoon"], "๐Ÿฅ„", [], "Food & Drink", "9.0"), + Emoji(["hocho", "knife"], "๐Ÿ”ช", ["cut", "chop"], "Food & Drink", "6.0"), + Emoji(["jar"], "๐Ÿซ™", [], "Food & Drink", "14.0"), + Emoji(["amphora"], "๐Ÿบ", [], "Food & Drink", "8.0"), + Emoji( + ["earth_africa"], + "๐ŸŒ", + ["globe", "world", "international"], + "Travel & Places", + "6.0", + ), + Emoji( + ["earth_americas"], + "๐ŸŒŽ", + ["globe", "world", "international"], + "Travel & Places", + "6.0", + ), + Emoji( + ["earth_asia"], + "๐ŸŒ", + ["globe", "world", "international"], + "Travel & Places", + "6.0", + ), + Emoji( + ["globe_with_meridians"], + "๐ŸŒ", + ["world", "global", "international"], + "Travel & Places", + "6.0", + ), + Emoji(["world_map"], "๐Ÿ—บ๏ธ", ["travel"], "Travel & Places", "7.0"), + Emoji(["japan"], "๐Ÿ—พ", [], "Travel & Places", "6.0"), + Emoji(["compass"], "๐Ÿงญ", [], "Travel & Places", "11.0"), + Emoji(["mountain_snow"], "๐Ÿ”๏ธ", [], "Travel & Places", "7.0"), + Emoji(["mountain"], "โ›ฐ๏ธ", [], "Travel & Places", "5.2"), + Emoji(["volcano"], "๐ŸŒ‹", [], "Travel & Places", "6.0"), + Emoji(["mount_fuji"], "๐Ÿ—ป", [], "Travel & Places", "6.0"), + Emoji(["camping"], "๐Ÿ•๏ธ", [], "Travel & Places", "7.0"), + Emoji(["beach_umbrella"], "๐Ÿ–๏ธ", [], "Travel & Places", "7.0"), + Emoji(["desert"], "๐Ÿœ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["desert_island"], "๐Ÿ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["national_park"], "๐Ÿž๏ธ", [], "Travel & Places", "7.0"), + Emoji(["stadium"], "๐ŸŸ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["classical_building"], "๐Ÿ›๏ธ", [], "Travel & Places", "7.0"), + Emoji(["building_construction"], "๐Ÿ—๏ธ", [], "Travel & Places", "7.0"), + Emoji(["bricks"], "๐Ÿงฑ", [], "Travel & Places", "11.0"), + Emoji(["rock"], "๐Ÿชจ", [], "Travel & Places", "13.0"), + Emoji(["wood"], "๐Ÿชต", [], "Travel & Places", "13.0"), + Emoji(["hut"], "๐Ÿ›–", [], "Travel & Places", "13.0"), + Emoji(["houses"], "๐Ÿ˜๏ธ", [], "Travel & Places", "7.0"), + Emoji(["derelict_house"], "๐Ÿš๏ธ", [], "Travel & Places", "7.0"), + Emoji(["house"], "๐Ÿ ", [], "Travel & Places", "6.0"), + Emoji(["house_with_garden"], "๐Ÿก", [], "Travel & Places", "6.0"), + Emoji(["office"], "๐Ÿข", [], "Travel & Places", "6.0"), + Emoji(["post_office"], "๐Ÿฃ", [], "Travel & Places", "6.0"), + Emoji(["european_post_office"], "๐Ÿค", [], "Travel & Places", "6.0"), + Emoji(["hospital"], "๐Ÿฅ", [], "Travel & Places", "6.0"), + Emoji(["bank"], "๐Ÿฆ", [], "Travel & Places", "6.0"), + Emoji(["hotel"], "๐Ÿจ", [], "Travel & Places", "6.0"), + Emoji(["love_hotel"], "๐Ÿฉ", [], "Travel & Places", "6.0"), + Emoji(["convenience_store"], "๐Ÿช", [], "Travel & Places", "6.0"), + Emoji(["school"], "๐Ÿซ", [], "Travel & Places", "6.0"), + Emoji(["department_store"], "๐Ÿฌ", [], "Travel & Places", "6.0"), + Emoji(["factory"], "๐Ÿญ", [], "Travel & Places", "6.0"), + Emoji(["japanese_castle"], "๐Ÿฏ", [], "Travel & Places", "6.0"), + Emoji(["european_castle"], "๐Ÿฐ", [], "Travel & Places", "6.0"), + Emoji(["wedding"], "๐Ÿ’’", ["marriage"], "Travel & Places", "6.0"), + Emoji(["tokyo_tower"], "๐Ÿ—ผ", [], "Travel & Places", "6.0"), + Emoji(["statue_of_liberty"], "๐Ÿ—ฝ", [], "Travel & Places", "6.0"), + Emoji(["church"], "โ›ช", [], "Travel & Places", "5.2"), + Emoji(["mosque"], "๐Ÿ•Œ", [], "Travel & Places", "8.0"), + Emoji(["hindu_temple"], "๐Ÿ›•", [], "Travel & Places", "12.0"), + Emoji(["synagogue"], "๐Ÿ•", [], "Travel & Places", "8.0"), + Emoji(["shinto_shrine"], "โ›ฉ๏ธ", [], "Travel & Places", "5.2"), + Emoji(["kaaba"], "๐Ÿ•‹", [], "Travel & Places", "8.0"), + Emoji(["fountain"], "โ›ฒ", [], "Travel & Places", "5.2"), + Emoji(["tent"], "โ›บ", ["camping"], "Travel & Places", "5.2"), + Emoji(["foggy"], "๐ŸŒ", ["karl"], "Travel & Places", "6.0"), + Emoji(["night_with_stars"], "๐ŸŒƒ", [], "Travel & Places", "6.0"), + Emoji(["cityscape"], "๐Ÿ™๏ธ", ["skyline"], "Travel & Places", "7.0"), + Emoji(["sunrise_over_mountains"], "๐ŸŒ„", [], "Travel & Places", "6.0"), + Emoji(["sunrise"], "๐ŸŒ…", [], "Travel & Places", "6.0"), + Emoji(["city_sunset"], "๐ŸŒ†", [], "Travel & Places", "6.0"), + Emoji(["city_sunrise"], "๐ŸŒ‡", [], "Travel & Places", "6.0"), + Emoji(["bridge_at_night"], "๐ŸŒ‰", [], "Travel & Places", "6.0"), + Emoji(["hotsprings"], "โ™จ๏ธ", [], "Travel & Places", ""), + Emoji(["carousel_horse"], "๐ŸŽ ", [], "Travel & Places", "6.0"), + Emoji(["playground_slide"], "๐Ÿ›", [], "Travel & Places", "14.0"), + Emoji(["ferris_wheel"], "๐ŸŽก", [], "Travel & Places", "6.0"), + Emoji(["roller_coaster"], "๐ŸŽข", [], "Travel & Places", "6.0"), + Emoji(["barber"], "๐Ÿ’ˆ", [], "Travel & Places", "6.0"), + Emoji(["circus_tent"], "๐ŸŽช", [], "Travel & Places", "6.0"), + Emoji(["steam_locomotive"], "๐Ÿš‚", ["train"], "Travel & Places", "6.0"), + Emoji(["railway_car"], "๐Ÿšƒ", [], "Travel & Places", "6.0"), + Emoji(["bullettrain_side"], "๐Ÿš„", ["train"], "Travel & Places", "6.0"), + Emoji(["bullettrain_front"], "๐Ÿš…", ["train"], "Travel & Places", "6.0"), + Emoji(["train2"], "๐Ÿš†", [], "Travel & Places", "6.0"), + Emoji(["metro"], "๐Ÿš‡", [], "Travel & Places", "6.0"), + Emoji(["light_rail"], "๐Ÿšˆ", [], "Travel & Places", "6.0"), + Emoji(["station"], "๐Ÿš‰", [], "Travel & Places", "6.0"), + Emoji(["tram"], "๐ŸšŠ", [], "Travel & Places", "6.0"), + Emoji(["monorail"], "๐Ÿš", [], "Travel & Places", "6.0"), + Emoji(["mountain_railway"], "๐Ÿšž", [], "Travel & Places", "6.0"), + Emoji(["train"], "๐Ÿš‹", [], "Travel & Places", "6.0"), + Emoji(["bus"], "๐ŸšŒ", [], "Travel & Places", "6.0"), + Emoji(["oncoming_bus"], "๐Ÿš", [], "Travel & Places", "6.0"), + Emoji(["trolleybus"], "๐ŸšŽ", [], "Travel & Places", "6.0"), + Emoji(["minibus"], "๐Ÿš", [], "Travel & Places", "6.0"), + Emoji(["ambulance"], "๐Ÿš‘", [], "Travel & Places", "6.0"), + Emoji(["fire_engine"], "๐Ÿš’", [], "Travel & Places", "6.0"), + Emoji(["police_car"], "๐Ÿš“", [], "Travel & Places", "6.0"), + Emoji(["oncoming_police_car"], "๐Ÿš”", [], "Travel & Places", "6.0"), + Emoji(["taxi"], "๐Ÿš•", [], "Travel & Places", "6.0"), + Emoji(["oncoming_taxi"], "๐Ÿš–", [], "Travel & Places", "6.0"), + Emoji(["car", "red_car"], "๐Ÿš—", [], "Travel & Places", "6.0"), + Emoji(["oncoming_automobile"], "๐Ÿš˜", [], "Travel & Places", "6.0"), + Emoji(["blue_car"], "๐Ÿš™", [], "Travel & Places", "6.0"), + Emoji(["pickup_truck"], "๐Ÿ›ป", [], "Travel & Places", "13.0"), + Emoji(["truck"], "๐Ÿšš", [], "Travel & Places", "6.0"), + Emoji(["articulated_lorry"], "๐Ÿš›", [], "Travel & Places", "6.0"), + Emoji(["tractor"], "๐Ÿšœ", [], "Travel & Places", "6.0"), + Emoji(["racing_car"], "๐ŸŽ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["motorcycle"], "๐Ÿ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["motor_scooter"], "๐Ÿ›ต", [], "Travel & Places", "9.0"), + Emoji(["manual_wheelchair"], "๐Ÿฆฝ", [], "Travel & Places", "12.0"), + Emoji(["motorized_wheelchair"], "๐Ÿฆผ", [], "Travel & Places", "12.0"), + Emoji(["auto_rickshaw"], "๐Ÿ›บ", [], "Travel & Places", "12.0"), + Emoji(["bike"], "๐Ÿšฒ", ["bicycle"], "Travel & Places", "6.0"), + Emoji(["kick_scooter"], "๐Ÿ›ด", [], "Travel & Places", "9.0"), + Emoji(["skateboard"], "๐Ÿ›น", [], "Travel & Places", "11.0"), + Emoji(["roller_skate"], "๐Ÿ›ผ", [], "Travel & Places", "13.0"), + Emoji(["busstop"], "๐Ÿš", [], "Travel & Places", "6.0"), + Emoji(["motorway"], "๐Ÿ›ฃ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["railway_track"], "๐Ÿ›ค๏ธ", [], "Travel & Places", "7.0"), + Emoji(["oil_drum"], "๐Ÿ›ข๏ธ", [], "Travel & Places", "7.0"), + Emoji(["fuelpump"], "โ›ฝ", [], "Travel & Places", "5.2"), + Emoji(["wheel"], "๐Ÿ›ž", [], "Travel & Places", "14.0"), + Emoji(["rotating_light"], "๐Ÿšจ", ["911", "emergency"], "Travel & Places", "6.0"), + Emoji(["traffic_light"], "๐Ÿšฅ", [], "Travel & Places", "6.0"), + Emoji(["vertical_traffic_light"], "๐Ÿšฆ", ["semaphore"], "Travel & Places", "6.0"), + Emoji(["stop_sign"], "๐Ÿ›‘", [], "Travel & Places", "9.0"), + Emoji(["construction"], "๐Ÿšง", ["wip"], "Travel & Places", "6.0"), + Emoji(["anchor"], "โš“", ["ship"], "Travel & Places", "4.1"), + Emoji(["ring_buoy"], "๐Ÿ›Ÿ", ["life preserver"], "Travel & Places", "14.0"), + Emoji(["boat", "sailboat"], "โ›ต", [], "Travel & Places", "5.2"), + Emoji(["canoe"], "๐Ÿ›ถ", [], "Travel & Places", "9.0"), + Emoji(["speedboat"], "๐Ÿšค", ["ship"], "Travel & Places", "6.0"), + Emoji(["passenger_ship"], "๐Ÿ›ณ๏ธ", ["cruise"], "Travel & Places", "7.0"), + Emoji(["ferry"], "โ›ด๏ธ", [], "Travel & Places", "5.2"), + Emoji(["motor_boat"], "๐Ÿ›ฅ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["ship"], "๐Ÿšข", [], "Travel & Places", "6.0"), + Emoji(["airplane"], "โœˆ๏ธ", ["flight"], "Travel & Places", ""), + Emoji(["small_airplane"], "๐Ÿ›ฉ๏ธ", ["flight"], "Travel & Places", "7.0"), + Emoji(["flight_departure"], "๐Ÿ›ซ", [], "Travel & Places", "7.0"), + Emoji(["flight_arrival"], "๐Ÿ›ฌ", [], "Travel & Places", "7.0"), + Emoji(["parachute"], "๐Ÿช‚", [], "Travel & Places", "12.0"), + Emoji(["seat"], "๐Ÿ’บ", [], "Travel & Places", "6.0"), + Emoji(["helicopter"], "๐Ÿš", [], "Travel & Places", "6.0"), + Emoji(["suspension_railway"], "๐ŸšŸ", [], "Travel & Places", "6.0"), + Emoji(["mountain_cableway"], "๐Ÿš ", [], "Travel & Places", "6.0"), + Emoji(["aerial_tramway"], "๐Ÿšก", [], "Travel & Places", "6.0"), + Emoji(["artificial_satellite"], "๐Ÿ›ฐ๏ธ", ["orbit", "space"], "Travel & Places", "7.0"), + Emoji(["rocket"], "๐Ÿš€", ["ship", "launch"], "Travel & Places", "6.0"), + Emoji(["flying_saucer"], "๐Ÿ›ธ", ["ufo"], "Travel & Places", "11.0"), + Emoji(["bellhop_bell"], "๐Ÿ›Ž๏ธ", [], "Travel & Places", "7.0"), + Emoji(["luggage"], "๐Ÿงณ", [], "Travel & Places", "11.0"), + Emoji(["hourglass"], "โŒ›", ["time"], "Travel & Places", ""), + Emoji(["hourglass_flowing_sand"], "โณ", ["time"], "Travel & Places", "6.0"), + Emoji(["watch"], "โŒš", ["time"], "Travel & Places", ""), + Emoji(["alarm_clock"], "โฐ", ["morning"], "Travel & Places", "6.0"), + Emoji(["stopwatch"], "โฑ๏ธ", [], "Travel & Places", "6.0"), + Emoji(["timer_clock"], "โฒ๏ธ", [], "Travel & Places", "6.0"), + Emoji(["mantelpiece_clock"], "๐Ÿ•ฐ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["clock12"], "๐Ÿ•›", [], "Travel & Places", "6.0"), + Emoji(["clock1230"], "๐Ÿ•ง", [], "Travel & Places", "6.0"), + Emoji(["clock1"], "๐Ÿ•", [], "Travel & Places", "6.0"), + Emoji(["clock130"], "๐Ÿ•œ", [], "Travel & Places", "6.0"), + Emoji(["clock2"], "๐Ÿ•‘", [], "Travel & Places", "6.0"), + Emoji(["clock230"], "๐Ÿ•", [], "Travel & Places", "6.0"), + Emoji(["clock3"], "๐Ÿ•’", [], "Travel & Places", "6.0"), + Emoji(["clock330"], "๐Ÿ•ž", [], "Travel & Places", "6.0"), + Emoji(["clock4"], "๐Ÿ•“", [], "Travel & Places", "6.0"), + Emoji(["clock430"], "๐Ÿ•Ÿ", [], "Travel & Places", "6.0"), + Emoji(["clock5"], "๐Ÿ•”", [], "Travel & Places", "6.0"), + Emoji(["clock530"], "๐Ÿ• ", [], "Travel & Places", "6.0"), + Emoji(["clock6"], "๐Ÿ••", [], "Travel & Places", "6.0"), + Emoji(["clock630"], "๐Ÿ•ก", [], "Travel & Places", "6.0"), + Emoji(["clock7"], "๐Ÿ•–", [], "Travel & Places", "6.0"), + Emoji(["clock730"], "๐Ÿ•ข", [], "Travel & Places", "6.0"), + Emoji(["clock8"], "๐Ÿ•—", [], "Travel & Places", "6.0"), + Emoji(["clock830"], "๐Ÿ•ฃ", [], "Travel & Places", "6.0"), + Emoji(["clock9"], "๐Ÿ•˜", [], "Travel & Places", "6.0"), + Emoji(["clock930"], "๐Ÿ•ค", [], "Travel & Places", "6.0"), + Emoji(["clock10"], "๐Ÿ•™", [], "Travel & Places", "6.0"), + Emoji(["clock1030"], "๐Ÿ•ฅ", [], "Travel & Places", "6.0"), + Emoji(["clock11"], "๐Ÿ•š", [], "Travel & Places", "6.0"), + Emoji(["clock1130"], "๐Ÿ•ฆ", [], "Travel & Places", "6.0"), + Emoji(["new_moon"], "๐ŸŒ‘", [], "Travel & Places", "6.0"), + Emoji(["waxing_crescent_moon"], "๐ŸŒ’", [], "Travel & Places", "6.0"), + Emoji(["first_quarter_moon"], "๐ŸŒ“", [], "Travel & Places", "6.0"), + Emoji(["moon", "waxing_gibbous_moon"], "๐ŸŒ”", [], "Travel & Places", "6.0"), + Emoji(["full_moon"], "๐ŸŒ•", [], "Travel & Places", "6.0"), + Emoji(["waning_gibbous_moon"], "๐ŸŒ–", [], "Travel & Places", "6.0"), + Emoji(["last_quarter_moon"], "๐ŸŒ—", [], "Travel & Places", "6.0"), + Emoji(["waning_crescent_moon"], "๐ŸŒ˜", [], "Travel & Places", "6.0"), + Emoji(["crescent_moon"], "๐ŸŒ™", ["night"], "Travel & Places", "6.0"), + Emoji(["new_moon_with_face"], "๐ŸŒš", [], "Travel & Places", "6.0"), + Emoji(["first_quarter_moon_with_face"], "๐ŸŒ›", [], "Travel & Places", "6.0"), + Emoji(["last_quarter_moon_with_face"], "๐ŸŒœ", [], "Travel & Places", "6.0"), + Emoji(["thermometer"], "๐ŸŒก๏ธ", [], "Travel & Places", "7.0"), + Emoji(["sunny"], "โ˜€๏ธ", ["weather"], "Travel & Places", ""), + Emoji(["full_moon_with_face"], "๐ŸŒ", [], "Travel & Places", "6.0"), + Emoji(["sun_with_face"], "๐ŸŒž", ["summer"], "Travel & Places", "6.0"), + Emoji(["ringed_planet"], "๐Ÿช", [], "Travel & Places", "12.0"), + Emoji(["star"], "โญ", [], "Travel & Places", "5.1"), + Emoji(["star2"], "๐ŸŒŸ", [], "Travel & Places", "6.0"), + Emoji(["stars"], "๐ŸŒ ", [], "Travel & Places", "6.0"), + Emoji(["milky_way"], "๐ŸŒŒ", [], "Travel & Places", "6.0"), + Emoji(["cloud"], "โ˜๏ธ", [], "Travel & Places", ""), + Emoji(["partly_sunny"], "โ›…", ["weather", "cloud"], "Travel & Places", "5.2"), + Emoji(["cloud_with_lightning_and_rain"], "โ›ˆ๏ธ", [], "Travel & Places", "5.2"), + Emoji(["sun_behind_small_cloud"], "๐ŸŒค๏ธ", [], "Travel & Places", "7.0"), + Emoji(["sun_behind_large_cloud"], "๐ŸŒฅ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["sun_behind_rain_cloud"], "๐ŸŒฆ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["cloud_with_rain"], "๐ŸŒง๏ธ", [], "Travel & Places", "7.0"), + Emoji(["cloud_with_snow"], "๐ŸŒจ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["cloud_with_lightning"], "๐ŸŒฉ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["tornado"], "๐ŸŒช๏ธ", [], "Travel & Places", "7.0"), + Emoji(["fog"], "๐ŸŒซ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["wind_face"], "๐ŸŒฌ๏ธ", [], "Travel & Places", "7.0"), + Emoji(["cyclone"], "๐ŸŒ€", ["swirl"], "Travel & Places", "6.0"), + Emoji(["rainbow"], "๐ŸŒˆ", [], "Travel & Places", "6.0"), + Emoji(["closed_umbrella"], "๐ŸŒ‚", ["weather", "rain"], "Travel & Places", "6.0"), + Emoji(["open_umbrella"], "โ˜‚๏ธ", [], "Travel & Places", ""), + Emoji(["umbrella"], "โ˜”", ["rain", "weather"], "Travel & Places", "4.0"), + Emoji(["parasol_on_ground"], "โ›ฑ๏ธ", ["beach_umbrella"], "Travel & Places", "5.2"), + Emoji(["zap"], "โšก", ["lightning", "thunder"], "Travel & Places", "4.0"), + Emoji(["snowflake"], "โ„๏ธ", ["winter", "cold", "weather"], "Travel & Places", ""), + Emoji(["snowman_with_snow"], "โ˜ƒ๏ธ", ["winter", "christmas"], "Travel & Places", ""), + Emoji(["snowman"], "โ›„", ["winter"], "Travel & Places", "5.2"), + Emoji(["comet"], "โ˜„๏ธ", [], "Travel & Places", ""), + Emoji(["fire"], "๐Ÿ”ฅ", ["burn"], "Travel & Places", "6.0"), + Emoji(["droplet"], "๐Ÿ’ง", ["water"], "Travel & Places", "6.0"), + Emoji(["ocean"], "๐ŸŒŠ", ["sea"], "Travel & Places", "6.0"), + Emoji(["jack_o_lantern"], "๐ŸŽƒ", ["halloween"], "Activities", "6.0"), + Emoji(["christmas_tree"], "๐ŸŽ„", [], "Activities", "6.0"), + Emoji(["fireworks"], "๐ŸŽ†", ["festival", "celebration"], "Activities", "6.0"), + Emoji(["sparkler"], "๐ŸŽ‡", [], "Activities", "6.0"), + Emoji(["firecracker"], "๐Ÿงจ", [], "Activities", "11.0"), + Emoji(["sparkles"], "โœจ", ["shiny"], "Activities", "6.0"), + Emoji(["balloon"], "๐ŸŽˆ", ["party", "birthday"], "Activities", "6.0"), + Emoji(["tada"], "๐ŸŽ‰", ["hooray", "party"], "Activities", "6.0"), + Emoji(["confetti_ball"], "๐ŸŽŠ", [], "Activities", "6.0"), + Emoji(["tanabata_tree"], "๐ŸŽ‹", [], "Activities", "6.0"), + Emoji(["bamboo"], "๐ŸŽ", [], "Activities", "6.0"), + Emoji(["dolls"], "๐ŸŽŽ", [], "Activities", "6.0"), + Emoji(["flags"], "๐ŸŽ", [], "Activities", "6.0"), + Emoji(["wind_chime"], "๐ŸŽ", [], "Activities", "6.0"), + Emoji(["rice_scene"], "๐ŸŽ‘", [], "Activities", "6.0"), + Emoji(["red_envelope"], "๐Ÿงง", [], "Activities", "11.0"), + Emoji(["ribbon"], "๐ŸŽ€", [], "Activities", "6.0"), + Emoji(["gift"], "๐ŸŽ", ["present", "birthday", "christmas"], "Activities", "6.0"), + Emoji(["reminder_ribbon"], "๐ŸŽ—๏ธ", [], "Activities", "7.0"), + Emoji(["tickets"], "๐ŸŽŸ๏ธ", [], "Activities", "7.0"), + Emoji(["ticket"], "๐ŸŽซ", [], "Activities", "6.0"), + Emoji(["medal_military"], "๐ŸŽ–๏ธ", [], "Activities", "7.0"), + Emoji(["trophy"], "๐Ÿ†", ["award", "contest", "winner"], "Activities", "6.0"), + Emoji(["medal_sports"], "๐Ÿ…", ["gold", "winner"], "Activities", "7.0"), + Emoji(["1st_place_medal"], "๐Ÿฅ‡", ["gold"], "Activities", "9.0"), + Emoji(["2nd_place_medal"], "๐Ÿฅˆ", ["silver"], "Activities", "9.0"), + Emoji(["3rd_place_medal"], "๐Ÿฅ‰", ["bronze"], "Activities", "9.0"), + Emoji(["soccer"], "โšฝ", ["sports"], "Activities", "5.2"), + Emoji(["baseball"], "โšพ", ["sports"], "Activities", "5.2"), + Emoji(["softball"], "๐ŸฅŽ", [], "Activities", "11.0"), + Emoji(["basketball"], "๐Ÿ€", ["sports"], "Activities", "6.0"), + Emoji(["volleyball"], "๐Ÿ", [], "Activities", "8.0"), + Emoji(["football"], "๐Ÿˆ", ["sports"], "Activities", "6.0"), + Emoji(["rugby_football"], "๐Ÿ‰", [], "Activities", "6.0"), + Emoji(["tennis"], "๐ŸŽพ", ["sports"], "Activities", "6.0"), + Emoji(["flying_disc"], "๐Ÿฅ", [], "Activities", "11.0"), + Emoji(["bowling"], "๐ŸŽณ", [], "Activities", "6.0"), + Emoji(["cricket_game"], "๐Ÿ", [], "Activities", "8.0"), + Emoji(["field_hockey"], "๐Ÿ‘", [], "Activities", "8.0"), + Emoji(["ice_hockey"], "๐Ÿ’", [], "Activities", "8.0"), + Emoji(["lacrosse"], "๐Ÿฅ", [], "Activities", "11.0"), + Emoji(["ping_pong"], "๐Ÿ“", [], "Activities", "8.0"), + Emoji(["badminton"], "๐Ÿธ", [], "Activities", "8.0"), + Emoji(["boxing_glove"], "๐ŸฅŠ", [], "Activities", "9.0"), + Emoji(["martial_arts_uniform"], "๐Ÿฅ‹", [], "Activities", "9.0"), + Emoji(["goal_net"], "๐Ÿฅ…", [], "Activities", "9.0"), + Emoji(["golf"], "โ›ณ", [], "Activities", "5.2"), + Emoji(["ice_skate"], "โ›ธ๏ธ", ["skating"], "Activities", "5.2"), + Emoji(["fishing_pole_and_fish"], "๐ŸŽฃ", [], "Activities", "6.0"), + Emoji(["diving_mask"], "๐Ÿคฟ", [], "Activities", "12.0"), + Emoji(["running_shirt_with_sash"], "๐ŸŽฝ", ["marathon"], "Activities", "6.0"), + Emoji(["ski"], "๐ŸŽฟ", [], "Activities", "6.0"), + Emoji(["sled"], "๐Ÿ›ท", [], "Activities", "11.0"), + Emoji(["curling_stone"], "๐ŸฅŒ", [], "Activities", "11.0"), + Emoji(["dart"], "๐ŸŽฏ", ["target"], "Activities", "6.0"), + Emoji(["yo_yo"], "๐Ÿช€", [], "Activities", "12.0"), + Emoji(["kite"], "๐Ÿช", [], "Activities", "12.0"), + Emoji(["8ball"], "๐ŸŽฑ", ["pool", "billiards"], "Activities", "6.0"), + Emoji(["crystal_ball"], "๐Ÿ”ฎ", ["fortune"], "Activities", "6.0"), + Emoji(["magic_wand"], "๐Ÿช„", [], "Activities", "13.0"), + Emoji(["nazar_amulet"], "๐Ÿงฟ", [], "Activities", "11.0"), + Emoji(["hamsa"], "๐Ÿชฌ", [], "Activities", "14.0"), + Emoji(["video_game"], "๐ŸŽฎ", ["play", "controller", "console"], "Activities", "6.0"), + Emoji(["joystick"], "๐Ÿ•น๏ธ", [], "Activities", "7.0"), + Emoji(["slot_machine"], "๐ŸŽฐ", [], "Activities", "6.0"), + Emoji(["game_die"], "๐ŸŽฒ", ["dice", "gambling"], "Activities", "6.0"), + Emoji(["jigsaw"], "๐Ÿงฉ", [], "Activities", "11.0"), + Emoji(["teddy_bear"], "๐Ÿงธ", [], "Activities", "11.0"), + Emoji(["pinata"], "๐Ÿช…", [], "Activities", "13.0"), + Emoji(["mirror_ball"], "๐Ÿชฉ", ["disco", "party"], "Activities", "14.0"), + Emoji(["nesting_dolls"], "๐Ÿช†", [], "Activities", "13.0"), + Emoji(["spades"], "โ™ ๏ธ", [], "Activities", ""), + Emoji(["hearts"], "โ™ฅ๏ธ", [], "Activities", ""), + Emoji(["diamonds"], "โ™ฆ๏ธ", [], "Activities", ""), + Emoji(["clubs"], "โ™ฃ๏ธ", [], "Activities", ""), + Emoji(["chess_pawn"], "โ™Ÿ๏ธ", [], "Activities", "11.0"), + Emoji(["black_joker"], "๐Ÿƒ", [], "Activities", "6.0"), + Emoji(["mahjong"], "๐Ÿ€„", [], "Activities", ""), + Emoji(["flower_playing_cards"], "๐ŸŽด", [], "Activities", "6.0"), + Emoji(["performing_arts"], "๐ŸŽญ", ["theater", "drama"], "Activities", "6.0"), + Emoji(["framed_picture"], "๐Ÿ–ผ๏ธ", [], "Activities", "7.0"), + Emoji(["art"], "๐ŸŽจ", ["design", "paint"], "Activities", "6.0"), + Emoji(["thread"], "๐Ÿงต", [], "Activities", "11.0"), + Emoji(["sewing_needle"], "๐Ÿชก", [], "Activities", "13.0"), + Emoji(["yarn"], "๐Ÿงถ", [], "Activities", "11.0"), + Emoji(["knot"], "๐Ÿชข", [], "Activities", "13.0"), + Emoji(["eyeglasses"], "๐Ÿ‘“", ["glasses"], "Objects", "6.0"), + Emoji(["dark_sunglasses"], "๐Ÿ•ถ๏ธ", [], "Objects", "7.0"), + Emoji(["goggles"], "๐Ÿฅฝ", [], "Objects", "11.0"), + Emoji(["lab_coat"], "๐Ÿฅผ", [], "Objects", "11.0"), + Emoji(["safety_vest"], "๐Ÿฆบ", [], "Objects", "12.0"), + Emoji(["necktie"], "๐Ÿ‘”", ["shirt", "formal"], "Objects", "6.0"), + Emoji(["shirt", "tshirt"], "๐Ÿ‘•", [], "Objects", "6.0"), + Emoji(["jeans"], "๐Ÿ‘–", ["pants"], "Objects", "6.0"), + Emoji(["scarf"], "๐Ÿงฃ", [], "Objects", "11.0"), + Emoji(["gloves"], "๐Ÿงค", [], "Objects", "11.0"), + Emoji(["coat"], "๐Ÿงฅ", [], "Objects", "11.0"), + Emoji(["socks"], "๐Ÿงฆ", [], "Objects", "11.0"), + Emoji(["dress"], "๐Ÿ‘—", [], "Objects", "6.0"), + Emoji(["kimono"], "๐Ÿ‘˜", [], "Objects", "6.0"), + Emoji(["sari"], "๐Ÿฅป", [], "Objects", "12.0"), + Emoji(["one_piece_swimsuit"], "๐Ÿฉฑ", [], "Objects", "12.0"), + Emoji(["swim_brief"], "๐Ÿฉฒ", [], "Objects", "12.0"), + Emoji(["shorts"], "๐Ÿฉณ", [], "Objects", "12.0"), + Emoji(["bikini"], "๐Ÿ‘™", ["beach"], "Objects", "6.0"), + Emoji(["womans_clothes"], "๐Ÿ‘š", [], "Objects", "6.0"), + Emoji(["purse"], "๐Ÿ‘›", [], "Objects", "6.0"), + Emoji(["handbag"], "๐Ÿ‘œ", ["bag"], "Objects", "6.0"), + Emoji(["pouch"], "๐Ÿ‘", ["bag"], "Objects", "6.0"), + Emoji(["shopping"], "๐Ÿ›๏ธ", ["bags"], "Objects", "7.0"), + Emoji(["school_satchel"], "๐ŸŽ’", [], "Objects", "6.0"), + Emoji(["thong_sandal"], "๐Ÿฉด", [], "Objects", "13.0"), + Emoji(["mans_shoe", "shoe"], "๐Ÿ‘ž", [], "Objects", "6.0"), + Emoji(["athletic_shoe"], "๐Ÿ‘Ÿ", ["sneaker", "sport", "running"], "Objects", "6.0"), + Emoji(["hiking_boot"], "๐Ÿฅพ", [], "Objects", "11.0"), + Emoji(["flat_shoe"], "๐Ÿฅฟ", [], "Objects", "11.0"), + Emoji(["high_heel"], "๐Ÿ‘ ", ["shoe"], "Objects", "6.0"), + Emoji(["sandal"], "๐Ÿ‘ก", ["shoe"], "Objects", "6.0"), + Emoji(["ballet_shoes"], "๐Ÿฉฐ", [], "Objects", "12.0"), + Emoji(["boot"], "๐Ÿ‘ข", [], "Objects", "6.0"), + Emoji(["crown"], "๐Ÿ‘‘", ["king", "queen", "royal"], "Objects", "6.0"), + Emoji(["womans_hat"], "๐Ÿ‘’", [], "Objects", "6.0"), + Emoji(["tophat"], "๐ŸŽฉ", ["hat", "classy"], "Objects", "6.0"), + Emoji( + ["mortar_board"], + "๐ŸŽ“", + ["education", "college", "university", "graduation"], + "Objects", + "6.0", + ), + Emoji(["billed_cap"], "๐Ÿงข", [], "Objects", "11.0"), + Emoji(["military_helmet"], "๐Ÿช–", [], "Objects", "13.0"), + Emoji(["rescue_worker_helmet"], "โ›‘๏ธ", [], "Objects", "5.2"), + Emoji(["prayer_beads"], "๐Ÿ“ฟ", [], "Objects", "8.0"), + Emoji(["lipstick"], "๐Ÿ’„", ["makeup"], "Objects", "6.0"), + Emoji(["ring"], "๐Ÿ’", ["wedding", "marriage", "engaged"], "Objects", "6.0"), + Emoji(["gem"], "๐Ÿ’Ž", ["diamond"], "Objects", "6.0"), + Emoji(["mute"], "๐Ÿ”‡", ["sound", "volume"], "Objects", "6.0"), + Emoji(["speaker"], "๐Ÿ”ˆ", [], "Objects", "6.0"), + Emoji(["sound"], "๐Ÿ”‰", ["volume"], "Objects", "6.0"), + Emoji(["loud_sound"], "๐Ÿ”Š", ["volume"], "Objects", "6.0"), + Emoji(["loudspeaker"], "๐Ÿ“ข", ["announcement"], "Objects", "6.0"), + Emoji(["mega"], "๐Ÿ“ฃ", [], "Objects", "6.0"), + Emoji(["postal_horn"], "๐Ÿ“ฏ", [], "Objects", "6.0"), + Emoji(["bell"], "๐Ÿ””", ["sound", "notification"], "Objects", "6.0"), + Emoji(["no_bell"], "๐Ÿ”•", ["volume", "off"], "Objects", "6.0"), + Emoji(["musical_score"], "๐ŸŽผ", [], "Objects", "6.0"), + Emoji(["musical_note"], "๐ŸŽต", [], "Objects", "6.0"), + Emoji(["notes"], "๐ŸŽถ", ["music"], "Objects", "6.0"), + Emoji(["studio_microphone"], "๐ŸŽ™๏ธ", ["podcast"], "Objects", "7.0"), + Emoji(["level_slider"], "๐ŸŽš๏ธ", [], "Objects", "7.0"), + Emoji(["control_knobs"], "๐ŸŽ›๏ธ", [], "Objects", "7.0"), + Emoji(["microphone"], "๐ŸŽค", ["sing"], "Objects", "6.0"), + Emoji(["headphones"], "๐ŸŽง", ["music", "earphones"], "Objects", "6.0"), + Emoji(["radio"], "๐Ÿ“ป", ["podcast"], "Objects", "6.0"), + Emoji(["saxophone"], "๐ŸŽท", [], "Objects", "6.0"), + Emoji(["accordion"], "๐Ÿช—", [], "Objects", "13.0"), + Emoji(["guitar"], "๐ŸŽธ", ["rock"], "Objects", "6.0"), + Emoji(["musical_keyboard"], "๐ŸŽน", ["piano"], "Objects", "6.0"), + Emoji(["trumpet"], "๐ŸŽบ", [], "Objects", "6.0"), + Emoji(["violin"], "๐ŸŽป", [], "Objects", "6.0"), + Emoji(["banjo"], "๐Ÿช•", [], "Objects", "12.0"), + Emoji(["drum"], "๐Ÿฅ", [], "Objects", ""), + Emoji(["long_drum"], "๐Ÿช˜", [], "Objects", "13.0"), + Emoji(["iphone"], "๐Ÿ“ฑ", ["smartphone", "mobile"], "Objects", "6.0"), + Emoji(["calling"], "๐Ÿ“ฒ", ["call", "incoming"], "Objects", "6.0"), + Emoji(["phone", "telephone"], "โ˜Ž๏ธ", [], "Objects", ""), + Emoji(["telephone_receiver"], "๐Ÿ“ž", ["phone", "call"], "Objects", "6.0"), + Emoji(["pager"], "๐Ÿ“Ÿ", [], "Objects", "6.0"), + Emoji(["fax"], "๐Ÿ“ ", [], "Objects", "6.0"), + Emoji(["battery"], "๐Ÿ”‹", ["power"], "Objects", "6.0"), + Emoji(["low_battery"], "๐Ÿชซ", [], "Objects", "14.0"), + Emoji(["electric_plug"], "๐Ÿ”Œ", [], "Objects", "6.0"), + Emoji(["computer"], "๐Ÿ’ป", ["desktop", "screen"], "Objects", "6.0"), + Emoji(["desktop_computer"], "๐Ÿ–ฅ๏ธ", [], "Objects", "7.0"), + Emoji(["printer"], "๐Ÿ–จ๏ธ", [], "Objects", "7.0"), + Emoji(["keyboard"], "โŒจ๏ธ", [], "Objects", ""), + Emoji(["computer_mouse"], "๐Ÿ–ฑ๏ธ", [], "Objects", "7.0"), + Emoji(["trackball"], "๐Ÿ–ฒ๏ธ", [], "Objects", "7.0"), + Emoji(["minidisc"], "๐Ÿ’ฝ", [], "Objects", "6.0"), + Emoji(["floppy_disk"], "๐Ÿ’พ", ["save"], "Objects", "6.0"), + Emoji(["cd"], "๐Ÿ’ฟ", [], "Objects", "6.0"), + Emoji(["dvd"], "๐Ÿ“€", [], "Objects", "6.0"), + Emoji(["abacus"], "๐Ÿงฎ", [], "Objects", "11.0"), + Emoji(["movie_camera"], "๐ŸŽฅ", ["film", "video"], "Objects", "6.0"), + Emoji(["film_strip"], "๐ŸŽž๏ธ", [], "Objects", "7.0"), + Emoji(["film_projector"], "๐Ÿ“ฝ๏ธ", [], "Objects", "7.0"), + Emoji(["clapper"], "๐ŸŽฌ", ["film"], "Objects", "6.0"), + Emoji(["tv"], "๐Ÿ“บ", [], "Objects", "6.0"), + Emoji(["camera"], "๐Ÿ“ท", ["photo"], "Objects", "6.0"), + Emoji(["camera_flash"], "๐Ÿ“ธ", ["photo"], "Objects", "7.0"), + Emoji(["video_camera"], "๐Ÿ“น", [], "Objects", "6.0"), + Emoji(["vhs"], "๐Ÿ“ผ", [], "Objects", "6.0"), + Emoji(["mag"], "๐Ÿ”", ["search", "zoom"], "Objects", "6.0"), + Emoji(["mag_right"], "๐Ÿ”Ž", [], "Objects", "6.0"), + Emoji(["candle"], "๐Ÿ•ฏ๏ธ", [], "Objects", "7.0"), + Emoji(["bulb"], "๐Ÿ’ก", ["idea", "light"], "Objects", "6.0"), + Emoji(["flashlight"], "๐Ÿ”ฆ", [], "Objects", "6.0"), + Emoji(["izakaya_lantern", "lantern"], "๐Ÿฎ", [], "Objects", "6.0"), + Emoji(["diya_lamp"], "๐Ÿช”", [], "Objects", "12.0"), + Emoji(["notebook_with_decorative_cover"], "๐Ÿ“”", [], "Objects", "6.0"), + Emoji(["closed_book"], "๐Ÿ“•", [], "Objects", "6.0"), + Emoji(["book", "open_book"], "๐Ÿ“–", [], "Objects", "6.0"), + Emoji(["green_book"], "๐Ÿ“—", [], "Objects", "6.0"), + Emoji(["blue_book"], "๐Ÿ“˜", [], "Objects", "6.0"), + Emoji(["orange_book"], "๐Ÿ“™", [], "Objects", "6.0"), + Emoji(["books"], "๐Ÿ“š", ["library"], "Objects", "6.0"), + Emoji(["notebook"], "๐Ÿ““", [], "Objects", "6.0"), + Emoji(["ledger"], "๐Ÿ“’", [], "Objects", "6.0"), + Emoji(["page_with_curl"], "๐Ÿ“ƒ", [], "Objects", "6.0"), + Emoji(["scroll"], "๐Ÿ“œ", ["document"], "Objects", "6.0"), + Emoji(["page_facing_up"], "๐Ÿ“„", ["document"], "Objects", "6.0"), + Emoji(["newspaper"], "๐Ÿ“ฐ", ["press"], "Objects", "6.0"), + Emoji(["newspaper_roll"], "๐Ÿ—ž๏ธ", ["press"], "Objects", "7.0"), + Emoji(["bookmark_tabs"], "๐Ÿ“‘", [], "Objects", "6.0"), + Emoji(["bookmark"], "๐Ÿ”–", [], "Objects", "6.0"), + Emoji(["label"], "๐Ÿท๏ธ", ["tag"], "Objects", "7.0"), + Emoji(["moneybag"], "๐Ÿ’ฐ", ["dollar", "cream"], "Objects", "6.0"), + Emoji(["coin"], "๐Ÿช™", [], "Objects", "13.0"), + Emoji(["yen"], "๐Ÿ’ด", [], "Objects", "6.0"), + Emoji(["dollar"], "๐Ÿ’ต", ["money"], "Objects", "6.0"), + Emoji(["euro"], "๐Ÿ’ถ", [], "Objects", "6.0"), + Emoji(["pound"], "๐Ÿ’ท", [], "Objects", "6.0"), + Emoji(["money_with_wings"], "๐Ÿ’ธ", ["dollar"], "Objects", "6.0"), + Emoji(["credit_card"], "๐Ÿ’ณ", ["subscription"], "Objects", "6.0"), + Emoji(["receipt"], "๐Ÿงพ", [], "Objects", "11.0"), + Emoji(["chart"], "๐Ÿ’น", [], "Objects", "6.0"), + Emoji(["envelope"], "โœ‰๏ธ", ["letter", "email"], "Objects", ""), + Emoji(["email", "e-mail"], "๐Ÿ“ง", [], "Objects", "6.0"), + Emoji(["incoming_envelope"], "๐Ÿ“จ", [], "Objects", "6.0"), + Emoji(["envelope_with_arrow"], "๐Ÿ“ฉ", [], "Objects", "6.0"), + Emoji(["outbox_tray"], "๐Ÿ“ค", [], "Objects", "6.0"), + Emoji(["inbox_tray"], "๐Ÿ“ฅ", [], "Objects", "6.0"), + Emoji(["package"], "๐Ÿ“ฆ", ["shipping"], "Objects", "6.0"), + Emoji(["mailbox"], "๐Ÿ“ซ", [], "Objects", "6.0"), + Emoji(["mailbox_closed"], "๐Ÿ“ช", [], "Objects", "6.0"), + Emoji(["mailbox_with_mail"], "๐Ÿ“ฌ", [], "Objects", "6.0"), + Emoji(["mailbox_with_no_mail"], "๐Ÿ“ญ", [], "Objects", "6.0"), + Emoji(["postbox"], "๐Ÿ“ฎ", [], "Objects", "6.0"), + Emoji(["ballot_box"], "๐Ÿ—ณ๏ธ", [], "Objects", "7.0"), + Emoji(["pencil2"], "โœ๏ธ", [], "Objects", ""), + Emoji(["black_nib"], "โœ’๏ธ", [], "Objects", ""), + Emoji(["fountain_pen"], "๐Ÿ–‹๏ธ", [], "Objects", "7.0"), + Emoji(["pen"], "๐Ÿ–Š๏ธ", [], "Objects", "7.0"), + Emoji(["paintbrush"], "๐Ÿ–Œ๏ธ", [], "Objects", "7.0"), + Emoji(["crayon"], "๐Ÿ–๏ธ", [], "Objects", "7.0"), + Emoji(["memo", "pencil"], "๐Ÿ“", ["document", "note"], "Objects", "6.0"), + Emoji(["briefcase"], "๐Ÿ’ผ", ["business"], "Objects", "6.0"), + Emoji(["file_folder"], "๐Ÿ“", ["directory"], "Objects", "6.0"), + Emoji(["open_file_folder"], "๐Ÿ“‚", [], "Objects", "6.0"), + Emoji(["card_index_dividers"], "๐Ÿ—‚๏ธ", [], "Objects", "7.0"), + Emoji(["date"], "๐Ÿ“…", ["calendar", "schedule"], "Objects", "6.0"), + Emoji(["calendar"], "๐Ÿ“†", ["schedule"], "Objects", "6.0"), + Emoji(["spiral_notepad"], "๐Ÿ—’๏ธ", [], "Objects", "7.0"), + Emoji(["spiral_calendar"], "๐Ÿ—“๏ธ", [], "Objects", "7.0"), + Emoji(["card_index"], "๐Ÿ“‡", [], "Objects", "6.0"), + Emoji(["chart_with_upwards_trend"], "๐Ÿ“ˆ", ["graph", "metrics"], "Objects", "6.0"), + Emoji(["chart_with_downwards_trend"], "๐Ÿ“‰", ["graph", "metrics"], "Objects", "6.0"), + Emoji(["bar_chart"], "๐Ÿ“Š", ["stats", "metrics"], "Objects", "6.0"), + Emoji(["clipboard"], "๐Ÿ“‹", [], "Objects", "6.0"), + Emoji(["pushpin"], "๐Ÿ“Œ", ["location"], "Objects", "6.0"), + Emoji(["round_pushpin"], "๐Ÿ“", ["location"], "Objects", "6.0"), + Emoji(["paperclip"], "๐Ÿ“Ž", [], "Objects", "6.0"), + Emoji(["paperclips"], "๐Ÿ–‡๏ธ", [], "Objects", "7.0"), + Emoji(["straight_ruler"], "๐Ÿ“", [], "Objects", "6.0"), + Emoji(["triangular_ruler"], "๐Ÿ“", [], "Objects", "6.0"), + Emoji(["scissors"], "โœ‚๏ธ", ["cut"], "Objects", ""), + Emoji(["card_file_box"], "๐Ÿ—ƒ๏ธ", [], "Objects", "7.0"), + Emoji(["file_cabinet"], "๐Ÿ—„๏ธ", [], "Objects", "7.0"), + Emoji(["wastebasket"], "๐Ÿ—‘๏ธ", ["trash"], "Objects", "7.0"), + Emoji(["lock"], "๐Ÿ”’", ["security", "private"], "Objects", "6.0"), + Emoji(["unlock"], "๐Ÿ”“", ["security"], "Objects", "6.0"), + Emoji(["lock_with_ink_pen"], "๐Ÿ”", [], "Objects", "6.0"), + Emoji(["closed_lock_with_key"], "๐Ÿ”", ["security"], "Objects", "6.0"), + Emoji(["key"], "๐Ÿ”‘", ["lock", "password"], "Objects", "6.0"), + Emoji(["old_key"], "๐Ÿ—๏ธ", [], "Objects", "7.0"), + Emoji(["hammer"], "๐Ÿ”จ", ["tool"], "Objects", "6.0"), + Emoji(["axe"], "๐Ÿช“", [], "Objects", "12.0"), + Emoji(["pick"], "โ›๏ธ", [], "Objects", "5.2"), + Emoji(["hammer_and_pick"], "โš’๏ธ", [], "Objects", "4.1"), + Emoji(["hammer_and_wrench"], "๐Ÿ› ๏ธ", [], "Objects", "7.0"), + Emoji(["dagger"], "๐Ÿ—ก๏ธ", [], "Objects", "7.0"), + Emoji(["crossed_swords"], "โš”๏ธ", [], "Objects", "4.1"), + Emoji(["gun"], "๐Ÿ”ซ", ["shoot", "weapon"], "Objects", "6.0"), + Emoji(["boomerang"], "๐Ÿชƒ", [], "Objects", "13.0"), + Emoji(["bow_and_arrow"], "๐Ÿน", ["archery"], "Objects", "8.0"), + Emoji(["shield"], "๐Ÿ›ก๏ธ", [], "Objects", "7.0"), + Emoji(["carpentry_saw"], "๐Ÿชš", [], "Objects", "13.0"), + Emoji(["wrench"], "๐Ÿ”ง", ["tool"], "Objects", "6.0"), + Emoji(["screwdriver"], "๐Ÿช›", [], "Objects", "13.0"), + Emoji(["nut_and_bolt"], "๐Ÿ”ฉ", [], "Objects", "6.0"), + Emoji(["gear"], "โš™๏ธ", [], "Objects", "4.1"), + Emoji(["clamp"], "๐Ÿ—œ๏ธ", [], "Objects", "7.0"), + Emoji(["balance_scale"], "โš–๏ธ", [], "Objects", "4.1"), + Emoji(["probing_cane"], "๐Ÿฆฏ", [], "Objects", "12.0"), + Emoji(["link"], "๐Ÿ”—", [], "Objects", "6.0"), + Emoji(["chains"], "โ›“๏ธ", [], "Objects", "5.2"), + Emoji(["hook"], "๐Ÿช", [], "Objects", "13.0"), + Emoji(["toolbox"], "๐Ÿงฐ", [], "Objects", "11.0"), + Emoji(["magnet"], "๐Ÿงฒ", [], "Objects", "11.0"), + Emoji(["ladder"], "๐Ÿชœ", [], "Objects", "13.0"), + Emoji(["alembic"], "โš—๏ธ", [], "Objects", "4.1"), + Emoji(["test_tube"], "๐Ÿงช", [], "Objects", "11.0"), + Emoji(["petri_dish"], "๐Ÿงซ", [], "Objects", "11.0"), + Emoji(["dna"], "๐Ÿงฌ", [], "Objects", "11.0"), + Emoji( + ["microscope"], "๐Ÿ”ฌ", ["science", "laboratory", "investigate"], "Objects", "6.0" + ), + Emoji(["telescope"], "๐Ÿ”ญ", [], "Objects", "6.0"), + Emoji(["satellite"], "๐Ÿ“ก", ["signal"], "Objects", "6.0"), + Emoji(["syringe"], "๐Ÿ’‰", ["health", "hospital", "needle"], "Objects", "6.0"), + Emoji(["drop_of_blood"], "๐Ÿฉธ", [], "Objects", "12.0"), + Emoji(["pill"], "๐Ÿ’Š", ["health", "medicine"], "Objects", "6.0"), + Emoji(["adhesive_bandage"], "๐Ÿฉน", [], "Objects", "12.0"), + Emoji(["crutch"], "๐Ÿฉผ", [], "Objects", "14.0"), + Emoji(["stethoscope"], "๐Ÿฉบ", [], "Objects", "12.0"), + Emoji(["x_ray"], "๐Ÿฉป", [], "Objects", "14.0"), + Emoji(["door"], "๐Ÿšช", [], "Objects", "6.0"), + Emoji(["elevator"], "๐Ÿ›—", [], "Objects", "13.0"), + Emoji(["mirror"], "๐Ÿชž", [], "Objects", "13.0"), + Emoji(["window"], "๐ŸชŸ", [], "Objects", "13.0"), + Emoji(["bed"], "๐Ÿ›๏ธ", [], "Objects", "7.0"), + Emoji(["couch_and_lamp"], "๐Ÿ›‹๏ธ", [], "Objects", "7.0"), + Emoji(["chair"], "๐Ÿช‘", [], "Objects", "12.0"), + Emoji(["toilet"], "๐Ÿšฝ", ["wc"], "Objects", "6.0"), + Emoji(["plunger"], "๐Ÿช ", [], "Objects", "13.0"), + Emoji(["shower"], "๐Ÿšฟ", ["bath"], "Objects", "6.0"), + Emoji(["bathtub"], "๐Ÿ›", [], "Objects", "6.0"), + Emoji(["mouse_trap"], "๐Ÿชค", [], "Objects", "13.0"), + Emoji(["razor"], "๐Ÿช’", [], "Objects", "12.0"), + Emoji(["lotion_bottle"], "๐Ÿงด", [], "Objects", "11.0"), + Emoji(["safety_pin"], "๐Ÿงท", [], "Objects", "11.0"), + Emoji(["broom"], "๐Ÿงน", [], "Objects", "11.0"), + Emoji(["basket"], "๐Ÿงบ", [], "Objects", "11.0"), + Emoji(["roll_of_paper"], "๐Ÿงป", ["toilet"], "Objects", "11.0"), + Emoji(["bucket"], "๐Ÿชฃ", [], "Objects", "13.0"), + Emoji(["soap"], "๐Ÿงผ", [], "Objects", "11.0"), + Emoji(["bubbles"], "๐Ÿซง", [], "Objects", "14.0"), + Emoji(["toothbrush"], "๐Ÿชฅ", [], "Objects", "13.0"), + Emoji(["sponge"], "๐Ÿงฝ", [], "Objects", "11.0"), + Emoji(["fire_extinguisher"], "๐Ÿงฏ", [], "Objects", "11.0"), + Emoji(["shopping_cart"], "๐Ÿ›’", [], "Objects", "9.0"), + Emoji(["smoking"], "๐Ÿšฌ", ["cigarette"], "Objects", "6.0"), + Emoji(["coffin"], "โšฐ๏ธ", ["funeral"], "Objects", "4.1"), + Emoji(["headstone"], "๐Ÿชฆ", [], "Objects", "13.0"), + Emoji(["funeral_urn"], "โšฑ๏ธ", [], "Objects", "4.1"), + Emoji(["moyai"], "๐Ÿ—ฟ", ["stone"], "Objects", "6.0"), + Emoji(["placard"], "๐Ÿชง", [], "Objects", "13.0"), + Emoji(["identification_card"], "๐Ÿชช", [], "Objects", "14.0"), + Emoji(["atm"], "๐Ÿง", [], "Symbols", "6.0"), + Emoji(["put_litter_in_its_place"], "๐Ÿšฎ", [], "Symbols", "6.0"), + Emoji(["potable_water"], "๐Ÿšฐ", [], "Symbols", "6.0"), + Emoji(["wheelchair"], "โ™ฟ", ["accessibility"], "Symbols", "4.1"), + Emoji(["mens"], "๐Ÿšน", [], "Symbols", "6.0"), + Emoji(["womens"], "๐Ÿšบ", [], "Symbols", "6.0"), + Emoji(["restroom"], "๐Ÿšป", ["toilet"], "Symbols", "6.0"), + Emoji(["baby_symbol"], "๐Ÿšผ", [], "Symbols", "6.0"), + Emoji(["wc"], "๐Ÿšพ", ["toilet", "restroom"], "Symbols", "6.0"), + Emoji(["passport_control"], "๐Ÿ›‚", [], "Symbols", "6.0"), + Emoji(["customs"], "๐Ÿ›ƒ", [], "Symbols", "6.0"), + Emoji(["baggage_claim"], "๐Ÿ›„", ["airport"], "Symbols", "6.0"), + Emoji(["left_luggage"], "๐Ÿ›…", [], "Symbols", "6.0"), + Emoji(["warning"], "โš ๏ธ", ["wip"], "Symbols", "4.0"), + Emoji(["children_crossing"], "๐Ÿšธ", [], "Symbols", "6.0"), + Emoji(["no_entry"], "โ›”", ["limit"], "Symbols", "5.2"), + Emoji(["no_entry_sign"], "๐Ÿšซ", ["block", "forbidden"], "Symbols", "6.0"), + Emoji(["no_bicycles"], "๐Ÿšณ", [], "Symbols", "6.0"), + Emoji(["no_smoking"], "๐Ÿšญ", [], "Symbols", "6.0"), + Emoji(["do_not_litter"], "๐Ÿšฏ", [], "Symbols", "6.0"), + Emoji(["non-potable_water"], "๐Ÿšฑ", [], "Symbols", "6.0"), + Emoji(["no_pedestrians"], "๐Ÿšท", [], "Symbols", "6.0"), + Emoji(["no_mobile_phones"], "๐Ÿ“ต", [], "Symbols", "6.0"), + Emoji(["underage"], "๐Ÿ”ž", [], "Symbols", "6.0"), + Emoji(["radioactive"], "โ˜ข๏ธ", [], "Symbols", ""), + Emoji(["biohazard"], "โ˜ฃ๏ธ", [], "Symbols", ""), + Emoji(["arrow_up"], "โฌ†๏ธ", [], "Symbols", "4.0"), + Emoji(["arrow_upper_right"], "โ†—๏ธ", [], "Symbols", ""), + Emoji(["arrow_right"], "โžก๏ธ", [], "Symbols", ""), + Emoji(["arrow_lower_right"], "โ†˜๏ธ", [], "Symbols", ""), + Emoji(["arrow_down"], "โฌ‡๏ธ", [], "Symbols", "4.0"), + Emoji(["arrow_lower_left"], "โ†™๏ธ", [], "Symbols", ""), + Emoji(["arrow_left"], "โฌ…๏ธ", [], "Symbols", "4.0"), + Emoji(["arrow_upper_left"], "โ†–๏ธ", [], "Symbols", ""), + Emoji(["arrow_up_down"], "โ†•๏ธ", [], "Symbols", ""), + Emoji(["left_right_arrow"], "โ†”๏ธ", [], "Symbols", ""), + Emoji(["leftwards_arrow_with_hook"], "โ†ฉ๏ธ", ["return"], "Symbols", ""), + Emoji(["arrow_right_hook"], "โ†ช๏ธ", [], "Symbols", ""), + Emoji(["arrow_heading_up"], "โคด๏ธ", [], "Symbols", ""), + Emoji(["arrow_heading_down"], "โคต๏ธ", [], "Symbols", ""), + Emoji(["arrows_clockwise"], "๐Ÿ”ƒ", [], "Symbols", "6.0"), + Emoji(["arrows_counterclockwise"], "๐Ÿ”„", ["sync"], "Symbols", "6.0"), + Emoji(["back"], "๐Ÿ”™", [], "Symbols", "6.0"), + Emoji(["end"], "๐Ÿ”š", [], "Symbols", "6.0"), + Emoji(["on"], "๐Ÿ”›", [], "Symbols", "6.0"), + Emoji(["soon"], "๐Ÿ”œ", [], "Symbols", "6.0"), + Emoji(["top"], "๐Ÿ”", [], "Symbols", "6.0"), + Emoji(["place_of_worship"], "๐Ÿ›", [], "Symbols", "8.0"), + Emoji(["atom_symbol"], "โš›๏ธ", [], "Symbols", "4.1"), + Emoji(["om"], "๐Ÿ•‰๏ธ", [], "Symbols", "7.0"), + Emoji(["star_of_david"], "โœก๏ธ", [], "Symbols", ""), + Emoji(["wheel_of_dharma"], "โ˜ธ๏ธ", [], "Symbols", ""), + Emoji(["yin_yang"], "โ˜ฏ๏ธ", [], "Symbols", ""), + Emoji(["latin_cross"], "โœ๏ธ", [], "Symbols", ""), + Emoji(["orthodox_cross"], "โ˜ฆ๏ธ", [], "Symbols", ""), + Emoji(["star_and_crescent"], "โ˜ช๏ธ", [], "Symbols", ""), + Emoji(["peace_symbol"], "โ˜ฎ๏ธ", [], "Symbols", ""), + Emoji(["menorah"], "๐Ÿ•Ž", [], "Symbols", "8.0"), + Emoji(["six_pointed_star"], "๐Ÿ”ฏ", [], "Symbols", "6.0"), + Emoji(["aries"], "โ™ˆ", [], "Symbols", ""), + Emoji(["taurus"], "โ™‰", [], "Symbols", ""), + Emoji(["gemini"], "โ™Š", [], "Symbols", ""), + Emoji(["cancer"], "โ™‹", [], "Symbols", ""), + Emoji(["leo"], "โ™Œ", [], "Symbols", ""), + Emoji(["virgo"], "โ™", [], "Symbols", ""), + Emoji(["libra"], "โ™Ž", [], "Symbols", ""), + Emoji(["scorpius"], "โ™", [], "Symbols", ""), + Emoji(["sagittarius"], "โ™", [], "Symbols", ""), + Emoji(["capricorn"], "โ™‘", [], "Symbols", ""), + Emoji(["aquarius"], "โ™’", [], "Symbols", ""), + Emoji(["pisces"], "โ™“", [], "Symbols", ""), + Emoji(["ophiuchus"], "โ›Ž", [], "Symbols", "6.0"), + Emoji(["twisted_rightwards_arrows"], "๐Ÿ”€", ["shuffle"], "Symbols", "6.0"), + Emoji(["repeat"], "๐Ÿ”", ["loop"], "Symbols", "6.0"), + Emoji(["repeat_one"], "๐Ÿ”‚", [], "Symbols", "6.0"), + Emoji(["arrow_forward"], "โ–ถ๏ธ", [], "Symbols", ""), + Emoji(["fast_forward"], "โฉ", [], "Symbols", "6.0"), + Emoji(["next_track_button"], "โญ๏ธ", [], "Symbols", "6.0"), + Emoji(["play_or_pause_button"], "โฏ๏ธ", [], "Symbols", "6.0"), + Emoji(["arrow_backward"], "โ—€๏ธ", [], "Symbols", ""), + Emoji(["rewind"], "โช", [], "Symbols", "6.0"), + Emoji(["previous_track_button"], "โฎ๏ธ", [], "Symbols", "6.0"), + Emoji(["arrow_up_small"], "๐Ÿ”ผ", [], "Symbols", "6.0"), + Emoji(["arrow_double_up"], "โซ", [], "Symbols", "6.0"), + Emoji(["arrow_down_small"], "๐Ÿ”ฝ", [], "Symbols", "6.0"), + Emoji(["arrow_double_down"], "โฌ", [], "Symbols", "6.0"), + Emoji(["pause_button"], "โธ๏ธ", [], "Symbols", "7.0"), + Emoji(["stop_button"], "โน๏ธ", [], "Symbols", "7.0"), + Emoji(["record_button"], "โบ๏ธ", [], "Symbols", "7.0"), + Emoji(["eject_button"], "โ๏ธ", [], "Symbols", "11.0"), + Emoji(["cinema"], "๐ŸŽฆ", ["film", "movie"], "Symbols", "6.0"), + Emoji(["low_brightness"], "๐Ÿ”…", [], "Symbols", "6.0"), + Emoji(["high_brightness"], "๐Ÿ”†", [], "Symbols", "6.0"), + Emoji(["signal_strength"], "๐Ÿ“ถ", ["wifi"], "Symbols", "6.0"), + Emoji(["vibration_mode"], "๐Ÿ“ณ", [], "Symbols", "6.0"), + Emoji(["mobile_phone_off"], "๐Ÿ“ด", ["mute", "off"], "Symbols", "6.0"), + Emoji(["female_sign"], "โ™€๏ธ", [], "Symbols", "11.0"), + Emoji(["male_sign"], "โ™‚๏ธ", [], "Symbols", "11.0"), + Emoji(["transgender_symbol"], "โšง๏ธ", [], "Symbols", "13.0"), + Emoji(["heavy_multiplication_x"], "โœ–๏ธ", [], "Symbols", ""), + Emoji(["heavy_plus_sign"], "โž•", [], "Symbols", "6.0"), + Emoji(["heavy_minus_sign"], "โž–", [], "Symbols", "6.0"), + Emoji(["heavy_division_sign"], "โž—", [], "Symbols", "6.0"), + Emoji(["heavy_equals_sign"], "๐ŸŸฐ", [], "Symbols", "14.0"), + Emoji(["infinity"], "โ™พ๏ธ", [], "Symbols", "11.0"), + Emoji(["bangbang"], "โ€ผ๏ธ", [], "Symbols", ""), + Emoji(["interrobang"], "โ‰๏ธ", [], "Symbols", "3.0"), + Emoji(["question"], "โ“", ["confused"], "Symbols", "6.0"), + Emoji(["grey_question"], "โ”", [], "Symbols", "6.0"), + Emoji(["grey_exclamation"], "โ•", [], "Symbols", "6.0"), + Emoji(["exclamation", "heavy_exclamation_mark"], "โ—", ["bang"], "Symbols", "5.2"), + Emoji(["wavy_dash"], "ใ€ฐ๏ธ", [], "Symbols", ""), + Emoji(["currency_exchange"], "๐Ÿ’ฑ", [], "Symbols", "6.0"), + Emoji(["heavy_dollar_sign"], "๐Ÿ’ฒ", [], "Symbols", "6.0"), + Emoji(["medical_symbol"], "โš•๏ธ", [], "Symbols", "11.0"), + Emoji(["recycle"], "โ™ป๏ธ", ["environment", "green"], "Symbols", "3.2"), + Emoji(["fleur_de_lis"], "โšœ๏ธ", [], "Symbols", "4.1"), + Emoji(["trident"], "๐Ÿ”ฑ", [], "Symbols", "6.0"), + Emoji(["name_badge"], "๐Ÿ“›", [], "Symbols", "6.0"), + Emoji(["beginner"], "๐Ÿ”ฐ", [], "Symbols", "6.0"), + Emoji(["o"], "โญ•", [], "Symbols", "5.2"), + Emoji(["white_check_mark"], "โœ…", [], "Symbols", "6.0"), + Emoji(["ballot_box_with_check"], "โ˜‘๏ธ", [], "Symbols", ""), + Emoji(["heavy_check_mark"], "โœ”๏ธ", [], "Symbols", ""), + Emoji(["x"], "โŒ", [], "Symbols", "6.0"), + Emoji(["negative_squared_cross_mark"], "โŽ", [], "Symbols", "6.0"), + Emoji(["curly_loop"], "โžฐ", [], "Symbols", "6.0"), + Emoji(["loop"], "โžฟ", [], "Symbols", "6.0"), + Emoji(["part_alternation_mark"], "ใ€ฝ๏ธ", [], "Symbols", "3.2"), + Emoji(["eight_spoked_asterisk"], "โœณ๏ธ", [], "Symbols", ""), + Emoji(["eight_pointed_black_star"], "โœด๏ธ", [], "Symbols", ""), + Emoji(["sparkle"], "โ‡๏ธ", [], "Symbols", ""), + Emoji(["copyright"], "ยฉ๏ธ", [], "Symbols", ""), + Emoji(["registered"], "ยฎ๏ธ", [], "Symbols", ""), + Emoji(["tm"], "โ„ข๏ธ", ["trademark"], "Symbols", ""), + Emoji(["hash"], "#๏ธโƒฃ", ["number"], "Symbols", ""), + Emoji(["asterisk"], "*๏ธโƒฃ", [], "Symbols", ""), + Emoji(["zero"], "0๏ธโƒฃ", [], "Symbols", ""), + Emoji(["one"], "1๏ธโƒฃ", [], "Symbols", ""), + Emoji(["two"], "2๏ธโƒฃ", [], "Symbols", ""), + Emoji(["three"], "3๏ธโƒฃ", [], "Symbols", ""), + Emoji(["four"], "4๏ธโƒฃ", [], "Symbols", ""), + Emoji(["five"], "5๏ธโƒฃ", [], "Symbols", ""), + Emoji(["six"], "6๏ธโƒฃ", [], "Symbols", ""), + Emoji(["seven"], "7๏ธโƒฃ", [], "Symbols", ""), + Emoji(["eight"], "8๏ธโƒฃ", [], "Symbols", ""), + Emoji(["nine"], "9๏ธโƒฃ", [], "Symbols", ""), + Emoji(["keycap_ten"], "๐Ÿ”Ÿ", [], "Symbols", "6.0"), + Emoji(["capital_abcd"], "๐Ÿ” ", ["letters"], "Symbols", "6.0"), + Emoji(["abcd"], "๐Ÿ”ก", [], "Symbols", "6.0"), + Emoji(["1234"], "๐Ÿ”ข", ["numbers"], "Symbols", "6.0"), + Emoji(["symbols"], "๐Ÿ”ฃ", [], "Symbols", "6.0"), + Emoji(["abc"], "๐Ÿ”ค", ["alphabet"], "Symbols", "6.0"), + Emoji(["a"], "๐Ÿ…ฐ๏ธ", [], "Symbols", "6.0"), + Emoji(["ab"], "๐Ÿ†Ž", [], "Symbols", "6.0"), + Emoji(["b"], "๐Ÿ…ฑ๏ธ", [], "Symbols", "6.0"), + Emoji(["cl"], "๐Ÿ†‘", [], "Symbols", "6.0"), + Emoji(["cool"], "๐Ÿ†’", [], "Symbols", "6.0"), + Emoji(["free"], "๐Ÿ†“", [], "Symbols", "6.0"), + Emoji(["information_source"], "โ„น๏ธ", [], "Symbols", "3.0"), + Emoji(["id"], "๐Ÿ†”", [], "Symbols", "6.0"), + Emoji(["m"], "โ“‚๏ธ", [], "Symbols", ""), + Emoji(["new"], "๐Ÿ†•", ["fresh"], "Symbols", "6.0"), + Emoji(["ng"], "๐Ÿ†–", [], "Symbols", "6.0"), + Emoji(["o2"], "๐Ÿ…พ๏ธ", [], "Symbols", "6.0"), + Emoji(["ok"], "๐Ÿ†—", ["yes"], "Symbols", "6.0"), + Emoji(["parking"], "๐Ÿ…ฟ๏ธ", [], "Symbols", "5.2"), + Emoji(["sos"], "๐Ÿ†˜", ["help", "emergency"], "Symbols", "6.0"), + Emoji(["up"], "๐Ÿ†™", [], "Symbols", "6.0"), + Emoji(["vs"], "๐Ÿ†š", [], "Symbols", "6.0"), + Emoji(["koko"], "๐Ÿˆ", [], "Symbols", "6.0"), + Emoji(["sa"], "๐Ÿˆ‚๏ธ", [], "Symbols", "6.0"), + Emoji(["u6708"], "๐Ÿˆท๏ธ", [], "Symbols", "6.0"), + Emoji(["u6709"], "๐Ÿˆถ", [], "Symbols", "6.0"), + Emoji(["u6307"], "๐Ÿˆฏ", [], "Symbols", ""), + Emoji(["ideograph_advantage"], "๐Ÿ‰", [], "Symbols", "6.0"), + Emoji(["u5272"], "๐Ÿˆน", [], "Symbols", "6.0"), + Emoji(["u7121"], "๐Ÿˆš", [], "Symbols", ""), + Emoji(["u7981"], "๐Ÿˆฒ", [], "Symbols", "6.0"), + Emoji(["accept"], "๐Ÿ‰‘", [], "Symbols", "6.0"), + Emoji(["u7533"], "๐Ÿˆธ", [], "Symbols", "6.0"), + Emoji(["u5408"], "๐Ÿˆด", [], "Symbols", "6.0"), + Emoji(["u7a7a"], "๐Ÿˆณ", [], "Symbols", "6.0"), + Emoji(["congratulations"], "ใŠ—๏ธ", [], "Symbols", ""), + Emoji(["secret"], "ใŠ™๏ธ", [], "Symbols", ""), + Emoji(["u55b6"], "๐Ÿˆบ", [], "Symbols", "6.0"), + Emoji(["u6e80"], "๐Ÿˆต", [], "Symbols", "6.0"), + Emoji(["red_circle"], "๐Ÿ”ด", [], "Symbols", "6.0"), + Emoji(["orange_circle"], "๐ŸŸ ", [], "Symbols", "12.0"), + Emoji(["yellow_circle"], "๐ŸŸก", [], "Symbols", "12.0"), + Emoji(["green_circle"], "๐ŸŸข", [], "Symbols", "12.0"), + Emoji(["large_blue_circle"], "๐Ÿ”ต", [], "Symbols", "6.0"), + Emoji(["purple_circle"], "๐ŸŸฃ", [], "Symbols", "12.0"), + Emoji(["brown_circle"], "๐ŸŸค", [], "Symbols", "12.0"), + Emoji(["black_circle"], "โšซ", [], "Symbols", "4.1"), + Emoji(["white_circle"], "โšช", [], "Symbols", "4.1"), + Emoji(["red_square"], "๐ŸŸฅ", [], "Symbols", "12.0"), + Emoji(["orange_square"], "๐ŸŸง", [], "Symbols", "12.0"), + Emoji(["yellow_square"], "๐ŸŸจ", [], "Symbols", "12.0"), + Emoji(["green_square"], "๐ŸŸฉ", [], "Symbols", "12.0"), + Emoji(["blue_square"], "๐ŸŸฆ", [], "Symbols", "12.0"), + Emoji(["purple_square"], "๐ŸŸช", [], "Symbols", "12.0"), + Emoji(["brown_square"], "๐ŸŸซ", [], "Symbols", "12.0"), + Emoji(["black_large_square"], "โฌ›", [], "Symbols", "5.1"), + Emoji(["white_large_square"], "โฌœ", [], "Symbols", "5.1"), + Emoji(["black_medium_square"], "โ—ผ๏ธ", [], "Symbols", "3.2"), + Emoji(["white_medium_square"], "โ—ป๏ธ", [], "Symbols", "3.2"), + Emoji(["black_medium_small_square"], "โ—พ", [], "Symbols", "3.2"), + Emoji(["white_medium_small_square"], "โ—ฝ", [], "Symbols", "3.2"), + Emoji(["black_small_square"], "โ–ช๏ธ", [], "Symbols", ""), + Emoji(["white_small_square"], "โ–ซ๏ธ", [], "Symbols", ""), + Emoji(["large_orange_diamond"], "๐Ÿ”ถ", [], "Symbols", "6.0"), + Emoji(["large_blue_diamond"], "๐Ÿ”ท", [], "Symbols", "6.0"), + Emoji(["small_orange_diamond"], "๐Ÿ”ธ", [], "Symbols", "6.0"), + Emoji(["small_blue_diamond"], "๐Ÿ”น", [], "Symbols", "6.0"), + Emoji(["small_red_triangle"], "๐Ÿ”บ", [], "Symbols", "6.0"), + Emoji(["small_red_triangle_down"], "๐Ÿ”ป", [], "Symbols", "6.0"), + Emoji(["diamond_shape_with_a_dot_inside"], "๐Ÿ’ ", [], "Symbols", "6.0"), + Emoji(["radio_button"], "๐Ÿ”˜", [], "Symbols", "6.0"), + Emoji(["white_square_button"], "๐Ÿ”ณ", [], "Symbols", "6.0"), + Emoji(["black_square_button"], "๐Ÿ”ฒ", [], "Symbols", "6.0"), + Emoji(["checkered_flag"], "๐Ÿ", ["milestone", "finish"], "Flags", "6.0"), + Emoji(["triangular_flag_on_post"], "๐Ÿšฉ", [], "Flags", "6.0"), + Emoji(["crossed_flags"], "๐ŸŽŒ", [], "Flags", "6.0"), + Emoji(["black_flag"], "๐Ÿด", [], "Flags", "7.0"), + Emoji(["white_flag"], "๐Ÿณ๏ธ", [], "Flags", "7.0"), + Emoji(["rainbow_flag"], "๐Ÿณ๏ธโ€๐ŸŒˆ", ["pride"], "Flags", "6.0"), + Emoji(["transgender_flag"], "๐Ÿณ๏ธโ€โšง๏ธ", [], "Flags", "13.0"), + Emoji(["pirate_flag"], "๐Ÿดโ€โ˜ ๏ธ", [], "Flags", "11.0"), + Emoji(["ascension_island"], "๐Ÿ‡ฆ๐Ÿ‡จ", [], "Flags", "11.0"), + Emoji(["andorra"], "๐Ÿ‡ฆ๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["united_arab_emirates"], "๐Ÿ‡ฆ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["afghanistan"], "๐Ÿ‡ฆ๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["antigua_barbuda"], "๐Ÿ‡ฆ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["anguilla"], "๐Ÿ‡ฆ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["albania"], "๐Ÿ‡ฆ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["armenia"], "๐Ÿ‡ฆ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["angola"], "๐Ÿ‡ฆ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["antarctica"], "๐Ÿ‡ฆ๐Ÿ‡ถ", [], "Flags", "6.0"), + Emoji(["argentina"], "๐Ÿ‡ฆ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["american_samoa"], "๐Ÿ‡ฆ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["austria"], "๐Ÿ‡ฆ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["australia"], "๐Ÿ‡ฆ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["aruba"], "๐Ÿ‡ฆ๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["aland_islands"], "๐Ÿ‡ฆ๐Ÿ‡ฝ", [], "Flags", "6.0"), + Emoji(["azerbaijan"], "๐Ÿ‡ฆ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["bosnia_herzegovina"], "๐Ÿ‡ง๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["barbados"], "๐Ÿ‡ง๐Ÿ‡ง", [], "Flags", "6.0"), + Emoji(["bangladesh"], "๐Ÿ‡ง๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["belgium"], "๐Ÿ‡ง๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["burkina_faso"], "๐Ÿ‡ง๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["bulgaria"], "๐Ÿ‡ง๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["bahrain"], "๐Ÿ‡ง๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["burundi"], "๐Ÿ‡ง๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["benin"], "๐Ÿ‡ง๐Ÿ‡ฏ", [], "Flags", "6.0"), + Emoji(["st_barthelemy"], "๐Ÿ‡ง๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["bermuda"], "๐Ÿ‡ง๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["brunei"], "๐Ÿ‡ง๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["bolivia"], "๐Ÿ‡ง๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["caribbean_netherlands"], "๐Ÿ‡ง๐Ÿ‡ถ", [], "Flags", "6.0"), + Emoji(["brazil"], "๐Ÿ‡ง๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["bahamas"], "๐Ÿ‡ง๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["bhutan"], "๐Ÿ‡ง๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["bouvet_island"], "๐Ÿ‡ง๐Ÿ‡ป", [], "Flags", "11.0"), + Emoji(["botswana"], "๐Ÿ‡ง๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["belarus"], "๐Ÿ‡ง๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["belize"], "๐Ÿ‡ง๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["canada"], "๐Ÿ‡จ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["cocos_islands"], "๐Ÿ‡จ๐Ÿ‡จ", ["keeling"], "Flags", "6.0"), + Emoji(["congo_kinshasa"], "๐Ÿ‡จ๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["central_african_republic"], "๐Ÿ‡จ๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["congo_brazzaville"], "๐Ÿ‡จ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["switzerland"], "๐Ÿ‡จ๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["cote_divoire"], "๐Ÿ‡จ๐Ÿ‡ฎ", ["ivory"], "Flags", "6.0"), + Emoji(["cook_islands"], "๐Ÿ‡จ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["chile"], "๐Ÿ‡จ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["cameroon"], "๐Ÿ‡จ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["cn"], "๐Ÿ‡จ๐Ÿ‡ณ", ["china"], "Flags", "6.0"), + Emoji(["colombia"], "๐Ÿ‡จ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["clipperton_island"], "๐Ÿ‡จ๐Ÿ‡ต", [], "Flags", "11.0"), + Emoji(["costa_rica"], "๐Ÿ‡จ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["cuba"], "๐Ÿ‡จ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["cape_verde"], "๐Ÿ‡จ๐Ÿ‡ป", [], "Flags", "6.0"), + Emoji(["curacao"], "๐Ÿ‡จ๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["christmas_island"], "๐Ÿ‡จ๐Ÿ‡ฝ", [], "Flags", "6.0"), + Emoji(["cyprus"], "๐Ÿ‡จ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["czech_republic"], "๐Ÿ‡จ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["de"], "๐Ÿ‡ฉ๐Ÿ‡ช", ["flag", "germany"], "Flags", "6.0"), + Emoji(["diego_garcia"], "๐Ÿ‡ฉ๐Ÿ‡ฌ", [], "Flags", "11.0"), + Emoji(["djibouti"], "๐Ÿ‡ฉ๐Ÿ‡ฏ", [], "Flags", "6.0"), + Emoji(["denmark"], "๐Ÿ‡ฉ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["dominica"], "๐Ÿ‡ฉ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["dominican_republic"], "๐Ÿ‡ฉ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["algeria"], "๐Ÿ‡ฉ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["ceuta_melilla"], "๐Ÿ‡ช๐Ÿ‡ฆ", [], "Flags", "11.0"), + Emoji(["ecuador"], "๐Ÿ‡ช๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["estonia"], "๐Ÿ‡ช๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["egypt"], "๐Ÿ‡ช๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["western_sahara"], "๐Ÿ‡ช๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["eritrea"], "๐Ÿ‡ช๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["es"], "๐Ÿ‡ช๐Ÿ‡ธ", ["spain"], "Flags", "6.0"), + Emoji(["ethiopia"], "๐Ÿ‡ช๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["eu", "european_union"], "๐Ÿ‡ช๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["finland"], "๐Ÿ‡ซ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["fiji"], "๐Ÿ‡ซ๐Ÿ‡ฏ", [], "Flags", "6.0"), + Emoji(["falkland_islands"], "๐Ÿ‡ซ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["micronesia"], "๐Ÿ‡ซ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["faroe_islands"], "๐Ÿ‡ซ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["fr"], "๐Ÿ‡ซ๐Ÿ‡ท", ["france", "french"], "Flags", "6.0"), + Emoji(["gabon"], "๐Ÿ‡ฌ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["gb", "uk"], "๐Ÿ‡ฌ๐Ÿ‡ง", ["flag", "british"], "Flags", "6.0"), + Emoji(["grenada"], "๐Ÿ‡ฌ๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["georgia"], "๐Ÿ‡ฌ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["french_guiana"], "๐Ÿ‡ฌ๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["guernsey"], "๐Ÿ‡ฌ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["ghana"], "๐Ÿ‡ฌ๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["gibraltar"], "๐Ÿ‡ฌ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["greenland"], "๐Ÿ‡ฌ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["gambia"], "๐Ÿ‡ฌ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["guinea"], "๐Ÿ‡ฌ๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["guadeloupe"], "๐Ÿ‡ฌ๐Ÿ‡ต", [], "Flags", "6.0"), + Emoji(["equatorial_guinea"], "๐Ÿ‡ฌ๐Ÿ‡ถ", [], "Flags", "6.0"), + Emoji(["greece"], "๐Ÿ‡ฌ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["south_georgia_south_sandwich_islands"], "๐Ÿ‡ฌ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["guatemala"], "๐Ÿ‡ฌ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["guam"], "๐Ÿ‡ฌ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["guinea_bissau"], "๐Ÿ‡ฌ๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["guyana"], "๐Ÿ‡ฌ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["hong_kong"], "๐Ÿ‡ญ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["heard_mcdonald_islands"], "๐Ÿ‡ญ๐Ÿ‡ฒ", [], "Flags", "11.0"), + Emoji(["honduras"], "๐Ÿ‡ญ๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["croatia"], "๐Ÿ‡ญ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["haiti"], "๐Ÿ‡ญ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["hungary"], "๐Ÿ‡ญ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["canary_islands"], "๐Ÿ‡ฎ๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["indonesia"], "๐Ÿ‡ฎ๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["ireland"], "๐Ÿ‡ฎ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["israel"], "๐Ÿ‡ฎ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["isle_of_man"], "๐Ÿ‡ฎ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["india"], "๐Ÿ‡ฎ๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["british_indian_ocean_territory"], "๐Ÿ‡ฎ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["iraq"], "๐Ÿ‡ฎ๐Ÿ‡ถ", [], "Flags", "6.0"), + Emoji(["iran"], "๐Ÿ‡ฎ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["iceland"], "๐Ÿ‡ฎ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["it"], "๐Ÿ‡ฎ๐Ÿ‡น", ["italy"], "Flags", "6.0"), + Emoji(["jersey"], "๐Ÿ‡ฏ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["jamaica"], "๐Ÿ‡ฏ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["jordan"], "๐Ÿ‡ฏ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["jp"], "๐Ÿ‡ฏ๐Ÿ‡ต", ["japan"], "Flags", "6.0"), + Emoji(["kenya"], "๐Ÿ‡ฐ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["kyrgyzstan"], "๐Ÿ‡ฐ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["cambodia"], "๐Ÿ‡ฐ๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["kiribati"], "๐Ÿ‡ฐ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["comoros"], "๐Ÿ‡ฐ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["st_kitts_nevis"], "๐Ÿ‡ฐ๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["north_korea"], "๐Ÿ‡ฐ๐Ÿ‡ต", [], "Flags", "6.0"), + Emoji(["kr"], "๐Ÿ‡ฐ๐Ÿ‡ท", ["korea"], "Flags", "6.0"), + Emoji(["kuwait"], "๐Ÿ‡ฐ๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["cayman_islands"], "๐Ÿ‡ฐ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["kazakhstan"], "๐Ÿ‡ฐ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["laos"], "๐Ÿ‡ฑ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["lebanon"], "๐Ÿ‡ฑ๐Ÿ‡ง", [], "Flags", "6.0"), + Emoji(["st_lucia"], "๐Ÿ‡ฑ๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["liechtenstein"], "๐Ÿ‡ฑ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["sri_lanka"], "๐Ÿ‡ฑ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["liberia"], "๐Ÿ‡ฑ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["lesotho"], "๐Ÿ‡ฑ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["lithuania"], "๐Ÿ‡ฑ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["luxembourg"], "๐Ÿ‡ฑ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["latvia"], "๐Ÿ‡ฑ๐Ÿ‡ป", [], "Flags", "6.0"), + Emoji(["libya"], "๐Ÿ‡ฑ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["morocco"], "๐Ÿ‡ฒ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["monaco"], "๐Ÿ‡ฒ๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["moldova"], "๐Ÿ‡ฒ๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["montenegro"], "๐Ÿ‡ฒ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["st_martin"], "๐Ÿ‡ฒ๐Ÿ‡ซ", [], "Flags", "11.0"), + Emoji(["madagascar"], "๐Ÿ‡ฒ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["marshall_islands"], "๐Ÿ‡ฒ๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["macedonia"], "๐Ÿ‡ฒ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["mali"], "๐Ÿ‡ฒ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["myanmar"], "๐Ÿ‡ฒ๐Ÿ‡ฒ", ["burma"], "Flags", "6.0"), + Emoji(["mongolia"], "๐Ÿ‡ฒ๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["macau"], "๐Ÿ‡ฒ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["northern_mariana_islands"], "๐Ÿ‡ฒ๐Ÿ‡ต", [], "Flags", "6.0"), + Emoji(["martinique"], "๐Ÿ‡ฒ๐Ÿ‡ถ", [], "Flags", "6.0"), + Emoji(["mauritania"], "๐Ÿ‡ฒ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["montserrat"], "๐Ÿ‡ฒ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["malta"], "๐Ÿ‡ฒ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["mauritius"], "๐Ÿ‡ฒ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["maldives"], "๐Ÿ‡ฒ๐Ÿ‡ป", [], "Flags", "6.0"), + Emoji(["malawi"], "๐Ÿ‡ฒ๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["mexico"], "๐Ÿ‡ฒ๐Ÿ‡ฝ", [], "Flags", "6.0"), + Emoji(["malaysia"], "๐Ÿ‡ฒ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["mozambique"], "๐Ÿ‡ฒ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["namibia"], "๐Ÿ‡ณ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["new_caledonia"], "๐Ÿ‡ณ๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["niger"], "๐Ÿ‡ณ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["norfolk_island"], "๐Ÿ‡ณ๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["nigeria"], "๐Ÿ‡ณ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["nicaragua"], "๐Ÿ‡ณ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["netherlands"], "๐Ÿ‡ณ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["norway"], "๐Ÿ‡ณ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["nepal"], "๐Ÿ‡ณ๐Ÿ‡ต", [], "Flags", "6.0"), + Emoji(["nauru"], "๐Ÿ‡ณ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["niue"], "๐Ÿ‡ณ๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["new_zealand"], "๐Ÿ‡ณ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["oman"], "๐Ÿ‡ด๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["panama"], "๐Ÿ‡ต๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["peru"], "๐Ÿ‡ต๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["french_polynesia"], "๐Ÿ‡ต๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["papua_new_guinea"], "๐Ÿ‡ต๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["philippines"], "๐Ÿ‡ต๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["pakistan"], "๐Ÿ‡ต๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["poland"], "๐Ÿ‡ต๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["st_pierre_miquelon"], "๐Ÿ‡ต๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["pitcairn_islands"], "๐Ÿ‡ต๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["puerto_rico"], "๐Ÿ‡ต๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["palestinian_territories"], "๐Ÿ‡ต๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["portugal"], "๐Ÿ‡ต๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["palau"], "๐Ÿ‡ต๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["paraguay"], "๐Ÿ‡ต๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["qatar"], "๐Ÿ‡ถ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["reunion"], "๐Ÿ‡ท๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["romania"], "๐Ÿ‡ท๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["serbia"], "๐Ÿ‡ท๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["ru"], "๐Ÿ‡ท๐Ÿ‡บ", ["russia"], "Flags", "6.0"), + Emoji(["rwanda"], "๐Ÿ‡ท๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["saudi_arabia"], "๐Ÿ‡ธ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["solomon_islands"], "๐Ÿ‡ธ๐Ÿ‡ง", [], "Flags", "6.0"), + Emoji(["seychelles"], "๐Ÿ‡ธ๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["sudan"], "๐Ÿ‡ธ๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["sweden"], "๐Ÿ‡ธ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["singapore"], "๐Ÿ‡ธ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["st_helena"], "๐Ÿ‡ธ๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["slovenia"], "๐Ÿ‡ธ๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["svalbard_jan_mayen"], "๐Ÿ‡ธ๐Ÿ‡ฏ", [], "Flags", "11.0"), + Emoji(["slovakia"], "๐Ÿ‡ธ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["sierra_leone"], "๐Ÿ‡ธ๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["san_marino"], "๐Ÿ‡ธ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["senegal"], "๐Ÿ‡ธ๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["somalia"], "๐Ÿ‡ธ๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["suriname"], "๐Ÿ‡ธ๐Ÿ‡ท", [], "Flags", "6.0"), + Emoji(["south_sudan"], "๐Ÿ‡ธ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["sao_tome_principe"], "๐Ÿ‡ธ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["el_salvador"], "๐Ÿ‡ธ๐Ÿ‡ป", [], "Flags", "6.0"), + Emoji(["sint_maarten"], "๐Ÿ‡ธ๐Ÿ‡ฝ", [], "Flags", "6.0"), + Emoji(["syria"], "๐Ÿ‡ธ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["swaziland"], "๐Ÿ‡ธ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["tristan_da_cunha"], "๐Ÿ‡น๐Ÿ‡ฆ", [], "Flags", "11.0"), + Emoji(["turks_caicos_islands"], "๐Ÿ‡น๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["chad"], "๐Ÿ‡น๐Ÿ‡ฉ", [], "Flags", "6.0"), + Emoji(["french_southern_territories"], "๐Ÿ‡น๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["togo"], "๐Ÿ‡น๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["thailand"], "๐Ÿ‡น๐Ÿ‡ญ", [], "Flags", "6.0"), + Emoji(["tajikistan"], "๐Ÿ‡น๐Ÿ‡ฏ", [], "Flags", "6.0"), + Emoji(["tokelau"], "๐Ÿ‡น๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["timor_leste"], "๐Ÿ‡น๐Ÿ‡ฑ", [], "Flags", "6.0"), + Emoji(["turkmenistan"], "๐Ÿ‡น๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["tunisia"], "๐Ÿ‡น๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["tonga"], "๐Ÿ‡น๐Ÿ‡ด", [], "Flags", "6.0"), + Emoji(["tr"], "๐Ÿ‡น๐Ÿ‡ท", ["turkey"], "Flags", "8.0"), + Emoji(["trinidad_tobago"], "๐Ÿ‡น๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["tuvalu"], "๐Ÿ‡น๐Ÿ‡ป", [], "Flags", "6.0"), + Emoji(["taiwan"], "๐Ÿ‡น๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["tanzania"], "๐Ÿ‡น๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["ukraine"], "๐Ÿ‡บ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["uganda"], "๐Ÿ‡บ๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["us_outlying_islands"], "๐Ÿ‡บ๐Ÿ‡ฒ", [], "Flags", "11.0"), + Emoji(["united_nations"], "๐Ÿ‡บ๐Ÿ‡ณ", [], "Flags", "11.0"), + Emoji(["us"], "๐Ÿ‡บ๐Ÿ‡ธ", ["flag", "united", "america"], "Flags", "6.0"), + Emoji(["uruguay"], "๐Ÿ‡บ๐Ÿ‡พ", [], "Flags", "6.0"), + Emoji(["uzbekistan"], "๐Ÿ‡บ๐Ÿ‡ฟ", [], "Flags", "6.0"), + Emoji(["vatican_city"], "๐Ÿ‡ป๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["st_vincent_grenadines"], "๐Ÿ‡ป๐Ÿ‡จ", [], "Flags", "6.0"), + Emoji(["venezuela"], "๐Ÿ‡ป๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["british_virgin_islands"], "๐Ÿ‡ป๐Ÿ‡ฌ", [], "Flags", "6.0"), + Emoji(["us_virgin_islands"], "๐Ÿ‡ป๐Ÿ‡ฎ", [], "Flags", "6.0"), + Emoji(["vietnam"], "๐Ÿ‡ป๐Ÿ‡ณ", [], "Flags", "6.0"), + Emoji(["vanuatu"], "๐Ÿ‡ป๐Ÿ‡บ", [], "Flags", "6.0"), + Emoji(["wallis_futuna"], "๐Ÿ‡ผ๐Ÿ‡ซ", [], "Flags", "6.0"), + Emoji(["samoa"], "๐Ÿ‡ผ๐Ÿ‡ธ", [], "Flags", "6.0"), + Emoji(["kosovo"], "๐Ÿ‡ฝ๐Ÿ‡ฐ", [], "Flags", "6.0"), + Emoji(["yemen"], "๐Ÿ‡พ๐Ÿ‡ช", [], "Flags", "6.0"), + Emoji(["mayotte"], "๐Ÿ‡พ๐Ÿ‡น", [], "Flags", "6.0"), + Emoji(["south_africa"], "๐Ÿ‡ฟ๐Ÿ‡ฆ", [], "Flags", "6.0"), + Emoji(["zambia"], "๐Ÿ‡ฟ๐Ÿ‡ฒ", [], "Flags", "6.0"), + Emoji(["zimbabwe"], "๐Ÿ‡ฟ๐Ÿ‡ผ", [], "Flags", "6.0"), + Emoji(["england"], "๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", [], "Flags", "11.0"), + Emoji(["scotland"], "๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", [], "Flags", "11.0"), + Emoji(["wales"], "๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", [], "Flags", "11.0"), +] diff -uNr akris-desktop/akris_desktop/emojis/db/generator.py akris-desktop-genesis/akris_desktop/emojis/db/generator.py --- akris-desktop/akris_desktop/emojis/db/generator.py false +++ akris-desktop-genesis/akris_desktop/emojis/db/generator.py e465b5a89742a97cff7887985fda0a88adfd6a809affe0521a39d348e54693b5582ec7b960434f0607e5dc52b2cb51b954d935e37533fdf9a93611ff7a35214b @@ -0,0 +1,72 @@ +import argparse +import os + +from datetime import datetime + +import requests + +GEMOJI_RELEASE_URL = "https://api.github.com/repos/github/gemoji/releases" +GEMOJI_JSON_DB_URL = ( + "https://raw.githubusercontent.com/github/gemoji/{tag}/db/emoji.json" +) + + +def get_lastest_release(): + req = requests.get(GEMOJI_RELEASE_URL) + req.raise_for_status() + + data = req.json() + + latest = data[0] + + return latest["tag_name"], latest["name"] + + +def generate(path, dbname): + tag, name = get_lastest_release() + + req = requests.get(GEMOJI_JSON_DB_URL.format(tag=tag)) + req.raise_for_status() + + data = req.json() + + path = os.path.join(path, dbname) + + with open(path, "w", encoding="utf-8") as file: + file.write("### This is a generated file.\n") + file.write("### Do not edit this file.\n") + file.write("### Date: {0}\n".format(datetime.now().isoformat()[:-7])) + file.write("### This file is based on {0}.\n".format(name)) + file.write("\n") + file.write("from collections import namedtuple\n") + file.write("\n") + file.write( + 'Emoji = namedtuple("Emoji", ["aliases", "emoji", "tags", "category", "unicode_version"])\n' + ) + file.write("\n") + file.write("EMOJI_DB = [\n") + + for emoji in data: + if "emoji" in emoji: + file.write( + ' Emoji({aliases}, "{emoji}", {tags}, "{category}", "{unicode_version}"),\n'.format( + **{ + "aliases": emoji["aliases"], + "emoji": emoji["emoji"], + "tags": emoji["tags"], + "category": emoji["category"], + "unicode_version": emoji["unicode_version"], + } + ) + ) + + file.write("]\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generates the Emoji database.") + parser.add_argument("--dir", default=".", help="Database location") + parser.add_argument("--dbname", default="db.py", help="Database location") + args = parser.parse_args() + + generate(args.dir, args.dbname) diff -uNr akris-desktop/akris_desktop/emojis/db/utils.py akris-desktop-genesis/akris_desktop/emojis/db/utils.py --- akris-desktop/akris_desktop/emojis/db/utils.py false +++ akris-desktop-genesis/akris_desktop/emojis/db/utils.py 1f676d408fe0ca1a1c1b1f3d3bc5d9416f77e512ddc1805f5d5cd7b019e1cd38fb516fd5d8658af65c8f7abc9c20e5a3f4572e257a40240714d38a15705e5cb7 @@ -0,0 +1,81 @@ +from . import db + + +def get_emoji_aliases(): + """ + Returns all Emojis as a dict (key = alias, value = unicode). + + :rtype: dict + """ + emoji_aliases = {} + + for emoji in db.EMOJI_DB: + for alias in emoji.aliases: + alias = ":{0}:".format(alias) + emoji_aliases[alias] = emoji.emoji + + return emoji_aliases + + +def get_emoji_by_code(code): + """ + Returns Emoji by Unicode code. + + :param code: Emoji Unicode code. + :rtype: emojis.db.Emoji + """ + try: + return next(filter(lambda emoji: code == emoji.emoji, db.EMOJI_DB)) + except StopIteration: + return None + + +def get_emoji_by_alias(alias): + """ + Returns Emoji by alias. + + :param alias: Emoji alias. + :rtype: emojis.db.Emoji + """ + try: + return next(filter(lambda emoji: alias in emoji.aliases, db.EMOJI_DB)) + except StopIteration: + return None + + +def get_emojis_by_tag(tag): + """ + Returns all Emojis from selected tag. + + :param tag: Tag name to filter (case-insensitive). + :rtype: iter + """ + return filter(lambda emoji: tag.lower() in emoji.tags, db.EMOJI_DB) + + +def get_emojis_by_category(category): + """ + Returns all Emojis from selected category. + + :param tag: Category name to filter (case-insensitive). + :rtype: iter + """ + return filter(lambda emoji: category.lower() == emoji.category.lower(), db.EMOJI_DB) + + +def get_tags(): + """ + Returns all tags available. + + :rtype: set + """ + return {tag for emoji in db.EMOJI_DB for tag in emoji.tags} + + +def get_categories(): + """ + Returns all categories available. + + :rtype: set + """ + return {emoji.category for emoji in db.EMOJI_DB} diff -uNr akris-desktop/akris_desktop/emojis/emojis.py akris-desktop-genesis/akris_desktop/emojis/emojis.py --- akris-desktop/akris_desktop/emojis/emojis.py false +++ akris-desktop-genesis/akris_desktop/emojis/emojis.py 569665ff73138f1d5c69bd07e8f445e239550338866cd58c055ae49468d3824f0dd8227ecdaec7e34f8dd138c85996b8c95255c2a25c3704eb1c44dd886d9872 @@ -0,0 +1,84 @@ +import re + +from . import db + +ALIAS_TO_EMOJI = db.get_emoji_aliases() +EMOJI_TO_ALIAS = dict((v, k) for k, v in ALIAS_TO_EMOJI.items()) +EMOJI_TO_ALIAS_SORTED = sorted(ALIAS_TO_EMOJI.values(), key=len, reverse=True) + +RE_TEXT_TO_EMOJI_GROUP = "({0})".format( + "|".join([re.escape(emoji) for emoji in ALIAS_TO_EMOJI]) +) +RE_TEXT_TO_EMOJI = re.compile(RE_TEXT_TO_EMOJI_GROUP) + +RE_EMOJI_TO_TEXT_GROUP = "({0})".format( + "|".join([re.escape(emoji) for emoji in EMOJI_TO_ALIAS_SORTED]) +) +RE_EMOJI_TO_TEXT = re.compile(RE_EMOJI_TO_TEXT_GROUP) + + +def encode(msg): + """ + Encode Emoji aliases into unicode Emoji values. + + :param msg: String to encode. + :rtype: str + + Usage:: + + >>> import emojis + >>> emojis.encode('This is a message with emojis :smile: :snake:') + 'This is a message with emojis ๐Ÿ˜„ ๐Ÿ' + """ + msg = RE_TEXT_TO_EMOJI.sub(lambda match: ALIAS_TO_EMOJI[match.group(0)], msg) + return msg + + +def decode(msg): + """ + Decode unicode Emoji values into Emoji aliases. + + :param msg: String to decode. + :rtype: str + + Usage:: + + >>> import emojis + >>> emojis.decode('This is a message with emojis ๐Ÿ˜„ ๐Ÿ') + 'This is a message with emojis :smile: :snake:' + """ + msg = RE_EMOJI_TO_TEXT.sub(lambda match: EMOJI_TO_ALIAS[match.group(0)], msg) + return msg + + +def get(msg): + """ + Returns unique Emojis in the given string. + + :param msg: String to search for Emojis. + :rtype: set + """ + return {match.group() for match in RE_EMOJI_TO_TEXT.finditer(msg)} + + +def iter(msg): + """ + Iterates over all Emojis found in the message. + + :param msg: String to search for Emojis. + :rtype: iterator + """ + return (match.group() for match in RE_EMOJI_TO_TEXT.finditer(msg)) + + +def count(msg, unique=False): + """ + Returns Emoji count in the given string. + + :param msg: String to search for Emojis. + :param unique: (optional) Boolean, return unique values only. + :rtype: int + """ + if unique: + return len({match.group() for match in RE_EMOJI_TO_TEXT.finditer(msg)}) + return len([match.group() for match in RE_EMOJI_TO_TEXT.finditer(msg)]) diff -uNr akris-desktop/akris_desktop/handle_set_listener.py akris-desktop-genesis/akris_desktop/handle_set_listener.py --- akris-desktop/akris_desktop/handle_set_listener.py false +++ akris-desktop-genesis/akris_desktop/handle_set_listener.py 528d180d6cd5d2b50fe8540c0168a7e1df24f67b4f954906ece29db73ecefc510b12868e17f5d0d08c3502e8d161a74a05362398f2419c24e531e9b6bbf3411c @@ -0,0 +1,10 @@ +class HandleSetListener: + def __init__(self, app): + self.app = app + + def render_message(self, message): + # set handle on init + if message.get("command") in ["console_response"]: + if message.get("name") == "handle": + self.app.handle = message.get("value") + return \ No newline at end of file diff -uNr akris-desktop/akris_desktop/master.py akris-desktop-genesis/akris_desktop/master.py --- akris-desktop/akris_desktop/master.py false +++ akris-desktop-genesis/akris_desktop/master.py be6eee12127412f60c88094d0e6f12cea1c0398fad71f79aaa592ce225ac3a3a014048141179cae65b15b20ec8bd4744fd3976c8dd0b35a79eed49dc4279ba12 @@ -0,0 +1,143 @@ +import datetime +import tkinter as tk +import random +import webbrowser +from functools import partial +from tkinter.font import Font + + +class Master(tk.Frame): + def __init__(self, root, app): + self.app = app + self.chain = app.timeline + self.frame = tk.Frame(root) + self.frame.pack(fill=tk.BOTH, expand=True) + + self.table = tk.Text( + self.frame, wrap=tk.WORD, highlightthickness=0, padx=10, pady=10 + ) + self.table.configure(state="disabled") + self.table.tag_configure("speaker", font=Font(family="Courier New")) + self.table.tag_config("text", font=Font(family="Courier New"), lmargin2=260) + self.table.pack(fill=tk.BOTH, expand=True) + self.table.configure(background="black", foreground="white") + self.table.tag_configure("url", underline=True) + self.table.tag_bind( + "url", "", partial(self._on_link_leftclick, "url") + ) + self.table.tag_bind( + "url", "", (lambda e: self.table.config(cursor="hand2")) + ) + self.table.tag_bind( + "url", "", (lambda e: self.table.config(cursor="xterm")) + ) + self.context_menu = tk.Menu(root, tearoff=0) + self.context_menu.add_command( + label="Copy", command=lambda: self.copy_selected_text() + ) + self.context_menu.bind("", lambda e: self.context_menu.unpost()) + self.table.bind("", self.show_context_menu) + + def insert(self, message, index): + msg_datetime = datetime.datetime.fromtimestamp(message["timestamp"]).strftime( + "%H:%M:%S" + ) + self.table.configure(state="normal") + start = index + message["index"] = str(index) + datetime_str = "[{}] ".format(msg_datetime) + self.table.insert(index, datetime_str) + speaker = f"{message['speaker']:>{15}}" + self.table.tag_configure( + "colorize_" + speaker, foreground=self.colorize(speaker) + ) + self.table.insert( + "{}.{}".format(int(float(index)), len(datetime_str)), + speaker, + ("speaker", "colorize_" + speaker), + ) + body = "{}".format( + ": " + message["body"], + ) + self.table.insert( + "{}.{}".format(int(float(index)), len(datetime_str) + len(speaker)), + "{}{}".format(body, "\n"), + ("text",), + ) + end = self.table.index(tk.INSERT) + self.table.configure(state="disabled") + self.find_and_tag_urls(self.table, start, end) + self.table.see(tk.END) + + def render_message(self, message): + if message["command"] == "broadcast_text": + # self.chain.add_message(message) + # index = self.chain.index(message["message_hash"]) + # self.insert(message, index) + pass + + def colorize(self, speaker): + # Set the seed based on the peer name + random.seed(speaker) + + # Generate random RGB values for lighter colors + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + + # Generate the color code + color_code = "#{:02X}{:02X}{:02X}".format(r, g, b) + + return color_code + + def find_and_tag_urls(self, textwidget: tk.Text, start: str, end: str) -> None: + search_start = start + while True: + match_start = textwidget.search( + r"\mhttps?://[a-z0-9:]", search_start, end, nocase=True, regexp=True + ) + if not match_start: # empty string means not found + break + + url = textwidget.get(match_start, f"{match_start} lineend") + + url = url.split()[0] + url = url.split("'")[0] + url = url.split('"')[0] + url = url.split("`")[0] + + # URL, and URL. URL? URL! (also URL). (also URL.) + url = url.rstrip(".,?!") + if "(" not in url: # urls can contain spaces (e.g. wikipedia) + url = url.rstrip(")") + url = url.rstrip(".,?!") + + # [url][foobar] + if "]" in url: + pos = url.find("]") + if pos < url.find("["): + url = url[:pos] + + match_end = f"{match_start} + {len(url)} chars" + textwidget.tag_add("url", match_start, match_end) + search_start = f"{match_end} + 1 char" + + def _on_link_leftclick(self, tag, event): + # To test this, set up 3 URLs, and try clicking first and last char of middle URL. + # That finds bugs where it finds the wrong URL, or only works in the middle of URL, etc. + tag_range = event.widget.tag_prevrange(tag, "current + 1 char") + assert tag_range + start, end = tag_range + text = event.widget.get(start, end) + webbrowser.open(text) + + def copy_selected_text(self): + # Get the selected text + selected_text = self.table.get("sel.first", "sel.last") + + # Copy the selected text to the clipboard + self.table.clipboard_clear() + self.table.clipboard_append(selected_text) + + def show_context_menu(self, event): + self.context_menu.post(event.x_root, event.y_root) diff -uNr akris-desktop/akris_desktop/message_entry.py akris-desktop-genesis/akris_desktop/message_entry.py --- akris-desktop/akris_desktop/message_entry.py false +++ akris-desktop-genesis/akris_desktop/message_entry.py 0eb990cce77ed1c514e28876f2ba8d629210f29058e10013b664117c49a7da6e3ec35d5cbd3e9571c50b5d1bc3f748e9a8d16a03f21a57908daba490994fd564 @@ -0,0 +1,141 @@ +import re +import tkinter as tk + +from akris_desktop import emojis + + +def bytes_for_char_utf_8(c): + return len(c.encode("utf-8")) + + +def count_bytes(s): + b = 0 + total = 0 + for i, c in enumerate(s): + cs = bytes_for_char_utf_8(c) + total += cs + return total + + +class MessageEntry(tk.Frame): + def __init__(self, root, app): + self.root = root + self.app = app + + # Create a resizable pane with vertical orientation + self.frame = tk.Frame(root) + self.frame.pack(side=tk.BOTTOM, fill=tk.X) + + self.placehodler_text = "Write a message..." + + # Create a ScrolledText widget + self.text = tk.Text( + self.frame, wrap=tk.WORD, height=5, highlightthickness=0, padx=10, pady=10 + ) + self.text.configure( + insertbackground="white", background="#333232", foreground="white" + ) + self.text.pack(side=tk.LEFT, fill=tk.X, expand=True) + + # Add placeholder text + self.text.insert(tk.END, self.placehodler_text) + self.text.config(foreground="gray") + + self.context_menu = tk.Menu(root, tearoff=0) + self.text.bind("", self.show_context_menu) + self.configure_context_menu() + + # Bind events to the widget + self.text.bind("", self.on_entry_click) + self.text.bind("", self.on_focus_out) + self.text.bind("", self.handle_enter) + self.text.bind("", self.autocomplete) + self.text.bind("", self.handle_paste) + + def configure_context_menu(self): + self.context_menu.add_command( + label="Paste", command=lambda: self.handle_context_menu_paste() + ) + self.context_menu.bind("", lambda e: self.context_menu.unpost()) + + def show_context_menu(self, event): + self.context_menu.post(event.x_root, event.y_root) + + def handle_context_menu_paste(self): + content = self.root.clipboard_get() + clean_content = emojis.decode(content) + self.text.insert(tk.INSERT, clean_content) + + def on_entry_click(self, event): + if self.text.get("1.0", "end-1c") == self.placehodler_text: + self.text.delete("1.0", tk.END) + self.text.config(foreground="white") + + def on_focus_out(self, event): + if self.text.get("1.0", "end-1c") == "": + self.text.insert(tk.END, self.placehodler_text) + self.text.config(foreground="#ECECEC") + + def ignore_enter(self, event): + return "break" + + def handle_enter(self, event): + if event.state == 0: + # Handle Enter key (send message) + self.send_message() + elif event.state == 1: + self.insert_new_line() + return "break" + + def insert_new_line(self): + # Insert a new line character at the current cursor position + self.text.insert(tk.INSERT, "\n") + + def autocomplete(self, event) -> None: + cursor_pos = self.text.index("insert") + match = re.fullmatch( + r"(.*\s)?([^\s:]+):? ?", self.text.get("0.0", self.text.index(tk.INSERT)) + ) + if match is None: + return "break" + preceding_text, last_word = match.groups() # preceding_text can be None + + nicks = self.app.broadcast_tab.peers.get_peers() + if last_word in nicks: + completion = nicks[(nicks.index(last_word) + 1) % len(nicks)] + else: + try: + completion = next( + username + for username in nicks + if username.lower().startswith(last_word.lower()) + ) + except StopIteration: + return "break" + + if preceding_text: + new_text = preceding_text + completion + " " + else: + new_text = completion + ": " + self.text.delete("0.0", cursor_pos) + self.text.insert("0.0", new_text) + self.text.mark_set("insert", str(float(len(new_text)))) + return "break" + + def handle_paste(self, event): + content = self.root.clipboard_get() + clean_content = emojis.decode(content) + self.text.insert(tk.INSERT, clean_content) + return "break" + + def encode_action(self, message): + pattern = r"\x01ACTION (.*?)\x01" + if message.startswith("/me"): + if len(message.encode("utf-8")) <= 325: + pattern = r"^/me(.*)" + match = re.search(pattern, message) + if match: + predicate = match.group(1) + action_message = re.sub(pattern, f"\x01ACTION{predicate}\x01", message, count=1) + return action_message + return message \ No newline at end of file diff -uNr akris-desktop/akris_desktop/message_stats_listener.py akris-desktop-genesis/akris_desktop/message_stats_listener.py --- akris-desktop/akris_desktop/message_stats_listener.py false +++ akris-desktop-genesis/akris_desktop/message_stats_listener.py b61441a0f8266e4081d12729181de618255d7652792b4a52065dec38a87115c80cd4b66d4663e66c208530e8ff0448ae76972d37e112727bac768ec12f428e7f @@ -0,0 +1,15 @@ +class MessageStatsListener: + def __init__(self, app): + self.app = app + + def render_message(self, message): + if message.get("command") in ["console_response"]: + if message.get("type") == "message_stats": + self.app.message_stats = { + "latest_broadcast_message_timestamp": message.get( + "latest_broadcast_message_timestamp" + ), + "direct_message_timestamps": message.get( + "direct_message_timestamps" + ), + } diff -uNr akris-desktop/akris_desktop/new_handle_listener.py akris-desktop-genesis/akris_desktop/new_handle_listener.py --- akris-desktop/akris_desktop/new_handle_listener.py false +++ akris-desktop-genesis/akris_desktop/new_handle_listener.py eb94a15519e3b20aa1a67c5f7ef9aa3c24ebeb14708af4a7c74855caa718ebbf74dd2baaa795a8c81555109e3bff491bd4ab65275eee84c04a8a61cd5084d3a1 @@ -0,0 +1,15 @@ +class NewHandleListener: + def __init__(self, app): + self.app = app + + def render_message(self, message): + if message.get("ref_link_page_response"): + return + + if message["command"] in ["direct_text", "direct_text_m"]: + handle = message.get("handle") + if handle in self.app.broadcast_tab.peers.get_peers(): + if handle not in [ + self.app.notebook.tab(t, "text") for t in self.app.direct_tabs + ]: + self.app.add_direct_message_tab(handle) diff -uNr akris-desktop/akris_desktop/new_ref_link_listener.py akris-desktop-genesis/akris_desktop/new_ref_link_listener.py --- akris-desktop/akris_desktop/new_ref_link_listener.py false +++ akris-desktop-genesis/akris_desktop/new_ref_link_listener.py 02a679bc9c7b2cba1e1e84310ae84226739a469ae43753bc7e0a70b211329e8de2389523bad2fa8d99b8b720d5361dd19029f1aa38da718cece2ef6f678744ce @@ -0,0 +1,12 @@ +class NewRefLinkListener: + def __init__(self, app): + self.app = app + + def render_message(self, message): + if not message.get("ref_link_page_response"): + return + + if message.get("ref_link_hash") not in [ + self.app.notebook.tab(t, "text") for t in self.app.ref_link_tabs + ]: + self.app.add_ref_link_tab(message.get("ref_link_hash")) diff -uNr akris-desktop/akris_desktop/notifications.py akris-desktop-genesis/akris_desktop/notifications.py --- akris-desktop/akris_desktop/notifications.py false +++ akris-desktop-genesis/akris_desktop/notifications.py a1149d80f0fee84bfc9f44db33e552e910b86a9389de1591665566e4d3e740e903378c318d0d3ad3311a45707d72d8a0e127227b4cac22e41dee501a5fbdc289 @@ -0,0 +1,27 @@ +import tkinter as tk + + +class Notifications(tk.Frame): + def __init__(self, root, app, timeline_view): + super().__init__(root, bg="black", borderwidth=0, height=30, pady=10, padx=0) + self.app = app + self.root = root + self.timeline_view = timeline_view + self.load_more_button = None + + def display_load_previous(self): + if not self.load_more_button: + self.load_more_button = tk.Button( + self, + text="more...", + command=self.timeline_view.load_previous, + height=1, + pady=0, + padx=0, + ) + self.load_more_button.pack(side=tk.RIGHT) + + def clear(self): + if self.load_more_button: + self.load_more_button.destroy() + self.load_more_button = None diff -uNr akris-desktop/akris_desktop/page_monitor.py akris-desktop-genesis/akris_desktop/page_monitor.py --- akris-desktop/akris_desktop/page_monitor.py false +++ akris-desktop-genesis/akris_desktop/page_monitor.py e596821b914fddff66a32548bfd362d237eb71dd13d565747e1e87a31a2347f0af025578bc3b3cf876a0e2216400420d44e1ee8f904cfa5260fee6520554af89 @@ -0,0 +1,22 @@ +import datetime +import time + + +class PageMonitor: + def __init__(self): + self.days_before = 1 + + def in_window(self, message): + now = time.time() + ts = message.get("timestamp") + if ( + ts + >= ( + datetime.datetime.fromtimestamp(now) + - datetime.timedelta(days=self.days_before) + ).timestamp() + ): + return True + + def button_label(self): + return f"ใ€Š prev {self.days_before + 1} days" diff -uNr akris-desktop/akris_desktop/peers.py akris-desktop-genesis/akris_desktop/peers.py --- akris-desktop/akris_desktop/peers.py false +++ akris-desktop-genesis/akris_desktop/peers.py 89817132b4ec2e6c58a6b2438a60b3399d069ab1f97412b7851d719aa27c3bed4ca40ac3bde3877ab929c694e4aeafe3aee7e51a73bb8080e5e663fab65a55ca @@ -0,0 +1,83 @@ +import tkinter as tk + + +class Peers(tk.Frame): + def __init__(self, root, app): + self.app = app + self.frame = tk.Frame( + root, borderwidth=0, highlightthickness=0, width=200, background="black" + ) + self.frame.pack(side=tk.RIGHT, fill=tk.Y) + + # peer list + self.peer_list = tk.Text( + self.frame, highlightthickness=0, borderwidth=0, width=20 + ) + self.peer_list.configure(background="black", foreground="white") + self.peer_list.pack(fill=tk.BOTH, side=tk.BOTTOM, expand=True, padx=10, pady=10) + self.peer_list.config(state="disabled") + self.peer_list.config(cursor="arrow") + self.peer_list.tag_config("offline", font=("", "12", "italic")) + self.peer_list.tag_config("online", font=("", "12")) + self.peer_list.tag_bind("clickable", "", self.on_peer_click) + self.peer_list.tag_bind( + "clickable", "", (lambda e: self.peer_list.config(cursor="hand2")) + ) + self.peer_list.tag_bind( + "clickable", "", (lambda e: self.peer_list.config(cursor="xterm")) + ) + + def on_peer_click(self, event): + # Get the index of the mouse pointer + index = self.peer_list.index("@%s,%s" % (event.x, event.y)) + + # Get the line number + line_number = index.split(".")[0] + + # Get the line text + handle = self.peer_list.get(f"{line_number}.0", f"{line_number}.end") + print("clicked on peer: %s" % handle) + self.app.open_direct_message_tab(handle) + + def render_message(self, message): + self.peer_list.config(state="normal") + if message["command"] == "presence": + handle = message["handle"] + if not handle in self.peer_list.get("1.0", tk.END).splitlines(): + self.peer_list.insert(tk.END, handle + "\n") + self.tag_peer(handle, "clickable") + if message["type"] == "online": + self.tag_peer(handle, "online") + elif message["type"] == "offline": + self.tag_peer(handle, "offline") + else: + self.remove_peer(handle) + self.peer_list.config(state="disabled") + + def tag_peer(self, handle, tag): + peer_index = self.get_peer_index(handle) + if peer_index: + if tag == "offline": + self.peer_list.tag_remove("online", peer_index, peer_index + "+1l") + self.peer_list.tag_add("offline", peer_index, peer_index + "+1l") + elif tag == "online": + self.peer_list.tag_remove("offline", peer_index, peer_index + "+1l") + self.peer_list.tag_add("online", peer_index, peer_index + "+1l") + else: + self.peer_list.tag_add(tag, peer_index, peer_index + "+1l") + + def get_peer_index(self, handle): + for i, peer in enumerate(self.get_peers()): + if peer == handle: + return str(float(i) + 1.0) + return None + + def get_peers(self): + peers = self.peer_list.get("1.0", tk.END).splitlines() + if len(peers) > 0: + return peers[:-1] + return [] + + def remove_peer(self, handle): + peer_index = self.get_peer_index(handle) + self.peer_list.delete(peer_index, peer_index + "+1l") diff -uNr akris-desktop/akris_desktop/ref_link_tab.py akris-desktop-genesis/akris_desktop/ref_link_tab.py --- akris-desktop/akris_desktop/ref_link_tab.py false +++ akris-desktop-genesis/akris_desktop/ref_link_tab.py 3354a71e4355a4fea3d53c7e02c7574d2f5fe8b477c383bef3c69e7ae6be36335a870e53835321e35a29da661ec5d95ff77de64b1abb4a8cc50bc1c38e01a622 @@ -0,0 +1,33 @@ +import tkinter as tk + +from .ref_link_view import RefLinkView + + +class RefLinkTab(tk.Frame): + def __init__(self, root, message_hash, app): + super().__init__(root) + self.toolbar_frame = tk.Frame(self, background="#333232") + self.close_link = tk.Label( + self.toolbar_frame, + text="๐Ÿ—™", + fg="white", + cursor="hand2", + background="red", + ) + self.close_link.pack(side=tk.RIGHT) + self.close_link.bind("", lambda e: self.app.close_ref_link_tab(self)) + # self.close_button = ttk.Button(self.toolbar_frame, text="close", command=lambda: self.app.close_ref_link_tab(self)) + # self.close_button.pack(side=tk.RIGHT) + self.toolbar_frame.pack(side=tk.TOP, fill=tk.X) + self.message_hash = message_hash + self.app = app + self.messages_frame = tk.Frame(self) + self.messages_frame.pack(fill=tk.BOTH, expand=True) + self.message_table = RefLinkView(self.messages_frame, app, message_hash, self) + self.app.register_message_listener(self.message_table) + + def title(self): + title = self.message_table.title + if not title: + return self.message_hash[0:15] + return title diff -uNr akris-desktop/akris_desktop/ref_link_view.py akris-desktop-genesis/akris_desktop/ref_link_view.py --- akris-desktop/akris_desktop/ref_link_view.py false +++ akris-desktop-genesis/akris_desktop/ref_link_view.py 359835d56716be6123a2520dd839661aa0d801de6a5e2d693aa810c0ea9afb3263ad860bd55c1779d562180966f10d50bbf2e8990b3a218b48bca6b800edf9e2 @@ -0,0 +1,60 @@ +from .timeline_view import TimelineView, MultipartMessage + + +class RefLinkView(TimelineView): + def __init__(self, root, app, message_hash, tab): + super().__init__(root, app) + self.tab = tab + self.title = None + self.message_hash = message_hash + self.text.tag_configure("reflink_highlight", foreground="black", background="#FAF182") + + def render_message(self, message): + super().render_message(message) + + if not message.get("ref_link_page_response"): + return + + if self.message_hash != message.get("ref_link_hash"): + return + + if message.get("command") == "broadcast_text_m": + self.filter[message.get("message_hash")] = True + if not message.get("text_hash") in self.multipart_staging: + self.multipart_staging[message.get("text_hash")] = MultipartMessage( + message + ) + else: + self.multipart_staging[message.get("text_hash")].add_part(message) + + multipart_message = self.multipart_staging[message.get("text_hash")] + if multipart_message.is_complete(): + message["body"] = multipart_message.assembled_body() + del self.multipart_staging[message.get("text_hash")] + else: + return + + # set the title from the linked message + if message.get("message_hash") == self.message_hash: + self.title = message.get("body")[0:15] + self.app.notebook.tab(self.tab, text=self.title) + + if self.is_hearsay(message): + message["hearsay"] = True + + self.filter[message.get("message_hash")] = True + self.add_message(message) + + if message.get("end"): + self.text.see(f"{self.highlighed_line_index}.0") + + def highlight_line(self, line_index): + self.text.tag_add("reflink_highlight", f"{line_index}.0", f"{line_index + 1}.0") + self.text.see(f"{line_index}.0") + + def add_message(self, message): + super().add_message(message) + if message.get("message_hash") == self.message_hash: + line_index = self.timeline.calculate_message_index(message) + self.highlighed_line_index = line_index + self.highlight_line(line_index) diff -uNr akris-desktop/akris_desktop/text_utils.py akris-desktop-genesis/akris_desktop/text_utils.py --- akris-desktop/akris_desktop/text_utils.py false +++ akris-desktop-genesis/akris_desktop/text_utils.py 59ca735d7fc3e18e85231bc39be13ce770ebdb7acf266421695b2c5dab5dff549245c73b1835268705b1dcc53a913dd352cb27f358077d45fadeb77628faebac @@ -0,0 +1,16 @@ +import random + + +def colorize(speaker): + # Set the seed based on the peer name + random.seed(speaker) + + # Generate random RGB values for lighter colors + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + + # Generate the color code + color_code = "#{:02X}{:02X}{:02X}".format(r, g, b) + + return color_code diff -uNr akris-desktop/akris_desktop/timeline.py akris-desktop-genesis/akris_desktop/timeline.py --- akris-desktop/akris_desktop/timeline.py false +++ akris-desktop-genesis/akris_desktop/timeline.py a0867d0fc7811b6eae8fbbbd597b76d11525097329cf9038729d9790b63d3f6b3f892eef193f7e06be06711eaa213a88db683eb646ebbcc55d4d7ff3e410b72c @@ -0,0 +1,184 @@ +import bisect +from akris.log_config import LogConfig + +logger = LogConfig.get_instance().get_logger("akris.timeline") + +BEFORE = 0 +AFTER = 1 + + +class Annotation: + def __init__(self, timestamp, content, position=AFTER): + self.content = content + self.timestamp = timestamp + self.position = position + + +class TimestampGroup: + def __init__(self): + self.messages = [] + + def add_message(self, new_message): + # sort results with identical timestamps by message hash + # this is kind of a heuristic because messages with identical timestamps are not + # guaranteed to be descendents or antecedents of one another + added = False + for existing_message in self.messages: + # if new_message is a descendent of message + if existing_message.get("message_hash") in [ + new_message.get("net_chain"), + new_message.get("self_chain"), + ]: + self.messages.append(new_message) + added = True + break + + # new_message is an antecedent of message + if new_message["message_hash"] in [ + existing_message.get("net_chain"), + existing_message.get("self_chain"), + ]: + self.messages.insert(self.messages.index(existing_message), new_message) + added = True + break + if not added: + self.messages.append(new_message) + + def count(self): + message_count = len(self.messages) + newline_count = 0 + multiline_header_count = 0 + for message in self.messages: + newline_count += message.get("body").count("\n") + + # we must account for the multiline message headers as well + for message in self.messages: + if message.get("body").count("\n") > 0: + multiline_header_count += 1 + + return message_count + newline_count + multiline_header_count + + def newlines_before(self, message): + message_index = self.messages.index(message) + newline_count = 0 + for message in self.messages[:message_index]: + newline_count += message.get("body").count("\n") + return newline_count + + +class Timeline: + def __init__(self): + self.ordered_timestamps = [] + self.annotations = {} + self.timestamp_groups = {} + + def get_line_count(self, message): + if "\n" in message.get("body"): + return 0 + + # need to add one for the header + return message.get("body").count("\n") + 1 + + def calculate_message_index(self, message): + ts = message.get("timestamp") + tsg = self.get_timestamp_group(ts) + + index = 0 + index += self.antecedent_timestamp_group_count(ts) + index += tsg.messages.index(message) + index += tsg.newlines_before(message) + index += self.count_annotations_before_ts_group(ts) + + # Text indexing starts at 1 + index += 1 + return index + + def calculate_annotation_index(self, annotation): + ts = annotation.timestamp + index = 0 + index += self.antecedent_timestamp_group_count(ts) + index += self.count_earlier_annotations(ts) + if annotation.position == AFTER: + index += self.get_timestamp_group(ts).count() + + # Text indexing starts at 1 + index += 1 + return index + + def antecedent_timestamp_group_count(self, timestamp): + count = 0 + for ts in self.timestamp_groups: + if ts < timestamp: + count += self.timestamp_groups[ts].count() + return count + + def get_antecedent_timestamp(self, timestamp): + previous_ts = None + for ts in self.ordered_timestamps: + if ts < timestamp: + previous_ts = ts + return previous_ts + + def get_decedent_timestamp(self, timestamp): + decedent_ts = None + for ts in reversed(self.ordered_timestamps): + if ts > timestamp: + decedent_ts = ts + return decedent_ts + + def add_message(self, message): + ts = message["timestamp"] + if ts not in self.ordered_timestamps: + bisect.insort(self.ordered_timestamps, ts) + tsg = self.get_timestamp_group(ts) + tsg.add_message(message) + return self.calculate_message_index(message) + + def add_annotation(self, annotation): + self.annotations[annotation.timestamp] = annotation + return self.calculate_annotation_index(annotation) + + def remove_annotation(self, annotation): + annotations_copy = self.annotations.copy() + for ts in annotations_copy: + if annotations_copy[ts].content == annotation.content: + del self.annotations[ts] + + def annotated(self, timestamp): + return self.annotations.get(timestamp) + + def get_annotation(self, content): + for ts in self.annotations: + if self.annotations[ts].content == content: + return self.annotations[ts] + + def get_first_timestamp(self): + return self.ordered_timestamps[0] + + def get_last_timestamp(self): + return self.ordered_timestamps[-1] + + def get_timestamp_group(self, timestamp): + if not self.timestamp_groups.get(timestamp): + tsg = TimestampGroup() + self.timestamp_groups[timestamp] = tsg + return tsg + else: + return self.timestamp_groups[timestamp] + + def count_earlier_annotations(self, timestamp): + count = 0 + for ts in self.annotations: + if ts < timestamp: + count += 1 + return count + + def count_annotations_before_ts_group(self, timestamp): + count = 0 + for ts in self.annotations: + if ts < timestamp: + count += 1 + if self.annotations.get(timestamp): + if self.annotations[timestamp].position == BEFORE: + count += 1 + return count diff -uNr akris-desktop/akris_desktop/timeline_view.py akris-desktop-genesis/akris_desktop/timeline_view.py --- akris-desktop/akris_desktop/timeline_view.py false +++ akris-desktop-genesis/akris_desktop/timeline_view.py 4e09ba25c60a8b22ad1204b33c0f195ec004fd8fd2062ea1ec32c73b5c3ea7a63fc6aaf4fe3629bb51d53d06846e54d19ea69140f350018c8cf038c079b4ff4f @@ -0,0 +1,666 @@ +import datetime +import re +import time +import tkinter as tk +from tkinter import ttk +import webbrowser +from functools import partial +from tkinter.font import Font +from akris.log_config import LogConfig + +from .page_monitor import PageMonitor +from .text_utils import colorize +from .timeline import Timeline, Annotation, BEFORE + +logger = LogConfig.get_instance().get_logger("akris_desktop.timeline_view") + + +# "unicode surrogates" seen in at least one message from phf - origin and purpose +# as yet unclear, but can't be rendered by the Text +def remove_unicode_surrogates(json_string): + return re.sub( + r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF\u2600-\u26FF\u2700-\u27BF\U000024C2-\U0001F251]", + "X", + json_string, + ) + + +def indexize(index): + return str(float(index)) + + +def is_new_day(timestamp1, timestamp2): + # Convert the timestamps to datetime objects + dt1 = datetime.datetime.fromtimestamp(timestamp1) + dt2 = datetime.datetime.fromtimestamp(timestamp2) + + # Return True if the dates are different, False otherwise + return dt1.date() != dt2.date() + + +class DuplicateMessageException(Exception): + pass + + +class CustomText(tk.Text): + def __init__(self, *args, scroll_handler=None, **kwargs): + self.scroll_handler = scroll_handler + super().__init__(*args, **kwargs) + + def yview(self, *args): + self.scroll_handler(*args) + return super().yview(*args) + + +class MultipartMessage: + def __init__(self, message): + self.parts = [message] + self.of = message.get("of") + + def add_part(self, message): + for part in self.parts: + if message.get("n") < part.get("n"): + self.parts.insert(self.parts.index(part), message) + return + self.parts.append(message) + + def is_complete(self): + return len(self.parts) == self.of + + def assembled_body(self): + body = "" + for part in self.parts: + body += part.get("body") + return body + + +class TimelineView(tk.Frame): + def __init__(self, root, app): + super().__init__(root) + self.message_at_index = {} + self.root = root + self.app = app + self.timeline = Timeline() + self.frame = tk.Frame(root) + self.frame.pack(fill=tk.BOTH, expand=True) + self.text = CustomText( + self.frame, + wrap=tk.WORD, + highlightthickness=0, + padx=10, + pady=10, + scroll_handler=self.handle_scroll, + borderwidth=0, + ) + self.configure_scrollbar_style() + self.scrollbar = ttk.Scrollbar(self.frame) + self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Associate the Scrollbar with the Text widget + self.text.config(yscrollcommand=self.scrollbar.set) + self.scrollbar.config(command=self.text.yview) + + self.text.configure(state="disabled") + self.text.pack(fill=tk.BOTH, expand=True) + self.text.configure( + background="black", foreground="white", font=("Courier New", 10) + ) + self.context_menu = tk.Menu(root, tearoff=0) + self.configure_context_menu() + + # tooltip display + self.tooltip_label = None + + # configuration + self.configure_tags() + self.configure_bindings() + + self.character_insertion_index = 0 + + # we need to keep track of this in order to determine when + # to move to the end after receiving a new non page_response + # message + self.hit_bottom = True + self.hit_top = False + + # paging + self.filter = {} + self.should_refresh_window = True + self.page_monitor = PageMonitor() + + # for assembly of multipart messages + self.multipart_staging = {} + + def show_tooltip(self, text): + if self.tooltip_label: + self.hide_tooltip() + self.tooltip_label = tk.Label(self.root, fg="black", background="white", borderwidth=1, height=1, padx=0, pady=0) + self.tooltip_label.place(x=0, y=0) + self.tooltip_label.config(text=text, width=len(text)) + + def hide_tooltip(self): + if self.tooltip_label: + self.tooltip_label.place_forget() + self.tooltip_label.destroy() + self.tooltip_label = None + + def await_message_stats(self): + while not self.app.message_stats: + self.root.after(100, self.await_message_stats) + return + self.page_monitor.days_before = self.minimum_days_before() + self.tab.more_link.config(text=self.page_monitor.button_label()) + self.start_refresh_loop() + + def minimum_days_before(self): + latest_ts = self.app.message_stats.get("latest_broadcast_message_timestamp") + if not latest_ts: + return 1 + latest_message_date = datetime.datetime.fromtimestamp(latest_ts) + now = datetime.datetime.now() + return (now - latest_message_date).days + 1 + + def start_refresh_loop(self): + if self.should_refresh_window: + self.should_refresh_window = False + self.load_page("page_up") + + self.root.after(500, self.start_refresh_loop) + + def render_message(self, message): + if self.filter.get(message.get("message_hash")): + raise DuplicateMessageException() + + def load_previous(self): + self.hit_top = False + self.page_monitor.days_before += 1 + self.tab.more_link.config(text=self.page_monitor.button_label()) + self.should_refresh_window = True + + def load_page(self, command): + if command == "page_up": + # Get the line number of the first visible line + self.app.api_client.send_command( + { + "command": command, + "args": [time.time(), self.page_monitor.days_before], + } + ) + + def configure_scrollbar_style(self): + style = ttk.Style() + style.theme_use("default") + style.configure( + "TScrollbar", + foreground="darkgray", + troughcolor="black", + background="darkgray", + arrowcolor="white", + bordercolor="black", + ) + + def configure_tags(self): + lmargin = tk.font.Font(family="Courier New", size=11).measure( + "[08:05:35] asciilifeform | " + ) + self.text.tag_configure( + "speaker", font=Font(family="Courier New", weight="bold") + ) + self.text.tag_configure( + "text", font=Font(family="Courier New"), lmargin2=lmargin + ) + self.text.tag_configure( + "highlight_speaker", foreground="#E3ED1C", background="#D60D93" + ) + self.text.tag_configure("ref_link", underline=True) + self.text.tag_configure("url", underline=True) + self.text.tag_configure("multiline_header", font=("", "11", "bold italic")) + self.text.tag_configure("date_break", foreground="#1CEAED") + self.text.tag_configure("timestamp", foreground="#B9BFC0") + self.text.tag_configure("highlight", background="#D60D93") + self.text.tag_configure("hearsay", foreground="#F6D12D") + + def configure_bindings(self): + self.text.tag_bind( + "ref_link", "", partial(self.on_ref_link_leftclick, "ref_link") + ) + self.text.tag_bind( + "ref_link", "", (lambda e: self.text.config(cursor="hand2")) + ) + self.text.tag_bind( + "ref_link", "", (lambda e: self.text.config(cursor="xterm")) + ) + self.text.tag_bind("url", "", partial(self.on_link_leftclick, "url")) + self.text.tag_bind( + "url", "", (lambda e: self.text.config(cursor="hand2")) + ) + self.text.tag_bind( + "url", "", (lambda e: self.text.config(cursor="xterm")) + ) + self.text.tag_bind( + "timestamp", "", (lambda e: self.text.config(cursor="hand2")) + ) + self.text.tag_bind( + "timestamp", "", (lambda e: self.text.config(cursor="xterm")) + ) + self.text.tag_bind( + "multiline_header", "", (lambda e: self.text.config(cursor="hand2")) + ) + self.text.tag_bind( + "multiline_header", "", (lambda e: self.text.config(cursor="xterm")) + ) + self.text.tag_bind("timestamp", "", (lambda e: self.copy_ref_link(e))) + self.text.tag_bind("multiline_header", "", (lambda e: self.copy_ref_link(e))) + self.text.bind("", self.handle_page_up) + + def configure_context_menu(self): + self.context_menu.add_command( + label="Copy", command=lambda: self.copy_selected_text() + ) + self.context_menu.bind("", lambda e: self.context_menu.unpost()) + self.text.bind("", self.show_context_menu) + + def copy_ref_link(self, event): + index = int(event.widget.index(f"@{event.x},{event.y}").split(".")[0]) + self.root.clipboard_clear() + line = self.text.get(f"{index}.0", f"{index}.end") + logger.info(line) + message = self.message_at_index.get(index) + self.root.clipboard_append(f"pest://{message.get('message_hash')}") + + def handle_scroll(*args): + self = args[0] + if len(args) == 3: + if args[1] == "moveto": + pos = self.text.yview() + if pos[0] == 0.0: + if not self.hit_top: + self.hit_top = True + if pos[1] == 1.0: + if not self.hit_bottom: + self.hit_bottom = True + self.hit_top = False + else: + self.hit_bottom = False + + def get_insertion_index(self, line_index): + return "{}.{}".format(line_index, self.character_insertion_index) + + def insert_text(self, text, line_index, tags=()): + self.inserting_text = True + self.text.configure(state="normal") + self.text.insert( + self.get_insertion_index(line_index), + text, + tags, + ) + self.text.configure(state="disabled") + self.character_insertion_index += len(text) + + def not_inserting(): + self.inserting_text = False + + self.root.after(500, not_inserting) + + def render_timestamp(self, timestamp, line_index, message): + message_info_tag = f"message-info-{message.get('message_hash')}" + tags = ["timestamp", message_info_tag] + if message.get("hearsay"): + tags.append("hearsay") + msg_datetime = datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") + self.insert_text("[{}] ".format(msg_datetime), line_index, tags) + self.bind_reporting_peers_tooltip(message_info_tag, message) + + def bind_reporting_peers_tooltip(self, message_info_tag, message): + reporting_peers = message.get("reporting_peers") + if reporting_peers: + formatted_reporting_peers = ", ".join(reporting_peers) + self.text.tag_bind(message_info_tag, "", partial(self.on_timestamp_enter, formatted_reporting_peers)) + self.text.tag_bind(message_info_tag, "", self.on_timestamp_leave) + + def render_speaker(self, message, is_action, line_index): + if is_action: + speaker = "*" + else: + speaker = message.get("speaker") + body = message.get("body") + tags = ["speaker"] + justified_speaker = f"{speaker:>{15}}" + if not is_action: + self.text.tag_configure("colorize_" + speaker, foreground=colorize(speaker)) + if self.app.handle in body and not self.app.handle == message.get("speaker"): + tags.append("highlight_speaker") + else: + tags.append("colorize_" + speaker) + self.insert_text( + justified_speaker, + line_index, + tags, + ) + + def render_divider(self, line_index): + tags = [] + self.insert_text( + " | ", + line_index, + tags, + ) + + def render_body(self, message, line_index): + formatted_body = remove_unicode_surrogates(message.get("body")) + self.insert_text( + f"{formatted_body}\n", + line_index, + ("text",), + ) + + def extract_action_message(self, speaker, body): + pattern = r"\x01ACTION (.*?)\x01" + match = re.search(pattern, body) + if match: + return f"{speaker} {match.group(1)}" + else: + return body + + def render_annotation(self, annotation, line_index): + self.character_insertion_index = 0 + self.insert_text(f"{annotation.content}\n", line_index, ("text", "date_break")) + + def render_multiline_header(self, message, line_index): + self.character_insertion_index = 0 + ts = message.get("timestamp") + msg_date = datetime.datetime.fromtimestamp(ts).strftime("%m/%d") + msg_time = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") + + message_info_tag = f"message-info-{message.get('message_hash')}" + tags = ["text", "multiline_header", message_info_tag] + if message.get("hearsay"): + tags.append("hearsay") + self.insert_text( + f"On {msg_date} at {msg_time} {message.get('speaker')} wrote:\n", + line_index, + tags, + ) + reporting_peers = message.get("reporting_peers") + if reporting_peers: + formatted_reporting_peers = ", ".join(reporting_peers) + self.text.tag_bind(message_info_tag, "", partial(self.on_timestamp_enter, formatted_reporting_peers)) + self.text.tag_bind(message_info_tag, "", self.on_timestamp_leave) + + def render_multiline(self, message, line_index): + start = indexize(line_index + 1) + body = message.get("body") + lines = body.split("\n") + for i, line in enumerate(lines): + self.character_insertion_index = 0 + url_info = self.replace_bracket_pairs(line_index + i + 1, line) + line_body = url_info.get("body") + self.insert_text(f"{line_body}\n", line_index + i + 1, ("text",)) + self.tag_bracket_pairs(url_info) + end = self.text.index(self.get_insertion_index(line_index + len(lines) + 1)) + self.find_and_tag_ref_links(self.text, start, end) + self.find_and_tag_urls(self.text, start, end) + + def render_line(self, message, line_index): + # initialize character insertion index + self.character_insertion_index = 0 + is_action = False + start = indexize(line_index) + if "\x01" in message.get("body"): + message["body"] = self.extract_action_message( + message.get("speaker"), message.get("body") + ) + is_action = True + self.render_timestamp(message.get("timestamp"), line_index, message) + self.render_speaker(message, is_action, line_index) + self.render_divider(line_index) + url_info = self.replace_bracket_pairs(line_index, message.get("body")) + message["body"] = url_info.get("body") + self.render_body(message, line_index) + self.tag_bracket_pairs(url_info) + + end = self.text.index(self.get_insertion_index(line_index)) + + self.find_and_tag_ref_links(self.text, start, end) + self.find_and_tag_urls(self.text, start, end) + self.reposition(message) + + def add_message(self, message): + self.add_date_change(message.get("timestamp")) + index = self.timeline.add_message(message) + self.message_at_index[index] = message + if "\n" in message.get("body"): + self.render_multiline_header(message, index) + self.render_multiline(message, index) + else: + self.render_line(message, index) + + def remove_message(self, message): + logger.info("*********************** remove_message()") + logger.info(message) + index = self.timeline.calculate_message_index(message) + line_count = self.timeline.get_line_count(message) + self.text.configure(state="normal") + self.text.delete( + indexize(index), + indexize(index + line_count), + ) + self.text.configure(state="disabled") + + def add_date_change(self, timestamp): + antecedent_timestamp = self.timeline.get_antecedent_timestamp(timestamp) + decedent_timestamp = self.timeline.get_decedent_timestamp(timestamp) + if antecedent_timestamp: + if is_new_day(antecedent_timestamp, timestamp): + self.insert_date_change(timestamp) + if decedent_timestamp: + if is_new_day(timestamp, decedent_timestamp): + self.insert_date_change(decedent_timestamp) + + def insert_date_change(self, timestamp): + if not self.timeline.annotated(timestamp): + date = datetime.datetime.fromtimestamp(timestamp).strftime("%A, %B %d, %Y") + annotation = Annotation(timestamp, f"--- {date} ---", BEFORE) + self.remove_existing_annotation(annotation) + annotation_index = self.timeline.add_annotation(annotation) + self.render_annotation(annotation, annotation_index) + + def remove_existing_annotation(self, annotation): + existing_annotation = self.timeline.get_annotation(annotation.content) + if existing_annotation: + existing_annotation_index = self.timeline.calculate_annotation_index( + existing_annotation + ) + self.delete_line(existing_annotation_index) + self.timeline.remove_annotation(existing_annotation) + + def delete_line(self, line_index): + self.text.configure(state="normal") + start = f"{line_index}.0" + end = f"{line_index + 1}.0" + self.text.delete(start, end) + self.text.configure(state="disabled") + + # handle positioning after receiving page responses + def reposition(self, message): + if not self.hit_bottom: + return + + if self.hit_top: + if not message.get("end"): + return + + self.text.see("end") + + def find_and_tag_ref_links(self, textwidget: tk.Text, start: str, end: str) -> None: + search_start = start + while True: + match_start = textwidget.search( + r"\mpest://[a-zA-Z0-9=/+]", search_start, end, nocase=True, regexp=True + ) + if not match_start: + break + + url = textwidget.get(match_start, f"{match_start} lineend") + + url = url.split()[0] + url = url.split("'")[0] + url = url.split('"')[0] + url = url.split("`")[0] + + # URL, and URL. URL? URL! (also URL). (also URL.) + url = url.rstrip(".,?!") + if "(" not in url: # urls can contain spaces (e.g. wikipedia) + url = url.rstrip(")") + url = url.rstrip(".,?!") + + # [url][foobar] + if "]" in url: + pos = url.find("]") + if pos < url.find("["): + url = url[:pos] + + match_end = f"{match_start} + {len(url)} chars" + textwidget.tag_add("ref_link", match_start, match_end) + search_start = f"{match_end} + 1 char" + + def replace_bracket_pairs(self, line_index, body): + positions = [] + + def url_replacer(m): + url = m.group(1) + text = m.group(2) + + positions.append( + { + "url": url, + "match_start": f"{line_index}.{self.character_insertion_index + match.start()}", + "match_end": f"{line_index}.{self.character_insertion_index + match.start() + len(text)}", + "tag_name": f"url-{url}", + } + ) + return text + + pattern = r'\[([^\[\]]+?)\]\[([^\[\]]+?)\]' + match = re.search(pattern, body) + while (match): + body = re.sub(pattern, url_replacer, body, count=1) + match = re.search(pattern, body) + return { + "body": body, + "positions": positions + } + + def tag_bracket_pairs(self, url_info): + positions = url_info.get("positions") + for position in positions: + self.text.tag_add("url", position.get("match_start"), position.get("match_end")) + self.text.tag_add(position.get("tag_name"), position.get("match_start"), position.get("match_end")) + if position.get("url").startswith("pest://"): + self.text.tag_bind(position.get("tag_name"), "", + partial(self.on_named_ref_link_click, position.get("url"))) + else: + self.text.tag_bind(position.get("tag_name"), "", + partial(self.on_named_link_click, position.get("url"))) + self.text.tag_bind(position.get("tag_name"), "", partial(self.on_link_enter, position.get("url"))) + self.text.tag_bind(position.get("tag_name"), "", self.on_link_leave) + + def find_and_tag_urls(self, textwidget: tk.Text, start: str, end: str) -> None: + search_start = start + while True: + match_start = textwidget.search( + r"\mhttps?://[a-z0-9:]", search_start, end, nocase=True, regexp=True + ) + if not match_start: # empty string means not found + break + + url = textwidget.get(match_start, f"{match_start} lineend") + + url = url.split()[0] + url = url.split("'")[0] + url = url.split('"')[0] + url = url.split("`")[0] + + # URL, and URL. URL? URL! (also URL). (also URL.) + url = url.rstrip(".,?!") + if "(" not in url: # urls can contain spaces (e.g. wikipedia) + url = url.rstrip(")") + url = url.rstrip(".,?!") + + # [url][foobar] + if "]" in url: + pos = url.find("]") + if pos < url.find("["): + url = url[:pos] + + match_end = f"{match_start} + {len(url)} chars" + textwidget.tag_add("url", match_start, match_end) + search_start = f"{match_end} + 1 char" + + def on_ref_link_leftclick(self, tag, event): + tag_range = event.widget.tag_prevrange(tag, "current + 1 char") + assert tag_range + start, end = tag_range + text = event.widget.get(start, end) + message_hash = text.split("pest://")[1] + self.app.api_client.send_command( + {"command": "page_around", "args": [message_hash]} + ) + + def on_named_ref_link_click(self, url, event): + message_hash = url.split("pest://")[1] + self.app.api_client.send_command( + {"command": "page_around", "args": [message_hash]} + ) + + def on_named_link_click(self, url, event): + webbrowser.open(url) + + def on_link_leftclick(self, tag, event): + # To test this, set up 3 URLs, and try clicking first and last char of middle URL. + # That finds bugs where it finds the wrong URL, or only works in the middle of URL, etc. + tag_range = event.widget.tag_prevrange(tag, "current + 1 char") + assert tag_range + start, end = tag_range + text = event.widget.get(start, end) + webbrowser.open(text) + + def copy_selected_text(self): + # Get the selected text + selected_text = self.text.get("sel.first", "sel.last") + + # Copy the selected text to the clipboard + self.text.clipboard_clear() + self.text.clipboard_append(selected_text) + + def show_context_menu(self, event): + self.context_menu.post(event.x_root, event.y_root) + + def handle_page_up(self, event): + self.text.yview_scroll(-1, "pages") + if self.text.yview()[0] == 0.0: + pass + + def highlight_line(self, line_index): + self.text.tag_add("highlight", f"{line_index}.0", f"{line_index + 1}.0") + + def on_link_enter(self, url, event): + self.show_tooltip(url) + + def on_link_leave(self, event): + self.hide_tooltip() + + def on_timestamp_enter(self, reporting_peers, event): + self.show_tooltip(reporting_peers) + + def on_timestamp_leave(self, event): + self.hide_tooltip() + + def is_hearsay(self, message): + if message.get("immediate"): + return False + + if message.get("speaker") == self.app.handle: + return False + + return True diff -uNr akris-desktop/akris_desktop/version.py akris-desktop-genesis/akris_desktop/version.py --- akris-desktop/akris_desktop/version.py false +++ akris-desktop-genesis/akris_desktop/version.py ddeed8ce266953e24676cf836de4699d633de27a36871a415b52dd599e1485edcecea13a306555936ab3dfd5245f929e37314204f2e9fb8670f2e67a19961903 @@ -0,0 +1,5 @@ +import os + +current_path = os.path.dirname(os.path.realpath(__file__)) +with open(os.path.join(current_path, "VERSION")) as version_file: + VERSION = version_file.read().strip() diff -uNr akris-desktop/bin/main.py akris-desktop-genesis/bin/main.py --- akris-desktop/bin/main.py false +++ akris-desktop-genesis/bin/main.py 9eb56dcb99a95419b7fefa2b035ff1ae3507a35e26ffb78cce68a4321209ec78d8a1676c2163308fa1692740f3e8d0327e8dfc1109cf502b62a546ca2dea4edb @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import time +import os +import logging +import tkinter as tk + +import argparse + +PEST_HOST = "0.0.0.0" +PEST_PORT = 8080 +API_HOST = "127.0.0.1" +API_PORT = 8081 +AKRIS_DATA_PATH = os.path.join(os.path.expanduser("~"), ".akris") +AKRIS_DATABASE_NAME = "akris.db" + + +def get_options(): + parser = argparse.ArgumentParser() + parser.add_argument("--pest-host", default=PEST_HOST, help="pest station host") + parser.add_argument("--api-host", default=API_HOST, help="api service host") + parser.add_argument("--pest-port", default=PEST_PORT, help="pest station port") + parser.add_argument("--api-port", default=API_PORT, help="api service port") + parser.add_argument("--akris-data-path", default=AKRIS_DATA_PATH, help="path at which to locate persistent Akris data") + parser.add_argument("--akris-database-name", default=AKRIS_DATABASE_NAME, help="name to use for the Akris database file") + parser.add_argument("--disable-multipart", action='store_true', default=False, help="disable sending multipart messages") + parser.add_argument("--remote-station", action='store_true', default=False, help="connect to a remote station rather than spinning up a local one") + return parser.parse_args() + + +if __name__ == '__main__': + options = get_options() + + from akris.log_config import LogConfig + LogConfig(data_path=options.akris_data_path).get_logger("bin.main") + from akris.station import Station + from akris import client + from akris_desktop.app import App + + logging.basicConfig(level=logging.INFO) + + if not options.remote_station: + station = Station( + tcp_host=options.api_host, + tcp_port=options.api_port, + udp_port=options.pest_port, + host=options.pest_host, + data_path=options.akris_data_path, + database_name=options.akris_database_name, + ) + station.start() + while not station.ready(): + time.sleep(0.1) + + c = client.Client(host=options.api_host, port=int(options.api_port)) + root = tk.Tk() + root.configure(bg="black") + app = App(root, c, options=options) + app.run() + root.mainloop() + if not options.remote_station: + station.stop() diff -uNr akris-desktop/setup.py akris-desktop-genesis/setup.py --- akris-desktop/setup.py false +++ akris-desktop-genesis/setup.py 54295b6e5ccba2c8eee5912033c47f01f5765e55559735541fd7420d1577cd1a316c3368321f5904a27d43b462d3922ffcb791c6a33b1f8ecec5897336318490 @@ -0,0 +1,36 @@ +from setuptools import setup, find_packages +from akris_desktop.version import VERSION + +setup( + name="akris-desktop", + version=VERSION, + description="A gui client for akris", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="Adam Thorsen", + author_email="awt@fastmail.fm", + url="http://v.alethepedia.com/akris_desktop", + license="VPL", + packages=find_packages(), + install_requires=["Pillow"], + extras_require={ + "dev": [ + "black", + "pytest", + "pytest-mock", + "autopep8", + "pyinstaller", + "markdown2", + "staticx", + "pylint", + ] + }, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 3", + ], + keywords="p2p pest forum", + python_requires=">=3.11", +) diff -uNr akris-desktop/tests/test_at_command.py akris-desktop-genesis/tests/test_at_command.py --- akris-desktop/tests/test_at_command.py false +++ akris-desktop-genesis/tests/test_at_command.py ffcc245dd38a793876dfb2b1b2409f969b859065d73722347b01fbffd75f35492e5bbca9d1391609464af7008f60f5d86cafd751b3f453eee1e752b28b69c948 @@ -0,0 +1,60 @@ +import time + +import pytest +from akris_desktop.app import App +from akris.client import Client +from unittest.mock import MagicMock +from queue import Queue +import tkinter as tk + + +@pytest.fixture +def client(): + client = MagicMock(Client) + client.message_queue = Queue() + timestamp = int(time.time()) + messages = [ + { + 'command': 'console_response', + 'type': 'at', + 'body': [ + { + 'handle': 'atahualpa', + 'address': '122.122.0.1:8081', + 'active_at': '2023-05-21 07:13:26.541438', + 'active_at_unixtime': 1684678406 + } + ] + } + ] + for message in messages: + client.message_queue.put(message) + + yield client + +@pytest.fixture +def app(client): + root = tk.Tk() + app = App(root, client) + app.wait_visibility() + app.check_message_queue() + app.notebook.select(1) + yield app + # root.destroy() + + +def test_at_command(app): + app.console.entry.focus_force() + app.console.entry.insert(0, 'at') + app.update() + app.console.entry.event_generate('') + app.update() + assert(app.api_client.send_command.called_with({ + 'command': 'at', + 'args': [] + })) + app.mainloop() + +def test_at_response_display(app): + app.update() + app.mainloop() diff -uNr akris-desktop/tests/test_display_broadcast_text.py akris-desktop-genesis/tests/test_display_broadcast_text.py --- akris-desktop/tests/test_display_broadcast_text.py false +++ akris-desktop-genesis/tests/test_display_broadcast_text.py 62c56092a5a164afc00087902c42cabc7e7327f4fa3d2bbfd0e3be18a0b9dcce7d0c96dd890f60dd5f7c9bc895d86216748e7a3efbe6d54c55f13f69f5b5a7fe @@ -0,0 +1,101 @@ +import hashlib +import random +import sqlite3 +import json +import string +import time + +import pytest +import pytest_mock +from akris_desktop.app import App +from akris.client import Client +import akris.lib.simple_graph_sqlite.database as db +from unittest.mock import MagicMock +from queue import Queue +import tkinter as tk + +def generate_random_string(length): + """Helper function to generate random strings of given length.""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + + +def generate_chat_messages(num_messages): + """Generate a list of num_messages random chat messages.""" + messages = [] + prev_hash = '' + for i in range(num_messages): + timestamp = int(time.time()) + body = generate_random_string(20) + hash = hashlib.sha256(body.encode()).hexdigest() + handle = generate_random_string(10) + netchain = prev_hash if prev_hash else '' + message = { + 'command': 'broadcast_text', + 'timestamp': timestamp, + 'body': body, + 'message_hash': hash, + 'speaker': handle, + 'net_chain': netchain + } + messages.append(message) + prev_hash = hash + return messages + +@pytest.fixture +def client(): + akris_path = '/home/awt/PycharmProjects/pine-beetle/tests/fixtures/akris.db' + db.initialize(akris_path) + connection = sqlite3.connect(akris_path) + cursor = connection.cursor() + latest_message = json.loads(cursor.execute("SELECT * FROM nodes ORDER BY json_extract(body, '$.timestamp') DESC LIMIT 1;").fetchone()[0]) + messages = db.traverse(akris_path, latest_message['message_hash'], + neighbors_fn=db.find_inbound_neighbors, with_bodies=True) + parsed_messages = [] + for message_row in messages: + message = json.loads(message_row[2]) + if not message: + pass + else: + message["command"] = "broadcast_text" + parsed_messages.append(message) + + # parsed_messages = generate_chat_messages(1000) + client = MagicMock(Client) + client.message_queue = Queue() + for message in parsed_messages: + client.message_queue.put(message) + + yield client + + cursor.close() + connection.close() + +@pytest.fixture +def app(client): + root = tk.Tk() + app = App(root, client) + yield app + # root.destroy() + + +def test_display_broadcast_text(app): + app.wait_visibility() + app.check_message_queue() + app.update() + # assert(len(app.message_table.table.get_children()) == 40) + app.mainloop() + +def test_console_command(app): + app.wait_visibility() + app.check_message_queue() + app.notebook.select(1) + app.console.entry.focus_force() + app.console.entry.insert(0, 'at') + app.update() + app.console.entry.event_generate('') + app.update() + assert(app.api_client.send_command.called_with({ + 'command': 'at', + 'args': [] + })) + app.mainloop() diff -uNr akris-desktop/tests/test_knob_command.py akris-desktop-genesis/tests/test_knob_command.py --- akris-desktop/tests/test_knob_command.py false +++ akris-desktop-genesis/tests/test_knob_command.py 970855ce92c66622857198320a4601c58bb195c19d6497d7a30b6639ecebff4dc9c18c08474c7b591452ba424e1dc6f829764370de107353105f15cb445ee45c @@ -0,0 +1,81 @@ +import time + +import pytest +from akris_desktop.app import App +from akris.client import Client +from unittest.mock import MagicMock +from queue import Queue +import tkinter as tk + + +@pytest.fixture +def client(): + client = MagicMock(Client) + client.message_queue = Queue() + timestamp = int(time.time()) + messages = [ + { + 'command': 'console_response', + 'type': 'at', + 'body': [ + { + 'handle': 'atahualpa', + 'address': '122.122.0.1:8081', + 'active_at': '2023-05-21 07:13:26.541438', + 'active_at_unixtime': 1684678406 + } + ] + } + ] + for message in messages: + client.message_queue.put(message) + + yield client + +@pytest.fixture +def app(client): + root = tk.Tk() + app = App(root, client) + app.wait_visibility() + app.check_message_queue() + app.notebook.select(1) + yield app + # root.destroy() + + +def test_knob(app): + app.console.entry.focus_force() + app.console.entry.insert(0, 'knob') + app.update() + app.console.entry.event_generate('') + app.update() + assert(app.api_client.send_command.called_with({ + 'command': 'knob', + 'args': [] + })) + app.update() + app.mainloop() + +def test_knob_set(app): + app.console.entry.focus_force() + app.console.entry.insert(0, 'knob max_bounces 10') + app.update() + app.console.entry.event_generate('') + app.update() + assert(app.api_client.send_command.called_with({ + 'command': 'knob', + 'args': [] + })) + app.mainloop() + +def test_knob_get(app): + app.console.entry.focus_force() + app.console.entry.insert(0, 'knob max_bounces') + app.update() + app.console.entry.event_generate('') + app.update() + assert(app.api_client.send_command.called_with({ + 'command': 'knob', + 'args': [] + })) + app.mainloop() \ No newline at end of file diff -uNr akris-desktop/tests/test_populate_peers.py akris-desktop-genesis/tests/test_populate_peers.py --- akris-desktop/tests/test_populate_peers.py false +++ akris-desktop-genesis/tests/test_populate_peers.py cdf3d696768d17a546926e5735c37bad04a6a1cb3a2f8f949e6a8ab3eae2dcbcf32ccf177113d79ea676d24cae02cd99443ce866db458a730835efd3fc00a90f @@ -0,0 +1,48 @@ +import hashlib +import random +import sqlite3 +import string +import time +import json + +from akris_desktop.app import App +from locust.client import Client +from unittest.mock import MagicMock +from queue import Queue +import tkinter as tk + + +# def test_populate_peers(): +# root = tk.Tk() +# client = MagicMock(Client) +# client.message_queue = Queue() +# for peer in generate_peers(): +# client.message_queue.put(peer) +# app = App(root, client) +# app.run() +# root.mainloop() + + +def generate_peers(): + return [ + { + "command": "peer", + "status": "online", + "handle": "asciilifeform", + }, + { + "command": "peer", + "status": "online", + "handle": "ben_vulpes", + }, + { + "command": "peer", + "status": "online", + "handle": "signpost", + }, + { + "command": "peer", + "status": "online", + "handle": "awt", + }, + ] diff -uNr akris-desktop/tests/test_timeline.py akris-desktop-genesis/tests/test_timeline.py --- akris-desktop/tests/test_timeline.py false +++ akris-desktop-genesis/tests/test_timeline.py 3a3f349649d2eb2d5e95b41d622b9d30efc1913e9eae19ebdecca8532f1d241cf7e2b327b81a174b613469f968b37713e23cdd1a534fae70e1b0951ac4d7c480 @@ -0,0 +1,130 @@ +from akris_desktop.timeline import Timeline, Annotation, BEFORE, AFTER + +def test_add_message(): + timeline = Timeline() + message = { + "body": "test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100 + } + index = timeline.add_message(message) + assert index == 1 + +def test_messages_ordered_by_chain(): + timeline = Timeline() + m1 = { + "body": "test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100, + "message_hash": "baz" + } + m2 = { + "body": "test", + "self_chain": "baz", + "net_chain": "baz", + "timestamp": 100, + "message_hash": "xyz", + } + timeline.add_message(m1) + index = timeline.add_message(m2) + assert index == 2 + +def test_add_annotation(): + timeline = Timeline() + message = { + "body": "test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100 + } + timeline.add_message(message) + annotation = Annotation(100, "test", BEFORE) + index = timeline.add_annotation(annotation) + assert index == 1 + +def test_annotated_message_index(): + timeline = Timeline() + message = { + "body": "test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100 + } + timeline.add_message(message) + annotation = Annotation(100, "test", BEFORE) + timeline.add_annotation(annotation) + index = timeline.calculate_message_index(message) + assert index == 2 + +def test_account_for_antecedent_timestamp_groups(): + timeline = Timeline() + m1 = { + "body": "test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100, + "message_hash": "baz" + } + m2 = { + "body": "test", + "self_chain": "baz", + "net_chain": "baz", + "timestamp": 100, + "message_hash": "xyz", + } + m3 = { + "body": "test", + "self_chain": "baz", + "net_chain": "baz", + "timestamp": 300, + "message_hash": "xyz", + } + timeline.add_message(m1) + timeline.add_message(m2) + timeline.add_message(m3) + index = timeline.calculate_message_index(m3) + assert index == 3 + +def test_account_for_newlines(): + timeline = Timeline() + m1 = { + "body": "test\n test\n test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100, + "message_hash": "baz" + } + m2 = { + "body": "test", + "self_chain": "baz", + "net_chain": "baz", + "timestamp": 100, + "message_hash": "xyz", + } + timeline.add_message(m1) + timeline.add_message(m2) + index = timeline.calculate_message_index(m2) + assert index == 4 + +def test_account_for_newlines_in_antecedent_ts_groups(): + timeline = Timeline() + m1 = { + "body": "test\n test\n test", + "self_chain": "foo", + "net_chain": "bar", + "timestamp": 100, + "message_hash": "baz" + } + m2 = { + "body": "test", + "self_chain": "baz", + "net_chain": "baz", + "timestamp": 200, + "message_hash": "xyz", + } + timeline.add_message(m1) + timeline.add_message(m2) + index = timeline.calculate_message_index(m2) + assert index == 4