commit e4aa416bfb5dbc4bd3130a4f0ed1f8f22792f83e Author: Dominic Zimmer Date: Thu Aug 8 09:50:07 2019 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f02daf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/**/* +*.swp +*.session +*/**/__pycache__/ +__pycache__/* +*.session-journal diff --git a/README.md b/README.md new file mode 100644 index 0000000..29e7f0d --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# TTTC, the telegram client + +TTTC is an unofficial [Telegram](https://telegram.org/) commandline client. +It aims to provide a user experience similar to that of [VIM](https://www.vim.org/). + + +## Requirements +TTTC uses the `curses` and `telethon` python libraries. Curses is usually shipped with +your python installation, but `telethon` can easily be installed via `pip`. + +In order to use TTTC, you will need your own Telegram `api_id` and `api_hash`. +You can read more about how to get them [here](https://core.telegram.org/api/obtaining_api_id). + +Once you obtained your own api key and hash, you need to set them as your environment variables +`TTTC_API_ID` and `TTTC_API_HASH`, respectively. + +The client can be run with `python3 tttc.py`. \ No newline at end of file diff --git a/authview.py b/authview.py new file mode 100644 index 0000000..4255eb1 --- /dev/null +++ b/authview.py @@ -0,0 +1,80 @@ +from asyncio import Condition +import telethon +import resources +import curses +from tttcutils import debug, show_stacktrace + +def bla(scr): + lines = resources.tttc_logo + tttw,ttth = len(lines[4]), len(lines) + w, h = curses.COLS, curses.LINES + yoff = h//2 - ttth//2 + xoff = w//2 - tttw//2 + for a in range(len(lines)): + scr.addstr(yoff + a, xoff, lines[a]) + scr.refresh() + +class AuthView(): + def __init__(self, client, stdscr): + self.stdscr = stdscr + self.client = client + self.inputevent = Condition() + self.inputs = "" + self.w, self.h = curses.COLS, curses.LINES + self.fin = False + + + async def textinput(self): + self.stdscr.addstr("\n> ") + self.stdscr.refresh() + self.inputs = "" + with await self.inputevent: + await self.inputevent.wait() + out = self.inputs + self.inputs = "" + self.stdscr.addstr("\n") + self.stdscr.refresh() + return out + + async def run(self): + await self.client.connect() + self.stdscr.addstr("connected") + self.auth = await self.client.is_user_authorized() + bla(self.stdscr) + if not self.auth: + while True: + self.stdscr.addstr("Please enter your phone number: ") + self.stdscr.refresh() + self.phone = await self.textinput() + try: + response = await self.client.send_code_request(self.phone) + if not response.phone_registered: + self.stdscr.addstr("This phone number is not registered in telegram. ") + self.stdscr.refresh() + else: + break + except telethon.errors.rpcerrorlist.FloodWaitError as err: + self.stdscr.addstr(f"The telegram servers blocked you for too many retries ({err.seconds}s remaining). ") + self.stdscr.refresh() + except Exception as e: + self.stdscr.addstr("Incorrect phone number. ") + self.stdscr.refresh() + self.stdscr.addstr("auth with code now.") + self.stdscr.refresh() + self.code = await self.textinput() + await self.client.sign_in(self.phone, self.code) + self.stdscr.addstr(f"auth successful. ") + self.stdscr.refresh() + + async def handle_key(self, key): + if key == "RETURN": + with await self.inputevent: + self.inputevent.notify() + elif key == "BACKSPACE": + self.inputs = self.inputs[0:-1] + else: + self.inputs += key + self.stdscr.addstr(20, 50, self.inputs) + self.stdscr.clrtoeol() + self.stdscr.refresh() + diff --git a/commandline.py b/commandline.py new file mode 100644 index 0000000..44abd26 --- /dev/null +++ b/commandline.py @@ -0,0 +1,167 @@ +import argparse +import sys +import commandline +import re +import curses +from telethon import sync, TelegramClient +import os +import tttcutils + + +def handle(): + parser = argparse.ArgumentParser(description="Run with no arguments to start interactive mode") + parser.add_argument("--verbose", "-v", action="store_true", help="Be verbose") + parser.add_argument("--colortest", action="store_true", help="Test the used curses color pallet") + + contacts = parser.add_argument_group("contacts") + contacts.add_argument("--list", "-l", action="store_true", help="List available dialogs with chat ids") + contacts.add_argument("--startswith", "-s", metavar="S", + help="Search for a contact starting with S (case sensitive). Can be used with messaging options if result is unique.") + contacts.add_argument("--contains", "-c", metavar="C", + help="Search for a contact containing S (case sensitive). Can be used with messaging options if result is unique.") + contacts.add_argument("--matches", "-x", metavar="M", + help="Search for a contact matching regular expression M. Can be used with messaging options if result is unique.") + + messaging = parser.add_argument_group("messaging") + messaging.add_argument("--me", action="store_true", help="Send the message to yourself") + messaging.add_argument("--target", "-t", metavar="chatid", type=int, help="Send the message to the given chat") + messaging.add_argument("--message", "--msg", "-m", help="Provide a message.") + messaging.add_argument("--stdin", "-i", action="store_true", help="Read a message from stdin.") + parsed = parser.parse_args() + global debug + if parsed.verbose: + def debug(*args, **kwargs): + print(*args, **kwargs) + else: + def debug(*args, **kwargs): + pass + + interactive = len(sys.argv) == 1 + if interactive: + return False + elif parsed.colortest: + with ColorSample() as c: + c.show() + return True + else: + api_id, api_hash = tttcutils.assert_environment() + client = TelegramClient("tttc", api_id, api_hash) + debug("Connecting...", file=sys.stderr) + client.connect() + debug("Connected.", file=sys.stderr) + if not client.is_user_authorized(): + print("Please use the interactive client first to authorize and create a tttc.session file. Aborting.", file=sys.stderr) + exit(1) + debug("Client is authorized.", file=sys.stderr) + if parsed.startswith or parsed.list or parsed.matches or parsed.contains: + global chats + debug("Fetching chats...", file=sys.stderr) + chats = client.get_dialogs() + + filtered = chats if parsed.list else filter_chats((parsed.startswith, parsed.contains, parsed.matches)) + unique = None + if filtered: + if len(filtered) == 0: + print("No matching chats found.") + elif len(filtered) > 1: + for result in reversed(filtered): + print(str(result.id).rjust(16) + " "*4 + result.name) + else: + if not (parsed.message or parsed.stdin): + for result in reversed(filtered): + print(str(result.id).rjust(16) + " "*4 + result.name) + exit() + unique = filtered[0].id + if not (parsed.message or parsed.stdin): + exit() # we are done here + + recipient = unique or parsed.target + if not recipient and parsed.me: + recipient = client.get_me().id + if not recipient: + if len(filtered) > 1: + print("Recipient ambiguous. Aborting.") + else: + print("No recipient provided.") + exit() + #try: + # pass + # #recipient = client.get_input_entity(recipient) + # #client.get_entity(recipient) + #except ValueError: + # print("Illegal entity id. Aborting.", file=sys.stderr) + # exit(1) + + if parsed.message or parsed.stdin: + send_message(client, recipient, message=parsed.message) + return True + +def send_message(client, chat_id, message): + #print(f"call to {client} {chat_id} {message}") + try: + recipient = client.get_input_entity(chat_id) + except ValueError: + print("Could not find the entity for this entity id. Aborting.", file=sys.stderr) + exit(1) + debug("Chat exists. Sending message.", file=sys.stderr) + if message is None: + debug("No message specified. Reading from stdin.", file=sys.stderr) + message = "".join([line for line in sys.stdin]) + if message.strip() == "": + print("The message must not be empty.", file=sys.stderr) + exit() + client.send_message(chat_id, message) + +def filter_chats(filt): + if filt == (None, None, None): + return None + starts, contains, matches = filt + if starts: + return [ chat for chat in chats if chat.name.startswith(starts) ] + elif contains: + return [ chat for chat in chats if contains in chat.name ] + else: + reg = re.compile(matches) + return [ chat for chat in chats if reg.match(chat.name) ] + +class ColorSample: + def __init__(self): + pass + + def __enter__(self): + self.stdscr = curses.initscr() + curses.start_color() + curses.use_default_colors() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(1) + self.stdscr.refresh() + return self + + def __exit__(self, *args): + curses.nocbreak() + self.stdscr.keypad(0) + curses.echo() + curses.endwin() + return False + + def show(self): + self.stdscr.addstr("These are all the colors your terminal supports (and their codes):\n") + for i in range(curses.COLORS): + curses.init_pair(i, i, -1); + self.stdscr.addstr(f" {i} ", curses.color_pair(i)) + self.stdscr.addstr(f" {i} ", curses.A_STANDOUT | curses.color_pair(i)) + self.stdscr.addstr("\n\n") + self.stdscr.addstr("Press any key to continue.") + self.stdscr.getch() + self.stdscr.refresh() + self.stdscr.clear() + self.stdscr.addstr("TTTC uses these colors. Refer to the previous page for their color codes. You can adjust them in the config:\n", curses.color_pair(0)) + from config import colors + for (i, (k, v)) in enumerate(colors.colors.items()): + f, b = v + curses.init_pair(i+1, f, b); + self.stdscr.addstr(f"{k.ljust(25)} {v}\n", curses.color_pair(i+1)) + self.stdscr.refresh() + self.stdscr.refresh() + self.stdscr.getch() diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/colors.py b/config/colors.py new file mode 100644 index 0000000..7e57034 --- /dev/null +++ b/config/colors.py @@ -0,0 +1,29 @@ +import curses + +colors_256 = { + "default": (255, -1), + "default_highlight": (255, 8), + "primary": (14, -1), + "secondary": (10, -1), + "ternary": (11, -1), + "standout": (0, 3), + "error": (9, -1), + "accent": (237, -1) +} +colors_8 = { + "default": (7, -1), + "default_highlight": (0, 7), + "primary": (6, -1), + "secondary": (2, -1), + "ternary": (5, -1), + "standout": (7, 3), + "error": (1, -1), + "accent": (7, -1) +} +colors = colors_256 if curses.COLORS >= 256 else colors_8 +def get_colors(): + for (i, (k, v)) in enumerate(colors.items()): + f, b = v + curses.init_pair(i+1, f, b); + return { k: curses.color_pair(i+1) for (i, (k,v)) in enumerate(colors.items()) } + diff --git a/config/tttcrc.py b/config/tttcrc.py new file mode 100644 index 0000000..dd4438e --- /dev/null +++ b/config/tttcrc.py @@ -0,0 +1,69 @@ +# Colors +# How much space should the contacts area use? +split_ratio = 0.3 +# set to None for transparency +background = None +primary = curses.COLOR_CYAN +highlight = curses.COLOR_RED + +# General +bootscreen_duration = 0.4 +contacts_scroll_offset = 0 +messages_scroll_offset = 0 + +## Contacts +pinned_group = True +pinned_symbol = "" +# indicate the amount of unread messages next to the contacts name +new_messages = True + +# characters to indicate chat type +symbol_read = "✔" +symbol_channel = "C" +symbol_group = "G" +symbol_supergroup = "S" +symbol_private = "P" + +time_today = "%I:%M %p" +# 6 days to prevent confusion with last weeks +# ie if today is monday "Mon" would refer to last weeks monday instead of today. change to 7*86400 if you're fine with that +time_lastweek = "%a" +lastweek = 6*86400 +# anything that's not withing lastweek is considered longtimeago +time_longtimeago = "%d.%m.%y" + +## Messages +message_edited = "edited" +message_forwarded = " via " +# when author and from match +message_forwarded_self = "via " + +# Keys +tttc_quit = "q" +vimline_open = ":" +contacts_search = "/" +contacts_top = "gg" +contacts_bottom = "G" +contacts_next = "c" +contacts_prev = "C" + +# mind the escape sequence +messages_search = "\\" +messages_compose = "m" +# use xdg-open to open file/image +messages_visual_link = "l" +messages_visual_open = "o" +messages_visual_reply = "r" +messages_visual_next = "n" +messages_visual_prev = "N" +messages_visual_toggle_select = "space" +messages_visual_forward = "w" + +compose_send = "y" +compose_edit = "e" +# add a file to the message, the message itself then becomes the caption +compose_file = "f" +# add image to the message, the message itself then becomes the caption +compose_image = "i" +# after having composed a message you can still select it as a reply to another message +compose_select_reply ="r" diff --git a/drawtool.py b/drawtool.py new file mode 100644 index 0000000..96f40cf --- /dev/null +++ b/drawtool.py @@ -0,0 +1,335 @@ +import curses +import math +from telethon.utils import get_display_name +import emojis +import os +import datetime +import time +import config +import textwrap +from tttcutils import show_stacktrace, debug + +class Drawtool(): + def __init__(self, main_view): + self.client = main_view.client + self.main_view = main_view + self.stdscr = main_view.stdscr + + self.chat_ratio = 0.3 + + self.H, self.W = self.stdscr.getmaxyx() + self.recompute_dimensions() + + self.chat_rows = 5 + self.chat_offset_fraction = 0.3 + self.single_chat_fraction = 0.3 + self.dialog_fraction = 0.25 + self.show_indices = False + + def recompute_dimensions(self): + self.min_input_lines = int(0.1 * self.H) + self.max_input_lines = int(0.3 * self.H) + try: + self.input_lines = min(self.max_input_lines, max(len(self._get_input_lines(self.main_view.inputs, width = self.W - 4)), self.min_input_lines)) + except: + show_stacktrace() + + self.chats_height = self.H - self.input_lines - 3 + self.chats_width = int(self.W * self.chat_ratio) + self.chats_num = self.chats_height // 3 + + async def resize(self): + self.H, self.W = self.stdscr.getmaxyx() + self.recompute_dimensions() + await self.redraw() + + def _get_input_lines(self, s, width = 50): + # in order to preserve user made linebreaks: + wrapper = textwrap.TextWrapper() + wrapper.width = width + wrapper.replace_whitespace = False + wrapper.drop_whitespace = False + + lines = s.split("\n") + newlines = [] + for line in lines: + if line: + newlines += wrapper.wrap(line) + else: + newlines += [""] + return newlines + #return textwrap.wrap(s, width = width) + + def _get_cursor_position(self, s, width = 50): + lines = self._get_input_lines(s, width = width)[-self.input_lines:] + if not lines: + return (0, 0) + x = len(lines[-1]) + y = len(lines) - 1 + return y, x + + async def redraw(self): + self.recompute_dimensions() + + self.stdscr.clear() + self.draw_chats() + await self.draw_messages() + if self.main_view.mode == "search": + if self.main_view.search_result == []: + self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box, self.main_view.colors["error"]) + else: + self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box) + elif self.main_view.mode == "vimmode": + self.stdscr.addstr(self.H - 1, 0, ":" + self.main_view.vimline_box) + else: + self.stdscr.addstr(self.H - 1, 0, "--" + self.main_view.mode.upper() + "--") + self.stdscr.addstr(self.H - 1, int(self.W * 2/3), self.main_view.command_box[:8]) + + for index, line in enumerate(self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]): + self.stdscr.addstr(self.H - self.input_lines - 2 + index, 2, f"{line}") + + if self.main_view.mode == "insert": + curses.curs_set(1) + y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4) + self.stdscr.move(self.H - self.input_lines - 2 + y, 2 + x) + elif self.main_view.mode == "search": + curses.curs_set(1) + self.stdscr.move(self.H - 1, 1 + len(self.main_view.search_box)) + elif self.main_view.mode == "vimmode": + curses.curs_set(1) + self.stdscr.move(self.H - 1, 1 + len(self.main_view.vimline_box)) + else: + curses.curs_set(0) + self.stdscr.refresh() + + def format(self, text, attributes = None, width = None, alignment = "left", inner_alignment = "left", truncation = "..."): + if attributes == None: + attributes = self.main_view.colors["default"] + if width == None: + width = len(text) + return { + "text": text, + "attributes": attributes, + "width": width, + "alignment": alignment, + "inner_alignment": inner_alignment, + "truncation": truncation + } + + def _datestring(self, date): + now = datetime.datetime.now().astimezone() + today = datetime.date.today() + if (now - date).total_seconds() < 20*3600: + out = date.strftime(f"%I:%M %p") + return out + if (now - date).total_seconds() < 6*86400: + return date.strftime("%A") + return date.strftime("%d.%m.%y") + + def draw_frame(self, yoff, xoff, h, w, chars = "││──┌┐└┘", attributes = 0): + for i in range(h): + self.stdscr.addstr(yoff + i, xoff, chars[0], attributes) + for i in range(h): + self.stdscr.addstr(yoff + i, xoff + w, chars[1], attributes) + for i in range(w): + self.stdscr.addstr(yoff, xoff + i, chars[2], attributes) + for i in range(w): + self.stdscr.addstr(yoff + h, xoff + i, chars[3], attributes) + self.stdscr.addstr(yoff, xoff, chars[4], attributes) + self.stdscr.addstr(yoff, xoff + w, chars[5], attributes) + self.stdscr.addstr(yoff + h, xoff, chars[6], attributes) + self.stdscr.addstr(yoff + h, xoff + w, chars[7], attributes) + + def draw_chats(self): + selected_chat_index = self.main_view.selected_chat - self.main_view.selected_chat_offset + offset = self.main_view.selected_chat_offset + try: + self.draw_frame(0,0, self.chats_height , self.chats_width) + index = 0 + y = 1 + for index in range(self.chats_num): + dialog = self.main_view.dialogs[index + offset] + message = dialog["messages"][0] if "messages" in dialog else dialog["dialog"].message + message_string = message.text if message.text else "[Non-text object]" + if self.main_view.text_emojis: + message_string = emojis.decode(message_string) + chat_name = get_display_name(dialog["dialog"].entity) + from_string = get_display_name(message.sender) + unread = dialog["unread_count"] + unread_string = f"({unread} new)" if unread else "" + date = dialog["dialog"].date + date = date.astimezone() + date_string = self._datestring(date) + pinned = "* " if dialog["dialog"].pinned else " " + selected = selected_chat_index == index + + self.draw_text( + [ + self.format("o" if dialog["online"] else " ", attributes = self.main_view.colors["secondary"]), + self.format(chat_name, attributes = self.main_view.colors["primary"] | curses.A_STANDOUT if selected else curses.A_BOLD, width = int(0.5 * self.chats_width)), + self.format(f" {str(index)} " if self.show_indices else "", attributes = self.main_view.colors["standout"]), + self.format(unread_string, attributes = self.main_view.colors["error"], alignment = "right"), + self.format(date_string, alignment = "right", attributes = self.main_view.colors["primary"]), + ], + y, 2, maxwidth = self.chats_width - 2) + self.draw_text( + [ + self.format(f"{from_string}:"), + self.format(message_string, width = self.chats_width - len(f"{from_string}: ") - 3) + ], + y + 1, 2, maxwidth = self.chats_width - 2) + y += 3 + index += 1 + except Exception: + show_stacktrace() + + def draw_text(self, format_dicts, y_off = 0, x_off = 0, screen = None, maxwidth = None, separator = " "): + if maxwidth == None: + maxwidth = sum(format_dict["width"] for format_dict in format_dicts) + if screen == None: + screen = self.stdscr + left = [ x for x in format_dicts if x["alignment"] == "left" ] + right = list(reversed([ x for x in format_dicts if x["alignment"] == "right" ])) + center = [ x for x in format_dicts if x["alignment"] == "center" ] + entries = [ (x, "left") for x in left ] + [ (x, "right") for x in right ] + [ (x, "center") for x in center ] + x_left = 0 + x_right = maxwidth -1 + for (format_dict, alignment) in entries: + text = format_dict["text"] + text = text.replace("\n", "") + attributes = format_dict["attributes"] + width = format_dict["width"] + inner_alignment = format_dict["inner_alignment"] + truncation = format_dict["truncation"] + # TODO: make this split preferrably at spaces and not show linebreaks + display_text = text.split("\n")[0] + if len(display_text) > width: + if truncation: + # TODO: make this split preferrably at spaces and not show linebreaks + display_text = display_text[:width - len(truncation)] + truncation + else: + display_text = display_text[:width] + rljust = " " * (width - len(display_text)) + + if alignment == "left": + if inner_alignment == "left": + screen.addstr(y_off, x_off + x_left, display_text, attributes) + x_left += len(display_text) + if rljust: + screen.addstr(y_off, x_off + x_left, rljust) + x_left += len(rljust) + elif inner_alignment == "right": + if rljust: + screen.addstr(y_off, x_off + x_left, rljust) + x_left += len(rljust) + screen.addstr(y_off, x_off + x_left, display_text, attributes) + x_left += len(display_text) + if left and format_dict != left[-1]: + screen.addstr(y_off, x_off + x_left, separator) + x_left += len(separator) + elif alignment == "right": + if inner_alignment == "left": + x_right -= len(text) + self.stdscr.addstr(y_off, x_off + x_right, text, attributes) + x_right -= len(rljust) + self.stdscr.addstr(y_off, x_off + x_right, rljust) + elif inner_alignment == "right": + x_right -= len(rljust) + self.stdscr.addstr(y_off, x_off + x_right, rljust) + x_right -= len(text) + self.stdscr.addstr(y_off, x_off + x_right, text, attributes) + if right and format_dict != right[-1]: + x_right -= len(separator) + self.stdscr.addstr(y_off, x_off + x_right, separator) + elif alignment == "center": + self.stdscr.addstr(y_off, maxwidth // 2 - len(display_text) // 2, display_text, attributes) + + + + + def draw_message(self, main_view, message, chat_idx): + maxtextwidth = int(self.single_chat_fraction * self.W) - 2 + lines = [] + if message.text: + message_lines = message.text.split("\n") + for message_line in message_lines: + if main_view.text_emojis: + message_line = emojis.decode(message_line) + if message_line == "": + lines += [""] + else: + lines += [ + message_line[maxtextwidth * i: maxtextwidth*i+maxtextwidth] + for i in range(int(math.ceil(len(message_line)/maxtextwidth))) + ] + if message.media: + media_type = message.media.to_dict()["_"] + if media_type == "MessageMediaPhoto": + media_type = "Photo" + elif media_type == "MessageMediaDocument": + atts = message.media.document.attributes + filename = [ x for x in atts if x.to_dict()["_"] == "DocumentAttributeFilename" ] + if filename: + filename = filename[0].to_dict()["file_name"] + media_type = f"Document ({filename})" + else: + media_type = f"Document ({message.media.document.mime_type})" + lines += [ f"[{media_type}]" ] + + reply = "" + if message.is_reply: + reply_id = message.reply_to_msg_id + reply = " r?? " + for idx2, message2 in enumerate(main_view.dialogs[main_view.selected_chat]["messages"]): + if message2.id == reply_id: + reply = f"r{idx2:02d}" + break + + from_message = message + from_user = "You" if message.out else get_display_name(from_message.sender) + via_user = f" via {get_display_name(from_message.forward.sender)}" if message.forward else "" + user_string = f"{from_user}{via_user} " + out = [] + if message.out: + out.append(f"{chat_idx} {user_string}{self._datestring(message.date.astimezone())}".rjust(maxtextwidth)) + for idx, text in enumerate(lines): + out.append(text.rjust(maxtextwidth - 4)) + #out.append(f"{chat_idx} {message.date.hour}:{message.date.minute:02d}".rjust(maxtextwidth) + ".") + if message.is_reply: + out.append(reply) + else: + out.append(f"{chat_idx} {user_string}{self._datestring(message.date.astimezone())}") + for idx, text in enumerate(lines): + out.append(" " + text) + if message.is_reply: + out.append(reply) + return (out, message) + + async def load_messages(self, chat_index): + main_view = self.main_view + index = chat_index + if not "messages" in main_view.dialogs[index]: + temp = await main_view.client.get_messages(main_view.dialogs[index]["dialog"], 50) + main_view.dialogs[index]["messages"] = [ message for message in temp ] + + async def draw_messages(self, offset = 0): + main_view = self.main_view + await self.load_messages(main_view.selected_chat) + messages = main_view.dialogs[main_view.selected_chat]["messages"] + max_rows = self.H - self.input_lines - 3 - 1 + lines = [] + chat_count = 0 + while len(lines) < max_rows + offset and chat_count < len(messages): + text, message = self.draw_message(main_view, messages[chat_count], chat_count) + for line in reversed(text): + lines.append((line, message)) + lines.append(("",message)) + chat_count += 1 + + for i in range(min(len(lines)-offset, max_rows)): + text, message = lines[i + offset] + if message.out: + self.stdscr.addstr(max_rows - i, int(self.W * (1-self.single_chat_fraction) - 4) + 2, text) + else: + self.stdscr.addstr(max_rows - i, int(self.W * self.chat_offset_fraction) + 2, text) + self.draw_frame(0, self.chats_width + 1, self.chats_height, self.W - self.chats_width - 2) diff --git a/emojis.py b/emojis.py new file mode 100644 index 0000000..c10ecdf --- /dev/null +++ b/emojis.py @@ -0,0 +1,3025 @@ +# Taken from +# https://github.com/muan/emojilib/blob/master/emojis.json +# TODO implement semi-automatic update function +emojis = { + "grinning": "😀", + "grimacing": "😬", + "grin": "😁", + "joy": "😂", + "rofl": "🤣", + "smiley": "😃", + "smile": "😄", + "sweat_smile": "😅", + "laughing": "😆", + "innocent": "😇", + "wink": "😉", + "blush": "😊", + "slightly_smiling_face": "🙂", + "upside_down_face": "🙃", + "relaxed": "☺️", + "yum": "😋", + "relieved": "😌", + "heart_eyes": "😍", + "kissing_heart": "😘", + "kissing": "😗", + "kissing_smiling_eyes": "😙", + "kissing_closed_eyes": "😚", + "stuck_out_tongue_winking_eye": "😜", + "zany": "🤪", + "raised_eyebrow": "🤨", + "monocle": "🧐", + "stuck_out_tongue_closed_eyes": "😝", + "stuck_out_tongue": "😛", + "money_mouth_face": "🤑", + "nerd_face": "🤓", + "sunglasses": "😎", + "star_struck": "🤩", + "clown_face": "🤡", + "cowboy_hat_face": "🤠", + "hugs": "🤗", + "smirk": "😏", + "no_mouth": "😶", + "neutral_face": "😐", + "expressionless": "😑", + "unamused": "😒", + "roll_eyes": "🙄", + "thinking": "🤔", + "lying_face": "🤥", + "hand_over_mouth": "🤭", + "shushing": "🤫", + "symbols_over_mouth": "🤬", + "exploding_head": "🤯", + "flushed": "😳", + "disappointed": "😞", + "worried": "😟", + "angry": "😠", + "rage": "😡", + "pensive": "😔", + "confused": "😕", + "slightly_frowning_face": "🙁", + "frowning_face": "☹", + "persevere": "😣", + "confounded": "😖", + "tired_face": "😫", + "weary": "😩", + "triumph": "😤", + "open_mouth": "😮", + "scream": "😱", + "fearful": "😨", + "cold_sweat": "😰", + "hushed": "😯", + "frowning": "😦", + "anguished": "😧", + "cry": "😢", + "disappointed_relieved": "😥", + "drooling_face": "🤤", + "sleepy": "😪", + "sweat": "😓", + "sob": "😭", + "dizzy_face": "😵", + "astonished": "😲", + "zipper_mouth_face": "🤐", + "nauseated_face": "🤢", + "sneezing_face": "🤧", + "vomiting": "🤮", + "mask": "😷", + "face_with_thermometer": "🤒", + "face_with_head_bandage": "🤕", + "sleeping": "😴", + "zzz": "💤", + "poop": "💩", + "smiling_imp": "😈", + "imp": "👿", + "japanese_ogre": "👹", + "japanese_goblin": "👺", + "skull": "💀", + "ghost": "👻", + "alien": "👽", + "robot": "🤖", + "smiley_cat": "😺", + "smile_cat": "😸", + "joy_cat": "😹", + "heart_eyes_cat": "😻", + "smirk_cat": "😼", + "kissing_cat": "😽", + "scream_cat": "🙀", + "crying_cat_face": "😿", + "pouting_cat": "😾", + "palms_up": "🤲", + "raised_hands": "🙌", + "clap": "👏", + "wave": "👋", + "call_me_hand": "🤙", + "+1": "👍", + "-1": "👎", + "facepunch": "👊", + "fist": "✊", + "fist_left": "🤛", + "fist_right": "🤜", + "v": "✌", + "ok_hand": "👌", + "raised_hand": "✋", + "raised_back_of_hand": "🤚", + "open_hands": "👐", + "muscle": "💪", + "pray": "🙏", + "handshake": "🤝", + "point_up": "☝", + "point_up_2": "👆", + "point_down": "👇", + "point_left": "👈", + "point_right": "👉", + "fu": "🖕", + "raised_hand_with_fingers_splayed": "🖐", + "love_you": "🤟", + "metal": "🤘", + "crossed_fingers": "🤞", + "vulcan_salute": "🖖", + "writing_hand": "✍", + "selfie": "🤳", + "nail_care": "💅", + "lips": "👄", + "tongue": "👅", + "ear": "👂", + "nose": "👃", + "eye": "👁", + "eyes": "👀", + "brain": "🧠", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "speaking_head": "🗣", + "baby": "👶", + "child": "🧒", + "boy": "👦", + "girl": "👧", + "adult": "🧑", + "man": "👨", + "woman": "👩", + "blonde_woman": "👱‍♀️", + "blonde_man": "👱", + "bearded_person": "🧔", + "older_adult": "🧓", + "older_man": "👴", + "older_woman": "👵", + "man_with_gua_pi_mao": "👲", + "woman_with_headscarf": "🧕", + "woman_with_turban": "👳‍♀️", + "man_with_turban": "👳", + "policewoman": "👮‍♀️", + "policeman": "👮", + "construction_worker_woman": "👷‍♀️", + "construction_worker_man": "👷", + "guardswoman": "💂‍♀️", + "guardsman": "💂", + "female_detective": "🕵️‍♀️", + "male_detective": "🕵", + "woman_health_worker": "👩‍⚕️", + "man_health_worker": "👨‍⚕️", + "woman_farmer": "👩‍🌾", + "man_farmer": "👨‍🌾", + "woman_cook": "👩‍🍳", + "man_cook": "👨‍🍳", + "woman_student": "👩‍🎓", + "man_student": "👨‍🎓", + "woman_singer": "👩‍🎤", + "man_singer": "👨‍🎤", + "woman_teacher": "👩‍🏫", + "man_teacher": "👨‍🏫", + "woman_factory_worker": "👩‍🏭", + "man_factory_worker": "👨‍🏭", + "woman_technologist": "👩‍💻", + "man_technologist": "👨‍💻", + "woman_office_worker": "👩‍💼", + "man_office_worker": "👨‍💼", + "woman_mechanic": "👩‍🔧", + "man_mechanic": "👨‍🔧", + "woman_scientist": "👩‍🔬", + "man_scientist": "👨‍🔬", + "woman_artist": "👩‍🎨", + "man_artist": "👨‍🎨", + "woman_firefighter": "👩‍🚒", + "man_firefighter": "👨‍🚒", + "woman_pilot": "👩‍✈️", + "man_pilot": "👨‍✈️", + "woman_astronaut": "👩‍🚀", + "man_astronaut": "👨‍🚀", + "woman_judge": "👩‍⚖️", + "man_judge": "👨‍⚖️", + "mrs_claus": "🤶", + "santa": "🎅", + "sorceress": "🧙‍♀️", + "wizard": "🧙‍♂️", + "woman_elf": "🧝‍♀️", + "man_elf": "🧝‍♂️", + "woman_vampire": "🧛‍♀️", + "man_vampire": "🧛‍♂️", + "woman_zombie": "🧟‍♀️", + "man_zombie": "🧟‍♂️", + "woman_genie": "🧞‍♀️", + "man_genie": "🧞‍♂️", + "mermaid": "🧜‍♀️", + "merman": "🧜‍♂️", + "woman_fairy": "🧚‍♀️", + "man_fairy": "🧚‍♂️", + "angel": "👼", + "pregnant_woman": "🤰", + "breastfeeding": "🤱", + "princess": "👸", + "prince": "🤴", + "bride_with_veil": "👰", + "man_in_tuxedo": "🤵", + "running_woman": "🏃‍♀️", + "running_man": "🏃", + "walking_woman": "🚶‍♀️", + "walking_man": "🚶", + "dancer": "💃", + "man_dancing": "🕺", + "dancing_women": "👯", + "dancing_men": "👯‍♂️", + "couple": "👫", + "two_men_holding_hands": "👬", + "two_women_holding_hands": "👭", + "bowing_woman": "🙇‍♀️", + "bowing_man": "🙇", + "man_facepalming": "🤦", + "woman_facepalming": "🤦‍♀️", + "woman_shrugging": "🤷", + "man_shrugging": "🤷‍♂️", + "tipping_hand_woman": "💁", + "tipping_hand_man": "💁‍♂️", + "no_good_woman": "🙅", + "no_good_man": "🙅‍♂️", + "ok_woman": "🙆", + "ok_man": "🙆‍♂️", + "raising_hand_woman": "🙋", + "raising_hand_man": "🙋‍♂️", + "pouting_woman": "🙎", + "pouting_man": "🙎‍♂️", + "frowning_woman": "🙍", + "frowning_man": "🙍‍♂️", + "haircut_woman": "💇", + "haircut_man": "💇‍♂️", + "massage_woman": "💆", + "massage_man": "💆‍♂️", + "woman_in_steamy_room": "🧖‍♀️", + "man_in_steamy_room": "🧖‍♂️", + "couple_with_heart_woman_man": "💑", + "couple_with_heart_woman_woman": "👩‍❤️‍👩", + "couple_with_heart_man_man": "👨‍❤️‍👨", + "couplekiss_man_woman": "💏", + "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", + "couplekiss_man_man": "👨‍❤️‍💋‍👨", + "family_man_woman_boy": "👪", + "family_man_woman_girl": "👨‍👩‍👧", + "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", + "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", + "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", + "family_woman_woman_boy": "👩‍👩‍👦", + "family_woman_woman_girl": "👩‍👩‍👧", + "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", + "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", + "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", + "family_man_man_boy": "👨‍👨‍👦", + "family_man_man_girl": "👨‍👨‍👧", + "family_man_man_girl_boy": "👨‍👨‍👧‍👦", + "family_man_man_boy_boy": "👨‍👨‍👦‍👦", + "family_man_man_girl_girl": "👨‍👨‍👧‍👧", + "family_woman_boy": "👩‍👦", + "family_woman_girl": "👩‍👧", + "family_woman_girl_boy": "👩‍👧‍👦", + "family_woman_boy_boy": "👩‍👦‍👦", + "family_woman_girl_girl": "👩‍👧‍👧", + "family_man_boy": "👨‍👦", + "family_man_girl": "👨‍👧", + "family_man_girl_boy": "👨‍👧‍👦", + "family_man_boy_boy": "👨‍👦‍👦", + "family_man_girl_girl": "👨‍👧‍👧", + "coat": "🧥", + "womans_clothes": "👚", + "tshirt": "👕", + "jeans": "👖", + "necktie": "👔", + "dress": "👗", + "bikini": "👙", + "kimono": "👘", + "lipstick": "💄", + "kiss": "💋", + "footprints": "👣", + "high_heel": "👠", + "sandal": "👡", + "boot": "👢", + "mans_shoe": "👞", + "athletic_shoe": "👟", + "socks": "🧦", + "gloves": "🧤", + "scarf": "🧣", + "womans_hat": "👒", + "tophat": "🎩", + "billed_hat": "🧢", + "rescue_worker_helmet": "⛑", + "mortar_board": "🎓", + "crown": "👑", + "school_satchel": "🎒", + "pouch": "👝", + "purse": "👛", + "handbag": "👜", + "briefcase": "💼", + "eyeglasses": "👓", + "dark_sunglasses": "🕶", + "ring": "💍", + "closed_umbrella": "🌂", + "dog": "🐶", + "cat": "🐱", + "mouse": "🐭", + "hamster": "🐹", + "rabbit": "🐰", + "fox_face": "🦊", + "bear": "🐻", + "panda_face": "🐼", + "koala": "🐨", + "tiger": "🐯", + "lion": "🦁", + "cow": "🐮", + "pig": "🐷", + "pig_nose": "🐽", + "frog": "🐸", + "squid": "🦑", + "octopus": "🐙", + "shrimp": "🦐", + "monkey_face": "🐵", + "gorilla": "🦍", + "see_no_evil": "🙈", + "hear_no_evil": "🙉", + "speak_no_evil": "🙊", + "monkey": "🐒", + "chicken": "🐔", + "penguin": "🐧", + "bird": "🐦", + "baby_chick": "🐤", + "hatching_chick": "🐣", + "hatched_chick": "🐥", + "duck": "🦆", + "eagle": "🦅", + "owl": "🦉", + "bat": "🦇", + "wolf": "🐺", + "boar": "🐗", + "horse": "🐴", + "unicorn": "🦄", + "honeybee": "🐝", + "bug": "🐛", + "butterfly": "🦋", + "snail": "🐌", + "beetle": "🐞", + "ant": "🐜", + "grasshopper": "🦗", + "spider": "🕷", + "scorpion": "🦂", + "crab": "🦀", + "snake": "🐍", + "lizard": "🦎", + "t-rex": "🦖", + "sauropod": "🦕", + "turtle": "🐢", + "tropical_fish": "🐠", + "fish": "🐟", + "blowfish": "🐡", + "dolphin": "🐬", + "shark": "🦈", + "whale": "🐳", + "whale2": "🐋", + "crocodile": "🐊", + "leopard": "🐆", + "zebra": "🦓", + "tiger2": "🐅", + "water_buffalo": "🐃", + "ox": "🐂", + "cow2": "🐄", + "deer": "🦌", + "dromedary_camel": "🐪", + "camel": "🐫", + "giraffe": "🦒", + "elephant": "🐘", + "rhinoceros": "🦏", + "goat": "🐐", + "ram": "🐏", + "sheep": "🐑", + "racehorse": "🐎", + "pig2": "🐖", + "rat": "🐀", + "mouse2": "🐁", + "rooster": "🐓", + "turkey": "🦃", + "dove": "🕊", + "dog2": "🐕", + "poodle": "🐩", + "cat2": "🐈", + "rabbit2": "🐇", + "chipmunk": "🐿", + "hedgehog": "🦔", + "paw_prints": "🐾", + "dragon": "🐉", + "dragon_face": "🐲", + "cactus": "🌵", + "christmas_tree": "🎄", + "evergreen_tree": "🌲", + "deciduous_tree": "🌳", + "palm_tree": "🌴", + "seedling": "🌱", + "herb": "🌿", + "shamrock": "☘", + "four_leaf_clover": "🍀", + "bamboo": "🎍", + "tanabata_tree": "🎋", + "leaves": "🍃", + "fallen_leaf": "🍂", + "maple_leaf": "🍁", + "ear_of_rice": "🌾", + "hibiscus": "🌺", + "sunflower": "🌻", + "rose": "🌹", + "wilted_flower": "🥀", + "tulip": "🌷", + "blossom": "🌼", + "cherry_blossom": "🌸", + "bouquet": "💐", + "mushroom": "🍄", + "chestnut": "🌰", + "jack_o_lantern": "🎃", + "shell": "🐚", + "spider_web": "🕸", + "earth_americas": "🌎", + "earth_africa": "🌍", + "earth_asia": "🌏", + "full_moon": "🌕", + "waning_gibbous_moon": "🌖", + "last_quarter_moon": "🌗", + "waning_crescent_moon": "🌘", + "new_moon": "🌑", + "waxing_crescent_moon": "🌒", + "first_quarter_moon": "🌓", + "waxing_gibbous_moon": "🌔", + "new_moon_with_face": "🌚", + "full_moon_with_face": "🌝", + "first_quarter_moon_with_face": "🌛", + "last_quarter_moon_with_face": "🌜", + "sun_with_face": "🌞", + "crescent_moon": "🌙", + "star": "⭐", + "star2": "🌟", + "dizzy": "💫", + "sparkles": "✨", + "comet": "☄", + "sunny": "☀️", + "sun_behind_small_cloud": "🌤", + "partly_sunny": "⛅", + "sun_behind_large_cloud": "🌥", + "sun_behind_rain_cloud": "🌦", + "cloud": "☁️", + "cloud_with_rain": "🌧", + "cloud_with_lightning_and_rain": "⛈", + "cloud_with_lightning": "🌩", + "zap": "⚡", + "fire": "🔥", + "boom": "💥", + "snowflake": "❄️", + "cloud_with_snow": "🌨", + "snowman": "⛄", + "snowman_with_snow": "☃", + "wind_face": "🌬", + "dash": "💨", + "tornado": "🌪", + "fog": "🌫", + "open_umbrella": "☂", + "umbrella": "☔", + "droplet": "💧", + "sweat_drops": "💦", + "ocean": "🌊", + "green_apple": "🍏", + "apple": "🍎", + "pear": "🍐", + "tangerine": "🍊", + "lemon": "🍋", + "banana": "🍌", + "watermelon": "🍉", + "grapes": "🍇", + "strawberry": "🍓", + "melon": "🍈", + "cherries": "🍒", + "peach": "🍑", + "pineapple": "🍍", + "coconut": "🥥", + "kiwi_fruit": "🥝", + "avocado": "🥑", + "broccoli": "🥦", + "tomato": "🍅", + "eggplant": "🍆", + "cucumber": "🥒", + "carrot": "🥕", + "hot_pepper": "🌶", + "potato": "🥔", + "corn": "🌽", + "sweet_potato": "🍠", + "peanuts": "🥜", + "honey_pot": "🍯", + "croissant": "🥐", + "bread": "🍞", + "baguette_bread": "🥖", + "pretzel": "🥨", + "cheese": "🧀", + "egg": "🥚", + "bacon": "🥓", + "steak": "🥩", + "pancakes": "🥞", + "poultry_leg": "🍗", + "meat_on_bone": "🍖", + "fried_shrimp": "🍤", + "fried_egg": "🍳", + "hamburger": "🍔", + "fries": "🍟", + "stuffed_flatbread": "🥙", + "hotdog": "🌭", + "pizza": "🍕", + "sandwich": "🥪", + "canned_food": "🥫", + "spaghetti": "🍝", + "taco": "🌮", + "burrito": "🌯", + "green_salad": "🥗", + "shallow_pan_of_food": "🥘", + "ramen": "🍜", + "stew": "🍲", + "fish_cake": "🍥", + "fortune_cookie": "🥠", + "sushi": "🍣", + "bento": "🍱", + "curry": "🍛", + "rice_ball": "🍙", + "rice": "🍚", + "rice_cracker": "🍘", + "oden": "🍢", + "dango": "🍡", + "shaved_ice": "🍧", + "ice_cream": "🍨", + "icecream": "🍦", + "pie": "🥧", + "cake": "🍰", + "birthday": "🎂", + "custard": "🍮", + "candy": "🍬", + "lollipop": "🍭", + "chocolate_bar": "🍫", + "popcorn": "🍿", + "dumpling": "🥟", + "doughnut": "🍩", + "cookie": "🍪", + "milk_glass": "🥛", + "beer": "🍺", + "beers": "🍻", + "clinking_glasses": "🥂", + "wine_glass": "🍷", + "tumbler_glass": "🥃", + "cocktail": "🍸", + "tropical_drink": "🍹", + "champagne": "🍾", + "sake": "🍶", + "tea": "🍵", + "cup_with_straw": "🥤", + "coffee": "☕", + "baby_bottle": "🍼", + "spoon": "🥄", + "fork_and_knife": "🍴", + "plate_with_cutlery": "🍽", + "bowl_with_spoon": "🥣", + "takeout_box": "🥡", + "chopsticks": "🥢", + "soccer": "⚽", + "basketball": "🏀", + "football": "🏈", + "baseball": "⚾", + "tennis": "🎾", + "volleyball": "🏐", + "rugby_football": "🏉", + "8ball": "🎱", + "golf": "⛳", + "golfing_woman": "🏌️‍♀️", + "golfing_man": "🏌", + "ping_pong": "🏓", + "badminton": "🏸", + "goal_net": "🥅", + "ice_hockey": "🏒", + "field_hockey": "🏑", + "cricket": "🏏", + "ski": "🎿", + "skier": "⛷", + "snowboarder": "🏂", + "person_fencing": "🤺", + "women_wrestling": "🤼‍♀️", + "men_wrestling": "🤼‍♂️", + "woman_cartwheeling": "🤸‍♀️", + "man_cartwheeling": "🤸‍♂️", + "woman_playing_handball": "🤾‍♀️", + "man_playing_handball": "🤾‍♂️", + "ice_skate": "⛸", + "curling_stone": "🥌", + "sled": "🛷", + "bow_and_arrow": "🏹", + "fishing_pole_and_fish": "🎣", + "boxing_glove": "🥊", + "martial_arts_uniform": "🥋", + "rowing_woman": "🚣‍♀️", + "rowing_man": "🚣", + "climbing_woman": "🧗‍♀️", + "climbing_man": "🧗‍♂️", + "swimming_woman": "🏊‍♀️", + "swimming_man": "🏊", + "woman_playing_water_polo": "🤽‍♀️", + "man_playing_water_polo": "🤽‍♂️", + "woman_in_lotus_position": "🧘‍♀️", + "man_in_lotus_position": "🧘‍♂️", + "surfing_woman": "🏄‍♀️", + "surfing_man": "🏄", + "bath": "🛀", + "basketball_woman": "⛹️‍♀️", + "basketball_man": "⛹", + "weight_lifting_woman": "🏋️‍♀️", + "weight_lifting_man": "🏋", + "biking_woman": "🚴‍♀️", + "biking_man": "🚴", + "mountain_biking_woman": "🚵‍♀️", + "mountain_biking_man": "🚵", + "horse_racing": "🏇", + "business_suit_levitating": "🕴", + "trophy": "🏆", + "running_shirt_with_sash": "🎽", + "medal_sports": "🏅", + "medal_military": "🎖", + "1st_place_medal": "🥇", + "2nd_place_medal": "🥈", + "3rd_place_medal": "🥉", + "reminder_ribbon": "🎗", + "rosette": "🏵", + "ticket": "🎫", + "tickets": "🎟", + "performing_arts": "🎭", + "art": "🎨", + "circus_tent": "🎪", + "woman_juggling": "🤹‍♀️", + "man_juggling": "🤹‍♂️", + "microphone": "🎤", + "headphones": "🎧", + "musical_score": "🎼", + "musical_keyboard": "🎹", + "drum": "🥁", + "saxophone": "🎷", + "trumpet": "🎺", + "guitar": "🎸", + "violin": "🎻", + "clapper": "🎬", + "video_game": "🎮", + "space_invader": "👾", + "dart": "🎯", + "game_die": "🎲", + "slot_machine": "🎰", + "bowling": "🎳", + "red_car": "🚗", + "taxi": "🚕", + "blue_car": "🚙", + "bus": "🚌", + "trolleybus": "🚎", + "racing_car": "🏎", + "police_car": "🚓", + "ambulance": "🚑", + "fire_engine": "🚒", + "minibus": "🚐", + "truck": "🚚", + "articulated_lorry": "🚛", + "tractor": "🚜", + "kick_scooter": "🛴", + "motorcycle": "🏍", + "bike": "🚲", + "motor_scooter": "🛵", + "rotating_light": "🚨", + "oncoming_police_car": "🚔", + "oncoming_bus": "🚍", + "oncoming_automobile": "🚘", + "oncoming_taxi": "🚖", + "aerial_tramway": "🚡", + "mountain_cableway": "🚠", + "suspension_railway": "🚟", + "railway_car": "🚃", + "train": "🚋", + "monorail": "🚝", + "bullettrain_side": "🚄", + "bullettrain_front": "🚅", + "light_rail": "🚈", + "mountain_railway": "🚞", + "steam_locomotive": "🚂", + "train2": "🚆", + "metro": "🚇", + "tram": "🚊", + "station": "🚉", + "flying_saucer": "🛸", + "helicopter": "🚁", + "small_airplane": "🛩", + "airplane": "✈️", + "flight_departure": "🛫", + "flight_arrival": "🛬", + "sailboat": "⛵", + "motor_boat": "🛥", + "speedboat": "🚤", + "ferry": "⛴", + "passenger_ship": "🛳", + "rocket": "🚀", + "artificial_satellite": "🛰", + "seat": "💺", + "canoe": "🛶", + "anchor": "⚓", + "construction": "🚧", + "fuelpump": "⛽", + "busstop": "🚏", + "vertical_traffic_light": "🚦", + "traffic_light": "🚥", + "checkered_flag": "🏁", + "ship": "🚢", + "ferris_wheel": "🎡", + "roller_coaster": "🎢", + "carousel_horse": "🎠", + "building_construction": "🏗", + "foggy": "🌁", + "tokyo_tower": "🗼", + "factory": "🏭", + "fountain": "⛲", + "rice_scene": "🎑", + "mountain": "⛰", + "mountain_snow": "🏔", + "mount_fuji": "🗻", + "volcano": "🌋", + "japan": "🗾", + "camping": "🏕", + "tent": "⛺", + "national_park": "🏞", + "motorway": "🛣", + "railway_track": "🛤", + "sunrise": "🌅", + "sunrise_over_mountains": "🌄", + "desert": "🏜", + "beach_umbrella": "🏖", + "desert_island": "🏝", + "city_sunrise": "🌇", + "city_sunset": "🌆", + "cityscape": "🏙", + "night_with_stars": "🌃", + "bridge_at_night": "🌉", + "milky_way": "🌌", + "stars": "🌠", + "sparkler": "🎇", + "fireworks": "🎆", + "rainbow": "🌈", + "houses": "🏘", + "european_castle": "🏰", + "japanese_castle": "🏯", + "stadium": "🏟", + "statue_of_liberty": "🗽", + "house": "🏠", + "house_with_garden": "🏡", + "derelict_house": "🏚", + "office": "🏢", + "department_store": "🏬", + "post_office": "🏣", + "european_post_office": "🏤", + "hospital": "🏥", + "bank": "🏦", + "hotel": "🏨", + "convenience_store": "🏪", + "school": "🏫", + "love_hotel": "🏩", + "wedding": "💒", + "classical_building": "🏛", + "church": "⛪", + "mosque": "🕌", + "synagogue": "🕍", + "kaaba": "🕋", + "shinto_shrine": "⛩", + "watch": "⌚", + "iphone": "📱", + "calling": "📲", + "computer": "💻", + "keyboard": "⌨", + "desktop_computer": "🖥", + "printer": "🖨", + "computer_mouse": "🖱", + "trackball": "🖲", + "joystick": "🕹", + "clamp": "🗜", + "minidisc": "💽", + "floppy_disk": "💾", + "cd": "💿", + "dvd": "📀", + "vhs": "📼", + "camera": "📷", + "camera_flash": "📸", + "video_camera": "📹", + "movie_camera": "🎥", + "film_projector": "📽", + "film_strip": "🎞", + "telephone_receiver": "📞", + "phone": "☎️", + "pager": "📟", + "fax": "📠", + "tv": "📺", + "radio": "📻", + "studio_microphone": "🎙", + "level_slider": "🎚", + "control_knobs": "🎛", + "stopwatch": "⏱", + "timer_clock": "⏲", + "alarm_clock": "⏰", + "mantelpiece_clock": "🕰", + "hourglass_flowing_sand": "⏳", + "hourglass": "⌛", + "satellite": "📡", + "battery": "🔋", + "electric_plug": "🔌", + "bulb": "💡", + "flashlight": "🔦", + "candle": "🕯", + "wastebasket": "🗑", + "oil_drum": "🛢", + "money_with_wings": "💸", + "dollar": "💵", + "yen": "💴", + "euro": "💶", + "pound": "💷", + "moneybag": "💰", + "credit_card": "💳", + "gem": "💎", + "balance_scale": "⚖", + "wrench": "🔧", + "hammer": "🔨", + "hammer_and_pick": "⚒", + "hammer_and_wrench": "🛠", + "pick": "⛏", + "nut_and_bolt": "🔩", + "gear": "⚙", + "chains": "⛓", + "gun": "🔫", + "bomb": "💣", + "hocho": "🔪", + "dagger": "🗡", + "crossed_swords": "⚔", + "shield": "🛡", + "smoking": "🚬", + "skull_and_crossbones": "☠", + "coffin": "⚰", + "funeral_urn": "⚱", + "amphora": "🏺", + "crystal_ball": "🔮", + "prayer_beads": "📿", + "barber": "💈", + "alembic": "⚗", + "telescope": "🔭", + "microscope": "🔬", + "hole": "🕳", + "pill": "💊", + "syringe": "💉", + "thermometer": "🌡", + "label": "🏷", + "bookmark": "🔖", + "toilet": "🚽", + "shower": "🚿", + "bathtub": "🛁", + "key": "🔑", + "old_key": "🗝", + "couch_and_lamp": "🛋", + "sleeping_bed": "🛌", + "bed": "🛏", + "door": "🚪", + "bellhop_bell": "🛎", + "framed_picture": "🖼", + "world_map": "🗺", + "parasol_on_ground": "⛱", + "moyai": "🗿", + "shopping": "🛍", + "shopping_cart": "🛒", + "balloon": "🎈", + "flags": "🎏", + "ribbon": "🎀", + "gift": "🎁", + "confetti_ball": "🎊", + "tada": "🎉", + "dolls": "🎎", + "wind_chime": "🎐", + "crossed_flags": "🎌", + "izakaya_lantern": "🏮", + "email": "✉️", + "envelope_with_arrow": "📩", + "incoming_envelope": "📨", + "e-mail": "📧", + "love_letter": "💌", + "postbox": "📮", + "mailbox_closed": "📪", + "mailbox": "📫", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "package": "📦", + "postal_horn": "📯", + "inbox_tray": "📥", + "outbox_tray": "📤", + "scroll": "📜", + "page_with_curl": "📃", + "bookmark_tabs": "📑", + "bar_chart": "📊", + "chart_with_upwards_trend": "📈", + "chart_with_downwards_trend": "📉", + "page_facing_up": "📄", + "date": "📅", + "calendar": "📆", + "spiral_calendar": "🗓", + "card_index": "📇", + "card_file_box": "🗃", + "ballot_box": "🗳", + "file_cabinet": "🗄", + "clipboard": "📋", + "spiral_notepad": "🗒", + "file_folder": "📁", + "open_file_folder": "📂", + "card_index_dividers": "🗂", + "newspaper_roll": "🗞", + "newspaper": "📰", + "notebook": "📓", + "closed_book": "📕", + "green_book": "📗", + "blue_book": "📘", + "orange_book": "📙", + "notebook_with_decorative_cover": "📔", + "ledger": "📒", + "books": "📚", + "open_book": "📖", + "link": "🔗", + "paperclip": "📎", + "paperclips": "🖇", + "scissors": "✂️", + "triangular_ruler": "📐", + "straight_ruler": "📏", + "pushpin": "📌", + "round_pushpin": "📍", + "triangular_flag_on_post": "🚩", + "white_flag": "🏳", + "black_flag": "🏴", + "rainbow_flag": "🏳️‍🌈", + "closed_lock_with_key": "🔐", + "lock": "🔒", + "unlock": "🔓", + "lock_with_ink_pen": "🔏", + "pen": "🖊", + "fountain_pen": "🖋", + "black_nib": "✒️", + "memo": "📝", + "pencil2": "✏️", + "crayon": "🖍", + "paintbrush": "🖌", + "mag": "🔍", + "mag_right": "🔎", + "heart": "❤️", + "orange_heart": "🧡", + "yellow_heart": "💛", + "green_heart": "💚", + "blue_heart": "💙", + "purple_heart": "💜", + "black_heart": "🖤", + "broken_heart": "💔", + "heavy_heart_exclamation": "❣", + "two_hearts": "💕", + "revolving_hearts": "💞", + "heartbeat": "💓", + "heartpulse": "💗", + "sparkling_heart": "💖", + "cupid": "💘", + "gift_heart": "💝", + "heart_decoration": "💟", + "peace_symbol": "☮", + "latin_cross": "✝", + "star_and_crescent": "☪", + "om": "🕉", + "wheel_of_dharma": "☸", + "star_of_david": "✡", + "six_pointed_star": "🔯", + "menorah": "🕎", + "yin_yang": "☯", + "orthodox_cross": "☦", + "place_of_worship": "🛐", + "ophiuchus": "⛎", + "aries": "♈", + "taurus": "♉", + "gemini": "♊", + "cancer": "♋", + "leo": "♌", + "virgo": "♍", + "libra": "♎", + "scorpius": "♏", + "sagittarius": "♐", + "capricorn": "♑", + "aquarius": "♒", + "pisces": "♓", + "id": "🆔", + "atom_symbol": "⚛", + "u7a7a": "🈳", + "u5272": "🈹", + "radioactive": "☢", + "biohazard": "☣", + "mobile_phone_off": "📴", + "vibration_mode": "📳", + "u6709": "🈶", + "u7121": "🈚", + "u7533": "🈸", + "u55b6": "🈺", + "u6708": "🈷️", + "eight_pointed_black_star": "✴️", + "vs": "🆚", + "accept": "🉑", + "white_flower": "💮", + "ideograph_advantage": "🉐", + "secret": "㊙️", + "congratulations": "㊗️", + "u5408": "🈴", + "u6e80": "🈵", + "u7981": "🈲", + "a": "🅰️", + "b": "🅱️", + "ab": "🆎", + "cl": "🆑", + "o2": "🅾️", + "sos": "🆘", + "no_entry": "⛔", + "name_badge": "📛", + "no_entry_sign": "🚫", + "x": "❌", + "o": "⭕", + "stop_sign": "🛑", + "anger": "💢", + "hotsprings": "♨️", + "no_pedestrians": "🚷", + "do_not_litter": "🚯", + "no_bicycles": "🚳", + "non-potable_water": "🚱", + "underage": "🔞", + "no_mobile_phones": "📵", + "exclamation": "❗", + "grey_exclamation": "❕", + "question": "❓", + "grey_question": "❔", + "bangbang": "‼️", + "interrobang": "⁉️", + "100": "💯", + "low_brightness": "🔅", + "high_brightness": "🔆", + "trident": "🔱", + "fleur_de_lis": "⚜", + "part_alternation_mark": "〽️", + "warning": "⚠️", + "children_crossing": "🚸", + "beginner": "🔰", + "recycle": "♻️", + "u6307": "🈯", + "chart": "💹", + "sparkle": "❇️", + "eight_spoked_asterisk": "✳️", + "negative_squared_cross_mark": "❎", + "white_check_mark": "✅", + "diamond_shape_with_a_dot_inside": "💠", + "cyclone": "🌀", + "loop": "➿", + "globe_with_meridians": "🌐", + "m": "Ⓜ️", + "atm": "🏧", + "sa": "🈂️", + "passport_control": "🛂", + "customs": "🛃", + "baggage_claim": "🛄", + "left_luggage": "🛅", + "wheelchair": "♿", + "no_smoking": "🚭", + "wc": "🚾", + "parking": "🅿️", + "potable_water": "🚰", + "mens": "🚹", + "womens": "🚺", + "baby_symbol": "🚼", + "restroom": "🚻", + "put_litter_in_its_place": "🚮", + "cinema": "🎦", + "signal_strength": "📶", + "koko": "🈁", + "ng": "🆖", + "ok": "🆗", + "up": "🆙", + "cool": "🆒", + "new": "🆕", + "free": "🆓", + "zero": "0️⃣", + "one": "1️⃣", + "two": "2️⃣", + "three": "3️⃣", + "four": "4️⃣", + "five": "5️⃣", + "six": "6️⃣", + "seven": "7️⃣", + "eight": "8️⃣", + "nine": "9️⃣", + "keycap_ten": "🔟", + "asterisk": "*⃣", + "1234": "🔢", + "eject_button": "⏏️", + "arrow_forward": "▶️", + "pause_button": "⏸", + "next_track_button": "⏭", + "stop_button": "⏹", + "record_button": "⏺", + "play_or_pause_button": "⏯", + "previous_track_button": "⏮", + "fast_forward": "⏩", + "rewind": "⏪", + "twisted_rightwards_arrows": "🔀", + "repeat": "🔁", + "repeat_one": "🔂", + "arrow_backward": "◀️", + "arrow_up_small": "🔼", + "arrow_down_small": "🔽", + "arrow_double_up": "⏫", + "arrow_double_down": "⏬", + "arrow_right": "➡️", + "arrow_left": "⬅️", + "arrow_up": "⬆️", + "arrow_down": "⬇️", + "arrow_upper_right": "↗️", + "arrow_lower_right": "↘️", + "arrow_lower_left": "↙️", + "arrow_upper_left": "↖️", + "arrow_up_down": "↕️", + "left_right_arrow": "↔️", + "arrows_counterclockwise": "🔄", + "arrow_right_hook": "↪️", + "leftwards_arrow_with_hook": "↩️", + "arrow_heading_up": "⤴️", + "arrow_heading_down": "⤵️", + "hash": "#️⃣", + "information_source": "ℹ️", + "abc": "🔤", + "abcd": "🔡", + "capital_abcd": "🔠", + "symbols": "🔣", + "musical_note": "🎵", + "notes": "🎶", + "wavy_dash": "〰️", + "curly_loop": "➰", + "heavy_check_mark": "✔️", + "arrows_clockwise": "🔃", + "heavy_plus_sign": "➕", + "heavy_minus_sign": "➖", + "heavy_division_sign": "➗", + "heavy_multiplication_x": "✖️", + "heavy_dollar_sign": "💲", + "currency_exchange": "💱", + "copyright": "©️", + "registered": "®️", + "tm": "™️", + "end": "🔚", + "back": "🔙", + "on": "🔛", + "top": "🔝", + "soon": "🔜", + "ballot_box_with_check": "☑️", + "radio_button": "🔘", + "white_circle": "⚪", + "black_circle": "⚫", + "red_circle": "🔴", + "large_blue_circle": "🔵", + "small_orange_diamond": "🔸", + "small_blue_diamond": "🔹", + "large_orange_diamond": "🔶", + "large_blue_diamond": "🔷", + "small_red_triangle": "🔺", + "black_small_square": "▪️", + "white_small_square": "▫️", + "black_large_square": "⬛", + "white_large_square": "⬜", + "small_red_triangle_down": "🔻", + "black_medium_square": "◼️", + "white_medium_square": "◻️", + "black_medium_small_square": "◾", + "white_medium_small_square": "◽", + "black_square_button": "🔲", + "white_square_button": "🔳", + "speaker": "🔈", + "sound": "🔉", + "loud_sound": "🔊", + "mute": "🔇", + "mega": "📣", + "loudspeaker": "📢", + "bell": "🔔", + "no_bell": "🔕", + "black_joker": "🃏", + "mahjong": "🀄", + "spades": "♠️", + "clubs": "♣️", + "hearts": "♥️", + "diamonds": "♦️", + "flower_playing_cards": "🎴", + "thought_balloon": "💭", + "right_anger_bubble": "🗯", + "speech_balloon": "💬", + "left_speech_bubble": "🗨", + "clock1": "🕐", + "clock2": "🕑", + "clock3": "🕒", + "clock4": "🕓", + "clock5": "🕔", + "clock6": "🕕", + "clock7": "🕖", + "clock8": "🕗", + "clock9": "🕘", + "clock10": "🕙", + "clock11": "🕚", + "clock12": "🕛", + "clock130": "🕜", + "clock230": "🕝", + "clock330": "🕞", + "clock430": "🕟", + "clock530": "🕠", + "clock630": "🕡", + "clock730": "🕢", + "clock830": "🕣", + "clock930": "🕤", + "clock1030": "🕥", + "clock1130": "🕦", + "clock1230": "🕧", + "afghanistan": "🇦🇫", + "aland_islands": "🇦🇽", + "albania": "🇦🇱", + "algeria": "🇩🇿", + "american_samoa": "🇦🇸", + "andorra": "🇦🇩", + "angola": "🇦🇴", + "anguilla": "🇦🇮", + "antarctica": "🇦🇶", + "antigua_barbuda": "🇦🇬", + "argentina": "🇦🇷", + "armenia": "🇦🇲", + "aruba": "🇦🇼", + "australia": "🇦🇺", + "austria": "🇦🇹", + "azerbaijan": "🇦🇿", + "bahamas": "🇧🇸", + "bahrain": "🇧🇭", + "bangladesh": "🇧🇩", + "barbados": "🇧🇧", + "belarus": "🇧🇾", + "belgium": "🇧🇪", + "belize": "🇧🇿", + "benin": "🇧🇯", + "bermuda": "🇧🇲", + "bhutan": "🇧🇹", + "bolivia": "🇧🇴", + "caribbean_netherlands": "🇧🇶", + "bosnia_herzegovina": "🇧🇦", + "botswana": "🇧🇼", + "brazil": "🇧🇷", + "british_indian_ocean_territory": "🇮🇴", + "british_virgin_islands": "🇻🇬", + "brunei": "🇧🇳", + "bulgaria": "🇧🇬", + "burkina_faso": "🇧🇫", + "burundi": "🇧🇮", + "cape_verde": "🇨🇻", + "cambodia": "🇰🇭", + "cameroon": "🇨🇲", + "canada": "🇨🇦", + "canary_islands": "🇮🇨", + "cayman_islands": "🇰🇾", + "central_african_republic": "🇨🇫", + "chad": "🇹🇩", + "chile": "🇨🇱", + "cn": "🇨🇳", + "christmas_island": "🇨🇽", + "cocos_islands": "🇨🇨", + "colombia": "🇨🇴", + "comoros": "🇰🇲", + "congo_brazzaville": "🇨🇬", + "congo_kinshasa": "🇨🇩", + "cook_islands": "🇨🇰", + "costa_rica": "🇨🇷", + "croatia": "🇭🇷", + "cuba": "🇨🇺", + "curacao": "🇨🇼", + "cyprus": "🇨🇾", + "czech_republic": "🇨🇿", + "denmark": "🇩🇰", + "djibouti": "🇩🇯", + "dominica": "🇩🇲", + "dominican_republic": "🇩🇴", + "ecuador": "🇪🇨", + "egypt": "🇪🇬", + "el_salvador": "🇸🇻", + "equatorial_guinea": "🇬🇶", + "eritrea": "🇪🇷", + "estonia": "🇪🇪", + "ethiopia": "🇪🇹", + "eu": "🇪🇺", + "falkland_islands": "🇫🇰", + "faroe_islands": "🇫🇴", + "fiji": "🇫🇯", + "finland": "🇫🇮", + "fr": "🇫🇷", + "french_guiana": "🇬🇫", + "french_polynesia": "🇵🇫", + "french_southern_territories": "🇹🇫", + "gabon": "🇬🇦", + "gambia": "🇬🇲", + "georgia": "🇬🇪", + "de": "🇩🇪", + "ghana": "🇬🇭", + "gibraltar": "🇬🇮", + "greece": "🇬🇷", + "greenland": "🇬🇱", + "grenada": "🇬🇩", + "guadeloupe": "🇬🇵", + "guam": "🇬🇺", + "guatemala": "🇬🇹", + "guernsey": "🇬🇬", + "guinea": "🇬🇳", + "guinea_bissau": "🇬🇼", + "guyana": "🇬🇾", + "haiti": "🇭🇹", + "honduras": "🇭🇳", + "hong_kong": "🇭🇰", + "hungary": "🇭🇺", + "iceland": "🇮🇸", + "india": "🇮🇳", + "indonesia": "🇮🇩", + "iran": "🇮🇷", + "iraq": "🇮🇶", + "ireland": "🇮🇪", + "isle_of_man": "🇮🇲", + "israel": "🇮🇱", + "it": "🇮🇹", + "cote_divoire": "🇨🇮", + "jamaica": "🇯🇲", + "jp": "🇯🇵", + "jersey": "🇯🇪", + "jordan": "🇯🇴", + "kazakhstan": "🇰🇿", + "kenya": "🇰🇪", + "kiribati": "🇰🇮", + "kosovo": "🇽🇰", + "kuwait": "🇰🇼", + "kyrgyzstan": "🇰🇬", + "laos": "🇱🇦", + "latvia": "🇱🇻", + "lebanon": "🇱🇧", + "lesotho": "🇱🇸", + "liberia": "🇱🇷", + "libya": "🇱🇾", + "liechtenstein": "🇱🇮", + "lithuania": "🇱🇹", + "luxembourg": "🇱🇺", + "macau": "🇲🇴", + "macedonia": "🇲🇰", + "madagascar": "🇲🇬", + "malawi": "🇲🇼", + "malaysia": "🇲🇾", + "maldives": "🇲🇻", + "mali": "🇲🇱", + "malta": "🇲🇹", + "marshall_islands": "🇲🇭", + "martinique": "🇲🇶", + "mauritania": "🇲🇷", + "mauritius": "🇲🇺", + "mayotte": "🇾🇹", + "mexico": "🇲🇽", + "micronesia": "🇫🇲", + "moldova": "🇲🇩", + "monaco": "🇲🇨", + "mongolia": "🇲🇳", + "montenegro": "🇲🇪", + "montserrat": "🇲🇸", + "morocco": "🇲🇦", + "mozambique": "🇲🇿", + "myanmar": "🇲🇲", + "namibia": "🇳🇦", + "nauru": "🇳🇷", + "nepal": "🇳🇵", + "netherlands": "🇳🇱", + "new_caledonia": "🇳🇨", + "new_zealand": "🇳🇿", + "nicaragua": "🇳🇮", + "niger": "🇳🇪", + "nigeria": "🇳🇬", + "niue": "🇳🇺", + "norfolk_island": "🇳🇫", + "northern_mariana_islands": "🇲🇵", + "north_korea": "🇰🇵", + "norway": "🇳🇴", + "oman": "🇴🇲", + "pakistan": "🇵🇰", + "palau": "🇵🇼", + "palestinian_territories": "🇵🇸", + "panama": "🇵🇦", + "papua_new_guinea": "🇵🇬", + "paraguay": "🇵🇾", + "peru": "🇵🇪", + "philippines": "🇵🇭", + "pitcairn_islands": "🇵🇳", + "poland": "🇵🇱", + "portugal": "🇵🇹", + "puerto_rico": "🇵🇷", + "qatar": "🇶🇦", + "reunion": "🇷🇪", + "romania": "🇷🇴", + "ru": "🇷🇺", + "rwanda": "🇷🇼", + "st_barthelemy": "🇧🇱", + "st_helena": "🇸🇭", + "st_kitts_nevis": "🇰🇳", + "st_lucia": "🇱🇨", + "st_pierre_miquelon": "🇵🇲", + "st_vincent_grenadines": "🇻🇨", + "samoa": "🇼🇸", + "san_marino": "🇸🇲", + "sao_tome_principe": "🇸🇹", + "saudi_arabia": "🇸🇦", + "senegal": "🇸🇳", + "serbia": "🇷🇸", + "seychelles": "🇸🇨", + "sierra_leone": "🇸🇱", + "singapore": "🇸🇬", + "sint_maarten": "🇸🇽", + "slovakia": "🇸🇰", + "slovenia": "🇸🇮", + "solomon_islands": "🇸🇧", + "somalia": "🇸🇴", + "south_africa": "🇿🇦", + "south_georgia_south_sandwich_islands": "🇬🇸", + "kr": "🇰🇷", + "south_sudan": "🇸🇸", + "es": "🇪🇸", + "sri_lanka": "🇱🇰", + "sudan": "🇸🇩", + "suriname": "🇸🇷", + "swaziland": "🇸🇿", + "sweden": "🇸🇪", + "switzerland": "🇨🇭", + "syria": "🇸🇾", + "taiwan": "🇹🇼", + "tajikistan": "🇹🇯", + "tanzania": "🇹🇿", + "thailand": "🇹🇭", + "timor_leste": "🇹🇱", + "togo": "🇹🇬", + "tokelau": "🇹🇰", + "tonga": "🇹🇴", + "trinidad_tobago": "🇹🇹", + "tunisia": "🇹🇳", + "tr": "🇹🇷", + "turkmenistan": "🇹🇲", + "turks_caicos_islands": "🇹🇨", + "tuvalu": "🇹🇻", + "uganda": "🇺🇬", + "ukraine": "🇺🇦", + "united_arab_emirates": "🇦🇪", + "uk": "🇬🇧", + "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "us": "🇺🇸", + "us_virgin_islands": "🇻🇮", + "uruguay": "🇺🇾", + "uzbekistan": "🇺🇿", + "vanuatu": "🇻🇺", + "vatican_city": "🇻🇦", + "venezuela": "🇻🇪", + "vietnam": "🇻🇳", + "wallis_futuna": "🇼🇫", + "western_sahara": "🇪🇭", + "yemen": "🇾🇪", + "zambia": "🇿🇲", + "zimbabwe": "🇿🇼", +} + +emojis_inv = { v:k for (k,v) in emojis.items() } + +def decode(s): + for (k,v) in emojis_inv.items(): + s = s.replace(k, f":{v}:") + return s + + +def encode(s): + for (k,v) in emojis.items(): + s = s.replace(f":{k}:", v) + return s + +_emojis = { + "grinning": "😀", + "grimacing": "😬", + "grin": "😁", + "joy": "😂", + "rofl": "🤣", + "smiley": "😃", + "smile": "😄", + "sweat_smile": "😅", + "laughing": "😆", + "innocent": "😇", + "wink": "😉", + "blush": "😊", + "slightly_smiling_face": "🙂", + "upside_down_face": "🙃", + "relaxed": "☺️", + "yum": "😋", + "relieved": "😌", + "heart_eyes": "😍", + "kissing_heart": "😘", + "kissing": "😗", + "kissing_smiling_eyes": "😙", + "kissing_closed_eyes": "😚", + "stuck_out_tongue_winking_eye": "😜", + "zany": "🤪", + "raised_eyebrow": "🤨", + "monocle": "🧐", + "stuck_out_tongue_closed_eyes": "😝", + "stuck_out_tongue": "😛", + "money_mouth_face": "🤑", + "nerd_face": "🤓", + "sunglasses": "😎", + "star_struck": "🤩", + "clown_face": "🤡", + "cowboy_hat_face": "🤠", + "hugs": "🤗", + "smirk": "😏", + "no_mouth": "😶", + "neutral_face": "😐", + "expressionless": "😑", + "unamused": "😒", + "roll_eyes": "🙄", + "thinking": "🤔", + "lying_face": "🤥", + "hand_over_mouth": "🤭", + "shushing": "🤫", + "symbols_over_mouth": "🤬", + "exploding_head": "🤯", + "flushed": "😳", + "disappointed": "😞", + "worried": "😟", + "angry": "😠", + "rage": "😡", + "pensive": "😔", + "confused": "😕", + "slightly_frowning_face": "🙁", + "frowning_face": "☹", + "persevere": "😣", + "confounded": "😖", + "tired_face": "😫", + "weary": "😩", + "triumph": "😤", + "open_mouth": "😮", + "scream": "😱", + "fearful": "😨", + "cold_sweat": "😰", + "hushed": "😯", + "frowning": "😦", + "anguished": "😧", + "cry": "😢", + "disappointed_relieved": "😥", + "drooling_face": "🤤", + "sleepy": "😪", + "sweat": "😓", + "sob": "😭", + "dizzy_face": "😵", + "astonished": "😲", + "zipper_mouth_face": "🤐", + "nauseated_face": "🤢", + "sneezing_face": "🤧", + "vomiting": "🤮", + "mask": "😷", + "face_with_thermometer": "🤒", + "face_with_head_bandage": "🤕", + "sleeping": "😴", + "zzz": "💤", + "poop": "💩", + "smiling_imp": "😈", + "imp": "👿", + "japanese_ogre": "👹", + "japanese_goblin": "👺", + "skull": "💀", + "ghost": "👻", + "alien": "👽", + "robot": "🤖", + "smiley_cat": "😺", + "smile_cat": "😸", + "joy_cat": "😹", + "heart_eyes_cat": "😻", + "smirk_cat": "😼", + "kissing_cat": "😽", + "scream_cat": "🙀", + "crying_cat_face": "😿", + "pouting_cat": "😾", + "palms_up": "🤲", + "raised_hands": "🙌", + "clap": "👏", + "wave": "👋", + "call_me_hand": "🤙", + "+1": "👍", + "-1": "👎", + "facepunch": "👊", + "fist": "✊", + "fist_left": "🤛", + "fist_right": "🤜", + "v": "✌", + "ok_hand": "👌", + "raised_hand": "✋", + "raised_back_of_hand": "🤚", + "open_hands": "👐", + "muscle": "💪", + "pray": "🙏", + "handshake": "🤝", + "point_up": "☝", + "point_up_2": "👆", + "point_down": "👇", + "point_left": "👈", + "point_right": "👉", + "fu": "🖕", + "raised_hand_with_fingers_splayed": "🖐", + "love_you": "🤟", + "metal": "🤘", + "crossed_fingers": "🤞", + "vulcan_salute": "🖖", + "writing_hand": "✍", + "selfie": "🤳", + "nail_care": "💅", + "lips": "👄", + "tongue": "👅", + "ear": "👂", + "nose": "👃", + "eye": "👁", + "eyes": "👀", + "brain": "🧠", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "speaking_head": "🗣", + "baby": "👶", + "child": "🧒", + "boy": "👦", + "girl": "👧", + "adult": "🧑", + "man": "👨", + "woman": "👩", + "blonde_woman": "👱‍♀️", + "blonde_man": "👱", + "bearded_person": "🧔", + "older_adult": "🧓", + "older_man": "👴", + "older_woman": "👵", + "man_with_gua_pi_mao": "👲", + "woman_with_headscarf": "🧕", + "woman_with_turban": "👳‍♀️", + "man_with_turban": "👳", + "policewoman": "👮‍♀️", + "policeman": "👮", + "construction_worker_woman": "👷‍♀️", + "construction_worker_man": "👷", + "guardswoman": "💂‍♀️", + "guardsman": "💂", + "female_detective": "🕵️‍♀️", + "male_detective": "🕵", + "woman_health_worker": "👩‍⚕️", + "man_health_worker": "👨‍⚕️", + "woman_farmer": "👩‍🌾", + "man_farmer": "👨‍🌾", + "woman_cook": "👩‍🍳", + "man_cook": "👨‍🍳", + "woman_student": "👩‍🎓", + "man_student": "👨‍🎓", + "woman_singer": "👩‍🎤", + "man_singer": "👨‍🎤", + "woman_teacher": "👩‍🏫", + "man_teacher": "👨‍🏫", + "woman_factory_worker": "👩‍🏭", + "man_factory_worker": "👨‍🏭", + "woman_technologist": "👩‍💻", + "man_technologist": "👨‍💻", + "woman_office_worker": "👩‍💼", + "man_office_worker": "👨‍💼", + "woman_mechanic": "👩‍🔧", + "man_mechanic": "👨‍🔧", + "woman_scientist": "👩‍🔬", + "man_scientist": "👨‍🔬", + "woman_artist": "👩‍🎨", + "man_artist": "👨‍🎨", + "woman_firefighter": "👩‍🚒", + "man_firefighter": "👨‍🚒", + "woman_pilot": "👩‍✈️", + "man_pilot": "👨‍✈️", + "woman_astronaut": "👩‍🚀", + "man_astronaut": "👨‍🚀", + "woman_judge": "👩‍⚖️", + "man_judge": "👨‍⚖️", + "mrs_claus": "🤶", + "santa": "🎅", + "sorceress": "🧙‍♀️", + "wizard": "🧙‍♂️", + "woman_elf": "🧝‍♀️", + "man_elf": "🧝‍♂️", + "woman_vampire": "🧛‍♀️", + "man_vampire": "🧛‍♂️", + "woman_zombie": "🧟‍♀️", + "man_zombie": "🧟‍♂️", + "woman_genie": "🧞‍♀️", + "man_genie": "🧞‍♂️", + "mermaid": "🧜‍♀️", + "merman": "🧜‍♂️", + "woman_fairy": "🧚‍♀️", + "man_fairy": "🧚‍♂️", + "angel": "👼", + "pregnant_woman": "🤰", + "breastfeeding": "🤱", + "princess": "👸", + "prince": "🤴", + "bride_with_veil": "👰", + "man_in_tuxedo": "🤵", + "running_woman": "🏃‍♀️", + "running_man": "🏃", + "walking_woman": "🚶‍♀️", + "walking_man": "🚶", + "dancer": "💃", + "man_dancing": "🕺", + "dancing_women": "👯", + "dancing_men": "👯‍♂️", + "couple": "👫", + "two_men_holding_hands": "👬", + "two_women_holding_hands": "👭", + "bowing_woman": "🙇‍♀️", + "bowing_man": "🙇", + "man_facepalming": "🤦", + "woman_facepalming": "🤦‍♀️", + "woman_shrugging": "🤷", + "man_shrugging": "🤷‍♂️", + "tipping_hand_woman": "💁", + "tipping_hand_man": "💁‍♂️", + "no_good_woman": "🙅", + "no_good_man": "🙅‍♂️", + "ok_woman": "🙆", + "ok_man": "🙆‍♂️", + "raising_hand_woman": "🙋", + "raising_hand_man": "🙋‍♂️", + "pouting_woman": "🙎", + "pouting_man": "🙎‍♂️", + "frowning_woman": "🙍", + "frowning_man": "🙍‍♂️", + "haircut_woman": "💇", + "haircut_man": "💇‍♂️", + "massage_woman": "💆", + "massage_man": "💆‍♂️", + "woman_in_steamy_room": "🧖‍♀️", + "man_in_steamy_room": "🧖‍♂️", + "couple_with_heart_woman_man": "💑", + "couple_with_heart_woman_woman": "👩‍❤️‍👩", + "couple_with_heart_man_man": "👨‍❤️‍👨", + "couplekiss_man_woman": "💏", + "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", + "couplekiss_man_man": "👨‍❤️‍💋‍👨", + "family_man_woman_boy": "👪", + "family_man_woman_girl": "👨‍👩‍👧", + "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", + "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", + "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", + "family_woman_woman_boy": "👩‍👩‍👦", + "family_woman_woman_girl": "👩‍👩‍👧", + "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", + "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", + "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", + "family_man_man_boy": "👨‍👨‍👦", + "family_man_man_girl": "👨‍👨‍👧", + "family_man_man_girl_boy": "👨‍👨‍👧‍👦", + "family_man_man_boy_boy": "👨‍👨‍👦‍👦", + "family_man_man_girl_girl": "👨‍👨‍👧‍👧", + "family_woman_boy": "👩‍👦", + "family_woman_girl": "👩‍👧", + "family_woman_girl_boy": "👩‍👧‍👦", + "family_woman_boy_boy": "👩‍👦‍👦", + "family_woman_girl_girl": "👩‍👧‍👧", + "family_man_boy": "👨‍👦", + "family_man_girl": "👨‍👧", + "family_man_girl_boy": "👨‍👧‍👦", + "family_man_boy_boy": "👨‍👦‍👦", + "family_man_girl_girl": "👨‍👧‍👧", + "coat": "🧥", + "womans_clothes": "👚", + "tshirt": "👕", + "jeans": "👖", + "necktie": "👔", + "dress": "👗", + "bikini": "👙", + "kimono": "👘", + "lipstick": "💄", + "kiss": "💋", + "footprints": "👣", + "high_heel": "👠", + "sandal": "👡", + "boot": "👢", + "mans_shoe": "👞", + "athletic_shoe": "👟", + "socks": "🧦", + "gloves": "🧤", + "scarf": "🧣", + "womans_hat": "👒", + "tophat": "🎩", + "billed_hat": "🧢", + "rescue_worker_helmet": "⛑", + "mortar_board": "🎓", + "crown": "👑", + "school_satchel": "🎒", + "pouch": "👝", + "purse": "👛", + "handbag": "👜", + "briefcase": "💼", + "eyeglasses": "👓", + "dark_sunglasses": "🕶", + "ring": "💍", + "closed_umbrella": "🌂", + "dog": "🐶", + "cat": "🐱", + "mouse": "🐭", + "hamster": "🐹", + "rabbit": "🐰", + "fox_face": "🦊", + "bear": "🐻", + "panda_face": "🐼", + "koala": "🐨", + "tiger": "🐯", + "lion": "🦁", + "cow": "🐮", + "pig": "🐷", + "pig_nose": "🐽", + "frog": "🐸", + "squid": "🦑", + "octopus": "🐙", + "shrimp": "🦐", + "monkey_face": "🐵", + "gorilla": "🦍", + "see_no_evil": "🙈", + "hear_no_evil": "🙉", + "speak_no_evil": "🙊", + "monkey": "🐒", + "chicken": "🐔", + "penguin": "🐧", + "bird": "🐦", + "baby_chick": "🐤", + "hatching_chick": "🐣", + "hatched_chick": "🐥", + "duck": "🦆", + "eagle": "🦅", + "owl": "🦉", + "bat": "🦇", + "wolf": "🐺", + "boar": "🐗", + "horse": "🐴", + "unicorn": "🦄", + "honeybee": "🐝", + "bug": "🐛", + "butterfly": "🦋", + "snail": "🐌", + "beetle": "🐞", + "ant": "🐜", + "grasshopper": "🦗", + "spider": "🕷", + "scorpion": "🦂", + "crab": "🦀", + "snake": "🐍", + "lizard": "🦎", + "t-rex": "🦖", + "sauropod": "🦕", + "turtle": "🐢", + "tropical_fish": "🐠", + "fish": "🐟", + "blowfish": "🐡", + "dolphin": "🐬", + "shark": "🦈", + "whale": "🐳", + "whale2": "🐋", + "crocodile": "🐊", + "leopard": "🐆", + "zebra": "🦓", + "tiger2": "🐅", + "water_buffalo": "🐃", + "ox": "🐂", + "cow2": "🐄", + "deer": "🦌", + "dromedary_camel": "🐪", + "camel": "🐫", + "giraffe": "🦒", + "elephant": "🐘", + "rhinoceros": "🦏", + "goat": "🐐", + "ram": "🐏", + "sheep": "🐑", + "racehorse": "🐎", + "pig2": "🐖", + "rat": "🐀", + "mouse2": "🐁", + "rooster": "🐓", + "turkey": "🦃", + "dove": "🕊", + "dog2": "🐕", + "poodle": "🐩", + "cat2": "🐈", + "rabbit2": "🐇", + "chipmunk": "🐿", + "hedgehog": "🦔", + "paw_prints": "🐾", + "dragon": "🐉", + "dragon_face": "🐲", + "cactus": "🌵", + "christmas_tree": "🎄", + "evergreen_tree": "🌲", + "deciduous_tree": "🌳", + "palm_tree": "🌴", + "seedling": "🌱", + "herb": "🌿", + "shamrock": "☘", + "four_leaf_clover": "🍀", + "bamboo": "🎍", + "tanabata_tree": "🎋", + "leaves": "🍃", + "fallen_leaf": "🍂", + "maple_leaf": "🍁", + "ear_of_rice": "🌾", + "hibiscus": "🌺", + "sunflower": "🌻", + "rose": "🌹", + "wilted_flower": "🥀", + "tulip": "🌷", + "blossom": "🌼", + "cherry_blossom": "🌸", + "bouquet": "💐", + "mushroom": "🍄", + "chestnut": "🌰", + "jack_o_lantern": "🎃", + "shell": "🐚", + "spider_web": "🕸", + "earth_americas": "🌎", + "earth_africa": "🌍", + "earth_asia": "🌏", + "full_moon": "🌕", + "waning_gibbous_moon": "🌖", + "last_quarter_moon": "🌗", + "waning_crescent_moon": "🌘", + "new_moon": "🌑", + "waxing_crescent_moon": "🌒", + "first_quarter_moon": "🌓", + "waxing_gibbous_moon": "🌔", + "new_moon_with_face": "🌚", + "full_moon_with_face": "🌝", + "first_quarter_moon_with_face": "🌛", + "last_quarter_moon_with_face": "🌜", + "sun_with_face": "🌞", + "crescent_moon": "🌙", + "star": "⭐", + "star2": "🌟", + "dizzy": "💫", + "sparkles": "✨", + "comet": "☄", + "sunny": "☀️", + "sun_behind_small_cloud": "🌤", + "partly_sunny": "⛅", + "sun_behind_large_cloud": "🌥", + "sun_behind_rain_cloud": "🌦", + "cloud": "☁️", + "cloud_with_rain": "🌧", + "cloud_with_lightning_and_rain": "⛈", + "cloud_with_lightning": "🌩", + "zap": "⚡", + "fire": "🔥", + "boom": "💥", + "snowflake": "❄️", + "cloud_with_snow": "🌨", + "snowman": "⛄", + "snowman_with_snow": "☃", + "wind_face": "🌬", + "dash": "💨", + "tornado": "🌪", + "fog": "🌫", + "open_umbrella": "☂", + "umbrella": "☔", + "droplet": "💧", + "sweat_drops": "💦", + "ocean": "🌊", + "green_apple": "🍏", + "apple": "🍎", + "pear": "🍐", + "tangerine": "🍊", + "lemon": "🍋", + "banana": "🍌", + "watermelon": "🍉", + "grapes": "🍇", + "strawberry": "🍓", + "melon": "🍈", + "cherries": "🍒", + "peach": "🍑", + "pineapple": "🍍", + "coconut": "🥥", + "kiwi_fruit": "🥝", + "avocado": "🥑", + "broccoli": "🥦", + "tomato": "🍅", + "eggplant": "🍆", + "cucumber": "🥒", + "carrot": "🥕", + "hot_pepper": "🌶", + "potato": "🥔", + "corn": "🌽", + "sweet_potato": "🍠", + "peanuts": "🥜", + "honey_pot": "🍯", + "croissant": "🥐", + "bread": "🍞", + "baguette_bread": "🥖", + "pretzel": "🥨", + "cheese": "🧀", + "egg": "🥚", + "bacon": "🥓", + "steak": "🥩", + "pancakes": "🥞", + "poultry_leg": "🍗", + "meat_on_bone": "🍖", + "fried_shrimp": "🍤", + "fried_egg": "🍳", + "hamburger": "🍔", + "fries": "🍟", + "stuffed_flatbread": "🥙", + "hotdog": "🌭", + "pizza": "🍕", + "sandwich": "🥪", + "canned_food": "🥫", + "spaghetti": "🍝", + "taco": "🌮", + "burrito": "🌯", + "green_salad": "🥗", + "shallow_pan_of_food": "🥘", + "ramen": "🍜", + "stew": "🍲", + "fish_cake": "🍥", + "fortune_cookie": "🥠", + "sushi": "🍣", + "bento": "🍱", + "curry": "🍛", + "rice_ball": "🍙", + "rice": "🍚", + "rice_cracker": "🍘", + "oden": "🍢", + "dango": "🍡", + "shaved_ice": "🍧", + "ice_cream": "🍨", + "icecream": "🍦", + "pie": "🥧", + "cake": "🍰", + "birthday": "🎂", + "custard": "🍮", + "candy": "🍬", + "lollipop": "🍭", + "chocolate_bar": "🍫", + "popcorn": "🍿", + "dumpling": "🥟", + "doughnut": "🍩", + "cookie": "🍪", + "milk_glass": "🥛", + "beer": "🍺", + "beers": "🍻", + "clinking_glasses": "🥂", + "wine_glass": "🍷", + "tumbler_glass": "🥃", + "cocktail": "🍸", + "tropical_drink": "🍹", + "champagne": "🍾", + "sake": "🍶", + "tea": "🍵", + "cup_with_straw": "🥤", + "coffee": "☕", + "baby_bottle": "🍼", + "spoon": "🥄", + "fork_and_knife": "🍴", + "plate_with_cutlery": "🍽", + "bowl_with_spoon": "🥣", + "takeout_box": "🥡", + "chopsticks": "🥢", + "soccer": "⚽", + "basketball": "🏀", + "football": "🏈", + "baseball": "⚾", + "tennis": "🎾", + "volleyball": "🏐", + "rugby_football": "🏉", + "8ball": "🎱", + "golf": "⛳", + "golfing_woman": "🏌️‍♀️", + "golfing_man": "🏌", + "ping_pong": "🏓", + "badminton": "🏸", + "goal_net": "🥅", + "ice_hockey": "🏒", + "field_hockey": "🏑", + "cricket": "🏏", + "ski": "🎿", + "skier": "⛷", + "snowboarder": "🏂", + "person_fencing": "🤺", + "women_wrestling": "🤼‍♀️", + "men_wrestling": "🤼‍♂️", + "woman_cartwheeling": "🤸‍♀️", + "man_cartwheeling": "🤸‍♂️", + "woman_playing_handball": "🤾‍♀️", + "man_playing_handball": "🤾‍♂️", + "ice_skate": "⛸", + "curling_stone": "🥌", + "sled": "🛷", + "bow_and_arrow": "🏹", + "fishing_pole_and_fish": "🎣", + "boxing_glove": "🥊", + "martial_arts_uniform": "🥋", + "rowing_woman": "🚣‍♀️", + "rowing_man": "🚣", + "climbing_woman": "🧗‍♀️", + "climbing_man": "🧗‍♂️", + "swimming_woman": "🏊‍♀️", + "swimming_man": "🏊", + "woman_playing_water_polo": "🤽‍♀️", + "man_playing_water_polo": "🤽‍♂️", + "woman_in_lotus_position": "🧘‍♀️", + "man_in_lotus_position": "🧘‍♂️", + "surfing_woman": "🏄‍♀️", + "surfing_man": "🏄", + "bath": "🛀", + "basketball_woman": "⛹️‍♀️", + "basketball_man": "⛹", + "weight_lifting_woman": "🏋️‍♀️", + "weight_lifting_man": "🏋", + "biking_woman": "🚴‍♀️", + "biking_man": "🚴", + "mountain_biking_woman": "🚵‍♀️", + "mountain_biking_man": "🚵", + "horse_racing": "🏇", + "business_suit_levitating": "🕴", + "trophy": "🏆", + "running_shirt_with_sash": "🎽", + "medal_sports": "🏅", + "medal_military": "🎖", + "1st_place_medal": "🥇", + "2nd_place_medal": "🥈", + "3rd_place_medal": "🥉", + "reminder_ribbon": "🎗", + "rosette": "🏵", + "ticket": "🎫", + "tickets": "🎟", + "performing_arts": "🎭", + "art": "🎨", + "circus_tent": "🎪", + "woman_juggling": "🤹‍♀️", + "man_juggling": "🤹‍♂️", + "microphone": "🎤", + "headphones": "🎧", + "musical_score": "🎼", + "musical_keyboard": "🎹", + "drum": "🥁", + "saxophone": "🎷", + "trumpet": "🎺", + "guitar": "🎸", + "violin": "🎻", + "clapper": "🎬", + "video_game": "🎮", + "space_invader": "👾", + "dart": "🎯", + "game_die": "🎲", + "slot_machine": "🎰", + "bowling": "🎳", + "red_car": "🚗", + "taxi": "🚕", + "blue_car": "🚙", + "bus": "🚌", + "trolleybus": "🚎", + "racing_car": "🏎", + "police_car": "🚓", + "ambulance": "🚑", + "fire_engine": "🚒", + "minibus": "🚐", + "truck": "🚚", + "articulated_lorry": "🚛", + "tractor": "🚜", + "kick_scooter": "🛴", + "motorcycle": "🏍", + "bike": "🚲", + "motor_scooter": "🛵", + "rotating_light": "🚨", + "oncoming_police_car": "🚔", + "oncoming_bus": "🚍", + "oncoming_automobile": "🚘", + "oncoming_taxi": "🚖", + "aerial_tramway": "🚡", + "mountain_cableway": "🚠", + "suspension_railway": "🚟", + "railway_car": "🚃", + "train": "🚋", + "monorail": "🚝", + "bullettrain_side": "🚄", + "bullettrain_front": "🚅", + "light_rail": "🚈", + "mountain_railway": "🚞", + "steam_locomotive": "🚂", + "train2": "🚆", + "metro": "🚇", + "tram": "🚊", + "station": "🚉", + "flying_saucer": "🛸", + "helicopter": "🚁", + "small_airplane": "🛩", + "airplane": "✈️", + "flight_departure": "🛫", + "flight_arrival": "🛬", + "sailboat": "⛵", + "motor_boat": "🛥", + "speedboat": "🚤", + "ferry": "⛴", + "passenger_ship": "🛳", + "rocket": "🚀", + "artificial_satellite": "🛰", + "seat": "💺", + "canoe": "🛶", + "anchor": "⚓", + "construction": "🚧", + "fuelpump": "⛽", + "busstop": "🚏", + "vertical_traffic_light": "🚦", + "traffic_light": "🚥", + "checkered_flag": "🏁", + "ship": "🚢", + "ferris_wheel": "🎡", + "roller_coaster": "🎢", + "carousel_horse": "🎠", + "building_construction": "🏗", + "foggy": "🌁", + "tokyo_tower": "🗼", + "factory": "🏭", + "fountain": "⛲", + "rice_scene": "🎑", + "mountain": "⛰", + "mountain_snow": "🏔", + "mount_fuji": "🗻", + "volcano": "🌋", + "japan": "🗾", + "camping": "🏕", + "tent": "⛺", + "national_park": "🏞", + "motorway": "🛣", + "railway_track": "🛤", + "sunrise": "🌅", + "sunrise_over_mountains": "🌄", + "desert": "🏜", + "beach_umbrella": "🏖", + "desert_island": "🏝", + "city_sunrise": "🌇", + "city_sunset": "🌆", + "cityscape": "🏙", + "night_with_stars": "🌃", + "bridge_at_night": "🌉", + "milky_way": "🌌", + "stars": "🌠", + "sparkler": "🎇", + "fireworks": "🎆", + "rainbow": "🌈", + "houses": "🏘", + "european_castle": "🏰", + "japanese_castle": "🏯", + "stadium": "🏟", + "statue_of_liberty": "🗽", + "house": "🏠", + "house_with_garden": "🏡", + "derelict_house": "🏚", + "office": "🏢", + "department_store": "🏬", + "post_office": "🏣", + "european_post_office": "🏤", + "hospital": "🏥", + "bank": "🏦", + "hotel": "🏨", + "convenience_store": "🏪", + "school": "🏫", + "love_hotel": "🏩", + "wedding": "💒", + "classical_building": "🏛", + "church": "⛪", + "mosque": "🕌", + "synagogue": "🕍", + "kaaba": "🕋", + "shinto_shrine": "⛩", + "watch": "⌚", + "iphone": "📱", + "calling": "📲", + "computer": "💻", + "keyboard": "⌨", + "desktop_computer": "🖥", + "printer": "🖨", + "computer_mouse": "🖱", + "trackball": "🖲", + "joystick": "🕹", + "clamp": "🗜", + "minidisc": "💽", + "floppy_disk": "💾", + "cd": "💿", + "dvd": "📀", + "vhs": "📼", + "camera": "📷", + "camera_flash": "📸", + "video_camera": "📹", + "movie_camera": "🎥", + "film_projector": "📽", + "film_strip": "🎞", + "telephone_receiver": "📞", + "phone": "☎️", + "pager": "📟", + "fax": "📠", + "tv": "📺", + "radio": "📻", + "studio_microphone": "🎙", + "level_slider": "🎚", + "control_knobs": "🎛", + "stopwatch": "⏱", + "timer_clock": "⏲", + "alarm_clock": "⏰", + "mantelpiece_clock": "🕰", + "hourglass_flowing_sand": "⏳", + "hourglass": "⌛", + "satellite": "📡", + "battery": "🔋", + "electric_plug": "🔌", + "bulb": "💡", + "flashlight": "🔦", + "candle": "🕯", + "wastebasket": "🗑", + "oil_drum": "🛢", + "money_with_wings": "💸", + "dollar": "💵", + "yen": "💴", + "euro": "💶", + "pound": "💷", + "moneybag": "💰", + "credit_card": "💳", + "gem": "💎", + "balance_scale": "⚖", + "wrench": "🔧", + "hammer": "🔨", + "hammer_and_pick": "⚒", + "hammer_and_wrench": "🛠", + "pick": "⛏", + "nut_and_bolt": "🔩", + "gear": "⚙", + "chains": "⛓", + "gun": "🔫", + "bomb": "💣", + "hocho": "🔪", + "dagger": "🗡", + "crossed_swords": "⚔", + "shield": "🛡", + "smoking": "🚬", + "skull_and_crossbones": "☠", + "coffin": "⚰", + "funeral_urn": "⚱", + "amphora": "🏺", + "crystal_ball": "🔮", + "prayer_beads": "📿", + "barber": "💈", + "alembic": "⚗", + "telescope": "🔭", + "microscope": "🔬", + "hole": "🕳", + "pill": "💊", + "syringe": "💉", + "thermometer": "🌡", + "label": "🏷", + "bookmark": "🔖", + "toilet": "🚽", + "shower": "🚿", + "bathtub": "🛁", + "key": "🔑", + "old_key": "🗝", + "couch_and_lamp": "🛋", + "sleeping_bed": "🛌", + "bed": "🛏", + "door": "🚪", + "bellhop_bell": "🛎", + "framed_picture": "🖼", + "world_map": "🗺", + "parasol_on_ground": "⛱", + "moyai": "🗿", + "shopping": "🛍", + "shopping_cart": "🛒", + "balloon": "🎈", + "flags": "🎏", + "ribbon": "🎀", + "gift": "🎁", + "confetti_ball": "🎊", + "tada": "🎉", + "dolls": "🎎", + "wind_chime": "🎐", + "crossed_flags": "🎌", + "izakaya_lantern": "🏮", + "email": "✉️", + "envelope_with_arrow": "📩", + "incoming_envelope": "📨", + "e-mail": "📧", + "love_letter": "💌", + "postbox": "📮", + "mailbox_closed": "📪", + "mailbox": "📫", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "package": "📦", + "postal_horn": "📯", + "inbox_tray": "📥", + "outbox_tray": "📤", + "scroll": "📜", + "page_with_curl": "📃", + "bookmark_tabs": "📑", + "bar_chart": "📊", + "chart_with_upwards_trend": "📈", + "chart_with_downwards_trend": "📉", + "page_facing_up": "📄", + "date": "📅", + "calendar": "📆", + "spiral_calendar": "🗓", + "card_index": "📇", + "card_file_box": "🗃", + "ballot_box": "🗳", + "file_cabinet": "🗄", + "clipboard": "📋", + "spiral_notepad": "🗒", + "file_folder": "📁", + "open_file_folder": "📂", + "card_index_dividers": "🗂", + "newspaper_roll": "🗞", + "newspaper": "📰", + "notebook": "📓", + "closed_book": "📕", + "green_book": "📗", + "blue_book": "📘", + "orange_book": "📙", + "notebook_with_decorative_cover": "📔", + "ledger": "📒", + "books": "📚", + "open_book": "📖", + "link": "🔗", + "paperclip": "📎", + "paperclips": "🖇", + "scissors": "✂️", + "triangular_ruler": "📐", + "straight_ruler": "📏", + "pushpin": "📌", + "round_pushpin": "📍", + "triangular_flag_on_post": "🚩", + "white_flag": "🏳", + "black_flag": "🏴", + "rainbow_flag": "🏳️‍🌈", + "closed_lock_with_key": "🔐", + "lock": "🔒", + "unlock": "🔓", + "lock_with_ink_pen": "🔏", + "pen": "🖊", + "fountain_pen": "🖋", + "black_nib": "✒️", + "memo": "📝", + "pencil2": "✏️", + "crayon": "🖍", + "paintbrush": "🖌", + "mag": "🔍", + "mag_right": "🔎", + "heart": "❤️", + "orange_heart": "🧡", + "yellow_heart": "💛", + "green_heart": "💚", + "blue_heart": "💙", + "purple_heart": "💜", + "black_heart": "🖤", + "broken_heart": "💔", + "heavy_heart_exclamation": "❣", + "two_hearts": "💕", + "revolving_hearts": "💞", + "heartbeat": "💓", + "heartpulse": "💗", + "sparkling_heart": "💖", + "cupid": "💘", + "gift_heart": "💝", + "heart_decoration": "💟", + "peace_symbol": "☮", + "latin_cross": "✝", + "star_and_crescent": "☪", + "om": "🕉", + "wheel_of_dharma": "☸", + "star_of_david": "✡", + "six_pointed_star": "🔯", + "menorah": "🕎", + "yin_yang": "☯", + "orthodox_cross": "☦", + "place_of_worship": "🛐", + "ophiuchus": "⛎", + "aries": "♈", + "taurus": "♉", + "gemini": "♊", + "cancer": "♋", + "leo": "♌", + "virgo": "♍", + "libra": "♎", + "scorpius": "♏", + "sagittarius": "♐", + "capricorn": "♑", + "aquarius": "♒", + "pisces": "♓", + "id": "🆔", + "atom_symbol": "⚛", + "u7a7a": "🈳", + "u5272": "🈹", + "radioactive": "☢", + "biohazard": "☣", + "mobile_phone_off": "📴", + "vibration_mode": "📳", + "u6709": "🈶", + "u7121": "🈚", + "u7533": "🈸", + "u55b6": "🈺", + "u6708": "🈷️", + "eight_pointed_black_star": "✴️", + "vs": "🆚", + "accept": "🉑", + "white_flower": "💮", + "ideograph_advantage": "🉐", + "secret": "㊙️", + "congratulations": "㊗️", + "u5408": "🈴", + "u6e80": "🈵", + "u7981": "🈲", + "a": "🅰️", + "b": "🅱️", + "ab": "🆎", + "cl": "🆑", + "o2": "🅾️", + "sos": "🆘", + "no_entry": "⛔", + "name_badge": "📛", + "no_entry_sign": "🚫", + "x": "❌", + "o": "⭕", + "stop_sign": "🛑", + "anger": "💢", + "hotsprings": "♨️", + "no_pedestrians": "🚷", + "do_not_litter": "🚯", + "no_bicycles": "🚳", + "non-potable_water": "🚱", + "underage": "🔞", + "no_mobile_phones": "📵", + "exclamation": "❗", + "grey_exclamation": "❕", + "question": "❓", + "grey_question": "❔", + "bangbang": "‼️", + "interrobang": "⁉️", + "100": "💯", + "low_brightness": "🔅", + "high_brightness": "🔆", + "trident": "🔱", + "fleur_de_lis": "⚜", + "part_alternation_mark": "〽️", + "warning": "⚠️", + "children_crossing": "🚸", + "beginner": "🔰", + "recycle": "♻️", + "u6307": "🈯", + "chart": "💹", + "sparkle": "❇️", + "eight_spoked_asterisk": "✳️", + "negative_squared_cross_mark": "❎", + "white_check_mark": "✅", + "diamond_shape_with_a_dot_inside": "💠", + "cyclone": "🌀", + "loop": "➿", + "globe_with_meridians": "🌐", + "m": "Ⓜ️", + "atm": "🏧", + "sa": "🈂️", + "passport_control": "🛂", + "customs": "🛃", + "baggage_claim": "🛄", + "left_luggage": "🛅", + "wheelchair": "♿", + "no_smoking": "🚭", + "wc": "🚾", + "parking": "🅿️", + "potable_water": "🚰", + "mens": "🚹", + "womens": "🚺", + "baby_symbol": "🚼", + "restroom": "🚻", + "put_litter_in_its_place": "🚮", + "cinema": "🎦", + "signal_strength": "📶", + "koko": "🈁", + "ng": "🆖", + "ok": "🆗", + "up": "🆙", + "cool": "🆒", + "new": "🆕", + "free": "🆓", + "zero": "0️⃣", + "one": "1️⃣", + "two": "2️⃣", + "three": "3️⃣", + "four": "4️⃣", + "five": "5️⃣", + "six": "6️⃣", + "seven": "7️⃣", + "eight": "8️⃣", + "nine": "9️⃣", + "keycap_ten": "🔟", + "asterisk": "*⃣", + "1234": "🔢", + "eject_button": "⏏️", + "arrow_forward": "▶️", + "pause_button": "⏸", + "next_track_button": "⏭", + "stop_button": "⏹", + "record_button": "⏺", + "play_or_pause_button": "⏯", + "previous_track_button": "⏮", + "fast_forward": "⏩", + "rewind": "⏪", + "twisted_rightwards_arrows": "🔀", + "repeat": "🔁", + "repeat_one": "🔂", + "arrow_backward": "◀️", + "arrow_up_small": "🔼", + "arrow_down_small": "🔽", + "arrow_double_up": "⏫", + "arrow_double_down": "⏬", + "arrow_right": "➡️", + "arrow_left": "⬅️", + "arrow_up": "⬆️", + "arrow_down": "⬇️", + "arrow_upper_right": "↗️", + "arrow_lower_right": "↘️", + "arrow_lower_left": "↙️", + "arrow_upper_left": "↖️", + "arrow_up_down": "↕️", + "left_right_arrow": "↔️", + "arrows_counterclockwise": "🔄", + "arrow_right_hook": "↪️", + "leftwards_arrow_with_hook": "↩️", + "arrow_heading_up": "⤴️", + "arrow_heading_down": "⤵️", + "hash": "#️⃣", + "information_source": "ℹ️", + "abc": "🔤", + "abcd": "🔡", + "capital_abcd": "🔠", + "symbols": "🔣", + "musical_note": "🎵", + "notes": "🎶", + "wavy_dash": "〰️", + "curly_loop": "➰", + "heavy_check_mark": "✔️", + "arrows_clockwise": "🔃", + "heavy_plus_sign": "➕", + "heavy_minus_sign": "➖", + "heavy_division_sign": "➗", + "heavy_multiplication_x": "✖️", + "heavy_dollar_sign": "💲", + "currency_exchange": "💱", + "copyright": "©️", + "registered": "®️", + "tm": "™️", + "end": "🔚", + "back": "🔙", + "on": "🔛", + "top": "🔝", + "soon": "🔜", + "ballot_box_with_check": "☑️", + "radio_button": "🔘", + "white_circle": "⚪", + "black_circle": "⚫", + "red_circle": "🔴", + "large_blue_circle": "🔵", + "small_orange_diamond": "🔸", + "small_blue_diamond": "🔹", + "large_orange_diamond": "🔶", + "large_blue_diamond": "🔷", + "small_red_triangle": "🔺", + "black_small_square": "▪️", + "white_small_square": "▫️", + "black_large_square": "⬛", + "white_large_square": "⬜", + "small_red_triangle_down": "🔻", + "black_medium_square": "◼️", + "white_medium_square": "◻️", + "black_medium_small_square": "◾", + "white_medium_small_square": "◽", + "black_square_button": "🔲", + "white_square_button": "🔳", + "speaker": "🔈", + "sound": "🔉", + "loud_sound": "🔊", + "mute": "🔇", + "mega": "📣", + "loudspeaker": "📢", + "bell": "🔔", + "no_bell": "🔕", + "black_joker": "🃏", + "mahjong": "🀄", + "spades": "♠️", + "clubs": "♣️", + "hearts": "♥️", + "diamonds": "♦️", + "flower_playing_cards": "🎴", + "thought_balloon": "💭", + "right_anger_bubble": "🗯", + "speech_balloon": "💬", + "left_speech_bubble": "🗨", + "clock1": "🕐", + "clock2": "🕑", + "clock3": "🕒", + "clock4": "🕓", + "clock5": "🕔", + "clock6": "🕕", + "clock7": "🕖", + "clock8": "🕗", + "clock9": "🕘", + "clock10": "🕙", + "clock11": "🕚", + "clock12": "🕛", + "clock130": "🕜", + "clock230": "🕝", + "clock330": "🕞", + "clock430": "🕟", + "clock530": "🕠", + "clock630": "🕡", + "clock730": "🕢", + "clock830": "🕣", + "clock930": "🕤", + "clock1030": "🕥", + "clock1130": "🕦", + "clock1230": "🕧", + "afghanistan": "🇦🇫", + "aland_islands": "🇦🇽", + "albania": "🇦🇱", + "algeria": "🇩🇿", + "american_samoa": "🇦🇸", + "andorra": "🇦🇩", + "angola": "🇦🇴", + "anguilla": "🇦🇮", + "antarctica": "🇦🇶", + "antigua_barbuda": "🇦🇬", + "argentina": "🇦🇷", + "armenia": "🇦🇲", + "aruba": "🇦🇼", + "australia": "🇦🇺", + "austria": "🇦🇹", + "azerbaijan": "🇦🇿", + "bahamas": "🇧🇸", + "bahrain": "🇧🇭", + "bangladesh": "🇧🇩", + "barbados": "🇧🇧", + "belarus": "🇧🇾", + "belgium": "🇧🇪", + "belize": "🇧🇿", + "benin": "🇧🇯", + "bermuda": "🇧🇲", + "bhutan": "🇧🇹", + "bolivia": "🇧🇴", + "caribbean_netherlands": "🇧🇶", + "bosnia_herzegovina": "🇧🇦", + "botswana": "🇧🇼", + "brazil": "🇧🇷", + "british_indian_ocean_territory": "🇮🇴", + "british_virgin_islands": "🇻🇬", + "brunei": "🇧🇳", + "bulgaria": "🇧🇬", + "burkina_faso": "🇧🇫", + "burundi": "🇧🇮", + "cape_verde": "🇨🇻", + "cambodia": "🇰🇭", + "cameroon": "🇨🇲", + "canada": "🇨🇦", + "canary_islands": "🇮🇨", + "cayman_islands": "🇰🇾", + "central_african_republic": "🇨🇫", + "chad": "🇹🇩", + "chile": "🇨🇱", + "cn": "🇨🇳", + "christmas_island": "🇨🇽", + "cocos_islands": "🇨🇨", + "colombia": "🇨🇴", + "comoros": "🇰🇲", + "congo_brazzaville": "🇨🇬", + "congo_kinshasa": "🇨🇩", + "cook_islands": "🇨🇰", + "costa_rica": "🇨🇷", + "croatia": "🇭🇷", + "cuba": "🇨🇺", + "curacao": "🇨🇼", + "cyprus": "🇨🇾", + "czech_republic": "🇨🇿", + "denmark": "🇩🇰", + "djibouti": "🇩🇯", + "dominica": "🇩🇲", + "dominican_republic": "🇩🇴", + "ecuador": "🇪🇨", + "egypt": "🇪🇬", + "el_salvador": "🇸🇻", + "equatorial_guinea": "🇬🇶", + "eritrea": "🇪🇷", + "estonia": "🇪🇪", + "ethiopia": "🇪🇹", + "eu": "🇪🇺", + "falkland_islands": "🇫🇰", + "faroe_islands": "🇫🇴", + "fiji": "🇫🇯", + "finland": "🇫🇮", + "fr": "🇫🇷", + "french_guiana": "🇬🇫", + "french_polynesia": "🇵🇫", + "french_southern_territories": "🇹🇫", + "gabon": "🇬🇦", + "gambia": "🇬🇲", + "georgia": "🇬🇪", + "de": "🇩🇪", + "ghana": "🇬🇭", + "gibraltar": "🇬🇮", + "greece": "🇬🇷", + "greenland": "🇬🇱", + "grenada": "🇬🇩", + "guadeloupe": "🇬🇵", + "guam": "🇬🇺", + "guatemala": "🇬🇹", + "guernsey": "🇬🇬", + "guinea": "🇬🇳", + "guinea_bissau": "🇬🇼", + "guyana": "🇬🇾", + "haiti": "🇭🇹", + "honduras": "🇭🇳", + "hong_kong": "🇭🇰", + "hungary": "🇭🇺", + "iceland": "🇮🇸", + "india": "🇮🇳", + "indonesia": "🇮🇩", + "iran": "🇮🇷", + "iraq": "🇮🇶", + "ireland": "🇮🇪", + "isle_of_man": "🇮🇲", + "israel": "🇮🇱", + "it": "🇮🇹", + "cote_divoire": "🇨🇮", + "jamaica": "🇯🇲", + "jp": "🇯🇵", + "jersey": "🇯🇪", + "jordan": "🇯🇴", + "kazakhstan": "🇰🇿", + "kenya": "🇰🇪", + "kiribati": "🇰🇮", + "kosovo": "🇽🇰", + "kuwait": "🇰🇼", + "kyrgyzstan": "🇰🇬", + "laos": "🇱🇦", + "latvia": "🇱🇻", + "lebanon": "🇱🇧", + "lesotho": "🇱🇸", + "liberia": "🇱🇷", + "libya": "🇱🇾", + "liechtenstein": "🇱🇮", + "lithuania": "🇱🇹", + "luxembourg": "🇱🇺", + "macau": "🇲🇴", + "macedonia": "🇲🇰", + "madagascar": "🇲🇬", + "malawi": "🇲🇼", + "malaysia": "🇲🇾", + "maldives": "🇲🇻", + "mali": "🇲🇱", + "malta": "🇲🇹", + "marshall_islands": "🇲🇭", + "martinique": "🇲🇶", + "mauritania": "🇲🇷", + "mauritius": "🇲🇺", + "mayotte": "🇾🇹", + "mexico": "🇲🇽", + "micronesia": "🇫🇲", + "moldova": "🇲🇩", + "monaco": "🇲🇨", + "mongolia": "🇲🇳", + "montenegro": "🇲🇪", + "montserrat": "🇲🇸", + "morocco": "🇲🇦", + "mozambique": "🇲🇿", + "myanmar": "🇲🇲", + "namibia": "🇳🇦", + "nauru": "🇳🇷", + "nepal": "🇳🇵", + "netherlands": "🇳🇱", + "new_caledonia": "🇳🇨", + "new_zealand": "🇳🇿", + "nicaragua": "🇳🇮", + "niger": "🇳🇪", + "nigeria": "🇳🇬", + "niue": "🇳🇺", + "norfolk_island": "🇳🇫", + "northern_mariana_islands": "🇲🇵", + "north_korea": "🇰🇵", + "norway": "🇳🇴", + "oman": "🇴🇲", + "pakistan": "🇵🇰", + "palau": "🇵🇼", + "palestinian_territories": "🇵🇸", + "panama": "🇵🇦", + "papua_new_guinea": "🇵🇬", + "paraguay": "🇵🇾", + "peru": "🇵🇪", + "philippines": "🇵🇭", + "pitcairn_islands": "🇵🇳", + "poland": "🇵🇱", + "portugal": "🇵🇹", + "puerto_rico": "🇵🇷", + "qatar": "🇶🇦", + "reunion": "🇷🇪", + "romania": "🇷🇴", + "ru": "🇷🇺", + "rwanda": "🇷🇼", + "st_barthelemy": "🇧🇱", + "st_helena": "🇸🇭", + "st_kitts_nevis": "🇰🇳", + "st_lucia": "🇱🇨", + "st_pierre_miquelon": "🇵🇲", + "st_vincent_grenadines": "🇻🇨", + "samoa": "🇼🇸", + "san_marino": "🇸🇲", + "sao_tome_principe": "🇸🇹", + "saudi_arabia": "🇸🇦", + "senegal": "🇸🇳", + "serbia": "🇷🇸", + "seychelles": "🇸🇨", + "sierra_leone": "🇸🇱", + "singapore": "🇸🇬", + "sint_maarten": "🇸🇽", + "slovakia": "🇸🇰", + "slovenia": "🇸🇮", + "solomon_islands": "🇸🇧", + "somalia": "🇸🇴", + "south_africa": "🇿🇦", + "south_georgia_south_sandwich_islands": "🇬🇸", + "kr": "🇰🇷", + "south_sudan": "🇸🇸", + "es": "🇪🇸", + "sri_lanka": "🇱🇰", + "sudan": "🇸🇩", + "suriname": "🇸🇷", + "swaziland": "🇸🇿", + "sweden": "🇸🇪", + "switzerland": "🇨🇭", + "syria": "🇸🇾", + "taiwan": "🇹🇼", + "tajikistan": "🇹🇯", + "tanzania": "🇹🇿", + "thailand": "🇹🇭", + "timor_leste": "🇹🇱", + "togo": "🇹🇬", + "tokelau": "🇹🇰", + "tonga": "🇹🇴", + "trinidad_tobago": "🇹🇹", + "tunisia": "🇹🇳", + "tr": "🇹🇷", + "turkmenistan": "🇹🇲", + "turks_caicos_islands": "🇹🇨", + "tuvalu": "🇹🇻", + "uganda": "🇺🇬", + "ukraine": "🇺🇦", + "united_arab_emirates": "🇦🇪", + "uk": "🇬🇧", + "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "us": "🇺🇸", + "us_virgin_islands": "🇻🇮", + "uruguay": "🇺🇾", + "uzbekistan": "🇺🇿", + "vanuatu": "🇻🇺", + "vatican_city": "🇻🇦", + "venezuela": "🇻🇪", + "vietnam": "🇻🇳", + "wallis_futuna": "🇼🇫", + "western_sahara": "🇪🇭", + "yemen": "🇾🇪", + "zambia": "🇿🇲", + "zimbabwe": "🇿🇼", +} diff --git a/functest.py b/functest.py new file mode 100644 index 0000000..4cc9427 --- /dev/null +++ b/functest.py @@ -0,0 +1,13 @@ +def f(): + print("very secret variable leaked") + +def g(): + #h = lambda : (print("hi"), print("hello")) + yn(f, "leak secret? ") + +def yn(task, question): + x = input(question) + if x == "y": + task() + +g() diff --git a/interactive.py b/interactive.py new file mode 100644 index 0000000..7682290 --- /dev/null +++ b/interactive.py @@ -0,0 +1,14 @@ +from telethon import sync, TelegramClient +import os + +if not ("TTTC_API_ID" in os.environ or "TTTC_API_HASH" in os.environ): + print("Please set your environment variables \"TTTC_API_ID\" and \"TTTC_API_HASH\" accordingly.") + print("Please consult https://core.telegram.org/api/obtaining_api_id on how to get your own API id and hash.") + quit(1) +api_id = os.environ["TTTC_API_ID"] +api_hash = os.environ["TTTC_API_HASH"] + +client = TelegramClient("tttc", api_id, api_hash) +client.connect() +chats = client.get_dialogs() +print("client: TelegramClient chats: [Dialog]") diff --git a/mainview.py b/mainview.py new file mode 100644 index 0000000..ef43c2c --- /dev/null +++ b/mainview.py @@ -0,0 +1,451 @@ +from asyncio import Condition +import telethon +from telethon import events +import resources +import os +import curses +from subprocess import call +import drawtool +import emojis +import shlex +import sqlite3 +from telethon.utils import get_display_name +import datetime +import re +from tttcutils import debug, show_stacktrace +import subprocess + + +class MainView(): + def __init__(self, client, stdscr): + self.stdscr = stdscr + self.client = client + self.inputevent = Condition() + self.client.add_event_handler(self.on_message, events.NewMessage) + self.client.add_event_handler(self.on_user_update, events.UserUpdate) + # TODO + # self.client.add_event_handler(self.on_read, events.MessageRead) + self.text_emojis = True + + self.inputs = "" + self.inputs_cursor = 0 + + self.drawtool = drawtool.Drawtool(self) + self.fin = False + from config import colors as colorconfig + self.colors = colorconfig.get_colors() + self.ready = False + + self.search_result = None + self.search_index = None + self.search_box = "" + self.vimline_box = "" + self.command_box = "" + + # index corresponds to the index in self.dialogs + self.selected_chat = 0 + # index offset + self.selected_chat_offset = 0 + + self.selected_message = None + + self.mode = "normal" + + async def quit(self): + self.fin = True + with await self.inputevent: + self.inputevent.notify() + + async def on_user_update(self, event): + user_id = event.user_id + if event.online != None: + for dialog in self.dialogs: + if event.online == True: + dialog["online_until"] = event.until + elif dialog["online_until"]: + now = datetime.datetime.now().astimezone() + until = dialog["online_until"].astimezone() + if (now - until).seconds > 0: + dialog["online_until"] = None + dialog["online"] = False + if dialog["dialog"].entity.id == user_id: + dialog["online"] = event.online + + async def on_message(self, event): + # move chats with news up + for idx, dialog in enumerate(self.dialogs): + if dialog["dialog"].id == event.chat_id: + # stuff to do upon arriving messages + newmessage = await self.client.get_messages(dialog["dialog"], 1) + dialog["messages"].insert(0, newmessage[0]) + if not event.out: + dialog["unread_count"] += 1 + front = self.dialogs.pop(idx) + self.dialogs = [front] + self.dialogs + break + # auto adjust relative replys to match shifted message offsets + if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id: + if self.inputs.startswith("r"): + num = self.inputs[1:].split()[0] + try: +# num = int(s[1:].split()[0]) + number = int(num) + msg = self.inputs.replace("r" + num, "r" + str(number+1)) + self.inputs = msg + except: + pass + # dont switch the dialoge upon arriving messages + if idx == self.selected_chat: + self.selected_chat = 0 + elif idx > self.selected_chat: + self.selected_chat += 1 + elif idx < self.selected_chat: + pass + await self.drawtool.redraw() + + async def textinput(self): + self.inputs = "" + with await self.inputevent: + await self.inputevent.wait() + if self.fin: + return "" + out = self.inputs + self.inputs = "" + return out + + async def run(self): + try: + chats = await self.client.get_dialogs() + except sqlite3.OperationalError: + self.stdscr.addstr("Database is locked. Cannot connect with this session. Aborting") + self.stdscr.refresh() + await self.textinput() + await self.quit() + self.dialogs = [ + { + "dialog": dialog, + "unread_count": dialog.unread_count, + "online": dialog.entity.status.to_dict()["_"] == "UserStatusOnline" if hasattr(dialog.entity, "status") and dialog.entity.status else None, + "online_until": None, + # "last_seen": dialog.entity.status.to_dict()["was_online"] if online == False else None, + } for dialog in chats ] + await self.drawtool.redraw() + self.ready = True + while True: + s = await self.textinput() + if self.fin: + return + if s.startswith("r"): + try: + num = int(s[1:].split()[0]) + except: + continue + s = s.replace("r" + str(num) + " ", "") + reply_msg = self.dialogs[self.selected_chat]["messages"][num] + s = emojis.encode(s) + reply = await reply_msg.reply(s) + await self.on_message(reply) + elif s.startswith("media"): + try: + num = int(s[5:].split()[0]) + except: + continue + message = self.dialogs[self.selected_chat]["messages"][num] + if message.media: + os.makedirs("/tmp/tttc/", exist_ok=True) + path = await self.client.download_media(message.media, "/tmp/tttc/") + # TODO mute calls + if message.media.photo: + sizes = message.media.photo.sizes + w, h = sizes[0].w, sizes[0].h + # w, h + basesize = 1500 + w3m_command=f"0;1;0;0;{basesize};{int(basesize*h/w)};;;;;{path}\n4;\n3;" + W3MIMGDISPLAY="/usr/lib/w3m/w3mimgdisplay" + os.system(f"echo -e '{w3m_command}' | {W3MIMGDISPLAY} & disown") + await self.textinput() + else: + subprocess.call(["xdg-open", "{shlex.quote(path)}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) + #os.system(f"(xdg-open {shlex.quote(path)} 2>&1 > /dev/null) & disown") + else: + s = emojis.encode(s) + outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) + await self.on_message(outgoing_message) + await self.drawtool.redraw() + + def select_next_chat(self): + # if wrapping not allowed: + # self.selected_chat = min(self.selected_chat + 1, len(self.dialogs) - 1) + self.selected_chat = (self.selected_chat + 1) % (len(self.dialogs)) + self.center_selected_chat() + + def select_prev_chat(self): + # if wrapping not allowed: + # self.selected_chat = max(self.selected_chat - 1, 0) + self.selected_chat = (self.selected_chat - 1) % (len(self.dialogs)) + self.center_selected_chat() + + def center_selected_chat(self): + if self.selected_chat < self.drawtool.chats_num // 2: + self.selected_chat_offset = 0 + elif self.selected_chat > len(self.dialogs) - self.drawtool.chats_num // 2: + self.selected_chat_offset = len(self.dialogs) - self.drawtool.chats_num + else: + self.selected_chat_offset = self.selected_chat - self.drawtool.chats_num // 2 + + def select_chat(self, index): + if index < -1 or index >= len(self.dialogs): + return + if index == -1: + index = len(self.dialogs) - 1 + while index < self.selected_chat: + self.select_prev_chat() + else: + while index > self.selected_chat: + self.select_next_chat() + + + def is_subsequence(self, xs, ys): + xs = list(xs) + for y in ys: + if xs and xs[0] == y: + xs.pop(0) + return not xs + + def search_chats(self, query = None): + if query is None: + query = self.search_box + if query is None: + return # we dont search for "" + filter_function = self.is_subsequence + filter_function = lambda x, y: x in y + self.search_result = [ idx for (idx, dialog) in enumerate(self.dialogs) + if filter_function(query.lower(), get_display_name(dialog["dialog"].entity).lower())] + self.search_index = -1 + + def search_next(self): + if not self.search_result: + return + if self.search_index == -1: + import bisect + self.search_index = bisect.bisect_left(self.search_result, self.selected_chat) + self.select_chat(self.search_result[self.search_index % len(self.search_result)]) + self.center_selected_chat() + return + self.search_index = (self.search_index + 1) % len(self.search_result) + index = self.search_result[self.search_index] + self.select_chat(index) + self.center_selected_chat() + + def search_prev(self): + if not self.search_result: + return + if self.search_index == -1: + import bisect + self.search_index = bisect.bisect_right(self.search_result, self.selected_chat) + self.select_chat(self.search_result[self.search_index]) + self.center_selected_chat() + return + self.search_index = (self.search_index - 1) % len(self.search_result) + self.select_chat(self.search_result[self.search_index % len(self.search_result)]) + self.center_selected_chat() + + async def call_command(self): + command = self.vimline_box + if command == "q": + await self.quit() + elif command == "pfd": + m = "" + for i in range(len(self.inputs)): + m += self.inputs[i].lower() if i%2==0 else self.inputs[i].lower().swapcase() + self.inputs = m + + async def send_message(self): + if not self.inputs: + return + s = self.inputs + s = emojis.encode(s) + outgoing_message = await self.dialogs[self.selected_chat]["dialog"].send_message(s) + await self.on_message(outgoing_message) + await self.mark_read() + self.center_selected_chat() + self.inputs = "" + + async def mark_read(self): + chat = self.dialogs[self.selected_chat] + dialog = chat["dialog"] + lastmessage = chat["messages"][0] + await self.client.send_read_acknowledge(dialog, lastmessage) + self.dialogs[self.selected_chat]["unread_count"] = 0 + + async def show_media(self, num = None): + if not num: + return + message = self.dialogs[self.selected_chat]["messages"][num] + if message.media: + os.makedirs("/tmp/tttc/", exist_ok=True) + # TODO test if file exists, ask for confirmation to replace or download again + path = await self.client.download_media(message.media, "/tmp/tttc/") + if hasattr(message.media, "photo") and False: + sizes = message.media.photo.sizes + w, h = sizes[0].w, sizes[0].h + # w, h + basesize = 300 + w3m_command=f"0;1;0;0;{basesize};{int(basesize*h/w)};;;;;{path}\n4;\n3;" + W3MIMGDISPLAY="/usr/lib/w3m/w3mimgdisplay" + echo_sp = subprocess.Popen(["echo", "-e", f"{w3m_command}"], stdout = subprocess.PIPE) + w3m_sp = subprocess.Popen([f"{W3MIMGDISPLAY}"], stdin = echo_sp.stdout) + else: + subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) + + async def handle_key(self, key): + if not self.ready: + return + if key == "RESIZE": + await self.drawtool.resize() + return + if self.mode == "search": + if key == "ESCAPE" or key == "RETURN": + self.mode = "normal" + elif key == "BACKSPACE": + if self.search_box == "": + self.mode = "normal" + else: + self.search_box = self.search_box[0:-1] + self.search_chats() + self.search_next() + else: + self.search_box += key + self.search_chats() + self.search_next() + elif self.mode == "vimmode": + if key == "ESCAPE": + self.mode = "normal" + elif key == "RETURN": + await self.call_command() + self.vimline_box = "" + self.mode = "normal" + elif key == "BACKSPACE": + if self.vimline_box == "": + self.mode = "normal" + else: + self.vimline_box = self.vimline_box[0:-1] + else: + self.vimline_box += key + elif self.mode == "normal": + num = None + try: + num = int(key) + except: + pass + if num is not None: + self.command_box += str(num) + await self.drawtool.redraw() + return + elif key == ":": + self.mode = "vimmode" + self.vimline_box = "" + elif key == "RETURN" or key == "y": + await self.send_message() + elif key == "q": + await self.quit() + #elif key == "D": + # for i in range(10): + # self.select_prev_chat() + #elif key == "d": + # for i in range(10): + # self.select_next_chat() + elif key == "C": + self.select_prev_chat() + elif key == "c": + self.select_next_chat() + elif key == "e": + self.text_emojis ^= True + elif key == "R": + await self.mark_read() + elif key == "d": + if self.command_box: + try: + n = int(self.command_box) + except: + return + if n >= len(self.dialogs[self.selected_chat]["messages"]): + #TODO: alert user + return + to_delete = self.dialogs[self.selected_chat]["messages"][n] + await to_delete.delete() + self.dialogs[self.selected_chat]["messages"].pop(n) + self.command_box = "" + await self.drawtool.redraw() + elif key == "r": + if self.command_box: + try: + n = int(self.command_box) + except: + return + reply_to = self.dialogs[self.selected_chat]["messages"][n] + s = emojis.encode(self.inputs) + reply = await reply_to.reply(s) + await self.on_message(reply) + self.command_box = "" + self.inputs = "" + elif key == "m": + if self.command_box: + try: + n = int(self.command_box) + except: + return + self.command_box = "" + await self.show_media(n) + elif key == "M": + self.center_selected_chat() + elif key == "HOME" or key == "g": + self.select_chat(0) + elif key == "END" or key == "G": + self.select_chat(-1) + elif key == "i": + self.mode = "insert" + elif key == "n": + self.search_next() + elif key == "N": + self.search_prev() + elif key == "/": + self.mode = "search" + self.search_box = "" + elif key == " ": + self.drawtool.show_indices ^= True + elif self.mode == "insert": + if key == "ESCAPE": + self.mode = "normal" + elif key == "LEFT": + self.insert_move_left() + elif key == "RIGHT": + self.insert_move_right() + elif key == "BACKSPACE": + self.inputs = self.inputs[0:-1] + elif key == "RETURN": + self.inputs += "\n" + else: + self.inputs += key + self.command_box = "" + await self.drawtool.redraw() + + def insert_move_left(self): + self.inputs_cursor = max(0, self.cursor - 1) + + def insert_move_right(self): + self.inputs_cursor = min(len(self.inputs), self.cursor + 1) + + async def handle_key_old(self, key): + if key == "RETURN": + with await self.inputevent: + self.inputevent.notify() + elif key == "": + chat = self.dialogs[self.selected_chat]["dialog"] + last_message = self.dialogs[self.selected_chat]["messages"][0] + await self.client.send_read_acknowledge(chat, max_id=last_message.id) + self.dialogs[self.selected_chat]["unread_count"] = 0 + else: + self.inputs += key + self.drawtool.redraw() + diff --git a/resources.py b/resources.py new file mode 100644 index 0000000..b516aa1 --- /dev/null +++ b/resources.py @@ -0,0 +1,45 @@ +key_mapping = { + "\n": "RETURN", + "\x1b":"ESCAPE", + "\t":"TAB", + 343: "NUM_RETURN", + 263: "BACKSPACE", + "": "BACKSPACE", + 330: "DEL", + 331: "INSERT", + 262: "HOME", + 360: "END", + 338: "PGDOWN", + 339: "PGUP", + 410: "RESIZE", + 258: "DOWN", + 259: "UP", + 260: "LEFT", + 261: "RIGHT" +} + +tttc_logo= \ +""" ____. + ____.----' ,##/ _ _ _ + ____.----' ,##" / | |_ | |_ | |_ ___ + _----' ,###" / | __| | __| | __| / __| + -_ .#####" / | |_ | |_ | |_ | (__ + '-._ .#######" / \__| \__| \__| \___| + '-..#######" / + \#####" / + \##/, / + \/ '-, / + '-, / + ' """.split("\n") + +auth_text = { + 0: ["", "Please enter your full phone number:", " %phone%^"], + 1: ["An error occured trying to sign you in.", "Please enter your full phone number:", " %phone%^"], + 2: ["", "Sending authentification code..."], + 3: ["", "You have been sent a message with", "an activation code. Please provide said code:", " %code%^"], + 4: ["Incorrect code.", "You have been sent a message with", "an activation code. Please provide said code:", " %code%^"], + 5: ["", "Signing in..."], + 6: ["", "Two factor authentification is enabled.", "Please provide your password: %pass%^"], + 7: ["Incorrect password.", "Two factor authentification is enabled.", "Please provide your password: %pass%^"], + 8: ["", "Signing in..."] +} diff --git a/tttc.py b/tttc.py new file mode 100755 index 0000000..5449219 --- /dev/null +++ b/tttc.py @@ -0,0 +1,76 @@ +#!/bin/python + +from authview import AuthView +from functools import partial +from mainview import MainView +from queue import Queue +from telethon import TelegramClient +from telethon import events +from time import sleep +import argparse +import asyncio +import commandline +import concurrent +import curses +import resources +import sys +import os +from tttcutils import debug, show_stacktrace +import tttcutils + +class Display: + def __init__(self, loop): + self.loop = loop + api_id, api_hash = tttcutils.assert_environment() + self.client = TelegramClient("tttc", api_id, api_hash, loop=self.loop) + + + def __enter__(self): + self.stdscr = curses.initscr() + curses.start_color() + curses.use_default_colors() + for i in range(curses.COLORS): + curses.init_pair(i, i, -1); + curses.noecho() + curses.cbreak() + self.stdscr.keypad(1) + self.stdscr.refresh() + return self + + def __exit__(self, *args): + curses.nocbreak() + self.stdscr.keypad(0) + curses.echo() + curses.endwin() + + async def main(self): + tasks = [ + self.run(), + self.get_ch() + ] + await asyncio.wait(tasks) + + + async def run(self): + self.view = AuthView(self.client, self.stdscr) + await self.view.run() + self.view = MainView(self.client, self.stdscr) + await self.view.run() + + async def get_ch(self): + while True: + pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) + a = await self.loop.run_in_executor(pool, self.stdscr.get_wch) + out = resources.key_mapping.get(a, str(a)) + if self.view: + await self.view.handle_key(out) + if self.view.fin: + return + + +if commandline.handle(): + exit() +elif __name__ == '__main__': + loop = asyncio.get_event_loop() + with Display(loop) as display: + loop.run_until_complete(display.main()) diff --git a/tttcutils.py b/tttcutils.py new file mode 100644 index 0000000..7c73a70 --- /dev/null +++ b/tttcutils.py @@ -0,0 +1,19 @@ +import os +import shlex +import traceback + +def debug(x): + with open("/tmp/tttc.log", "a") as f: + f.write(str(x) + "\n") + os.system(f"notify-send {shlex.quote(str(x))}") + +def show_stacktrace(): + a = traceback.format_exc() + os.system(f"notify-send {shlex.quote(a)}") + +def assert_environment(): + if not ("TTTC_API_ID" in os.environ or "TTTC_API_HASH" in os.environ): + print("Please set your environment variables \"TTTC_API_ID\" and \"TTTC_API_HASH\" accordingly.") + print("Please consult https://core.telegram.org/api/obtaining_api_id on how to get your own API id and hash.") + exit(1) + return os.environ["TTTC_API_ID"], os.environ["TTTC_API_HASH"] diff --git a/vimbindings.md b/vimbindings.md new file mode 100644 index 0000000..af1c23f --- /dev/null +++ b/vimbindings.md @@ -0,0 +1,60 @@ +A a append/append +B b back/BACK +C c change +D d delete +E e end/END +F f find/Find +G g go/GO +H h LEFT +I i INSERT +J j DOWN +K k UP +L l RIGHT +M m +N n +O o insert line +P p put +Q q quit +R r nReply, vReply/replace +S s +T t till +U u +V v visual mode + v - text visual + V - message visual + vV - text line visual +W w word +X x del +Y y yank +Z z +0 $ goto 0 $ +? help + +RETURN send?? + +^A +^B +^C kill +^D +^E +^F +^G +^H +^I +^J +^K +^L +^M +^N +^O +^P +^Q +^R +^S +^T +^U +^V +^W +^X +^Y +^Z