diff --git a/.editorconfig b/.editorconfig index 21d312a1..d58e13ec 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,5 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{yaml,yml}] +[*.{yaml,yml,py}] indent_style = space diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index ded17fd0..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,174 +0,0 @@ -{ - "env": { - "node": true, - "es6": true - }, - "extends": "airbnb-base", - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 8 - }, - "plugins": [ - "import" - ], - "rules": { - "indent": [ - "error", - "tab", - { - "FunctionDeclaration": { - "parameters": 2, - "body": 1 - }, - "FunctionExpression": { - "parameters": 2, - "body": 1 - }, - "VariableDeclarator": 2, - "CallExpression": { - "arguments": 2 - }, - "MemberExpression": "off", - "ImportDeclaration": "first" - } - ], - "object-curly-newline": [ - "error", - { - "consistent": true - } - ], - "one-var": [ - "error", - { - "initialized": "never", - "uninitialized": "always" - } - ], - "one-var-declaration-per-line": [ - "error", - "initializations" - ], - "quotes": [ - "error", - "double" - ], - "semi": [ - "error", - "never" - ], - "comma-dangle": [ - "error", - "always-multiline" - ], - "max-len": [ - "warn", - 120 - ], - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "after-used", - "varsIgnorePattern": "_" - } - ], - "space-before-function-paren": [ - "error", - { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - } - ], - "func-style": [ - "warn", - "declaration", - { - "allowArrowFunctions": true - } - ], - "id-length": [ - "warn", - { - "max": 25, - "exceptions": [ - "i", - "x", - "y", - "$", - "_" - ] - } - ], - "import/order": [ - "warn", - { - "groups": [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index" - ], - "newlines-between": "never" - } - ], - "arrow-body-style": [ - "error", - "as-needed" - ], - "complexity": [ - "warn", - 11 - ], - "new-cap": [ - "warn", - { - "newIsCap": true, - "capIsNew": true, - "capIsNewExceptions": ["MTProto"] - } - ], - "no-empty": [ - "error", - { - "allowEmptyCatch": true - } - ], - "no-cond-assign": [ - "error", - "except-parens" - ], - "function-paren-newline": "off", - "no-labels": "off", - "no-control-regex": "off", - "no-void": "off", - "func-names": "off", - "no-continue": "off", - "default-case": "off", - "no-plusplus": "off", - "no-use-before-define": "off", - "no-restricted-syntax": "off", - "no-return-assign": "off", - "no-param-reassign": "off", - "arrow-parens": "off", - "no-nested-ternary": "off", - "no-new": "off", - "no-tabs": "off", - "no-prototype-builtins": "off", - "no-console": "off", - "class-methods-use-this": "off", - "prefer-destructuring": "off", - "camelcase": "off", - "spaced-comment": "off", - "no-bitwise": "off", - "no-case-declarations": "off", - "no-template-curly-in-string": "off", - "no-await-in-loop": "off", - "no-restricted-globals": "off", - "no-fallthrough": "off", - "no-underscore-dangle": "off" - } -} diff --git a/.gitignore b/.gitignore index 76a76c87..1b019739 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ -node_modules/ .idea/ -jsdoc/ + +.venv +pip-selfcheck.json +*.pyc +__pycache__ + config.yaml registration.yaml *.db +*.session diff --git a/README.md b/README.md index a8159561..ddc0947b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ # mautrix-telegram -**Work in progress: Expect bugs, do not use in production.** - A Matrix-Telegram puppeting bridge. ## Discussion @@ -10,20 +8,26 @@ A Telegram chat will be created once the bridge is stable enough. ## Usage ### Setup -0. Clone the repository and install packages with `npm install`. -1. Create a copy of `example-config.yaml` and fill out the fields. -2. Generate the appservice registration with `./mautrix-telegram -g`. +0. Clone the repository +1. Set up the virtual environment + 1. Create with `virtualenv -p /usr/bin/python3 .venv` + 2. Activate with `source .venv/bin/activate` +2. Install dependencies with `pip install -r requirements.txt` +3. Create a copy of `example-config.yaml` and fill out the fields. +4. Generate the appservice registration with `python -m mautrix_telegram -g`. You can use the `-c` and `-r` flags to change the location of the config and registration files. They default to `config.yaml` and `registration.yaml` respectively. -3. Run the bridge `./mautrix-telegram`. You can also use forever: `forever start mautrix-telegram` (probably, I didn't actually test it). -4. Invite the appservice bot to a private room and view the commands with `help`. +5. Run the bridge `python -m mautrix_telegram`. +6. Invite the appservice bot to a private room and view the commands with `help`. ### Logging in -0. Make sure you have set up the bridge and have an open management room (a room with no other users than the appservice bot). +0. Make sure you have set up the bridge and have an open management room (a room with no other + users than the appservice bot). 1. Request a Telegram auth code with `login `. 2. Send your auth code to the management room. 3. If you have two-factor authentication enabled, send your password to the room. -4. If all prior steps were executed successfully, the bridge should now create rooms for all your Telegram dialogs and invite you to them. +4. If all prior steps were executed successfully, the bridge should now create rooms for all your + Telegram groups and channels and invite you to them. ### Chatting #### Group chats and channels @@ -32,73 +36,96 @@ You should be automatically invited into portal rooms for your groups and channe 2. receive a messages in the chat or 3. receive an invite to the chat -Support for inviting users both Telegram and Matrix users to Telegram portal rooms is planned, but not yet implemented. +Inviting Telegram puppets to rooms should work. However, please don't invite non-puppet Matrix +users to portal rooms yet. + +You can also create a Telegram chat for an existing Matrix room using `!tg create` in the room. +However, there are some restrictions: +* The room must have a title. +* The AS bot must be invited first (before puppets) and be given power level 100. +* The AS bot must be the only user to have power level 100. #### Private messaging You can start private chats by simply inviting the Matrix puppet of the Telegram user you want to chat with to a private room. If you don't know the MXID of the puppet, you can search for users using the `search ` management command. +You can also initiate chats with the `pm` command using the username, phone number or user ID. + #### Bot commands Initiating chats with bots is no different from initiating chats with real Telegram users. -The bridge translates `!commands` into `/commands`, which allows you to use Telegram bots without constantly escaping -the slash. Please note that when messaging a bot for the first time, it may expect you to run `!start` first. The bridge -does not do this automatically. +~~The bridge translates `!commands` into `/commands`, which allows you to use Telegram bots without constantly escaping +the slash.~~ Please note that when messaging a bot for the first time, it may expect you to run ~~`!start`~~ `/start` first. +The bridge does not do this automatically. ## Features & Roadmap * Matrix → Telegram * [x] Plaintext messages * [x] Formatted messages - * [x] Bot commands (!command -> /command) + * [ ] Bot commands (!command -> /command) * [x] Mentions - * [x] Locations - * [ ] Images - * [ ] Files - * [ ] Message redactions - * [ ] Presence (currently always shown as online on Telegram) + * [x] Rich quotes + * [ ] Locations (not implemented in Riot) + * [x] Images + * [x] Files + * [x] Message redactions + * [ ] Presence (may not be possible, currently always shown as online on Telegram) * [ ] Typing notifications (may not be possible) * [ ] Pinning messages - * [ ] Power level + * [x] Power level * [ ] Membership actions - * [x] Inviting - * [x] Kicking + * [x] Inviting puppets + * [ ] Inviting Matrix users who have logged in to Telegram + * [ ] Kicking * [ ] Joining/leaving * [ ] Room metadata changes - * [x] Room invites * Telegram → Matrix * [x] Plaintext messages * [x] Formatted messages * [x] Bot commands (/command -> !command) * [x] Mentions + * [x] Replies + * [x] Forwards * [x] Images * [x] Locations - * [ ] Stickers (somewhat works through document upload, no preview though) + * [x] Stickers * [x] Audio messages - * [ ] Video messages + * [x] Video messages * [x] Documents - * [ ] Message deletions + * [ ] Message deletions (no way to tell difference between user-specific deletion and global deletion) + * [ ] Message edits (not supported in Matrix) + * [x] Avatars * [x] Presence * [x] Typing notifications * [ ] Pinning messages - * [ ] Admin status + * [x] Admin/chat creator status * [x] Membership actions * [x] Inviting * [x] Kicking * [x] Joining/leaving * [x] Chat metadata changes + * [ ] Public channel username changes * [x] Initial chat metadata - * [ ] Message edits -* Initiating chats - * [x] Automatic portal creation for groups/channels at startup - * [x] Automatic portal creation for groups/channels when receiving invite/message - * [x] Private chat creation by inviting Telegram user to new room - * [ ] Joining public channels/supergroups using room aliases - * [x] Searching for Telegram users using management commands - * [x] Creating new Telegram chats from Matrix - * [x] Creating Telegram chats for existing Matrix rooms + * [x] Supergroup upgrade * Misc - * [ ] Use optional bot to relay messages for unauthenticated Matrix users - * [x] Properly handle upgrading groups to supergroups - * [x] Allow upgrading group to supergroup from Matrix - * [ ] Handle public channel username changes + * [x] Automatic portal creation + * [x] At startup + * [x] When receiving invite or message + * [x] Private chat creation by inviting Matrix puppet of Telegram user to new room + * [ ] Option to use bot to relay messages for unauthenticated Matrix users + * [ ] Option to use own Matrix account for messages sent from other Telegram clients + * [ ] Joining public channels/supergroups using room aliases + * [ ] Joining chats with room aliases + * [ ] Name of public channel/supergroup as alias + * [ ] (Maybe) Invite link token as alias +* Commands + * [x] Logging in and out (`login` + code entering, `logout`) + * [ ] Registering (`register`) + * [x] Searching for users (`search`) + * [ ] Searching contacts locally + * [x] Starting private chats (`pm`) + * [x] Joining chats with invite links (`join`) + * [x] Creating a Telegram chat for an existing Matrix room (`create`) + * [ ] Upgrading the chat of a portal room into a supergroup (`upgrade`) + * [ ] Getting the Telegram invite link to a Matrix room (`invitelink`) diff --git a/example-config.yaml b/example-config.yaml index 99d74995..ad3e1e72 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -4,25 +4,41 @@ homeserver: domain: matrix.org # Application service host/registration related details +# Changing these values requires regeneration of the registration. appservice: - # The protocol the homeserver should use when connecting to the appservice. + # The protocol the homeserver should use when connecting to this appservice. # Usually "http" or "https". protocol: http + + # The hostname and port where the homeserver can find this appservice. hostname: localhost port: 8080 - id: telegram + # Whether or not to enable debug messages in the console. debug: false - # Path to the registration file. This is automatically updated when generating a registration. - registration: ./registration.yaml + # The unique ID of this appservice. + id: telegram + # Username of the appservice bot. + bot_username: telegrambot + bot_displayname: Telegram bridge bot + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" # Bridge config bridge: - # ${ID} is replaced with the user ID of the Telegram user. - username_template: "telegram_${ID}" - # ${DISPLAYNAME} is replaced with the display name of the Telegram user. - displayname_template: "${DISPLAYNAME} (Telegram)" + # Localpart template of MXIDs for Telegram users. + # {userid} is replaced with the user ID of the Telegram user. + username_template: "telegram_{userid}" + # Localpart template of room aliases for Telegram portal rooms. + # {groupname} is replaced with the name part of the public channel/group invite link ( https://t.me/{} ) + alias_template: "telegram_{groupname}" + # Displayname template for Telegram users. + # {displayname} is replaced with the display name of the Telegram user. + displayname_template: "{displayname} (Telegram)" + # Set the preferred order of user identifiers which to use in the Matrix puppet display name. # In the (hopefully unlikely) scenario that none of the given keys are found, the numeric user ID is used. # @@ -30,30 +46,20 @@ bridge: # very well be empty. # # Valid keys: - # fullName (First and/or last name) - # fullNameReversed (Last and/or first name) - # firstName - # lastName - # username - # phoneNumber + # "full name" (First and/or last name) + # "full name reversed" (Last and/or first name) + # "first name" + # "last name" + # "username" + # "phone number" displayname_preference: - - fullName + - full name - username - - phoneNumber - # ${NAME} is replaced with the name part of the public channel/group invite link ( https://t.me/${NAME} ) - alias_template: "telegram_${NAME}" - # Username of the bot. The registration must be regenerated to change this. - bot_username: telegrambot + - phone number - # Bridge management command configuration - commands: - # The prefix for all management commands. - # Can be removed to disable management commands in rooms with more than two users. - prefix: "!tg" - # Enables the !tg api ... commands for debugging. - # Do not enable this in production, it allows all whitelisted users to call any Telegram API methods freely. - allow_direct_api_calls: false + # The prefix for commands. Only required in non-management rooms. + command_prefix: "!tg" # Whitelist of user IDs that are allowed to use this bridge. Leave empty to disable. # You can enter a domain without the localpart to allow all users from that homeserver to use the bridge. diff --git a/mautrix-telegram b/mautrix-telegram deleted file mode 120000 index 7f850106..00000000 --- a/mautrix-telegram +++ /dev/null @@ -1 +0,0 @@ -src/index.js \ No newline at end of file diff --git a/mautrix_appservice/__init__.py b/mautrix_appservice/__init__.py new file mode 100644 index 00000000..3d104ad9 --- /dev/null +++ b/mautrix_appservice/__init__.py @@ -0,0 +1,4 @@ +from .appservice import AppService + +__version__ = "0.1.0" +__author__ = "Tulir Asokan " diff --git a/mautrix_appservice/appservice.py b/mautrix_appservice/appservice.py new file mode 100644 index 00000000..bb2fde5f --- /dev/null +++ b/mautrix_appservice/appservice.py @@ -0,0 +1,208 @@ +# matrix-appservice-python - A Matrix Application Service framework written in Python. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Partly based on github.com/Cadair/python-appservice-framework (MIT license) +import asyncio +import logging +import aiohttp +from aiohttp import web +from functools import partial +from contextlib import contextmanager +from .intent_api import HTTPAPI + + +class StateStore: + def __init__(self): + self.memberships = {} + self.power_levels = {} + + def _get_membership(self, room, user): + return self.memberships.get(room, {}).get(user, "left") + + def is_joined(self, room, user): + return self._get_membership(room, user) == "join" + + def _set_membership(self, room, user, membership): + if room not in self.memberships: + self.memberships[room] = {} + self.memberships[room][user] = membership + + def joined(self, room, user): + return self._set_membership(room, user, "join") + + def invited(self, room, user): + return self._set_membership(room, user, "invite") + + def left(self, room, user): + return self._set_membership(room, user, "left") + + def has_power_level_data(self, room): + return room in self.power_levels + + def has_power_level(self, room, user, event): + room_levels = self.power_levels.get(room, {}) + required = room_levels["events"].get(event, 95) + has = room_levels["users"].get(user, 0) + return has >= required + + def set_power_level(self, room, user, level): + if not room in self.power_levels: + self.power_levels[room] = { + "users": {}, + "events": {}, + } + self.power_levels[room]["users"][user] = level + + def set_power_levels(self, room, content): + if "events" not in content: + content["events"] = {} + if "users" not in content: + content["users"] = {} + self.power_levels[room] = content + + +class AppService: + def __init__(self, server, domain, as_token, hs_token, bot_localpart, loop=None, log=None, + query_user=None, query_alias=None): + self.server = server + self.domain = domain + self.as_token = as_token + self.hs_token = hs_token + self.bot_mxid = f"@{bot_localpart}:{domain}" + self.state_store = StateStore() + + self.transactions = [] + + self._http_session = None + self._intent = None + + self.loop = loop or asyncio.get_event_loop() + self.log = log or logging.getLogger("mautrix_appservice") + + self.query_user = query_user or (lambda: None) + self.query_alias = query_alias or (lambda: None) + + self.event_handlers = [] + + self.app = web.Application(loop=self.loop) + self.app.router.add_route("PUT", "/transactions/{transaction_id}", + self._http_handle_transaction) + self.app.router.add_route("GET", "/rooms/{alias}", self._http_query_alias) + self.app.router.add_route("GET", "/users/{user_id}", self._http_query_user) + + @property + def http_session(self): + if self._http_session is None: + raise AttributeError("the http_session attribute can only be used " + "from within the `AppService.run` context manager") + else: + return self._http_session + + @property + def intent(self): + if self._intent is None: + raise AttributeError("the intent attribute can only be used from " + "within the `AppService.run` context manager") + else: + return self._intent + + @contextmanager + def run(self, host="127.0.0.1", port=8080): + self._http_session = aiohttp.ClientSession(loop=self.loop) + self._intent = HTTPAPI(base_url=self.server, bot_mxid=self.bot_mxid, token=self.as_token, + log=self.log, state_store=self.state_store).bot_intent() + + yield partial(aiohttp.web.run_app, self.app, host=host, port=port) + + self._intent = None + self._http_session.close() + self._http_session = None + + def _check_token(self, request): + try: + token = request.rel_url.query["access_token"] + except KeyError: + return False + + if token != self.hs_token: + return False + + return True + + async def _http_query_user(self, request): + if not self._check_token(request): + return web.Response(status=401) + + user_id = request.match_info["userId"] + + try: + response = self.query_user(user_id) + except: + self.log.exception("Exception in user query handler") + return web.Response(status=500) + + if not response: + return web.Response(status=404) + return web.json_response(response) + + async def _http_query_alias(self, request): + if not self._check_token(request): + return web.Response(status=401) + + alias = request.match_info["alias"] + + try: + response = self.query_alias(alias) + except: + self.log.exception("Exception in alias query handler") + return web.Response(status=500) + + if not response: + return web.Response(status=404) + return web.json_response(response) + + async def _http_handle_transaction(self, request): + if not self._check_token(request): + return web.Response(status=401) + + transaction_id = request.match_info["transaction_id"] + if transaction_id in self.transactions: + return web.Response(status=200) + + json = await request.json() + + try: + events = json["events"] + except KeyError: + return web.Response(status=400) + + for event in events: + self.handle_matrix_event(event) + + self.transactions.append(transaction_id) + + return web.json_response({}) + + def handle_matrix_event(self, event): + for handler in self.event_handlers: + try: + handler(event) + except: + self.log.exception("Exception in Matrix event handler") + + def matrix_event_handler(self, func): + self.event_handlers.append(func) + return func diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py new file mode 100644 index 00000000..edd37b85 --- /dev/null +++ b/mautrix_appservice/intent_api.py @@ -0,0 +1,348 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import re +import json +import magic +import urllib.request +from matrix_client.api import MatrixHttpApi +from matrix_client.errors import MatrixRequestError + + +class HTTPAPI(MatrixHttpApi): + def __init__(self, base_url, bot_mxid=None, token=None, identity=None, log=None, + state_store=None): + self.base_url = base_url + self.token = token + self.identity = identity + self.txn_id = 0 + self.bot_mxid = bot_mxid + self.intent_log = log.getChild("intent") + self.log = log.getChild("api") + self.validate_cert = True + self.state_store = state_store + self.children = {} + + def user(self, user): + try: + return self.children[user] + except KeyError: + child = ChildHTTPAPI(user, self) + self.children[user] = child + return child + + def bot_intent(self): + return IntentAPI(self.bot_mxid, self, state_store=self.state_store, log=self.intent_log) + + def intent(self, user): + return IntentAPI(user, self.user(user), self, self.state_store, self.intent_log) + + def _send(self, method, path, content=None, query_params={}, headers={}, + api_path="/_matrix/client/r0"): + if not query_params: + query_params = {} + query_params["user_id"] = self.identity + log_content = content if not isinstance(content, bytes) else f"<{len(content)} bytes>" + self.log.debug("%s %s %s", method, path, log_content) + return super()._send(method, path, content, query_params, headers, api_path=api_path) + + def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False, + invitees=(), initial_state=[]): + """Perform /createRoom. + Args: + alias (str): Optional. The room alias name to set for this room. + is_public (bool): Optional. The public/private visibility. + name (str): Optional. The name for the room. + topic (str): Optional. The topic for the room. + invitees (list): Optional. The list of user IDs to invite. + """ + content = { + "visibility": "public" if is_public else "private" + } + if alias: + content["room_alias_name"] = alias + if invitees: + content["invite"] = invitees + if name: + content["name"] = name + if topic: + content["topic"] = topic + if initial_state: + content["initial_state"] = initial_state + content["is_direct"] = is_direct + + return self._send("POST", "/createRoom", content) + + def set_presence(self, status="online", user=None): + content = { + "presence": status + } + user = user or self.identity + return self._send("PUT", f"/presence/{user}/status", content) + + def set_typing(self, room_id, is_typing=True, timeout=5000, user=None): + content = { + "typing": is_typing + } + if is_typing: + content["timeout"] = timeout + user = user or self.identity + return self._send("PUT", f"/rooms/{room_id}/typing/{user}", content) + + +class ChildHTTPAPI(HTTPAPI): + def __init__(self, user, parent): + self.identity = user + self.token = parent.token + self.base_url = parent.base_url + self.validate_cert = parent.validate_cert + self.log = parent.log + self.parent = parent + + @property + def txn_id(self): + return self.parent.txn_id + + @txn_id.setter + def txn_id(self, value): + self.parent.txn_id = value + + +class IntentError(Exception): + def __init__(self, message, source): + super().__init__(message) + self.source = source + + +def matrix_error_code(err): + try: + data = json.loads(err.content) + return data["errcode"] + except: + return err.content + + +class IntentAPI: + mxid_regex = re.compile("@(.+):(.+)") + + def __init__(self, mxid, client, bot=None, state_store=None, log=None): + self.client = client + self.bot = bot + self.mxid = mxid + self.log = log + + results = self.mxid_regex.search(mxid) + if not results: + raise ValueError("invalid MXID") + self.localpart = results.group(1) + + self.state_store = state_store + self.registered = False + + def user(self, user): + if not self.bot: + return self.client.intent(user) + else: + self.log.warning("Called IntentAPI#user() of child intent object.") + return self.bot.intent(user) + + # region User actions + + def set_display_name(self, name): + self._ensure_registered() + return self.client.set_display_name(self.mxid, name) + + def set_presence(self, status="online"): + self._ensure_registered() + return self.client.set_presence(status) + + def set_avatar(self, url): + self._ensure_registered() + return self.client.set_avatar_url(self.mxid, url) + + def upload_file(self, data, mime_type=None): + self._ensure_registered() + mime_type = mime_type or magic.from_buffer(data, mime=True) + return self.client.media_upload(data, mime_type) + + def download_file(self, url): + self._ensure_registered() + url = self.client.get_download_url(url) + response = urllib.request.urlopen(url) + return response.read() + + # endregion + # region Room actions + + def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False, + invitees=(), initial_state=[]): + self._ensure_registered() + return self.client.create_room(alias, is_public, name, topic, is_direct, invitees, + initial_state) + + def invite(self, room_id, user_id): + self._ensure_joined(room_id) + try: + response = self.client.invite_user(room_id, user_id) + self.state_store.invited(room_id, user_id) + return response + except MatrixRequestError as e: + if matrix_error_code(e) != "M_FORBIDDEN": + raise IntentError(f"Failed to invite {user_id} to {room_id}", e) + + def set_room_avatar(self, room_id, avatar_url, info=None): + content = { + "url": avatar_url, + } + if info: + content["info"] = info + return self.send_state_event(room_id, "m.room.avatar", content) + + def set_room_name(self, room_id, name): + self._ensure_joined(room_id) + self._ensure_has_power_level_for(room_id, "m.room.name") + return self.client.set_room_name(room_id, name) + + def get_power_levels(self, room_id): + self._ensure_joined(room_id) + levels = self.client.get_power_levels(room_id) + self.state_store.set_power_levels(room_id, levels) + return levels + + def set_power_levels(self, room_id, content): + response = self.send_state_event(room_id, "m.room.power_levels", content) + self.state_store.set_power_levels(room_id, content) + return response + + def set_typing(self, room_id, is_typing=True, timeout=5000): + self._ensure_joined(room_id) + return self.client.set_typing(room_id, is_typing, timeout) + + def send_notice(self, room_id, text, html=None): + return self.send_text(room_id, text, html, "m.notice") + + def send_emote(self, room_id, text, html=None): + return self.send_text(room_id, text, html, "m.emote") + + def send_image(self, room_id, url, info={}, text=None): + return self.send_file(room_id, url, info, text, "m.image") + + def send_file(self, room_id, url, info={}, text=None, type="m.file"): + return self.send_message(room_id, { + "msgtype": type, + "url": url, + "body": text or "Uploaded file", + "info": info, + }) + + def send_text(self, room_id, text, html=None, type="m.text"): + if html: + if not text: + text = html + return self.send_message(room_id, { + "body": text, + "msgtype": type, + "format": "org.matrix.custom.html", + "formatted_body": html or text, + }) + else: + return self.send_message(room_id, { + "body": text, + "msgtype": type, + }) + + def send_message(self, room_id, body): + return self.send_event(room_id, "m.room.message", body) + + def error_and_leave(self, room_id, text, html=None): + self._ensure_joined(room_id) + self.send_notice(room_id, text, html=html) + self.leave_room(room_id) + + def kick(self, room_id, user_id, message): + self._ensure_joined(room_id) + return self.client.kick_user(room_id, user_id, message) + + def send_event(self, room_id, type, body, txn_id=None): + self._ensure_joined(room_id) + self._ensure_has_power_level_for(room_id, type) + return self.client.send_message_event(room_id, type, body, txn_id) + + def send_state_event(self, room_id, type, body, state_key=""): + self._ensure_joined(room_id) + self._ensure_has_power_level_for(room_id, type) + return self.client.send_state_event(room_id, type, body, state_key) + + def join_room(self, room_id): + return self._ensure_joined(room_id, ignore_cache=True) + + def leave_room(self, room_id): + self.state_store.left(room_id, self.mxid) + return self.client.leave_room(room_id) + + def get_room_memberships(self, room_id): + return self.client.get_room_members(room_id) + + def get_room_members(self, room_id, allowed_memberships=("join",)): + memberships = self.get_room_memberships(room_id) + return [membership["state_key"] for membership in memberships["chunk"] if + membership["content"]["membership"] in allowed_memberships] + + def get_room_state(self, room_id): + self._ensure_joined(room_id) + return self.client.get_room_state(room_id) + + # endregion + # region Ensure functions + + def _ensure_joined(self, room_id, ignore_cache=False): + if not ignore_cache and self.state_store.is_joined(room_id, self.mxid): + return + self._ensure_registered() + try: + self.client.join_room(room_id) + self.state_store.joined(room_id, self.mxid) + except MatrixRequestError as e: + if matrix_error_code(e) != "M_FORBIDDEN" and not self.bot: + raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e) + try: + self.bot.invite_user(room_id, self.mxid) + self.client.join_room(room_id) + self.state_store.joined(room_id, self.mxid) + except MatrixRequestError as e2: + raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e2) + + def _ensure_registered(self): + if self.registered: + return + try: + self.client.register({"username": self.localpart}) + except MatrixRequestError as e: + if matrix_error_code(e) != "M_USER_IN_USE": + self.log.exception(f"Failed to register {self.mxid}!") + # raise IntentError(f"Failed to register {self.mxid}", e) + self.registered = True + + def _ensure_has_power_level_for(self, room_id, event_type): + if not self.state_store.has_power_level_data(room_id): + self.get_power_levels(room_id) + if self.state_store.has_power_level(room_id, self.mxid, event_type): + return + elif not self.bot: + pass + # raise IntentError(f"Power level of {self.mxid} is not enough for {event_type} in {room_id}") + # TODO implement + + # endregion diff --git a/mautrix_telegram/__init__.py b/mautrix_telegram/__init__.py new file mode 100644 index 00000000..c7327493 --- /dev/null +++ b/mautrix_telegram/__init__.py @@ -0,0 +1,2 @@ +__version__ = "0.1.0" +__author__ = "Tulir Asokan " diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py new file mode 100644 index 00000000..58345b5b --- /dev/null +++ b/mautrix_telegram/__main__.py @@ -0,0 +1,84 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import argparse +import sys +import logging + +import sqlalchemy as sql +from sqlalchemy import orm + +from mautrix_appservice import AppService + +from .base import Base +from .config import Config +from .matrix import MatrixHandler + +from .db import init as init_db +from .user import init as init_user +from .portal import init as init_portal +from .puppet import init as init_puppet +from .formatter import init as init_formatter + +log = logging.getLogger("mau") +time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s") +handler = logging.StreamHandler() +handler.setFormatter(time_formatter) +log.addHandler(handler) + +parser = argparse.ArgumentParser( + description="A Matrix-Telegram puppeting bridge.", + prog="python -m mautrix-telegram") +parser.add_argument("-c", "--config", type=str, default="config.yaml", + metavar="", help="the path to your config file") +parser.add_argument("-g", "--generate-registration", action="store_true", + help="generate registration and quit") +parser.add_argument("-r", "--registration", type=str, default="registration.yaml", + metavar="", help="the path to save the generated registration to") +args = parser.parse_args() + +config = Config(args.config, args.registration) +config.load() + +if args.generate_registration: + config.generate_registration() + config.save() + print(f"Registration generated and saved to {config.registration_path}") + sys.exit(0) + +if config["appservice.debug"]: + log.setLevel(logging.DEBUG) + log.debug("Debug messages enabled.") + +db_engine = sql.create_engine(config.get("appservice.database", "sqlite:///mautrix-telegram.db")) +db_factory = orm.sessionmaker(bind=db_engine) +db_session = orm.scoping.scoped_session(db_factory) +Base.metadata.bind = db_engine +Base.metadata.create_all() + +appserv = AppService(config["homeserver.address"], config["homeserver.domain"], + config["appservice.as_token"], config["appservice.hs_token"], + config["appservice.bot_username"], log=log.getChild("as")) +context = (appserv, db_session, log, config) + + +with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start: + init_db(db_session) + init_formatter(context) + init_portal(context) + init_puppet(context) + init_user(context) + MatrixHandler(context) + start() diff --git a/mautrix_telegram/base.py b/mautrix_telegram/base.py new file mode 100644 index 00000000..c64447da --- /dev/null +++ b/mautrix_telegram/base.py @@ -0,0 +1,2 @@ +from sqlalchemy.ext.declarative import declarative_base +Base = declarative_base() diff --git a/mautrix_telegram/commands.py b/mautrix_telegram/commands.py new file mode 100644 index 00000000..ad59627e --- /dev/null +++ b/mautrix_telegram/commands.py @@ -0,0 +1,377 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from contextlib import contextmanager +import markdown +from telethon.errors import * +from telethon.tl.types import * +from telethon.tl.functions.contacts import SearchRequest +from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest +from telethon.tl.functions.channels import JoinChannelRequest +from . import puppet as pu, portal as po + +command_handlers = {} + + +def command_handler(func): + command_handlers[func.__name__] = func + + +class CommandHandler: + def __init__(self, context): + self.az, self.db, log, self.config = context + self.log = log.getChild("commands") + self.command_prefix = self.config["bridge.command_prefix"] + self._room_id = None + self._is_management = False + self._is_portal = False + + # region Utility functions for handling commands + + def handle(self, room, sender, command, args, is_management, is_portal): + with self.handler(sender, room, command, args, is_management, is_portal) as handle_command: + try: + handle_command(self, sender, args) + except: + self.reply("Fatal error while handling command. Check logs for more details.") + self.log.exception(f"Fatal error handling command " + f"'$cmdprefix {command} {''.join(args)}' from {sender.mxid}") + + @contextmanager + def handler(self, sender, room, command, args, is_management, is_portal): + self._room_id = room + try: + command = command_handlers[command] + except KeyError: + if sender.command_status and "next" in sender.command_status: + args.insert(0, command) + command = sender.command_status["next"] + else: + command = command_handlers["unknown_command"] + self._is_management = is_management + self._is_portal = is_portal + yield command + self._is_management = None + self._is_portal = None + self._room_id = None + + def reply(self, message, allow_html=False, render_markdown=True): + if not self._room_id: + raise AttributeError("the reply function can only be used from within" + "the `CommandHandler.run` context manager") + + message = message.replace("$cmdprefix+sp ", + "" if self._is_management else f"{self.command_prefix} ") + message = message.replace("$cmdprefix", self.command_prefix) + html = None + if render_markdown: + html = markdown.markdown(message, safe_mode="escape" if allow_html else False) + elif allow_html: + html = message + self.az.intent.send_notice(self._room_id, message, html=html) + + # endregion + # region Command handlers + + @command_handler + def ping(self, sender, args): + if not sender.logged_in: + return self.reply("You're not logged in.") + me = sender.client.get_me() + if me: + return self.reply(f"You're logged in as @{me.username}") + else: + return self.reply("You're not logged in.") + + # region Authentication commands + @command_handler + def register(self, sender, args): + self.reply("Not yet implemented.") + + @command_handler + def login(self, sender, args): + if not self._is_management: + return self.reply( + "`login` is a restricted command: you may only run it in management rooms.") + elif sender.logged_in: + return self.reply("You are already logged in.") + elif len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp login `") + phone_number = args[0] + sender.client.send_code_request(phone_number) + sender.client.sign_in(phone_number) + sender.command_status = { + "next": command_handlers["enter_code"], + "action": "Login", + } + return self.reply(f"Login code sent to {phone_number}. Please send the code here.") + + @command_handler + def enter_code(self, sender, args): + if not sender.command_status: + return self.reply("Request a login code first with `$cmdprefix+sp login `") + elif len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp enter_code ") + + try: + user = sender.client.sign_in(code=args[0]) + sender.post_login(user) + sender.command_status = None + return self.reply(f"Successfully logged in as @{user.username}") + except PhoneNumberUnoccupiedError: + return self.reply("That phone number has not been registered." + "Please register with `$cmdprefix+sp register `.") + except PhoneCodeExpiredError: + return self.reply( + "Phone code expired. Try again with `$cmdprefix+sp login `.") + except PhoneCodeInvalidError: + return self.reply("Invalid phone code.") + except PhoneNumberAppSignupForbiddenError: + return self.reply( + "Your phone number does not allow 3rd party apps to sign in.") + except PhoneNumberFloodError: + return self.reply( + "Your phone number has been temporarily blocked for flooding. " + "The block is usually applied for around a day.") + except PhoneNumberBannedError: + return self.reply("Your phone number has been banned from Telegram.") + except SessionPasswordNeededError: + sender.command_status = { + "next": command_handlers["enter_password"], + "action": "Login (password entry)", + } + return self.reply("Your account has two-factor authentication." + "Please send your password here.") + except: + self.log.exception() + return self.reply("Unhandled exception while sending code." + "Check console for more details.") + + @command_handler + def enter_password(self, sender, args): + if not sender.command_status: + return self.reply("Request a login code first with `$cmdprefix+sp login `") + elif len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp enter_password ") + + try: + user = sender.client.sign_in(password=args[0]) + sender.post_login(user) + sender.command_status = None + return self.reply(f"Successfully logged in as @{user.username}") + except PasswordHashInvalidError: + return self.reply("Incorrect password.") + except: + self.log.exception() + return self.reply("Unhandled exception while sending password. " + "Check console for more details.") + + @command_handler + def logout(self, sender, args): + if not sender.logged_in: + return self.reply("You're not logged in.") + if sender.log_out(): + return self.reply("Logged out successfully.") + return self.reply("Failed to log out.") + + # endregion + # region Telegram interaction commands + + @command_handler + def search(self, sender, args): + if len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] ") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + force_remote = False + if args[0] in {"-r", "--remote"}: + args.pop(0) + query = " ".join(args) + if len(query) < 5: + return self.reply("Minimum length of query for remote search is 5 characters.") + found = sender.client(SearchRequest(q=query, limit=10)) + print(found) + # reply = ["**People:**", ""] + reply = ["**Results from Telegram server:**", ""] + for result in found.users: + puppet = pu.Puppet.get(result.id) + puppet.update_info(sender, result) + reply.append( + f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): {puppet.id}") + # reply.extend(("", "**Chats:**", "")) + # for result in found.chats: + # reply.append(f"* {result.title}") + return self.reply("\n".join(reply)) + + @command_handler + def pm(self, sender, args): + if len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp pm `") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + + user = sender.client.get_entity(args[0]) + if not user: + return self.reply("User not found.") + elif not isinstance(user, User): + return self.reply("That doesn't seem to be a user.") + portal = po.Portal.get_by_entity(user, sender.tgid) + portal.create_matrix_room(sender, user, [sender.mxid]) + self.reply(f"Created private chat room with {pu.Puppet.get_displayname(user, False)}") + + def _strip_prefix(self, value, prefixes): + for prefix in prefixes: + if value.startswith(prefix): + return value[len(prefix):] + return value + + @command_handler + def join(self, sender, args): + if len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp join ") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + + regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)") + arg = regex.match(args[0]) + if not arg: + return self.reply("That doesn't look like a Telegram invite link.") + arg = arg.group(1) + if arg.startswith("joinchat/"): + invite_hash = arg[len("joinchat/"):] + try: + check = sender.client(CheckChatInviteRequest(invite_hash)) + print(check) + except InviteHashInvalidError: + return self.reply("Invalid invite link.") + except InviteHashExpiredError: + return self.reply("Invite link expired.") + try: + updates = sender.client(ImportChatInviteRequest(invite_hash)) + except UserAlreadyParticipantError: + return self.reply("You are already in that chat.") + else: + channel = sender.client.get_entity(arg) + if not channel: + return self.reply("Channel/supergroup not found.") + updates = sender.client(JoinChannelRequest(channel)) + for chat in updates.chats: + portal = po.Portal.get_by_entity(chat) + portal.create_matrix_room(sender, chat, [sender.mxid]) + self.reply(f"Created room for {portal.title}") + + @command_handler + def create(self, sender, args): + type = args[0] if len(args) > 0 else "group" + if type not in {"chat", "group", "supergroup", "channel"}: + return self.reply("**Usage:** `$cmdprefix+sp create [`group`/`supergroup`/`channel`]") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + + if po.Portal.get_by_mxid(self._room_id): + return self.reply("This is already a portal room.") + + state = self.az.intent.get_room_state(self._room_id) + title = None + levels = None + for event in state: + if event["type"] == "m.room.name": + title = event["content"]["name"] + elif event["type"] == "m.room.power_levels": + levels = event["content"] + if not title: + return self.reply("Please set a title before creating a Telegram chat.") + elif (not levels or not levels["users"] or self.az.intent.mxid not in levels["users"] or + levels["users"][self.az.intent.mxid] < 100): + return self.reply(f"Please give " + f"[the bridge bot](https://matrix.to/#/{self.az.intent.mxid}) " + f"a power level of 100 before creating a Telegram chat.") + else: + for user, level in levels["users"].items(): + if level >= 100 and user != self.az.intent.mxid: + return self.reply(f"Please make sure only the bridge bot has power level above" + f"99 before creating a Telegram chat.\n\n" + f"Use power level 95 instead of 100 for admins.") + + supergroup = type == "supergroup" + types = { + "supergroup": "channel", + "channel": "channel", + "chat": "chat", + "group": "chat", + } + + portal = po.Portal(tgid=None, mxid=self._room_id, title=title, peer_type=types[type]) + try: + portal.create_telegram_chat(sender, supergroup=supergroup) + except ValueError as e: + return self.reply(e.args[0]) + self.reply(f"Telegram chat created. ID: {portal.tgid}") + + @command_handler + def upgrade(self, sender, args): + self.reply("Not yet implemented.") + + # endregion + # region Command-related commands + @command_handler + def cancel(self, sender, args): + if sender.command_status: + action = sender.command_status["action"] + sender.command_status = None + return self.reply(f"{action} cancelled.") + else: + return self.reply("No ongoing command.") + + @command_handler + def unknown_command(self, sender, args): + if self._is_management: + return self.reply("Unknown command. Try `help` for help.") + else: + return self.reply("Unknown command. Try `$cmdprefix help` for help.") + + @command_handler + def help(self, sender, args): + if self._is_management: + management_status = ("This is a management room: prefixing commands" + "with `$cmdprefix` is not required.\n") + elif self._is_portal: + management_status = ("**This is a portal room**: you must always" + "prefix commands with `$cmdprefix`.\n" + "Management commands will not be sent to Telegram.") + else: + management_status = ("**This is not a management room**: you must" + "prefix commands with `$cmdprefix`.\n") + help = """ +_**Generic bridge commands**: commands for using the bridge that aren't related to Telegram._ +**help** - Show this help message. +**cancel** - Cancel an ongoing action (such as login). + +_**Telegram actions**: commands for using the bridge to interact with Telegram._ +**login** <_phone_> - Request an authentication code. +**logout** - Log out from Telegram. +**ping** - Check if you're logged into Telegram. +**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users. +**pm** <_identifier_> - Open a private chat with the given Telegram user. The identifier is either + the internal user ID, the username or the phone number. +**join** <_link_> - Join a chat with an invite link. +**create** [_type_] - Create a Telegram chat of the given type for the current Matrix room. + The type is either `group`, `supergroup` or `channel` (defaults to `group`). +**upgrade** - Upgrade a normal Telegram group to a supergroup. +""" + return self.reply(management_status + help) + + # endregion + # endregion diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py new file mode 100644 index 00000000..4316e5a4 --- /dev/null +++ b/mautrix_telegram/config.py @@ -0,0 +1,109 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import ruamel.yaml +import random +import string + +yaml = ruamel.yaml.YAML() + +class DictWithRecursion: + def __init__(self, data={}): + self._data = data + + def _recursive_get(self, data, key, default_value): + if '.' in key: + key, next_key = key.split('.', 1) + next_data = data.get(key, {}) + return self._recursive_get(next_data, next_key, default_value) + return data.get(key, default_value) + + def get(self, key, default_value, allow_recursion=True): + if allow_recursion and '.' in key: + return self._recursive_get(self._data, key, default_value) + return self._data.get(key, default_value) + + def __getitem__(self, key): + return self.get(key, None) + + def _recursive_set(self, data, key, value): + if '.' in key: + key, next_key = key.split('.', 1) + if key not in data: + data[key] = {} + next_data = data.get(key, {}) + self._recursive_set(next_data, next_key, value) + return + data[key] = value + + def set(self, key, value, allow_recursion=True): + if allow_recursion and '.' in key: + self._recursive_set(self._data, key, value) + return + self._data[key] = value + + def __setitem__(self, key, value): + self.set(key, value) + + +class Config(DictWithRecursion): + def __init__(self, path, registration_path): + super().__init__() + self.path = path + self.registration_path = registration_path + self._registration = None + + def load(self): + with open(self.path, 'r') as stream: + self._data = yaml.load(stream) + + def save(self): + with open(self.path, 'w') as stream: + yaml.dump(self._data, stream) + if self._registration and self.registration_path: + with open(self.registration_path, 'w') as stream: + yaml.dump(self._registration, stream) + + def _new_token(self): + return "".join(random.choices(string.ascii_lowercase + string.digits, k=64)) + + def generate_registration(self): + homeserver = self["homeserver.domain"] + + username_format = self.get("bridge.username_template", "telegram_{userid}").format(userid=".+") + alias_format = self.get("bridge.alias_template", "telegram_{groupname}").format(groupname=".+") + + self.set("appservice.as_token", self._new_token()) + self.set("appservice.hs_token", self._new_token()) + + appservice = self.get("appservice", {}) + self._registration = { + "id": appservice.get("id", "telegram"), + "as_token": appservice.get("as_token"), + "hs_token": appservice.get("hs_token"), + "namespaces": { + "users": [{ + "exclusive": True, + "regex": f"@{username_format}:{homeserver}" + }], + "aliases": [{ + "exclusive": True, + "regex": f"#{alias_format}:{homeserver}" + }] + }, + "url": f"{appservice.get('protocol')}://{appservice.get('hostname')}:{appservice.get('port')}", + "sender_localpart": appservice.get("bot_username"), + "rate_limited": False + } diff --git a/mautrix_telegram/db.py b/mautrix_telegram/db.py new file mode 100644 index 00000000..1aece700 --- /dev/null +++ b/mautrix_telegram/db.py @@ -0,0 +1,73 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from sqlalchemy import Column, ForeignKey, UniqueConstraint, Integer, String +from .base import Base + + +class Portal(Base): + query = None + __tablename__ = "portal" + + # Telegram chat information + tgid = Column(Integer, primary_key=True) + tg_receiver = Column(Integer, primary_key=True) + peer_type = Column(String) + + # Matrix portal information + mxid = Column(String, unique=True, nullable=True) + + # Telegram chat metadata + username = Column(String, nullable=True) + title = Column(String, nullable=True) + photo_id = Column(String, nullable=True) + + +class Message(Base): + query = None + __tablename__ = "message" + + mxid = Column(String) + mx_room = Column(String) + tgid = Column(Integer, primary_key=True) + user = Column(Integer, ForeignKey("user.tgid"), primary_key=True) + + __table_args__ = (UniqueConstraint('mxid', 'mx_room', 'user', name='_mx_id_room'), ) + + +class User(Base): + query = None + __tablename__ = "user" + + mxid = Column(String, primary_key=True) + tgid = Column(Integer, nullable=True) + tg_username = Column(String, nullable=True) + + +class Puppet(Base): + query = None + __tablename__ = "puppet" + + id = Column(Integer, primary_key=True) + displayname = Column(String, nullable=True) + username = Column(String, nullable=True) + photo_id = Column(String, nullable=True) + + +def init(db_session): + Portal.query = db_session.query_property() + Message.query = db_session.query_property() + User.query = db_session.query_property() + Puppet.query = db_session.query_property() diff --git a/mautrix_telegram/formatter.py b/mautrix_telegram/formatter.py new file mode 100644 index 00000000..6390c208 --- /dev/null +++ b/mautrix_telegram/formatter.py @@ -0,0 +1,314 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import re +from html import escape, unescape +from html.parser import HTMLParser +from collections import deque +from telethon.tl.types import * +from . import user as u, puppet as p +from .db import Message as DBMessage + +log = None + + +# region Matrix to Telegram + +class MessageEntityReply(MessageEntityUnknown): + def __init__(self, offset=0, length=0, msg_id=0): + super().__init__(offset, length) + self.msg_id = msg_id + + +class MatrixParser(HTMLParser): + mention_regex = re.compile("https://matrix.to/#/(@.+)") + reply_regex = re.compile(r"https://matrix.to/#/(!.+?)/(\$.+)") + + def __init__(self, user_id=None): + super().__init__() + self._user_id = user_id + self.text = "" + self.entities = [] + self._building_entities = {} + self._list_counter = 0 + self._open_tags = deque() + self._open_tags_meta = deque() + self._previous_ended_line = True + self._building_reply = False + + def handle_starttag(self, tag, attrs): + self._open_tags.appendleft(tag) + self._open_tags_meta.appendleft(0) + attrs = dict(attrs) + EntityType = None + args = {} + if tag == "strong" or tag == "b": + EntityType = MessageEntityBold + elif tag == "em" or tag == "i": + EntityType = MessageEntityItalic + elif tag == "code": + try: + pre = self._building_entities["pre"] + try: + pre.language = attrs["class"][len("language-"):] + except KeyError: + pass + except KeyError: + EntityType = MessageEntityCode + elif tag == "pre": + EntityType = MessageEntityPre + args["language"] = "" + elif tag == "a": + try: + url = attrs["href"] + except KeyError: + return + mention = self.mention_regex.search(url) + reply = self.reply_regex.search(url) + if mention: + mxid = mention.group(1) + puppet_match = p.Puppet.mxid_regex.search(mxid) + if puppet_match: + user = p.Puppet.get(puppet_match.group(1), create=False) + else: + user = u.User.get_by_mxid(mxid, create=False) + if not user: + return + if user.username: + EntityType = MessageEntityMention + url = f"@{user.username}" + else: + EntityType = MessageEntityMentionName + args["user_id"] = user.tgid + elif reply and self._user_id and ( + len(self.entities) == 0 and len(self._building_entities) == 0): + room_id = reply.group(1) + message_id = reply.group(2) + message = DBMessage.query.filter(DBMessage.mxid == message_id + and DBMessage.mx_room == room_id + and DBMessage.user == self._user_id).one_or_none() + if not message: + return + EntityType = MessageEntityReply + args["msg_id"] = message.tgid + self._building_reply = True + url = None + elif url.startswith("mailto:"): + url = url[len("mailto:"):] + EntityType = MessageEntityEmail + else: + if self.get_starttag_text() == url: + EntityType = MessageEntityUrl + else: + EntityType = MessageEntityTextUrl + args["url"] = url + url = None + self._open_tags_meta.popleft() + self._open_tags_meta.appendleft(url) + + if EntityType and tag not in self._building_entities: + self._building_entities[tag] = EntityType(offset=len(self.text), length=0, **args) + + def _list_depth(self): + depth = 0 + for tag in self._open_tags: + if tag == "ol" or tag == "ul": + depth += 1 + return depth + + def handle_data(self, text): + text = unescape(text) + if self._building_reply: + return + previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else "" + list_format_offset = 0 + if previous_tag == "a": + url = self._open_tags_meta[0] + if url: + text = url + elif len(self._open_tags) > 1 and self._previous_ended_line and previous_tag == "li": + list_type = self._open_tags[1] + indent = (self._list_depth() - 1) * 4 * " " + text = text.strip("\n") + if len(text) == 0: + return + elif list_type == "ul": + text = f"{indent}* {text}" + list_format_offset = len(indent) + 2 + elif list_type == "ol": + n = self._open_tags_meta[1] + n += 1 + self._open_tags_meta[1] = n + text = f"{indent}{n}. {text}" + list_format_offset = len(indent) + 3 + for tag, entity in self._building_entities.items(): + entity.length += len(text.strip("\n")) + entity.offset += list_format_offset + + if text.endswith("\n"): + self._previous_ended_line = True + else: + self._previous_ended_line = False + + self.text += text + + def handle_endtag(self, tag): + try: + self._open_tags.popleft() + self._open_tags_meta.popleft() + except IndexError: + pass + if tag == "a": + self._building_reply = False + if (tag == "ul" or tag == "ol") and self.text.endswith("\n"): + self.text = self.text[:-1] + entity = self._building_entities.pop(tag, None) + if entity: + self.entities.append(entity) + + +def matrix_to_telegram(html, user_id=None): + try: + parser = MatrixParser(user_id) + parser.feed(html) + return parser.text, parser.entities + except: + log.exception("Failed to convert Matrix format:\nhtml=%s", html) + + +# endregion +# region Telegram to Matrix + +def telegram_event_to_matrix(evt, source): + text = evt.message + html = telegram_to_matrix(evt.message, evt.entities) if evt.entities else None + + if evt.fwd_from: + if not html: + html = escape(text) + id = evt.fwd_from.from_id + user = u.User.get_by_tgid(id) + if user: + fwd_from = f"{user.mxid}" + else: + puppet = p.Puppet.get(id, create=False) + if puppet and puppet.displayname: + fwd_from = f"{puppet.displayname}" + else: + user = source.client.get_entity(id) + if user: + fwd_from = p.Puppet.get_displayname(user, format=False) + if not fwd_from: + fwd_from = "Unknown user" + html = (f"Forwarded message from {fwd_from}
" + f"
{html}
") + + if evt.reply_to_msg_id: + msg = DBMessage.query.get((evt.reply_to_msg_id, source.tgid)) + quote = f"Quote
" + if html: + html = quote + html + else: + html = quote + escape(text) + + return text, html + + +def telegram_to_matrix(text, entities): + try: + return _telegram_to_matrix(text, entities) + except: + log.exception("Failed to convert Telegram format:\n" + "message=%s\n" + "entities=%s", + text, entities) + + +def _telegram_to_matrix(text, entities): + if not entities: + return text + html = [] + last_offset = 0 + for entity in entities: + if entity.offset > last_offset: + html.append(escape(text[last_offset:entity.offset])) + elif entity.offset < last_offset: + continue + + skip_entity = False + entity_text = escape(text[entity.offset:entity.offset + entity.length]) + entity_type = type(entity) + + if entity_type == MessageEntityBold: + html.append(f"{entity_text}") + elif entity_type == MessageEntityItalic: + html.append(f"{entity_text}") + elif entity_type == MessageEntityCode: + html.append(f"{entity_text}") + elif entity_type == MessageEntityPre: + if entity.language: + html.append("
"
+                            f"{entity_text}"
+                            "
") + else: + html.append(f"
{entity_text}
") + elif entity_type == MessageEntityMention: + username = entity_text[1:] + + user = u.User.find_by_username(username) + if user: + mxid = user.mxid + else: + puppet = p.Puppet.find_by_username(username) + mxid = puppet.mxid if puppet else None + if mxid: + html.append(f"{entity_text}") + else: + skip_entity = True + elif entity_type == MessageEntityMentionName: + user = u.User.get_by_tgid(entity.user_id) + if user: + mxid = user.mxid + else: + puppet = p.Puppet.get(entity.user_id, create=False) + mxid = puppet.mxid if puppet else None + if mxid: + html.append(f"{entity_text}") + else: + skip_entity = True + elif entity_type == MessageEntityEmail: + html.append(f"{entity_text}") + elif entity_type == MessageEntityUrl: + html.append(f"{entity_text}") + elif entity_type == MessageEntityTextUrl: + html.append(f"{entity_text}") + elif entity_type == MessageEntityBotCommand: + html.append(f"!{entity_text[1:]}") + elif entity_type == MessageEntityHashtag: + html.append(f"{entity_text}") + else: + skip_entity = True + last_offset = entity.offset + (0 if skip_entity else entity.length) + html.append(text[last_offset:]) + + return "".join(html) + + +# endregion + +def init(context): + global log + _, _, parent_log, _ = context + log = parent_log.getChild("formatter") diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py new file mode 100644 index 00000000..73e007a8 --- /dev/null +++ b/mautrix_telegram/matrix.py @@ -0,0 +1,201 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from matrix_client.errors import MatrixRequestError + +from .user import User +from .portal import Portal +from .puppet import Puppet +from .commands import CommandHandler + + +class MatrixHandler: + def __init__(self, context): + self.az, self.db, log, self.config = context + self.log = log.getChild("mx") + self.commands = CommandHandler(context) + + self.az.matrix_event_handler(self.handle_event) + self.az.intent.set_display_name( + self.config.get("appservice.bot_displayname", "Telegram bridge bot")) + + def is_puppet(self, mxid): + match = Puppet.mxid_regex.match(mxid) + return True if match else False + + def get_puppet(self, mxid): + match = Puppet.mxid_regex.match(mxid) + if not match: + return None + return Puppet.get(int(match.group(1))) + + def handle_puppet_invite(self, room, puppet, inviter): + self.log.debug(f"{inviter} invited puppet for {puppet.tgid} to {room}") + if not inviter.logged_in: + puppet.intent.error_and_leave( + room, text="Please log in before inviting Telegram puppets.") + return + portal = Portal.get_by_mxid(room) + if portal: + if portal.peer_type == "user": + puppet.intent.error_and_leave( + room, text="You can not invite additional users to private chats.") + return + portal.invite_telegram(inviter, puppet) + puppet.intent.join_room(room) + return + try: + members = self.az.intent.get_room_members(room) + except MatrixRequestError: + members = [] + if self.az.intent.mxid not in members: + if len(members) > 1: + puppet.intent.error_and_leave(room, text=None, html=( + f"Please invite " + f"the bridge bot " + f"first if you want to create a Telegram chat.")) + return + + puppet.intent.join_room(room) + existing_portal = Portal.get_by_tgid(puppet.tgid, inviter.tgid, "user") + if existing_portal: + try: + puppet.intent.invite(existing_portal.mxid, inviter.mxid) + puppet.intent.send_notice(room, text=None, html=( + "You already have a private chat with me: " + f"" + "Link to room" + "")) + puppet.intent.leave_room(room) + return + except MatrixRequestError: + existing_portal.delete() + + portal = Portal(tgid=puppet.tgid, tg_receiver=inviter.tgid, peer_type="user", mxid=room) + portal.save() + puppet.intent.send_notice(room, "Portal to private chat created.") + else: + puppet.intent.join_room(room) + puppet.intent.send_notice(room, "This puppet will remain inactive until a Telegram " + "chat is created for this room.") + + def handle_invite(self, room, user, inviter): + inviter = User.get_by_mxid(inviter) + if not inviter.whitelisted: + return + elif user == self.az.bot_mxid: + self.az.intent.join_room(room) + return + puppet = self.get_puppet(user) + if puppet: + self.handle_puppet_invite(room, puppet, inviter) + return + # These can probably be ignored + self.log.debug(f"{inviter} invited {user} to {room}") + + def handle_join(self, room, user): + user = User.get_by_mxid(user) + + portal = Portal.get_by_mxid(room) + if not portal: + return + + if not user.whitelisted: + portal.main_intent.kick(room, user.mxid, + "You are not whitelisted on this Telegram bridge.") + return + elif not user.logged_in: + portal.main_intent.kick(room, user.mxid, + "You are not logged into this Telegram bridge.") + return + + self.log.debug(f"{user} joined {room}") + # TODO join Telegram chat if applicable + + def handle_part(self, room, user): + self.log.debug(f"{user} left {room}") + # user = User.get_by_mxid(user, create=False) + + def is_command(self, message): + text = message.get("body", "") + prefix = self.config["bridge.command_prefix"] + is_command = text.startswith(prefix) + if is_command: + text = text[len(prefix) + 1:] + return is_command, text + + def handle_message(self, room, sender, message, event_id): + self.log.debug(f"{sender} sent {message} to ${room}") + + is_command, text = self.is_command(message) + sender = User.get_by_mxid(sender) + + portal = Portal.get_by_mxid(room) + if sender.has_full_access and portal and not is_command: + portal.handle_matrix_message(sender, message, event_id) + return + + if message["msgtype"] != "m.text": + return + + is_management = len(self.az.intent.get_room_members(room)) == 2 + if is_command or is_management: + try: + command, arguments = text.split(" ", 1) + args = arguments.split(" ") + except ValueError: + # Not enough values to unpack, i.e. no arguments + command = text + args = [] + self.commands.handle(room, sender, command, args, is_management, + is_portal=portal is not None) + + def handle_redaction(self, room, sender, event_id): + portal = Portal.get_by_mxid(room) + sender = User.get_by_mxid(sender) + if sender.has_full_access and portal: + portal.handle_matrix_deletion(sender, event_id) + + def handle_power_levels(self, room, sender, new, old): + portal = Portal.get_by_mxid(room) + sender = User.get_by_mxid(sender) + if sender.has_full_access and portal: + sender = User.get_by_mxid(sender) + portal.handle_matrix_power_levels(sender, new["users"], old["users"]) + + def filter_matrix_event(self, event): + return event["sender"] == self.az.bot_mxid or self.is_puppet(event["sender"]) + + def handle_event(self, evt): + if self.filter_matrix_event(evt): + return + self.log.debug("Received event: %s", evt) + type = evt["type"] + content = evt.get("content", {}) + if type == "m.room.member": + membership = content.get("membership", {}) + if membership == "invite": + self.handle_invite(evt["room_id"], evt["state_key"], evt["sender"]) + elif membership == "leave": + self.handle_part(evt["room_id"], evt["state_key"]) + elif membership == "join": + self.handle_join(evt["room_id"], evt["state_key"]) + elif type == "m.room.message": + self.handle_message(evt["room_id"], evt["sender"], content, evt["event_id"]) + elif type == "m.room.redaction": + self.handle_redaction(evt["room_id"], evt["sender"], evt["redacts"]) + elif type == "m.room.power_levels": + self.handle_power_levels(evt["room_id"], evt["sender"], evt["content"], + evt["prev_content"]) diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py new file mode 100644 index 00000000..a03211bf --- /dev/null +++ b/mautrix_telegram/portal.py @@ -0,0 +1,652 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from telethon.tl.functions.messages import (GetFullChatRequest, EditChatAdminRequest, + CreateChatRequest, AddChatUserRequest) +from telethon.tl.functions.channels import (GetParticipantsRequest, CreateChannelRequest, + InviteToChannelRequest) +from telethon.errors.rpc_error_list import ChatAdminRequiredError, LocationInvalidError +from telethon.tl.types import * +from PIL import Image +from io import BytesIO +import mimetypes +import magic +from .db import Portal as DBPortal, Message as DBMessage +from . import puppet as p, user as u, formatter + +mimetypes.init() + +config = None + + +class Portal: + log = None + db = None + az = None + by_mxid = {} + by_tgid = {} + + def __init__(self, tgid, peer_type, tg_receiver=None, mxid=None, username=None, title=None, + photo_id=None): + self.mxid = mxid + self.tgid = tgid + self.tg_receiver = tg_receiver or tgid + self.peer_type = peer_type + self.username = username + self.title = title + self.photo_id = photo_id + self._main_intent = None + + if tgid: + self.by_tgid[self.tgid_full] = self + if mxid: + self.by_mxid[mxid] = self + + @property + def tgid_full(self): + return self.tgid, self.tg_receiver + + @property + def tgid_log(self): + if self.tgid == self.tg_receiver: + return self.tgid + return f"{self.tg_receiver}<->{self.tgid}" + + @property + def peer(self): + if self.peer_type == "user": + return PeerUser(user_id=self.tgid) + elif self.peer_type == "chat": + return PeerChat(chat_id=self.tgid) + elif self.peer_type == "channel": + return PeerChannel(channel_id=self.tgid) + + # region Matrix room info updating + + @property + def main_intent(self): + if not self._main_intent: + direct = self.peer_type == "user" + puppet = p.Puppet.get(self.tgid) if direct else None + self._main_intent = puppet.intent if direct else self.az.intent + return self._main_intent + + def invite_matrix(self, users=[]): + if isinstance(users, str): + self.main_intent.invite(self.mxid, users) + else: + for user in users: + self.main_intent.invite(self.mxid, user) + + def update_after_create(self, user, entity, direct, puppet=None): + if not direct: + self.update_info(user, entity) + users, participants = self.get_users(user, entity) + self.sync_telegram_users(user, users) + self.update_telegram_participants(participants) + else: + if not puppet: + puppet = p.Puppet.get(self.tgid) + puppet.update_info(user, entity) + puppet.intent.join_room(self.mxid) + + def create_matrix_room(self, user, entity=None, invites=[], update_if_exists=True): + if not entity: + entity = user.client.get_entity(self.peer) + self.log.debug("Fetched data: %s", entity) + direct = self.peer_type == "user" + + if self.mxid: + if update_if_exists: + self.update_after_create(user, entity, direct) + self.invite_matrix(invites) + return self.mxid + + self.log.debug(f"Creating room for {self.tgid_log}") + + try: + title = entity.title + except AttributeError: + title = None + + puppet = p.Puppet.get(self.tgid) if direct else None + intent = puppet.intent if direct else self.az.intent + + # TODO set room alias if public channel. + room = intent.create_room(invitees=invites, name=title, is_direct=direct) + if not room: + raise Exception(f"Failed to create room for {self.tgid_log}") + + self.mxid = room["room_id"] + self.by_mxid[self.mxid] = self + self.save() + + power_level_requirement = 0 if self.peer_type == "chat" else 50 + levels = self.main_intent.get_power_levels(self.mxid) + levels["ban"] = 100 + levels["invite"] = 50 + levels["events"]["m.room.name"] = power_level_requirement + levels["events"]["m.room.avatar"] = power_level_requirement + levels["events"]["m.room.topic"] = 50 if self.peer_type == "channel" else 100 + levels["events"]["m.room.power_levels"] = 95 + self.main_intent.set_power_levels(self.mxid, levels) + self.update_after_create(user, entity, direct, puppet) + + def sync_telegram_users(self, source, users=[]): + for entity in users: + puppet = p.Puppet.get(entity.id) + puppet.update_info(source, entity) + puppet.intent.join_room(self.mxid) + + def add_telegram_user(self, user_id, source=None): + puppet = p.Puppet.get(user_id) + if source: + entity = source.client.get_entity(user_id) + puppet.update_info(source, entity) + puppet.intent.join_room(self.mxid) + + user = u.User.get_by_tgid(user_id) + if user: + self.main_intent.invite(self.mxid, user.mxid) + + def delete_telegram_user(self, user_id, kick_message=None): + puppet = p.Puppet.get(user_id) + user = u.User.get_by_tgid(user_id) + if kick_message: + self.main_intent.kick(self.mxid, puppet.mxid, kick_message) + else: + puppet.intent.leave_room(self.mxid) + if user: + self.main_intent.kick(self.mxid, user.mxid, kick_message or "Left Telegram chat") + + def update_info(self, user, entity=None): + if self.peer_type == "user": + self.log.warn(f"Called update_info() for direct chat portal {self.tgid_log}") + return + + self.log.debug(f"Updating info of {self.tgid_log}") + if not entity: + entity = user.client.get_entity(self.peer) + self.log.debug("Fetched data: %s", entity) + changed = False + + if self.peer_type == "channel": + if self.username != entity.username: + # TODO update room alias + self.username = entity.username + changed = True + + changed = self.update_title(entity.title, self.main_intent) or changed + + if isinstance(entity.photo, ChatPhoto): + changed = self.update_avatar(user, entity.photo.photo_big, self.main_intent) or changed + + if changed: + self.save() + + def update_title(self, title, intent=None): + if self.title != title: + self.title = title + self.main_intent.set_room_name(self.mxid, self.title) + return True + return False + + def get_largest_photo_size(self, photo): + return max(photo.sizes, key=(lambda photo: ( + len(photo.bytes) if isinstance(photo, PhotoCachedSize) else photo.size))) + + def update_avatar(self, user, photo, intent=None): + photo_id = f"{photo.volume_id}-{photo.local_id}" + if self.photo_id != photo_id: + try: + file = user.download_file(photo) + except LocationInvalidError: + return False + uploaded = self.main_intent.upload_file(file) + self.main_intent.set_room_avatar(self.mxid, uploaded["content_uri"]) + self.photo_id = photo_id + return True + return False + + def get_users(self, user, entity): + if self.peer_type == "chat": + chat = user.client(GetFullChatRequest(chat_id=self.tgid)) + return chat.users, chat.full_chat.participants.participants + elif self.peer_type == "channel": + try: + participants = user.client(GetParticipantsRequest( + entity, ChannelParticipantsRecent(), offset=0, limit=100, hash=0 + )) + return participants.users, participants.participants + except ChatAdminRequiredError: + return [], [] + elif self.peer_type == "user": + return [entity], [] + + # endregion + # region Matrix event handling + + def _get_file_meta(self, body, mime): + file_name = None + try: + current_extension = body[body.rindex("."):] + if mimetypes.types_map[current_extension] == mime: + file_name = body + else: + file_name = f"matrix_upload{mimetypes.guess_extension(mime)}" + except (ValueError, KeyError): + file_name = f"matrix_upload{mimetypes.guess_extension(mime)}" + return file_name, None if file_name == body else body + + def handle_matrix_message(self, sender, message, event_id): + type = message["msgtype"] + if type == "m.text": + if "format" in message and message["format"] == "org.matrix.custom.html": + message, entities = formatter.matrix_to_telegram(message["formatted_body"], + sender.tgid) + reply_to = None + if len(entities) > 0 and isinstance(entities[0], formatter.MessageEntityReply): + reply = entities.pop(0) + # message = message[:reply.offset] + message[reply.offset + reply.length:] + reply_to = reply.msg_id + response = sender.send_message(self.peer, message, entities=entities, + reply_to=reply_to) + else: + response = sender.send_message(self.peer, message["body"]) + elif type in {"m.image", "m.file", "m.audio", "m.video"}: + file = self.main_intent.download_file(message["url"]) + + info = message["info"] + mime = info["mimetype"] + + file_name, caption = self._get_file_meta(message["body"], mime) + + attributes = [DocumentAttributeFilename(file_name=file_name)] + if "w" in info and "h" in info: + attributes.append(DocumentAttributeImageSize(w=info["w"], h=info["h"])) + + response = sender.send_file(self.peer, file, mime, caption, attributes, file_name) + else: + self.log.debug("Unhandled Matrix event: %s", message) + return + self.db.add( + DBMessage(tgid=response.id, mx_room=self.mxid, mxid=event_id, user=sender.tgid)) + self.db.commit() + + def handle_matrix_deletion(self, deleter, event_id): + message = DBMessage.query.filter(DBMessage.mxid == event_id and + DBMessage.user == deleter.tgid and + DBMessage.mx_room == self.mxid).one_or_none() + if not message: + return + deleter.client.delete_messages(self.peer, [message.tgid]) + + def handle_matrix_power_levels(self, sender, new_users, old_users): + for user, level in new_users.items(): + puppet_match = p.Puppet.mxid_regex.search(user) + if puppet_match: + user_id = int(puppet_match.group(1)) + else: + mx_user = u.User.get_by_mxid(user, create=False) + if not mx_user or not mx_user.tgid: + continue + user_id = mx_user.tgid + if user not in old_users or level != old_users[user]: + sender.client( + EditChatAdminRequest(chat_id=self.tgid, user_id=user_id, is_admin=level >= 50)) + + # endregion + # region Telegram chat info updating + + def _get_telegram_users_in_matrix_room(self): + user_tgids = set() + user_mxids = self.main_intent.get_room_members(self.mxid, ("join", "invite")) + for user in user_mxids: + if user == self.az.intent.mxid: + continue + mx_user = u.User.get_by_mxid(user, create=False) + if mx_user and mx_user.tgid: + user_tgids.add(mx_user.tgid) + puppet_match = p.Puppet.mxid_regex.match(user) + if puppet_match: + user_tgids.add(int(puppet_match.group(1))) + return user_tgids + + def create_telegram_chat(self, source, supergroup=False): + if not self.mxid: + raise ValueError("Can't create Telegram chat for portal without Matrix room.") + elif self.tgid: + raise ValueError("Can't create Telegram chat for portal with existing Telegram chat.") + + invites = self._get_telegram_users_in_matrix_room() + if len(invites) < 2: + # TODO when we get the option for a bot, this won't happen when the bot is activated. + raise ValueError("Not enough Telegram users to create a chat") + + invites = [source.client.get_input_entity(id) for id in invites] + + if self.peer_type == "chat": + updates = source.client(CreateChatRequest(title=self.title, users=invites)) + elif self.peer_type == "channel": + updates = source.client(CreateChannelRequest(title=self.title, megagroup=supergroup)) + # TODO invite people + else: + raise ValueError("Invalid peer type for Telegram chat creation") + + entity = updates.chats[0] + self.tgid = entity.id + self.tg_receiver = self.tgid + self.update_info(source, entity) + self.save() + + def invite_telegram(self, source, puppet): + if self.peer_type == "chat": + source.client(AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0)) + elif self.peer_type == "channel": + source.client(InviteToChannelRequest(channel=self.peer, + users=[InputUser(user_id=puppet.tgid)], + fwd_limit=0)) + else: + raise ValueError("Invalid peer type for Telegram user invite") + + # endregion + # region Telegram event handling + + def handle_telegram_typing(self, user, event): + if self.mxid: + user.intent.set_typing(self.mxid, is_typing=True) + + def handle_telegram_photo(self, source, sender, media): + largest_size = self.get_largest_photo_size(media.photo) + file = source.download_file(largest_size.location) + mime_type = magic.from_buffer(file, mime=True) + uploaded = sender.intent.upload_file(file, mime_type) + info = { + "h": largest_size.h, + "w": largest_size.w, + "size": len(largest_size.bytes) if ( + isinstance(largest_size, PhotoCachedSize)) else largest_size.size, + "orientation": 0, + "mimetype": mime_type, + } + name = media.caption + sender.intent.set_typing(self.mxid, is_typing=False) + return sender.intent.send_image(self.mxid, uploaded["content_uri"], info=info, text=name) + + @staticmethod + def convert_webp(file, to="png"): + try: + image = Image.open(BytesIO(file)).convert("RGBA") + new_file = BytesIO() + image.save(new_file, to) + return f"image/{to}", new_file.getvalue() + except: + return "image/webp", file + + def handle_telegram_document(self, source, sender, media): + file = source.download_file(media.document) + mime_type = magic.from_buffer(file, mime=True) + dont_change_mime = False + if mime_type == "image/webp": + mime_type, file = self.convert_webp(file, to="png") + dont_change_mime = True + uploaded = sender.intent.upload_file(file, mime_type) + name = media.caption + for attr in media.document.attributes: + if not name and isinstance(attr, DocumentAttributeFilename): + name = attr.file_name + if not dont_change_mime: + (mime_from_name, _) = mimetypes.guess_type(name) + mime_type = mime_from_name or mime_type + elif isinstance(attr, DocumentAttributeSticker): + name = f"Sticker for {attr.alt}" + mime_type = media.document.mime_type or mime_type + info = { + "size": media.document.size, + "mimetype": mime_type, + } + type = "m.file" + if mime_type.startswith("video/"): + type = "m.video" + elif mime_type.startswith("audio/"): + type = "m.audio" + elif mime_type.startswith("image/"): + type = "m.image" + sender.intent.set_typing(self.mxid, is_typing=False) + return sender.intent.send_file(self.mxid, uploaded["content_uri"], info=info, text=name, + type=type) + + def handle_telegram_location(self, source, sender, location): + long = location.long + lat = location.lat + long_char = "E" if long > 0 else "W" + lat_char = "N" if lat > 0 else "S" + rounded_long = abs(round(long * 100000) / 100000) + rounded_lat = abs(round(lat * 100000) / 100000) + + body = f"{rounded_lat}° {lat_char}, {rounded_long}° {long_char}" + + url = f"https://maps.google.com/?q={lat},{long}" + + formatted_body = f"Location: {body}" + # At least Riot ignores formatting in m.location messages, so we'll add a plaintext link. + body = f"Location: {body}\n{url}" + + return sender.intent.send_message(self.mxid, { + "msgtype": "m.location", + "geo_uri": f"geo:{lat},{long}", + "body": body, + "format": "org.matrix.custom.html", + "formatted_body": formatted_body, + }) + + def handle_telegram_text(self, source, sender, evt): + self.log.debug(f"Sending {evt.message} to {self.mxid} by {sender.id}") + text, html = formatter.telegram_event_to_matrix(evt, source) + sender.intent.set_typing(self.mxid, is_typing=False) + return sender.intent.send_text(self.mxid, text, html=html) + + def handle_telegram_message(self, source, sender, evt): + if not self.mxid: + self.create_matrix_room(source, invites=[source.mxid]) + + if evt.message: + response = self.handle_telegram_text(source, sender, evt) + elif evt.media: + if isinstance(evt.media, MessageMediaPhoto): + response = self.handle_telegram_photo(source, sender, evt.media) + elif isinstance(evt.media, MessageMediaDocument): + response = self.handle_telegram_document(source, sender, evt.media) + elif isinstance(evt.media, MessageMediaGeo): + response = self.handle_telegram_location(source, sender, evt.media.geo) + else: + self.log.debug("Unhandled Telegram media: %s", evt.media) + return + else: + self.log.debug("Unhandled Telegram message: %s", evt) + return + + self.db.add(DBMessage(tgid=evt.id, mx_room=self.mxid, mxid=response["event_id"], + user=source.tgid)) + self.db.commit() + + def handle_telegram_action(self, source, sender, action): + if not self.mxid: + create_and_exit = (MessageActionChatCreate, MessageActionChannelCreate) + create_and_continue = (MessageActionChatAddUser, MessageActionChatJoinedByLink) + if isinstance(action, create_and_exit + create_and_continue): + self.create_matrix_room(source, invites=[source.mxid]) + if not isinstance(action, create_and_continue): + return + + if isinstance(action, MessageActionChatEditTitle): + if self.update_title(action.title, self.main_intent): + self.save() + elif isinstance(action, MessageActionChatEditPhoto): + largest_size = self.get_largest_photo_size(action.photo) + if self.update_avatar(source, largest_size.location, self.main_intent): + self.save() + elif isinstance(action, MessageActionChatAddUser): + for user_id in action.users: + self.add_telegram_user(user_id, source) + elif isinstance(action, MessageActionChatJoinedByLink): + self.add_telegram_user(sender.id, source) + elif isinstance(action, MessageActionChatDeleteUser): + kick_message = None + if sender.id != action.user_id: + kick_message = f"Kicked by {sender.displayname}" + self.delete_telegram_user(action.user_id, kick_message) + elif isinstance(action, MessageActionChatMigrateTo): + self.peer_type = "channel" + self.migrate_and_save(action.channel_id) + sender.intent.send_emote(self.mxid, "upgraded this group to a supergroup.") + else: + self.log.debug("Unhandled Telegram action in %s: %s", self.title, action) + + def set_telegram_admin(self, puppet, user): + levels = self.main_intent.get_power_levels(self.mxid) + if user: + levels["users"][user.mxid] = 50 + if puppet: + levels["users"][puppet.mxid] = 50 + self.main_intent.set_power_levels(self.mxid, levels) + + def update_telegram_participants(self, participants): + levels = self.main_intent.get_power_levels(self.mxid) + levels["events"]["m.room.power_levels"] = 50 + for participant in participants: + puppet = p.Puppet.get(participant.user_id) + user = u.User.get_by_tgid(participant.user_id) + new_level = 0 + if isinstance(participant, ChatParticipantAdmin): + new_level = 50 + elif isinstance(participant, ChatParticipantCreator): + new_level = 95 + if user: + levels["users"][user.mxid] = new_level + if puppet: + levels["users"][puppet.mxid] = new_level + self.main_intent.set_power_levels(self.mxid, levels) + + def set_telegram_admins_enabled(self, enabled): + level = 50 if enabled else 10 + levels = self.main_intent.get_power_levels(self.mxid) + print(levels) + levels["invite"] = level + levels["events"]["m.room.name"] = level + levels["events"]["m.room.avatar"] = level + self.main_intent.set_power_levels(self.mxid, levels) + + # endregion + # region Database conversion + + def to_db(self): + return self.db.merge( + DBPortal(tgid=self.tgid, tg_receiver=self.tg_receiver, peer_type=self.peer_type, + mxid=self.mxid, username=self.username, title=self.title, + photo_id=self.photo_id)) + + def migrate_and_save(self, new_id): + existing = DBPortal.query.get(self.tgid_full) + if existing: + self.db.object_session(existing).delete(existing) + self.by_tgid[self.tgid_full] = None + self.tgid = new_id + self.by_tgid[self.tgid_full] = self + self.save() + + def save(self): + self.to_db() + self.db.commit() + + def delete(self): + self.db.delete(self.to_db()) + + @classmethod + def from_db(cls, db_portal): + return Portal(tgid=db_portal.tgid, tg_receiver=db_portal.tg_receiver, + peer_type=db_portal.peer_type, mxid=db_portal.mxid, + username=db_portal.username, title=db_portal.title, + photo_id=db_portal.photo_id) + + # endregion + # region Class instance lookup + + @classmethod + def get_by_mxid(cls, mxid): + try: + return cls.by_mxid[mxid] + except KeyError: + pass + + portal = DBPortal.query.filter(DBPortal.mxid == mxid).one_or_none() + if portal: + return cls.from_db(portal) + + return None + + @classmethod + def get_by_tgid(cls, tgid, tg_receiver=None, peer_type=None): + tg_receiver = tg_receiver or tgid + tgid_full = (tgid, tg_receiver) + try: + return cls.by_tgid[tgid_full] + except KeyError: + pass + + portal = DBPortal.query.get(tgid_full) + if portal: + return cls.from_db(portal) + + if peer_type: + portal = Portal(tgid, peer_type=peer_type, tg_receiver=tg_receiver) + cls.db.add(portal.to_db()) + portal.save() + return portal + + return None + + @classmethod + def get_by_entity(cls, entity, receiver_id=None): + entity_type = type(entity) + if entity_type in {Chat, ChatFull}: + type_name = "chat" + id = entity.id + elif entity_type in {PeerChat, InputPeerChat}: + type_name = "chat" + id = entity.chat_id + elif entity_type in {Channel, ChannelFull}: + type_name = "channel" + id = entity.id + elif entity_type in {PeerChannel, InputPeerChannel, InputChannel}: + type_name = "channel" + id = entity.channel_id + elif entity_type in {User, UserFull}: + type_name = "user" + id = entity.id + elif entity_type in {PeerUser, InputPeerUser, InputUser}: + type_name = "user" + id = entity.user_id + else: + raise ValueError(f"Unknown entity type {entity_type.__name__}") + return cls.get_by_tgid(id, receiver_id if type_name == "user" else id, type_name) + + # endregion + + +def init(context): + global config + Portal.az, Portal.db, log, config = context + Portal.log = log.getChild("portal") diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py new file mode 100644 index 00000000..bad72138 --- /dev/null +++ b/mautrix_telegram/puppet.py @@ -0,0 +1,157 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import re +from telethon.tl.types import UserProfilePhoto +from telethon.errors.rpc_error_list import LocationInvalidError +from .db import Puppet as DBPuppet + +config = None + + +class Puppet: + log = None + db = None + az = None + mxid_regex = None + cache = {} + + def __init__(self, id=None, username=None, displayname=None, photo_id=None): + self.id = id + + self.localpart = config.get("bridge.username_template", "telegram_{userid}").format( + userid=self.id) + hs = config["homeserver"]["domain"] + self.mxid = f"@{self.localpart}:{hs}" + self.username = username + self.displayname = displayname + self.photo_id = photo_id + self.intent = self.az.intent.user(self.mxid) + + self.cache[id] = self + + @property + def tgid(self): + return self.id + + def to_db(self): + return self.db.merge( + DBPuppet(id=self.id, username=self.username, displayname=self.displayname, + photo_id=self.photo_id)) + + @classmethod + def from_db(cls, db_puppet): + return Puppet(db_puppet.id, db_puppet.username, db_puppet.displayname, db_puppet.photo_id) + + def save(self): + self.to_db() + self.db.commit() + + @staticmethod + def get_displayname(info, format=True): + data = { + "phone number": info.phone, + "username": info.username, + "full name": " ".join([info.first_name or "", info.last_name or ""]).strip(), + "full name reversed": " ".join([info.first_name or "", info.last_name or ""]).strip(), + "first name": info.first_name, + "last name": info.last_name, + } + preferences = config.get("bridge", {}).get("displayname_preference", + ["full name", "username", "phone"]) + for preference in preferences: + name = data[preference] + if name: + break + if not name: + name = info.id + + if not format: + return name + return config.get("bridge.displayname_template", "{displayname} (Telegram)").format( + displayname=name) + + def update_info(self, source, info): + changed = False + if self.username != info.username: + self.username = info.username + changed = True + + changed = self.update_displayname(source, info) or changed + if isinstance(info.photo, UserProfilePhoto): + changed = self.update_avatar(source, info.photo.photo_big) + + if changed: + self.save() + + def update_displayname(self, source, info): + displayname = self.get_displayname(info) + if displayname != self.displayname: + self.intent.set_display_name(displayname) + self.displayname = displayname + return True + + def update_avatar(self, source, photo): + photo_id = f"{photo.volume_id}-{photo.local_id}" + if self.photo_id != photo_id: + try: + file = source.download_file(photo) + except LocationInvalidError: + return False + uploaded = self.intent.upload_file(file) + self.intent.set_avatar(uploaded["content_uri"]) + self.photo_id = photo_id + return True + return False + + @classmethod + def get(cls, id, create=True): + try: + return cls.cache[id] + except KeyError: + pass + + puppet = DBPuppet.query.get(id) + if puppet: + return cls.from_db(puppet) + + if create: + puppet = cls(id) + cls.db.add(puppet.to_db()) + cls.db.commit() + return puppet + + return None + + @classmethod + def find_by_username(cls, username): + for _, puppet in cls.cache.items(): + if puppet.username == username: + return puppet + + puppet = DBPuppet.query.filter(DBPuppet.username == username).one_or_none() + if puppet: + return cls.from_db(puppet) + + return None + + +def init(context): + global config + Puppet.az, Puppet.db, log, config = context + Puppet.log = log.getChild("puppet") + localpart = config.get("bridge.username_template", "telegram_{userid}").format(userid="(.+)") + hs = config["homeserver"]["domain"] + Puppet.mxid_regex = re.compile(f"@{localpart}:{hs}") diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py new file mode 100644 index 00000000..ce844d9b --- /dev/null +++ b/mautrix_telegram/user.py @@ -0,0 +1,326 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from io import BytesIO +from telethon import TelegramClient +from telethon.tl.types import * +from telethon.tl.types import User as TLUser +from telethon.tl.functions.messages import SendMessageRequest, SendMediaRequest +from .db import User as DBUser +from . import portal as po, puppet as pu + +config = None + + +class User: + log = None + db = None + az = None + by_mxid = {} + by_tgid = {} + + def __init__(self, mxid, tgid=None, username=None): + self.mxid = mxid + self.tgid = tgid + self.username = username + + self.command_status = None + self.connected = False + self.client = None + whitelist = config.get("bridge", {}).get("whitelist", [self.mxid]) + self.whitelisted = self.mxid in whitelist + if not self.whitelisted: + homeserver = self.mxid[self.mxid.index(":") + 1:] + self.whitelisted = homeserver in whitelist + + self.by_mxid[mxid] = self + if tgid: + self.by_tgid[tgid] = self + + @property + def logged_in(self): + return self.client.is_user_authorized() + + @property + def has_full_access(self): + return self.logged_in and self.whitelisted + + # region Database conversion + + def to_db(self): + return self.db.merge(DBUser(mxid=self.mxid, tgid=self.tgid, tg_username=self.username)) + + def save(self): + self.to_db() + self.db.commit() + + @classmethod + def from_db(cls, db_user): + return User(db_user.mxid, db_user.tgid, db_user.tg_username) + + # endregion + # region Telegram connection management + + def start(self): + self.client = TelegramClient(self.mxid, + config["telegram.api_id"], + config["telegram.api_hash"], + update_workers=2) + self.connected = self.client.connect() + if self.logged_in: + self.post_login() + self.client.add_update_handler(self.update_catch) + return self + + def post_login(self, info=None): + self.sync_dialogs() + self.update_info(info) + + def stop(self): + self.client.disconnect() + self.client = None + self.connected = False + + # endregion + # region Telegram actions that need custom methods + + def update_info(self, info=None): + info = info or self.client.get_me() + changed = False + if self.username != info.username: + self.username = info.username + changed = True + if self.tgid != info.id: + self.tgid = info.id + self.by_tgid[self.tgid] = self + if changed: + self.save() + + def log_out(self): + self.connected = False + if self.tgid: + try: + del self.by_tgid[self.tgid] + except KeyError: + pass + self.tgid = None + self.save() + return self.client.log_out() + + def send_message(self, entity, message, reply_to=None, entities=None, link_preview=True): + entity = self.client.get_input_entity(entity) + + request = SendMessageRequest( + peer=entity, + message=message, + entities=entities, + no_webpage=not link_preview, + reply_to_msg_id=self.client._get_reply_to(reply_to) + ) + result = self.client(request) + if isinstance(result, UpdateShortSentMessage): + return Message( + id=result.id, + to_id=entity, + message=message, + date=result.date, + out=result.out, + media=result.media, + entities=result.entities + ) + + return self.client._get_response_message(request, result) + + def send_file(self, entity, file, mime_type=None, caption=None, attributes=[], file_name=None, + reply_to=None): + entity = self.client.get_input_entity(entity) + reply_to = self.client._get_reply_to(reply_to) + + file_handle = self.client.upload_file(file, file_name=file_name, use_cache=False) + + if mime_type == "image/png": + media = InputMediaUploadedPhoto(file_handle, caption or "") + else: + attr_dict = {type(attr): attr for attr in attributes} + + media = InputMediaUploadedDocument( + file=file_handle, + mime_type=mime_type or "application/octet-stream", + attributes=list(attr_dict.values()), + caption=caption or "") + + request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to) + return self.client._get_response_message(request, self.client(request)) + + def download_file(self, location): + if isinstance(location, Document): + location = InputDocumentFileLocation(location.id, location.access_hash, + location.version) + elif not isinstance(location, (InputFileLocation, InputDocumentFileLocation)): + location = InputFileLocation(location.volume_id, location.local_id, location.secret) + + file = BytesIO() + + self.client.download_file(location, file) + + data = file.getvalue() + file.close() + return data + + def sync_dialogs(self): + dialogs = self.client.get_dialogs(limit=30) + for dialog in dialogs: + entity = dialog.entity + if (isinstance(entity, (TLUser, ChatForbidden, ChannelForbidden)) or ( + isinstance(entity, Chat) and entity.deactivated)): + continue + portal = po.Portal.get_by_entity(entity) + portal.create_matrix_room(self, entity, invites=[self.mxid]) + + # endregion + # region Telegram update handling + + def update_catch(self, update): + try: + self.update(update) + except: + self.log.exception("Failed to handle Telegram update") + + def update(self, update): + if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewMessage, + UpdateNewChannelMessage)): + self.update_message(update) + elif isinstance(update, (UpdateChatUserTyping, UpdateUserTyping)): + self.update_typing(update) + elif isinstance(update, UpdateUserStatus): + self.update_status(update) + elif isinstance(update, (UpdateChatAdmins, UpdateChatParticipantAdmin)): + self.update_admin(update) + elif isinstance(update, UpdateChatParticipants): + portal = po.Portal.get_by_tgid(update.participants.chat_id, peer_type="chat") + portal.update_telegram_participants(update.participants.participants) + else: + self.log.debug("Unhandled update: %s", update) + + def update_admin(self, update): + portal = po.Portal.get_by_tgid(update.chat_id, peer_type="chat") + if isinstance(update, UpdateChatAdmins): + portal.set_telegram_admins_enabled(update.enabled) + elif isinstance(update, UpdateChatParticipantAdmin): + puppet = pu.Puppet.get(update.user_id) + user = User.get_by_tgid(update.user_id) + portal.set_telegram_admin(puppet, user) + + def update_typing(self, update): + if isinstance(update, UpdateUserTyping): + portal = po.Portal.get_by_tgid(update.user_id, self.tgid, "user") + else: + portal = po.Portal.get_by_tgid(update.chat_id, peer_type="chat") + sender = pu.Puppet.get(update.user_id) + return portal.handle_telegram_typing(sender, update) + + def update_status(self, update): + puppet = pu.Puppet.get(update.user_id) + if isinstance(update.status, UserStatusOnline): + puppet.intent.set_presence("online") + elif isinstance(update.status, UserStatusOffline): + puppet.intent.set_presence("offline") + return + + def get_message_details(self, update): + if isinstance(update, UpdateShortChatMessage): + portal = po.Portal.get_by_tgid(update.chat_id, peer_type="chat") + sender = pu.Puppet.get(update.from_id) + elif isinstance(update, UpdateShortMessage): + portal = po.Portal.get_by_tgid(update.user_id, self.tgid, "user") + sender = pu.Puppet.get(self.tgid if update.out else update.user_id) + elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)): + update = update.message + sender = pu.Puppet.get(update.from_id) + portal = po.Portal.get_by_entity(update.to_id, receiver_id=self.tgid) + return update, sender, portal + + def update_message(self, update): + update, sender, portal = self.get_message_details(update) + + if isinstance(update, MessageService): + if isinstance(update.action, MessageActionChannelMigrateFrom): + self.log.debug(f"Ignoring action %s to %s by %d", update.action, portal.tgid_log, + sender.id) + return + self.log.debug("Handling action %s to %s by %d", update.action, portal.tgid_log, + sender.id) + portal.handle_telegram_action(self, sender, update.action) + else: + self.log.debug("Handling message %s to %s by %d", update, portal.tgid_log, sender.tgid) + portal.handle_telegram_message(self, sender, update) + + # endregion + # region Class instance lookup + + @classmethod + def get_by_mxid(cls, mxid, create=True): + try: + return cls.by_mxid[mxid] + except KeyError: + pass + + user = DBUser.query.get(mxid) + if user: + return cls.from_db(user).start() + + if create: + user = cls(mxid) + cls.db.add(user.to_db()) + cls.db.commit() + return user.start() + + return None + + @classmethod + def get_by_tgid(cls, tgid): + try: + return cls.by_tgid[tgid] + except KeyError: + pass + + user = DBUser.query.filter(DBUser.tgid == tgid).one_or_none() + if user: + return cls.from_db(user).start() + + return None + + @classmethod + def find_by_username(cls, username): + for _, user in cls.by_tgid.items(): + if user.username == username: + return user + + puppet = DBUser.query.filter(DBUser.tg_username == username).one_or_none() + if puppet: + return cls.from_db(puppet) + + return None + # endregion + + +def init(context): + global config + User.az, User.db, log, config = context + User.log = log.getChild("user") + + users = [User.from_db(user) for user in DBUser.query.all()] + for user in users: + user.start() diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 3e425344..00000000 --- a/package-lock.json +++ /dev/null @@ -1,4638 +0,0 @@ -{ - "name": "mautrix-telegram", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@goodmind/node-cryptojs-aes": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@goodmind/node-cryptojs-aes/-/node-cryptojs-aes-0.5.0.tgz", - "integrity": "sha1-hNJ9Lsdavw2u05IYHRwgUs4EbPI=" - }, - "@most/multicast": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@most/multicast/-/multicast-1.3.0.tgz", - "integrity": "sha512-DWH8AShgp5bXn+auGzf5tzPxvpmEvQJd0CNsApOci1LDF4eAEcnw4HQOr2Jaa+L92NbDYFKBSXxll+i7r1ikvw==", - "requires": { - "@most/prelude": "1.7.0" - } - }, - "@most/prelude": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@most/prelude/-/prelude-1.7.0.tgz", - "integrity": "sha512-OlDCH0+u2/ro/AHeAg63zvbvCsQC930hSKC9Kc1qSev1JkWK8Yk0HDHJ4o62HSjPwWGXEUZG1WhQKQh4ypreGA==" - }, - "@safareli/free": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@safareli/free/-/free-1.0.0.tgz", - "integrity": "sha1-bnPfLiLLy/x5y8rOCs+anEOp7Fo=", - "requires": { - "fantasy-land": "1.0.1", - "sanctuary-type-classes": "0.3.0" - } - }, - "Base64": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", - "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" - }, - "JSONStream": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", - "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "accepts": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", - "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", - "requires": { - "mime-types": "2.1.17", - "negotiator": "0.5.3" - } - }, - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "3.3.0" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=" - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "another-json": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", - "integrity": "sha1-tfQBnJc7bdXGUGotk0acttMq7tw=" - }, - "ansi-escapes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", - "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "requires": { - "color-convert": "1.9.1" - } - }, - "apropos": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/apropos/-/apropos-0.4.0.tgz", - "integrity": "sha512-S44zku+RHpZnlUUUVn/p+/12mgldrWuim1++Jf4RRR3YDvrfj+jUMSQhq1wHfK3PD6EwDUNAyiSo/uRh/VCK5g==" - }, - "argparse": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", - "requires": { - "sprintf-js": "1.0.3" - } - }, - "array-flatten": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", - "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", - "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=" - }, - "asn1.js": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz", - "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==", - "requires": { - "bn.js": "4.11.8", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0" - } - }, - "assert": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.3.0.tgz", - "integrity": "sha1-A5OaYiWCqBLMICMgoLmlbJuBWEk=", - "requires": { - "util": "0.10.3" - } - }, - "assert-plus": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", - "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=" - }, - "ast-types": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.8.15.tgz", - "integrity": "sha1-ju8IJ/BN/w7IhXupJavj/qYZTlI=" - }, - "astw": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", - "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", - "requires": { - "acorn": "4.0.13" - } - }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", - "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "axios": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", - "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=", - "requires": { - "follow-redirects": "1.3.0", - "is-buffer": "1.1.6" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.3", - "regenerator-runtime": "0.11.1" - } - }, - "babylon": { - "version": "7.0.0-beta.19", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", - "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base62": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/base62/-/base62-0.1.1.tgz", - "integrity": "sha1-e0F0wvlESXU7EcJlHAg9qEGnsIQ=" - }, - "base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" - }, - "basic-auth": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.4.tgz", - "integrity": "sha1-Awk1sB3nyblKgksp8/zLdQ06UpA=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "binary-search-tree": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz", - "integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=", - "requires": { - "underscore": "1.4.4" - } - }, - "bl": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", - "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", - "requires": { - "readable-stream": "1.0.34" - } - }, - "bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "body-parser": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.12.4.tgz", - "integrity": "sha1-CQcAxLoohiqFIO83g5X97l9hwik=", - "requires": { - "bytes": "1.0.0", - "content-type": "1.0.4", - "debug": "2.2.0", - "depd": "1.0.1", - "iconv-lite": "0.4.8", - "on-finished": "2.2.1", - "qs": "2.4.2", - "raw-body": "2.0.2", - "type-is": "1.6.15" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-pack": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-5.0.1.tgz", - "integrity": "sha1-QZdxmyDG4KqglFHFER5T77b7wY0=", - "requires": { - "JSONStream": "1.3.2", - "combine-source-map": "0.6.1", - "defined": "1.0.0", - "through2": "1.1.1", - "umd": "3.0.1" - } - }, - "browser-request": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", - "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" - }, - "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "browserify": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-10.2.6.tgz", - "integrity": "sha1-3L/veU9Uj1+EmCFIFPaXpcUMCJY=", - "requires": { - "JSONStream": "1.3.2", - "assert": "1.3.0", - "browser-pack": "5.0.1", - "browser-resolve": "1.11.2", - "browserify-zlib": "0.1.4", - "buffer": "3.6.0", - "builtins": "0.0.7", - "commondir": "0.0.1", - "concat-stream": "1.4.10", - "console-browserify": "1.1.0", - "constants-browserify": "0.0.1", - "crypto-browserify": "3.12.0", - "defined": "1.0.0", - "deps-sort": "1.3.9", - "domain-browser": "1.1.7", - "duplexer2": "0.0.2", - "events": "1.0.2", - "glob": "4.5.3", - "has": "1.0.1", - "htmlescape": "1.1.1", - "http-browserify": "1.7.0", - "https-browserify": "0.0.1", - "inherits": "2.0.3", - "insert-module-globals": "6.6.3", - "isarray": "0.0.1", - "labeled-stream-splicer": "1.0.2", - "module-deps": "3.9.1", - "os-browserify": "0.1.2", - "parents": "1.0.1", - "path-browserify": "0.0.0", - "process": "0.11.10", - "punycode": "1.4.1", - "querystring-es3": "0.2.1", - "read-only-stream": "1.1.1", - "readable-stream": "1.1.14", - "resolve": "1.5.0", - "shasum": "1.0.2", - "shell-quote": "0.0.1", - "stream-browserify": "1.0.0", - "string_decoder": "0.10.31", - "subarg": "1.0.0", - "syntax-error": "1.3.0", - "through2": "1.1.1", - "timers-browserify": "1.4.2", - "tty-browserify": "0.0.0", - "url": "0.10.3", - "util": "0.10.3", - "vm-browserify": "0.0.4", - "xtend": "4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "browserify-aes": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", - "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", - "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.1.3", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - }, - "browserify-cipher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", - "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", - "requires": { - "browserify-aes": "1.1.1", - "browserify-des": "1.0.0", - "evp_bytestokey": "1.0.3" - } - }, - "browserify-des": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", - "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", - "requires": { - "cipher-base": "1.0.4", - "des.js": "1.0.0", - "inherits": "2.0.3" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "requires": { - "bn.js": "4.11.8", - "randombytes": "2.0.5" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.1.3", - "create-hmac": "1.1.6", - "elliptic": "6.4.0", - "inherits": "2.0.3", - "parse-asn1": "5.1.0" - } - }, - "browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", - "requires": { - "pako": "0.2.9" - } - }, - "buffer": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", - "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", - "requires": { - "base64-js": "0.0.8", - "ieee754": "1.1.8", - "isarray": "1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - } - } - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "builtins": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-0.0.7.tgz", - "integrity": "sha1-NVIZzWzxjb58Acx/0tznZc/cVJo=" - }, - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "caseless": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.9.0.tgz", - "integrity": "sha1-t7Zc5r8UE4hlOc/VM/CzDv+pz4g=" - }, - "catharsis": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", - "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=", - "dev": true, - "requires": { - "underscore-contrib": "0.3.0" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - }, - "dependencies": { - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "requires": { - "has-flag": "2.0.0" - } - } - } - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "2.0.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" - }, - "combine-source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.6.1.tgz", - "integrity": "sha1-m0oJwxYDPXaODxHgKfonMOB5rZY=", - "requires": { - "convert-source-map": "1.1.3", - "inline-source-map": "0.5.0", - "lodash.memoize": "3.0.4", - "source-map": "0.4.4" - } - }, - "combined-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", - "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", - "requires": { - "delayed-stream": "0.0.5" - } - }, - "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==" - }, - "commondir": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-0.0.1.tgz", - "integrity": "sha1-ifAP3NUbUZxXhzP+xWPmptp/W+I=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.10.tgz", - "integrity": "sha1-rMO79WAsuMyYDGrIQPp9hgPj7zY=", - "requires": { - "inherits": "2.0.3", - "readable-stream": "1.1.14", - "typedarray": "0.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "concurrify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/concurrify/-/concurrify-1.0.2.tgz", - "integrity": "sha512-2DkAbP6AcScvaxhnZr8rPKtuicP83HTqcH8q3huJa/gMGJ79suhzQHRt2GVUtnUQZXeXsmUQ8n1VG/298LiNpA==", - "requires": { - "sanctuary-type-classes": "7.2.0", - "sanctuary-type-identifiers": "2.0.1" - }, - "dependencies": { - "sanctuary-type-classes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/sanctuary-type-classes/-/sanctuary-type-classes-7.2.0.tgz", - "integrity": "sha512-UXDC0BR+YTpLUY+K4QRotUAZ/2WfY7GJfF0pYXIU+YwuJUOg2MnSpr/E1R6jxPvwxnnWpHa20b0XbSCuHWMQyQ==", - "requires": { - "sanctuary-type-identifiers": "1.0.0" - }, - "dependencies": { - "sanctuary-type-identifiers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sanctuary-type-identifiers/-/sanctuary-type-identifiers-1.0.0.tgz", - "integrity": "sha1-6PNZ8AbLXmJM+4RkYD/BFGCL3p8=" - } - } - } - } - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "requires": { - "date-now": "0.1.4" - } - }, - "constants-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-0.0.1.tgz", - "integrity": "sha1-kld9tSe6bEzwpFaNhLwDH0QeIfI=" - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "content-disposition": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", - "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" - }, - "cookie": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", - "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "core-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", - "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz", - "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII=" - }, - "create-ecdh": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", - "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", - "requires": { - "bn.js": "4.11.8", - "elliptic": "6.4.0" - } - }, - "create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", - "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.3", - "ripemd160": "2.0.1", - "sha.js": "2.4.9" - } - }, - "create-hmac": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", - "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", - "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.1.3", - "inherits": "2.0.3", - "ripemd160": "2.0.1", - "safe-buffer": "5.1.1", - "sha.js": "2.4.9" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "4.1.1", - "shebang-command": "1.2.0", - "which": "1.3.0" - } - }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.10.1" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "1.0.0", - "browserify-sign": "4.0.4", - "create-ecdh": "4.0.0", - "create-hash": "1.1.3", - "create-hmac": "1.1.6", - "diffie-hellman": "5.0.2", - "inherits": "2.0.3", - "pbkdf2": "3.0.14", - "public-encrypt": "4.0.0", - "randombytes": "2.0.5", - "randomfill": "1.0.3" - } - }, - "ctype": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", - "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=" - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "requires": { - "es5-ext": "0.10.37" - } - }, - "daggy": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/daggy/-/daggy-0.0.1.tgz", - "integrity": "sha1-rpyTLJ9xNuZVoEr7Iei6rf0NNwc=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" - }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "requires": { - "ms": "0.7.1" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.2" - } - }, - "delayed-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", - "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" - }, - "denque": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.2.2.tgz", - "integrity": "sha512-x92Ql74lcTbGylXILO9Xf9S0cMpEPP04zVp2bB9e2C7G/n/Q1SgLl78RaSYEPSgpDX9uLgQXCEGAS5BI5dP3yA==" - }, - "depd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", - "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" - }, - "deps-sort": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-1.3.9.tgz", - "integrity": "sha1-Kd//U+F7Nq7K51MK27v2IsLtGnE=", - "requires": { - "JSONStream": "1.3.2", - "shasum": "1.0.2", - "subarg": "1.0.0", - "through2": "1.1.1" - } - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0" - } - }, - "destroy": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", - "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=" - }, - "detect-node": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", - "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" - }, - "detective": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", - "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", - "requires": { - "acorn": "5.3.0", - "defined": "1.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz", - "integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==" - } - } - }, - "diffie-hellman": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", - "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", - "requires": { - "bn.js": "4.11.8", - "miller-rabin": "4.0.1", - "randombytes": "2.0.5" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "2.0.2" - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" - } - } - }, - "domain-browser": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", - "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" - }, - "domhandler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", - "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", - "requires": { - "domelementtype": "1.3.0" - } - }, - "domutils": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.6.2.tgz", - "integrity": "sha1-GVjMC0yUJuntNn+xyOhUiRsPo/8=", - "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" - } - }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "requires": { - "readable-stream": "1.1.14" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "ee-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", - "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" - }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.3", - "hmac-drbg": "1.0.1", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, - "requires": { - "is-arrayish": "0.2.1" - } - }, - "es3ify": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/es3ify/-/es3ify-0.1.4.tgz", - "integrity": "sha1-rZ+l3xrjTz8x4SEbWBiy1RB439E=", - "requires": { - "esprima-fb": "3001.1.0-dev-harmony-fb", - "jstransform": "3.0.0", - "through": "2.3.8" - }, - "dependencies": { - "esprima-fb": { - "version": "3001.1.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", - "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=" - } - } - }, - "es5-ext": { - "version": "0.10.37", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", - "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", - "requires": { - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-symbol": "3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "eslint": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.15.0.tgz", - "integrity": "sha512-zEO/Z1ZUxIQ+MhDVKkVTUYpIPDTEJLXGMrkID+5v1NeQHtCz6FZikWuFRgxE1Q/RV2V4zVl1u3xmpPADHhMZ6A==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "babel-code-frame": "6.26.0", - "chalk": "2.3.0", - "concat-stream": "1.6.0", - "cross-spawn": "5.1.0", - "debug": "3.1.0", - "doctrine": "2.1.0", - "eslint-scope": "3.7.1", - "eslint-visitor-keys": "1.0.0", - "espree": "3.5.2", - "esquery": "1.0.0", - "esutils": "2.0.2", - "file-entry-cache": "2.0.0", - "functional-red-black-tree": "1.0.1", - "glob": "7.1.2", - "globals": "11.1.0", - "ignore": "3.3.7", - "imurmurhash": "0.1.4", - "inquirer": "3.3.0", - "is-resolvable": "1.0.1", - "js-yaml": "3.10.0", - "json-stable-stringify-without-jsonify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.4", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "optionator": "0.8.2", - "path-is-inside": "1.0.2", - "pluralize": "7.0.0", - "progress": "2.0.0", - "require-uncached": "1.0.3", - "semver": "5.4.1", - "strip-ansi": "4.0.0", - "strip-json-comments": "2.0.1", - "table": "4.0.2", - "text-table": "0.2.0" - }, - "dependencies": { - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - } - } - }, - "eslint-config-airbnb-base": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", - "integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==", - "dev": true, - "requires": { - "eslint-restricted-globals": "0.1.1" - } - }, - "eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", - "dev": true, - "requires": { - "debug": "2.6.9", - "resolve": "1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", - "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", - "dev": true, - "requires": { - "debug": "2.6.9", - "pkg-dir": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-import": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz", - "integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==", - "dev": true, - "requires": { - "builtin-modules": "1.1.1", - "contains-path": "0.1.0", - "debug": "2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "0.3.2", - "eslint-module-utils": "2.1.1", - "has": "1.0.1", - "lodash.cond": "4.5.2", - "minimatch": "3.0.4", - "read-pkg-up": "2.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-restricted-globals": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", - "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", - "dev": true - }, - "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", - "dev": true, - "requires": { - "esrecurse": "4.2.0", - "estraverse": "4.2.0" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", - "dev": true - }, - "esmangle-evaluator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esmangle-evaluator/-/esmangle-evaluator-1.0.1.tgz", - "integrity": "sha1-Yg2GbvSGGzMR91dm1SqFcrs8YzY=" - }, - "espree": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz", - "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==", - "dev": true, - "requires": { - "acorn": "5.3.0", - "acorn-jsx": "3.0.1" - }, - "dependencies": { - "acorn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz", - "integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" - }, - "esquery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", - "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", - "dev": true, - "requires": { - "estraverse": "4.2.0" - } - }, - "esrecurse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", - "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", - "dev": true, - "requires": { - "estraverse": "4.2.0", - "object-assign": "4.1.1" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "etag": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.6.0.tgz", - "integrity": "sha1-i8ssavElTEgd/IuZfJBu9ORCwgc=", - "requires": { - "crc": "3.2.1" - } - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37" - } - }, - "eventemitter2": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz", - "integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU=" - }, - "events": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/events/-/events-1.0.2.tgz", - "integrity": "sha1-dYSdz+k9EPsFfDAFWv29UdBqjiQ=" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "1.3.4", - "safe-buffer": "5.1.1" - } - }, - "express": { - "version": "4.12.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.12.4.tgz", - "integrity": "sha1-j+wlECVbxrLlgQfEgjnA+jB8GqI=", - "requires": { - "accepts": "1.2.13", - "content-disposition": "0.5.0", - "content-type": "1.0.4", - "cookie": "0.1.2", - "cookie-signature": "1.0.6", - "debug": "2.2.0", - "depd": "1.0.1", - "escape-html": "1.0.1", - "etag": "1.6.0", - "finalhandler": "0.3.6", - "fresh": "0.2.4", - "merge-descriptors": "1.0.0", - "methods": "1.1.2", - "on-finished": "2.2.1", - "parseurl": "1.3.2", - "path-to-regexp": "0.1.3", - "proxy-addr": "1.0.10", - "qs": "2.4.2", - "range-parser": "1.0.3", - "send": "0.12.3", - "serve-static": "1.9.3", - "type-is": "1.6.15", - "utils-merge": "1.0.0", - "vary": "1.0.1" - }, - "dependencies": { - "escape-html": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" - } - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "external-editor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", - "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", - "dev": true, - "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.19", - "tmp": "0.0.33" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "falafel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/falafel/-/falafel-1.2.0.tgz", - "integrity": "sha1-wY0k71CRF0pJfzGM0ksCaiXN2rQ=", - "requires": { - "acorn": "1.2.2", - "foreach": "2.0.5", - "isarray": "0.0.1", - "object-keys": "1.0.11" - }, - "dependencies": { - "acorn": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", - "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" - } - } - }, - "fantasy-combinators": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/fantasy-combinators/-/fantasy-combinators-0.0.1.tgz", - "integrity": "sha1-0YOIrtlL55CsQGkDH16gGDReM6E=" - }, - "fantasy-land": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fantasy-land/-/fantasy-land-1.0.1.tgz", - "integrity": "sha1-uc3W3KGJU5N3yE9HJPtyF64qZ3k=", - "requires": { - "daggy": "0.0.1", - "fantasy-combinators": "0.0.1" - } - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "1.0.5" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "1.3.0", - "object-assign": "4.1.1" - } - }, - "file-type": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-7.4.0.tgz", - "integrity": "sha1-KnyU9ioAMBULt9m2xwz6HT51nIY=" - }, - "finalhandler": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.6.tgz", - "integrity": "sha1-2vnEFhsbBuABRmsUEd/baXO+E4s=", - "requires": { - "debug": "2.2.0", - "escape-html": "1.0.1", - "on-finished": "2.2.1" - }, - "dependencies": { - "escape-html": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "circular-json": "0.3.3", - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" - } - }, - "fluture": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/fluture/-/fluture-7.2.2.tgz", - "integrity": "sha512-V9eRvHDUEvS/jkVNbexi4tzAe+2KLUuzYpSajOTY8JhTBRrsGoSbJJVzengcFWqU8TTsAmEHa6H94QaPJYiF1w==", - "requires": { - "concurrify": "1.0.2", - "denque": "1.2.2", - "inspect-f": "1.2.1", - "sanctuary-type-classes": "6.1.0", - "sanctuary-type-identifiers": "2.0.1" - }, - "dependencies": { - "sanctuary-type-classes": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sanctuary-type-classes/-/sanctuary-type-classes-6.1.0.tgz", - "integrity": "sha1-a9ZwRDWMhEDVYebiVWq4jdjNgAE=", - "requires": { - "sanctuary-type-identifiers": "1.0.0" - }, - "dependencies": { - "sanctuary-type-identifiers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sanctuary-type-identifiers/-/sanctuary-type-identifiers-1.0.0.tgz", - "integrity": "sha1-6PNZ8AbLXmJM+4RkYD/BFGCL3p8=" - } - } - } - } - }, - "folktale": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/folktale/-/folktale-2.0.1.tgz", - "integrity": "sha512-3kDSWVkSlErHIt/dC73vu+5zRqbW1mlnL46s2QfYN7Ps0JcS9MVtuLCrDQOBa7sanA+d9Fd8F+bn0VcyNe68Jw==" - }, - "follow-redirects": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.3.0.tgz", - "integrity": "sha1-9oSHH8EW0uMp/aVe9naH9Pq8kFw=", - "requires": { - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "forever-agent": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", - "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=" - }, - "form-data": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz", - "integrity": "sha1-Jvi8JtpkQOKZy9z7aQNcT3em5GY=", - "requires": { - "async": "0.9.2", - "combined-stream": "0.0.7", - "mime-types": "2.0.14" - }, - "dependencies": { - "mime-db": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", - "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" - }, - "mime-types": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", - "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", - "requires": { - "mime-db": "1.12.0" - } - } - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", - "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw=" - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "glob": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", - "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "2.0.10", - "once": "1.4.0" - } - }, - "globals": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.1.0.tgz", - "integrity": "sha512-uEuWt9mqTlPDwSqi+sHjD4nWU/1N+q0fiWI9T1mZpD2UENqX20CFD5T/ziLZvztPaBKl7ZylUi1q6Qfm7E2CiQ==", - "dev": true - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - } - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "requires": { - "function-bind": "1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", - "requires": { - "inherits": "2.0.3" - } - }, - "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.0" - } - }, - "hawk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-2.3.1.tgz", - "integrity": "sha1-HnMc45RH+h0PbXB/e87r7A/R7B8=", - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "1.1.3", - "minimalistic-assert": "1.0.0", - "minimalistic-crypto-utils": "1.0.1" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", - "dev": true - }, - "htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" - }, - "htmlparser2": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", - "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", - "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.4.1", - "domutils": "1.6.2", - "entities": "1.1.1", - "inherits": "2.0.3", - "readable-stream": "2.3.3" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - } - } - }, - "http-browserify": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz", - "integrity": "sha1-M3la3nLfiKz7/TZ3PO/tp2RzWyA=", - "requires": { - "Base64": "0.2.1", - "inherits": "2.0.3" - } - }, - "http-signature": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", - "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", - "requires": { - "asn1": "0.1.11", - "assert-plus": "0.1.5", - "ctype": "0.5.3" - } - }, - "https-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", - "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" - }, - "iconv-lite": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.8.tgz", - "integrity": "sha1-xgGadZXyzvynAuq2lKAQvNkpjSA=" - }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" - }, - "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", - "dev": true - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "inline-process-browser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/inline-process-browser/-/inline-process-browser-1.0.0.tgz", - "integrity": "sha1-RqYbFT3TybFiSxoAYm7bT39BTyI=", - "requires": { - "falafel": "1.2.0", - "through2": "0.6.5" - }, - "dependencies": { - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - } - } - }, - "inline-source-map": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.5.0.tgz", - "integrity": "sha1-Skxd2OT7Xps82mDIIt+tyu5m4K8=", - "requires": { - "source-map": "0.4.4" - } - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dev": true, - "requires": { - "ansi-escapes": "3.0.0", - "chalk": "2.3.0", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.1.0", - "figures": "2.0.0", - "lodash": "4.17.4", - "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rx-lite": "4.0.8", - "rx-lite-aggregates": "4.0.8", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" - } - }, - "insert-module-globals": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-6.6.3.tgz", - "integrity": "sha1-IGOOKaMPntHKLjqCX7wsulJG3fw=", - "requires": { - "JSONStream": "1.3.2", - "combine-source-map": "0.6.1", - "concat-stream": "1.4.10", - "is-buffer": "1.1.6", - "lexical-scope": "1.2.0", - "process": "0.11.10", - "through2": "1.1.1", - "xtend": "4.0.1" - } - }, - "inspect-f": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/inspect-f/-/inspect-f-1.2.1.tgz", - "integrity": "sha1-EXVztd9XpQneb+QBEx1Ca6wVkrU=" - }, - "ipaddr.js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", - "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", - "dev": true, - "requires": { - "is-path-inside": "1.0.1" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-resolvable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.1.tgz", - "integrity": "sha512-y5CXYbzvB3jTnWAZH1Nl7ykUWb6T3BcTs56HUruwBf8MhF56n1HWqhDWnVFo8GHrUPDgvUUNVhrc2U8W7iqz5g==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jayschema": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/jayschema/-/jayschema-0.3.2.tgz", - "integrity": "sha512-UHLk2ya7ItaLjmMVJWGE9b5t7jD3DZfmURdmz+rOVSiSYnrCtgcxvNXuQavcK7bhUBlXFmrXwRAPXkCMDxxANg==", - "requires": { - "when": "3.4.6" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", - "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" - } - }, - "js2xmlparser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", - "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", - "dev": true, - "requires": { - "xmlcreate": "1.0.2" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsdoc": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", - "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", - "dev": true, - "requires": { - "babylon": "7.0.0-beta.19", - "bluebird": "3.5.1", - "catharsis": "0.8.9", - "escape-string-regexp": "1.0.5", - "js2xmlparser": "3.0.0", - "klaw": "2.0.0", - "marked": "0.3.9", - "mkdirp": "0.5.1", - "requizzle": "0.2.1", - "strip-json-comments": "2.0.1", - "taffydb": "2.6.2", - "underscore": "1.8.3" - }, - "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true - }, - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", - "dev": true - } - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "jstransform": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-3.0.0.tgz", - "integrity": "sha1-olkats7o2XvzvoMNv6IxO4fNZAs=", - "requires": { - "base62": "0.1.1", - "esprima-fb": "3001.1.0-dev-harmony-fb", - "source-map": "0.1.31" - }, - "dependencies": { - "esprima-fb": { - "version": "3001.1.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", - "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=" - }, - "source-map": { - "version": "0.1.31", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.31.tgz", - "integrity": "sha1-n3BNDWnZ4TioG63267T94z0VHGE=", - "requires": { - "amdefine": "1.0.1" - } - } - } - }, - "klaw": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", - "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11" - } - }, - "labeled-stream-splicer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-1.0.2.tgz", - "integrity": "sha1-RhUzFTd4SYHo/SZOHzpDTE4N3WU=", - "requires": { - "inherits": "2.0.3", - "isarray": "0.0.1", - "stream-splicer": "1.3.2" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "lexical-scope": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", - "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", - "requires": { - "astw": "2.2.0" - } - }, - "lie": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.0.2.tgz", - "integrity": "sha1-/9oh17uibzd8rYZdNkmy/Izjn+o=", - "requires": { - "es3ify": "0.1.4", - "immediate": "3.0.6", - "inline-process-browser": "1.0.0", - "unreachable-branch-transform": "0.3.0" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - } - }, - "localforage": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.5.5.tgz", - "integrity": "sha1-VfwcOoikf2f1+sbxIxsl/xNVZCM=", - "requires": { - "lie": "3.0.2" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash-es": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", - "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.cond": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", - "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", - "dev": true - }, - "lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" - }, - "lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" - }, - "lodash.mergewith": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", - "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=" - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "3.0.2" - } - }, - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "requires": { - "es5-ext": "0.10.37" - } - }, - "marked": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", - "integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw==" - }, - "matrix-appservice": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-0.3.4.tgz", - "integrity": "sha1-M4ab3PMVJ0uUJUzDnZZdY8nLEyM=", - "requires": { - "body-parser": "1.12.4", - "express": "4.12.4", - "js-yaml": "3.10.0", - "morgan": "1.5.3", - "request": "2.53.0" - }, - "dependencies": { - "mime-db": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", - "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" - }, - "mime-types": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", - "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", - "requires": { - "mime-db": "1.12.0" - } - }, - "qs": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", - "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=" - }, - "request": { - "version": "2.53.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.53.0.tgz", - "integrity": "sha1-GAo66St7Y5gC5PlUXdj83rcddgw=", - "requires": { - "aws-sign2": "0.5.0", - "bl": "0.9.5", - "caseless": "0.9.0", - "combined-stream": "0.0.7", - "forever-agent": "0.5.2", - "form-data": "0.2.0", - "hawk": "2.3.1", - "http-signature": "0.10.1", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.0.14", - "node-uuid": "1.4.8", - "oauth-sign": "0.6.0", - "qs": "2.3.3", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.4.3" - } - } - } - }, - "matrix-appservice-bridge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/matrix-appservice-bridge/-/matrix-appservice-bridge-1.4.0.tgz", - "integrity": "sha1-RBaJH5FGFkcAOcg6epJ/ZtcHQ8Y=", - "requires": { - "bluebird": "2.11.0", - "extend": "3.0.1", - "jayschema": "0.3.2", - "js-yaml": "3.10.0", - "matrix-appservice": "0.3.4", - "matrix-js-sdk": "0.7.3", - "nedb": "1.8.0", - "nopt": "3.0.6", - "request": "2.83.0" - }, - "dependencies": { - "matrix-js-sdk": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.7.3.tgz", - "integrity": "sha1-H7NI3MFUK+yu16jnBFQrPtRNMn8=", - "requires": { - "another-json": "0.2.0", - "browser-request": "0.3.3", - "browserify": "10.2.6", - "olm": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz", - "q": "1.5.1", - "request": "2.83.0" - } - } - } - }, - "matrix-js-sdk": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.9.2.tgz", - "integrity": "sha1-KlIb3c9jfL796ROHhOYn6kNkW3c=", - "requires": { - "another-json": "0.2.0", - "babel-runtime": "6.26.0", - "bluebird": "3.5.1", - "browser-request": "0.3.3", - "content-type": "1.0.4", - "request": "2.83.0" - }, - "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - } - } - }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "1.1.6" - } - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" - }, - "dependencies": { - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - } - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "memoizee": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.11.tgz", - "integrity": "sha1-vemBdmPJ5A/bKk6hw2cpYIeujI8=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-weak-map": "2.0.2", - "event-emitter": "0.3.5", - "is-promise": "2.1.0", - "lru-queue": "0.1.0", - "next-tick": "1.0.0", - "timers-ext": "0.1.2" - } - }, - "merge-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz", - "integrity": "sha1-IWnPdTjhsMyH+4jhUC2EdLv3mGQ=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0" - } - }, - "mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", - "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "mimic-fn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", - "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", - "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "module-deps": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-3.9.1.tgz", - "integrity": "sha1-6nXK+RmQkNJbDVUStaysuW5/h/M=", - "requires": { - "JSONStream": "1.3.2", - "browser-resolve": "1.11.2", - "concat-stream": "1.4.10", - "defined": "1.0.0", - "detective": "4.7.1", - "duplexer2": "0.0.2", - "inherits": "2.0.3", - "parents": "1.0.1", - "readable-stream": "1.1.14", - "resolve": "1.5.0", - "stream-combiner2": "1.0.2", - "subarg": "1.0.0", - "through2": "1.1.1", - "xtend": "4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "morgan": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.5.3.tgz", - "integrity": "sha1-ittOcvnlxUNuXZP0KRCDX3nan98=", - "requires": { - "basic-auth": "1.0.4", - "debug": "2.2.0", - "depd": "1.0.1", - "on-finished": "2.2.1" - } - }, - "most": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/most/-/most-1.7.2.tgz", - "integrity": "sha512-jDSzUa7HPT79RqLEdrBnYpGZ5DXx8SSDYkS7W6ErBUgU0ewaNDLIdqLJ6GlzXK90eCaDzXm0B1C0KmfTT0XdZQ==", - "requires": { - "@most/multicast": "1.3.0", - "@most/prelude": "1.7.0", - "symbol-observable": "1.1.0" - } - }, - "most-subject": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/most-subject/-/most-subject-5.3.0.tgz", - "integrity": "sha1-Om4+/UNvqTvYszwqI5yQiCMKYQI=", - "requires": { - "@most/multicast": "1.3.0", - "@most/prelude": "1.7.0", - "most": "1.7.2" - } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" - }, - "mtproto-logger": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/mtproto-logger/-/mtproto-logger-0.1.9.tgz", - "integrity": "sha512-YohXpOVXQuc6DMASbyvLrGevhBZLATh371d3LJaqyuFjcY73C+dpn2tyApgJnJN1mraPEPzPi60BiRONLvZa7g==", - "requires": { - "array-flatten": "2.1.1", - "chalk": "2.3.0", - "debug": "2.6.9", - "eventemitter2": "4.1.2", - "most": "1.7.2", - "mtproto-shared": "0.1.8" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "mtproto-shared": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/mtproto-shared/-/mtproto-shared-0.1.8.tgz", - "integrity": "sha512-dyr4azn8JW3KWS7IJa8dEgIZtpWsFDdYNYBK6WL4u+jRSWmA2MSycIiHLDHYbAbM+7akn5Xi3l/RsDAYGUqtgg==", - "requires": { - "bluebird": "3.5.1", - "memoizee": "0.4.11" - }, - "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - } - } - }, - "mtproto-storage-fs": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/mtproto-storage-fs/-/mtproto-storage-fs-0.3.1.tgz", - "integrity": "sha512-QcsQf359e/8LGa9+6LsrA5IESBSZPw+1K0iNH1p269AmLeEOKx+dF7lcbtE/L0ydeyjqbYVPZZLsOeGUE0Lkjg==", - "requires": { - "bluebird": "3.5.1", - "fs-extra": "4.0.3", - "mtproto-logger": "0.1.9", - "mtproto-shared": "0.1.8" - }, - "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - } - } - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "nedb": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz", - "integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=", - "requires": { - "async": "0.2.10", - "binary-search-tree": "0.2.5", - "localforage": "1.5.5", - "mkdirp": "0.5.1", - "underscore": "1.4.4" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - } - } - }, - "negotiator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", - "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=" - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "node-uuid": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1.1.1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "2.5.0", - "is-builtin-module": "1.0.0", - "semver": "5.4.1", - "validate-npm-package-license": "3.0.1" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.6.0.tgz", - "integrity": "sha1-fb6uRPbKRU4fFoRR1jB0ZzWBPOM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" - }, - "olm": { - "version": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz", - "integrity": "sha512-OonRG3CfqT0UiUEDJ4T105z40dLKMxrAW8+CRhW07v9K/PYQ1F3mOlaJn8bdFX9BFJJMZxoqDzX8ojsek32nOA==", - "optional": true - }, - "on-finished": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz", - "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=", - "requires": { - "ee-first": "1.1.0" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "1.1.0" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - } - }, - "os-browserify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", - "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "dev": true, - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "1.2.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" - }, - "parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "requires": { - "path-platform": "0.11.15" - } - }, - "parse-asn1": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", - "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", - "requires": { - "asn1.js": "4.9.2", - "browserify-aes": "1.1.1", - "create-hash": "1.1.3", - "evp_bytestokey": "1.0.3", - "pbkdf2": "3.0.14" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "1.3.1" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" - }, - "path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=" - }, - "path-to-regexp": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", - "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss=" - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "2.3.0" - } - }, - "pbkdf2": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", - "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", - "requires": { - "create-hash": "1.1.3", - "create-hmac": "1.1.6", - "ripemd160": "2.0.1", - "safe-buffer": "5.1.1", - "sha.js": "2.4.9" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "pkg-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", - "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, - "requires": { - "find-up": "1.1.2" - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, - "postcss": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz", - "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==", - "requires": { - "chalk": "2.3.0", - "source-map": "0.6.1", - "supports-color": "5.1.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", - "dev": true - }, - "proxy-addr": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", - "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.0.5" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "public-encrypt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", - "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.1.3", - "parse-asn1": "5.1.0", - "randombytes": "2.0.5" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qs": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz", - "integrity": "sha1-9854jld33wtQENp/fE5zujJHD1o=" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "ramda": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz", - "integrity": "sha1-w7d1UZfzW43DUCIoJixMkd22uFc=" - }, - "randombytes": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "randomfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", - "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", - "requires": { - "randombytes": "2.0.5", - "safe-buffer": "5.1.1" - } - }, - "range-parser": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", - "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" - }, - "raw-body": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.0.2.tgz", - "integrity": "sha1-osL5jIUxzumcY9jSOLfel7tln8o=", - "requires": { - "bytes": "2.1.0", - "iconv-lite": "0.4.8" - }, - "dependencies": { - "bytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz", - "integrity": "sha1-rJPEEOL/ycx89LRks4KJBn9eR7Q=" - } - } - }, - "read-only-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-1.1.1.tgz", - "integrity": "sha1-Xad8eZ7ROI0++IoYRxu1kk+KC6E=", - "requires": { - "readable-stream": "1.0.34", - "readable-wrap": "1.0.0" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - } - } - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "readable-wrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/readable-wrap/-/readable-wrap-1.0.0.tgz", - "integrity": "sha1-O1ohHGMeEjA6VJkcgGwX564ga/8=", - "requires": { - "readable-stream": "1.1.14" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "recast": { - "version": "0.10.43", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.10.43.tgz", - "integrity": "sha1-uV1Q9tYHYaX2JS4V2AZ4FoSRzn8=", - "requires": { - "ast-types": "0.8.15", - "esprima-fb": "15001.1001.0-dev-harmony-fb", - "private": "0.1.8", - "source-map": "0.5.7" - }, - "dependencies": { - "esprima-fb": { - "version": "15001.1001.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz", - "integrity": "sha1-Q761fsJujPI3092LM+QlM1d/Jlk=" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "redux": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", - "requires": { - "lodash": "4.17.4", - "lodash-es": "4.17.4", - "loose-envify": "1.3.1", - "symbol-observable": "1.1.0" - } - }, - "redux-act": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/redux-act/-/redux-act-1.5.1.tgz", - "integrity": "sha512-GD7I/4kH1X/H7VI1s1CRzJun8PALba/B71KGVxG4c3fJdKBQN9ED6BIYZjfaWsTtYGr1i3TE6AEFbAcB+utgSQ==" - }, - "redux-most": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/redux-most/-/redux-most-0.6.2.tgz", - "integrity": "sha1-BFHaHWH3aqv/mtw4DTINPmc7FoY=", - "requires": { - "most-subject": "5.3.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.0" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - } - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" - } - }, - "requizzle": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", - "integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=", - "dev": true, - "requires": { - "underscore": "1.6.0" - }, - "dependencies": { - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true - } - } - }, - "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", - "requires": { - "path-parse": "1.0.5" - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "7.1.2" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - } - } - }, - "ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", - "requires": { - "hash-base": "2.0.2", - "inherits": "2.0.3" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "2.1.0" - } - }, - "rusha": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.11.tgz", - "integrity": "sha1-yqiWOx2/0inZBibdPyp4RDDWBY0=" - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", - "dev": true - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "dev": true, - "requires": { - "rx-lite": "4.0.8" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "sanctuary-type-classes": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/sanctuary-type-classes/-/sanctuary-type-classes-0.3.0.tgz", - "integrity": "sha1-SbknwDUzhna11G40jK07+/79MIc=" - }, - "sanctuary-type-identifiers": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/sanctuary-type-identifiers/-/sanctuary-type-identifiers-2.0.1.tgz", - "integrity": "sha1-/FJM9t2Szr/LsN2VCe/xkxWaIO0=" - }, - "sanitize-html": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.16.3.tgz", - "integrity": "sha512-XpAJGnkMfNM7AzXLRw225blBB/pE4dM4jzRn98g4r88cfxwN6g+5IsRmCAh/gbhYGm6u6i97zsatMOM7Lr8wyw==", - "requires": { - "htmlparser2": "3.9.2", - "lodash.clonedeep": "4.5.0", - "lodash.escaperegexp": "4.1.2", - "lodash.mergewith": "4.6.0", - "postcss": "6.0.16", - "srcset": "1.0.0", - "xtend": "4.0.1" - } - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - }, - "send": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/send/-/send-0.12.3.tgz", - "integrity": "sha1-zRLcWP3iHk+RkCs5sv2gWnptm9w=", - "requires": { - "debug": "2.2.0", - "depd": "1.0.1", - "destroy": "1.0.3", - "escape-html": "1.0.1", - "etag": "1.6.0", - "fresh": "0.2.4", - "mime": "1.3.4", - "ms": "0.7.1", - "on-finished": "2.2.1", - "range-parser": "1.0.3" - }, - "dependencies": { - "escape-html": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" - } - } - }, - "serve-static": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.9.3.tgz", - "integrity": "sha1-X42gcyOtOF/z3FQfGnkXsuQ261c=", - "requires": { - "escape-html": "1.0.1", - "parseurl": "1.3.2", - "send": "0.12.3", - "utils-merge": "1.0.0" - }, - "dependencies": { - "escape-html": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" - } - } - }, - "sha.js": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", - "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", - "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - } - }, - "shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", - "requires": { - "json-stable-stringify": "0.0.1", - "sha.js": "2.4.9" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-0.0.1.tgz", - "integrity": "sha1-GkEZbzwDM8SCMjWT1ohuzxU92YY=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "requires": { - "hoek": "2.16.3" - } - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": "1.0.1" - } - }, - "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", - "dev": true, - "requires": { - "spdx-license-ids": "1.2.2" - } - }, - "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", - "dev": true - }, - "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "srcset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz", - "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=", - "requires": { - "array-uniq": "1.0.3", - "number-is-nan": "1.0.1" - } - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "stream-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-1.0.0.tgz", - "integrity": "sha1-v5tKv7QrJ011FHnkTg/yZWtvEZM=", - "requires": { - "inherits": "2.0.3", - "readable-stream": "1.0.34" - } - }, - "stream-combiner2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.0.2.tgz", - "integrity": "sha1-unKmtQy/q/qVD8i8h2BL0B62BnE=", - "requires": { - "duplexer2": "0.0.2", - "through2": "0.5.1" - }, - "dependencies": { - "through2": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", - "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "3.0.0" - } - }, - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" - } - } - }, - "stream-splicer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-1.3.2.tgz", - "integrity": "sha1-PARBvhW5v04iYnXm3IOWR0VUZmE=", - "requires": { - "indexof": "0.0.1", - "inherits": "2.0.3", - "isarray": "0.0.1", - "readable-stream": "1.1.14", - "readable-wrap": "1.0.0", - "through2": "1.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "string-similarity": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.2.0.tgz", - "integrity": "sha1-11FTyzg4RjGLejmo2SkrtNtOnDA=", - "requires": { - "lodash": "4.17.4" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "requires": { - "minimist": "1.2.0" - } - }, - "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", - "requires": { - "has-flag": "2.0.0" - } - }, - "symbol-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz", - "integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw==" - }, - "syntax-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", - "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", - "requires": { - "acorn": "4.0.13" - } - }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "chalk": "2.3.0", - "lodash": "4.17.4", - "slice-ansi": "1.0.0", - "string-width": "2.1.1" - } - }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true - }, - "telegram-mtproto": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/telegram-mtproto/-/telegram-mtproto-3.2.7.tgz", - "integrity": "sha512-hBuUqr10EPoa7oCS2MywqNhSJ7G+pjKkkjAOQg0Z+866MchW8at20QhvUupPGFaR0WpQ+eKbHw+Gfc1bbGsYOQ==", - "requires": { - "@goodmind/node-cryptojs-aes": "0.5.0", - "@safareli/free": "1.0.0", - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "apropos": "0.4.0", - "axios": "0.16.2", - "bluebird": "3.5.1", - "detect-node": "2.0.3", - "eventemitter2": "4.1.2", - "fluture": "7.2.2", - "folktale": "2.0.1", - "most": "1.7.2", - "most-subject": "5.3.0", - "mtproto-logger": "0.1.9", - "mtproto-shared": "0.1.8", - "mtproto-storage-fs": "0.3.1", - "pako": "1.0.6", - "ramda": "0.24.1", - "randombytes": "2.0.5", - "redux": "3.7.2", - "redux-act": "1.5.1", - "redux-most": "0.6.2", - "rusha": "0.8.11", - "uuid": "3.1.0" - }, - "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", - "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", - "requires": { - "readable-stream": "1.1.14", - "xtend": "4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - } - } - }, - "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", - "requires": { - "process": "0.11.10" - } - }, - "timers-ext": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.2.tgz", - "integrity": "sha1-YcxHp2wavTGV8UUn+XjViulMUgQ=", - "requires": { - "es5-ext": "0.10.37", - "next-tick": "1.0.0" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "1.0.2" - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "1.4.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2" - } - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.17" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "umd": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", - "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=" - }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" - }, - "underscore-contrib": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz", - "integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=", - "dev": true, - "requires": { - "underscore": "1.6.0" - }, - "dependencies": { - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true - } - } - }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" - }, - "unreachable-branch-transform": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unreachable-branch-transform/-/unreachable-branch-transform-0.3.0.tgz", - "integrity": "sha1-2ZzExudG0mSSiEW2EdtUsPNHTKo=", - "requires": { - "esmangle-evaluator": "1.0.1", - "recast": "0.10.43", - "through2": "0.6.5" - }, - "dependencies": { - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - } - } - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", - "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" - }, - "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", - "dev": true, - "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" - } - }, - "vary": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", - "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "requires": { - "indexof": "0.0.1" - } - }, - "when": { - "version": "3.4.6", - "resolved": "https://registry.npmjs.org/when/-/when-3.4.6.tgz", - "integrity": "sha1-j7y3zBQ50sOmjEMfFRbm3M6a0ow=" - }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true, - "requires": { - "isexe": "2.0.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "0.5.1" - } - }, - "xmlcreate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", - "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "requires": { - "argparse": "1.0.9", - "glob": "7.1.2" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.8" - } - } - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 139e12aa..00000000 --- a/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "mautrix-telegram", - "version": "0.1.0", - "description": "A Matrix-Telegram puppeting bridge", - "author": "Tulir Asokan ", - "license": "GPL-3.0", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/tulir/mautrix-telegram.git" - }, - "dependencies": { - "chalk": "^2.3.0", - "colors": "1.1.x", - "commander": "2.12.x", - "escape-html": "1.0.x", - "file-type": "7.4.x", - "marked": "0.3.x", - "matrix-appservice-bridge": "1.x.x", - "matrix-js-sdk": "0.x.x", - "md5": "2.2.x", - "sanitize-html": "1.16.x", - "string-similarity": "1.2.x", - "telegram-mtproto": "3.2.7", - "yamljs": "0.3.x" - }, - "devDependencies": { - "eslint": "4.15.x", - "eslint-config-airbnb-base": "12.1.x", - "eslint-plugin-import": "2.8.x", - "jsdoc": "3.5.x" - }, - "scripts": { - "gen-jsdoc": "./node_modules/.bin/jsdoc src/ --recurse --package package.json --readme README.md --destination jsdoc" - } -} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..34771da1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +aiohttp +-e git+git://github.com/Cadair/matrix-python-sdk#egg=matrix_client +#matrix-client +ruamel.yaml +python-magic +SQLAlchemy +Telethon +Markdown +Pillow diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..03299893 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +import setuptools + +setuptools.setup( + name="mautrix_telegram", + version="0.1.0", + url="https://github.com/tulir/mautrix-telegram", + + author="Tulir Asokan", + author_email="tulir@maunium.net", + + description="A Matrix-Telegram puppeting bridge.", + long_description=open("README.md").read(), + + packages=setuptools.find_packages(), + + install_requires=["telethon", "matrix-client", "sqlalchemy"], + + classifiers=[ + "Development Status :: 2 - Pre-Alpha", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + ], + entry_points=""" + [console_scripts] + mautrix-telegram=mautrix_telegram.__main__:main + """, +) diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 944c635a..00000000 --- a/src/app.js +++ /dev/null @@ -1,728 +0,0 @@ -// mautrix-telegram - A Matrix-Telegram puppeting bridge -// Copyright (C) 2017 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -const { Bridge } = require("matrix-appservice-bridge") -const escapeHTML = require("escape-html") -const sanitizeHTML = require("sanitize-html") -const marked = require("marked") -const commands = require("./commands") -const MatrixUser = require("./matrix-user") -const TelegramUser = require("./telegram-user") -const TelegramPeer = require("./telegram-peer") -const Portal = require("./portal") -const chalk = require("chalk") - -/** - * The base class for the bridge. - */ -class MautrixTelegram { - /** - * Create a MautrixTelegram instance with the given config data. - * - * @param config The data from the config file. - */ - constructor(config) { - /** - * The app config. - * @type {Object} - */ - this.config = config - /** - * A special-cased {@link TelegramUser} that is used to send broadcasts to a channel. - * @type {TelegramUser} - */ - this.channelTelegramSender = new TelegramUser(this, -1) - - /** - * MXID -> {@link MatrixUser} cache. - * @private - * @type {Map} - */ - this.matrixUsersByID = new Map() - /** - * Telegram ID -> {@link MatrixUser} cache. - * @priavte - * @type {Map} - */ - this.matrixUsersByTelegramID = new Map() - /** - * Telegram ID -> {@link TelegramUser} cache. - * @private - * @type {Map} - */ - this.telegramUsersByID = new Map() - /** - * Telegram peer ID -> {@link Portal} cache. - * @private - * @type {Map} - */ - this.portalsByPeerID = new Map() - /** - * Matrix room ID -> {@link Portal} cache. - * @private - * @type {Map} - */ - this.portalsByRoomID = new Map() - /** - * List of management rooms. - * @type {string[]} - */ - this.managementRooms = [] - - /** - * A regular expression that matches MXIDs of Telegram user bridged by this bridge. - * @type {RegExp} - */ - this.usernameRegex = new RegExp( - `^@${ - this.config.bridge.username_template.replace("${ID}", "([0-9]+)") - }:${ - this.config.homeserver.domain - }$`) - - const self = this - /** - * The matrix-appservice-bridge Bridge instance. - * @private - * @type {Bridge} - */ - this.bridge = new Bridge({ - homeserverUrl: config.homeserver.address, - domain: config.homeserver.domain, - registration: config.appservice.registration, - controller: { - onUserQuery(/*user*/) { - return {} - }, - onLog: msg => self.debug("blue", msg), - async onEvent(request/*, context*/) { - try { - await self.handleMatrixEvent(request.getData()) - } catch (err) { - console.error("Matrix event handling failed:", err) - console.error(err.stack) - } - }, - }, - }) - } - - debug(color, ...message) { - if (this.config.appservice.debug) { - if (!chalk[color]) { - message.unshift(`[Invalid color: ${color}]`) - color = "bgRed" - } - console.log(chalk[color](...message)) - } - } - - debugErr(color, ...message) { - if (this.config.appservice.debug) { - if (!chalk[color]) { - message.unshift(`[Invalid color: ${color}]`) - color = "bgRed" - } - console.error(chalk[color](...message)) - } - } - - info(...message) { - console.log(...message) - } - - warn(...message) { - console.error(chalk.yellow(...message)) - } - - /** - * Start the bridge. - */ - async run() { - this.info("Appservice listening on port %s", this.config.appservice.port) - await this.bridge.run(this.config.appservice.port, {}) - - // Load all Matrix users to cache - const userEntries = await this.bridge.getUserStore().select({ type: "matrix" }) - - for (const entry of userEntries) { - const user = MatrixUser.fromEntry(this, entry) - this.matrixUsersByID.set(entry.id, user) - if (user.telegramUserID) { - this.matrixUsersByTelegramID.set(user.telegramUserID, user) - } - } - } - - /** - * The {@link MatrixClient} object for the appservice bot. - */ - get bot() { - return this.bridge.getBot() - } - - /** - * The {@link Intent} object for the appservice bot. - */ - get botIntent() { - return this.bridge.getIntent() - } - - /** - * Get the {@link Intent} for the Telegram user with the given ID. - * - * This does not care if a {@link TelegramUser} object for the user ID exists. - * It simply returns an intent for a Matrix puppet user with the correct MXID. - * - * @param {number} id The ID of the Telegram user. - * @returns {Intent} The Matrix puppet intent for the given Telegram user. - */ - getIntentForTelegramUser(id) { - if (id === -1) { - return this.botIntent - } - return this.bridge.getIntentFromLocalpart(this.getUsernameForTelegramUser(id)) - } - - /** - * Get the Matrix username localpart for the Telegram user with the given ID. - * - * @param {number} id The ID of the Telegram user. - * @returns {string} The Matrix username localpart for the given Telegram user. - */ - getUsernameForTelegramUser(id) { - return this.config.bridge.username_template.replace("${ID}", id) - } - - /** - * Get the full Matrix ID ({@code @localpart:homeserver.tld}) for the Telegram user with the given ID. - * - * @param {number} id The ID of the Telegram user. - * @returns {string} The full Matrix ID for the given Telegram user. - */ - getMXIDForTelegramUser(id) { - return `@${this.getUsernameForTelegramUser(id)}:${this.config.homeserver.domain}` - } - - /** - * Get the matrix.to link for the Matrix puppet of the Telegram user with the given ID. - * - * @param {number} id The ID of the Telegram user. - * @returns {string} A matrix.to link that points to the Matrix puppet of the given user. - */ - getMatrixToLinkForTelegramUser(id) { - return `https://matrix.to/#/${this.getMXIDForTelegramUser(id)}` - } - - /** - * Get a {@link Portal} by Telegram peer or peer ID. - * - * This will either get the room from the room cache or the bridge room database. - * If the room is not found, a new {@link Portal} object is created. - * - * You may set the {@code opts.createIfNotFound} parameter to change whether or not to create the Portal - * automatically. However, if the peer is just the ID, a new room will not be created in any case. - * - * @param {TelegramPeer|number} peer The TelegramPeer object OR the ID of the peer whose portal to get. - * If only a peer ID is given, it is assumed that the peer is a chat or a - * channel. Searching for user peers requires the receiver ID, thus here it - * requires the full TelegramPeer object. - * @param {object} [opts] Additional options. - * @param {boolean} opts.createIfNotFound Whether or not to create the room if it is not found - * @returns {Portal} The Portal object. - */ - async getPortalByPeer(peer, { createIfNotFound = true } = {}) { - if (typeof peer === "number") { - peer = { - id: peer, - } - createIfNotFound = false - } else if (!(peer instanceof TelegramPeer)) { - throw new Error("Invalid argument: peer is not a number or a TelegramPeer.") - } - let portal = this.portalsByPeerID.get(peer.id) - if (portal) { - return portal - } - - const query = { - type: "portal", - id: peer.id, - } - if (peer.type === "user") { - query.receiverID = peer.receiverID - } - const entries = await this.bridge.getRoomStore().select(query) - - // Handle possible db query race conditions - portal = this.portalsByPeerID.get(peer.id) - if (portal) { - return portal - } - - if (entries.length) { - portal = Portal.fromEntry(this, entries[0]) - } else if (createIfNotFound) { - portal = new Portal(this, undefined, peer) - } else { - return undefined - } - this.portalsByPeerID.set(peer.id, portal) - if (portal.roomID) { - this.portalsByRoomID.set(portal.roomID, portal) - } - return portal - } - - /** - * Get a {@link Portal} by Matrix room ID. - * - * This will either get the room from the room cache or the bridge room database. - * If the room is not found, this function WILL NOT create a new room, - * but rather just return {@code undefined}. - * - * @param {string} id The Matrix room ID of the portal to get. - * @returns {Portal} The Portal object. - */ - async getPortalByRoomID(id) { - let portal = this.portalsByRoomID.get(id) - if (portal) { - return portal - } - - // Check if we have it stored in the by-peer map - // FIXME this is probably useless - for (const [_, portalByPeer] of this.portalsByPeerID) { - if (portalByPeer.roomID === id) { - this.portalsByRoomID.set(id, portalByPeer) - return portalByPeer - } - } - - const entries = await this.bridge.getRoomStore().select({ - type: "portal", - roomID: id, - }) - - // Handle possible db query race conditions - portal = this.portalsByRoomID.get(id) - if (portal) { - return portal - } - - if (entries.length) { - portal = Portal.fromEntry(this, entries[0]) - } else { - // Don't create portals based on room ID - return undefined - } - this.portalsByPeerID.set(portal.id, portal) - this.portalsByRoomID.set(id, portal) - return portal - } - - /** - * Get a {@link TelegramUser} by ID. - * - * This will either get the user from the user cache or the bridge user database. - * If the user is not found, a new {@link TelegramUser} instance is created. - * - * @param {number} id The internal Telegram ID of the user to get. - * @returns {TelegramUser} The TelegramUser object. - */ - async getTelegramUser(id, { createIfNotFound = true } = {}) { - if (id === -1) { - return this.channelTelegramSender - } - // TODO remove this after bugs are fixed - if (isNaN(parseInt(id, 10))) { - const err = new Error("Fatal: non-int Telegram user ID") - console.error(err.stack) - throw err - } - let user = this.telegramUsersByID.get(id) - if (user) { - return user - } - - const entries = await this.bridge.getUserStore().select({ - type: "remote", - id, - }) - - // Handle possible db query race conditions - if (this.telegramUsersByID.has(id)) { - return this.telegramUsersByID.get(id) - } - - if (entries.length) { - user = TelegramUser.fromEntry(this, entries[0]) - } else if (createIfNotFound) { - user = new TelegramUser(this, id) - } else { - return undefined - } - this.telegramUsersByID.set(id, user) - return user - } - - /** - * Get a {@link MatrixUser} by Telegram user ID. - * - * This will either get the user from the user cache or the bridge user database. - * - * @param {number} id The Telegram user ID of the Matrix user to get. - * @returns {MatrixUser} The MatrixUser object. - */ - async getMatrixUserByTelegramID(id) { - let user = this.matrixUsersByTelegramID.get(id) - if (user) { - return user - } - - // Check if we have the user stored in the by- map - // FIXME this should be made useless by making sure we always add to the second map when appropriate - for (const [_, userByMXID] of this.matrixUsersByID) { - if (userByMXID.telegramUserID === id) { - this.matrixUsersByTelegramID.set(id, userByMXID) - return userByMXID - } - } - - const entries = this.bridge.getUserStore().select({ - type: "matrix", - telegramID: id, - }) - - // Handle possible db query race conditions - if (this.matrixUsersByTelegramID.has(id)) { - return this.matrixUsersByTelegramID.get(id) - } - - if (entries.length) { - user = MatrixUser.fromEntry(this, entries[0]) - } else { - return undefined - } - this.matrixUsersByID.set(user.userID, user) - this.matrixUsersByTelegramID.set(id, user) - return user - } - - /** - * Get a {@link MatrixUser} by ID. - * - * This will either get the user from the user cache or the bridge user database. - * If the user is not found, a new {@link MatrixUser} instance is created. - * - * @param {string} id The MXID of the Matrix user to get. - * @returns {MatrixUser} The MatrixUser object. - */ - async getMatrixUser(id, { createIfNotFound = true } = {}) { - let user = this.matrixUsersByID.get(id) - if (user) { - return user - } - - const entries = this.bridge.getUserStore().select({ - type: "matrix", - id, - }) - - // Handle possible db query race conditions - if (this.matrixUsersByID.has(id)) { - return this.matrixUsersByID.get(id) - } - - if (entries.length) { - user = MatrixUser.fromEntry(this, entries[0]) - } else if (createIfNotFound) { - user = new MatrixUser(this, id) - } else { - return undefined - } - this.matrixUsersByID.set(id, user) - if (user.telegramUserID) { - this.matrixUsersByID.set(user.telegramUserID, user) - } - return user - } - - /** - * Save a user to the bridge user database. - * - * @param {MatrixUser|TelegramUser} user The user object to save. - */ - putUser(user) { - const entry = user.toEntry() - return this.bridge.getUserStore() - .upsert({ - type: entry.type, - id: entry.id, - }, entry) - } - - /** - * Save a room to the bridge room database. - * - * @param {Room} room The Room object to save. - */ - putRoom(room) { - const entry = room.toEntry() - return this.bridge.getRoomStore() - .upsert({ - type: entry.type, - id: entry.id, - }, entry) - } - - /** - * Get the members in the given room. - * - * @param {string} roomID The ID of the room to search. - * @param {Intent} [intent] The Intent object to use when reading the room state. - * Uses {@link #botIntent} by default. - * @returns {string[]} The list of MXIDs who are in the room. - */ - async getRoomMembers(roomID, intent = this.botIntent) { - const roomState = await intent.roomState(roomID) - const members = [] - for (const event of roomState) { - if (event.type === "m.room.member" && event.membership === "join") { - members.push(event.user_id) - } - } - return members - } - - async getRoomTitle(roomID, intent = this.botIntent) { - const roomState = await intent.roomState(roomID) - for (const event of roomState) { - if (event.type === "m.room.name") { - return event.content.name - } - } - return undefined - } - - async handlePart(sender, evt) { - // TODO handle kicking real Matrix users who have logged in with Telegram? - const capture = this.usernameRegex.exec(evt.state_key) - if (!capture) { - return - } - - const telegramID = +capture[1] - if (!telegramID || isNaN(telegramID)) { - return - } - - const user = await this.getTelegramUser(telegramID) - - const portal = await this.getPortalByRoomID(evt.room_id) - if (!portal) { - return - } - await portal.kickTelegram(sender.telegramPuppet, user) - } - - /** - * Handle an invite to a Matrix room. - * - * @param {MatrixUser} sender The user who sent this invite. - * @param {MatrixEvent} evt The invite event. - */ - async handleInvite(sender, evt) { - const asBotID = this.bridge.getBot().getUserId() - if (evt.state_key === asBotID) { - // Accept all AS bot invites. - try { - await this.botIntent.join(evt.room_id) - } catch (err) { - console.error(`Failed to join room ${evt.room_id}:`, err) - if (err instanceof Error) { - console.error(err.stack) - } - } - return - } - if (evt.sender === asBotID || evt.sender === evt.state_key) { - return - } - - // Check if the invited user is a Telegram user. - const capture = this.usernameRegex.exec(evt.state_key) - if (!capture) { - return - } - - const telegramID = +capture[1] - if (!telegramID || isNaN(telegramID)) { - return - } - - const intent = this.getIntentForTelegramUser(telegramID) - try { - await intent.join(evt.room_id) - const members = await this.getRoomMembers(evt.room_id, intent) - const user = await this.getTelegramUser(telegramID) - if (members.length < 2) { - console.warn(`No members in room ${evt.room_id}`) - await intent.leave(evt.room_id) - } else if (members.length === 2) { - const peer = user.toPeer(sender.telegramPuppet) - const portal = await this.getPortalByPeer(peer) - if (portal.roomID) { - await intent.sendMessage(evt.room_id, { - msgtype: "m.notice", - body: "You already have a private chat room with me!\nI'll re-invite you to that room.", - }) - try { - await intent.invite(portal.roomID, sender.userID) - } catch (_) {} - await intent.leave(evt.room_id) - } else { - portal.roomID = evt.room_id - await portal.save() - await intent.sendMessage(portal.roomID, { - msgtype: "m.notice", - body: "Portal to Telegram private chat created.", - }) - await user.updateInfo(sender.telegramPuppet, undefined, { updateAvatar: true }) - } - } else if (!members.includes(asBotID)) { - await intent.sendMessage(evt.room_id, { - msgtype: "m.notice", - body: "Inviting additional Telegram users to private chats or non-portal rooms is not supported.", - }) - await intent.leave(evt.room_id) - } else { - const portal = await this.getPortalByRoomID(evt.room_id) - if (portal) { - await portal.inviteTelegram(sender.telegramPuppet, user) - } - } - } catch (err) { - console.error(`Failed to process invite to room ${evt.room_id} for Telegram user ${telegramID}: ${err}`) - if (err instanceof Error) { - console.error(err.stack) - } - } - } - - /** - * Handle a single received Matrix event. - * - * @param {MatrixEvent} evt The Matrix event that occurred. - */ - async handleMatrixEvent(evt) { - const user = await this.getMatrixUser(evt.sender) - if (!user.whitelisted) { - return - } - - const asBotID = this.bridge.getBot().getUserId() - if (evt.type === "m.room.member") { - if (evt.content.membership === "invite") { - await this.handleInvite(user, evt) - return - } else if (evt.content.membership === "leave") { - await this.handlePart(user, evt) - return - } - } - - if (evt.sender === asBotID || evt.type !== "m.room.message" || !evt.content) { - // Ignore own messages and non-message events. - return - } - - const cmdprefix = this.config.bridge.commands.prefix - const hasCommandPrefix = cmdprefix && evt.content.body.startsWith(`${cmdprefix} `) - - const portal = await this.getPortalByRoomID(evt.room_id) - if (portal && !hasCommandPrefix) { - portal.handleMatrixEvent(user, evt) - return - } - - let isManagement = this.managementRooms.includes(evt.room_id) || hasCommandPrefix - if (!isManagement) { - const roomMembers = await this.getRoomMembers(evt.room_id) - if (roomMembers.length === 2 && roomMembers.includes(asBotID)) { - this.managementRooms.push(evt.room_id) - isManagement = true - } - } - if (isManagement) { - const prefixLength = cmdprefix.length + 1 - if (cmdprefix && evt.content.body.startsWith(`${cmdprefix} `)) { - evt.content.body = evt.content.body.substr(prefixLength) - } - const args = evt.content.body.split(" ") - const command = args.shift() - const replyFunc = (reply, { allowHTML = false, markdown = true } = {}) => { - reply = reply.replace("$cmdprefix", cmdprefix || "") - if (!markdown && !allowHTML) { - reply = escapeHTML(reply) - } - if (markdown) { - reply = marked(reply, { - sanitize: !allowHTML, - }) - } - this.botIntent.sendMessage( - evt.room_id, { - body: sanitizeHTML(reply), - formatted_body: reply, - msgtype: "m.notice", - format: "org.matrix.custom.html", - }) - } - commands.run(user, command, args, replyFunc, { - app: this, - evt, - roomID: evt.room_id, - isManagement, - isPortal: !!portal, - }) - } - } - - /** - * Check whether the given user ID is allowed to use this bridge. - * - * @param {string} userID The full Matrix ID to check (@user:homeserver.tld) - * @returns {boolean} Whether or not the user should be allowed to use the bridge. - */ - checkWhitelist(userID) { - if (!this.config.bridge.whitelist || this.config.bridge.whitelist.length === 0) { - return true - } - - userID = userID.toLowerCase() - const userIDCapture = /@.+:(.+)/.exec(userID) - const homeserver = userIDCapture && userIDCapture.length > 1 ? userIDCapture[1] : undefined - for (let whitelisted of this.config.bridge.whitelist) { - whitelisted = whitelisted.toLowerCase() - if (whitelisted === userID || (homeserver && whitelisted === homeserver)) { - return true - } - } - return false - } -} - -module.exports = MautrixTelegram diff --git a/src/commands.js b/src/commands.js deleted file mode 100644 index 61c19102..00000000 --- a/src/commands.js +++ /dev/null @@ -1,425 +0,0 @@ -// mautrix-telegram - A Matrix-Telegram puppeting bridge -// Copyright (C) 2017 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -const makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash -const Portal = require("./portal") - -const commands = {} - -/** - * Module containing all management commands. - * - * @module commands - */ - -/** - * Run management command. - * - * @param {string} sender The MXID of the user who sent the command. - * @param {string} command The command itself. - * @param {Array} args A list of arguments. - * @param {function} reply A function that is called to reply to the command. - * @param {object} extra Extra information that the handlers may find useful. - * @param {MautrixTelegram} extra.app The app main class instance. - * @param {MatrixEvent} extra.evt The event that caused this call. - * @param {string} extra.roomID The ID of the Matrix room the command was sent to. - * @param {boolean} extra.isManagement Whether or not the Matrix room is a management room. - * @param {boolean} extra.isPortal Whether or not the Matrix room is a portal to a Telegram chat. - */ -function run(sender, command, args, reply, extra) { - const commandFunc = this.commands[command] - if (!commandFunc) { - if (sender.commandStatus) { - if (command === "cancel") { - reply(`${sender.commandStatus.action} cancelled.`) - sender.commandStatus = undefined - return undefined - } - args.unshift(command) - return sender.commandStatus.next(sender, args, reply, extra) - } - reply("Unknown command. Try `$cmdprefix help` for help.") - return undefined - } - try { - return commandFunc(sender, args, reply, extra) - } catch (err) { - reply(`Error running command: ${err}.`) - if (err instanceof Error) { - reply(["```", err.stack, "```"].join("")) - console.error(err.stack) - } - } - return undefined -} - -commands.cancel = () => "Nothing to cancel." - -commands.help = (sender, args, reply, { isManagement, isPortal }) => { - let replyMsg = "" - if (isManagement) { - replyMsg += "This is a management room: prefixing commands with `$cmdprefix` is not required.\n" - } else if (isPortal) { - replyMsg += "**This is a portal room**: you must always prefix commands with `$cmdprefix`.\n" + - "Management commands will not be sent to Telegram.\n" - } else { - replyMsg += "**This is not a management room**: you must prefix commands with `$cmdprefix`.\n" - } - replyMsg += ` -_**Generic bridge commands**: commands for using the bridge that aren't related to Telegram._
-**help** - Show this help message.
-**cancel** - Cancel an ongoing action (such as login).
-**setManagement** - Mark the room as a management room.
-**unsetManagement** - Undo management room marking. - -_**Telegram actions**: commands for using the bridge to interact with Telegram._
-**login** <_phone_> - Request an authentication code.
-**logout** - Log out from Telegram.
-**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.
-**create** <_group/channel_> [_room ID_] - Create a Telegram chat of the given type for a Matrix room. - If the room ID is not specified, a chat for the current room is created.
-**upgrade** - Upgrade a normal Telegram group to a supergroup. - -_**Temporary commands**: commands that will be replaced with more Matrix-y actions later._
-**pm** <_id_> - Open a private chat with the given Telegram user ID. - -_**Debug commands**: commands to help in debugging the bridge. Disabled by default._
-**api** <_method_> <_args_> - Call a Telegram API method. Args is always a single JSON object. -` - reply(replyMsg, { allowHTML: true }) -} - -commands.setManagement = (sender, _, reply, { app, roomID, isPortal }) => { - if (isPortal) { - reply("You may not mark portal rooms as management rooms.") - return - } - app.managementRooms.push(roomID) - reply("Room marked as management room. You can now run commands without the `$cmdprefix` prefix.") -} - -commands.unsetManagement = (sender, _, reply, { app, roomID }) => { - app.managementRooms.splice(app.managementRooms.indexOf(roomID), 1) - reply("Room unmarked as management room. You must now include the `$cmdprefix` prefix when running commands.") -} - - -///////////////////////////// -// Authentication handlers // -///////////////////////////// - -/** - * Two-factor authentication handler. - */ -commands.enterPassword = async (sender, args, reply, { isManagement }) => { - if (!isManagement) { - reply("Logging in is considered a confidential action, and thus is only allowed in management rooms.") - return - } else if (args.length === 0) { - reply("**Usage:** `$cmdprefix [salt]`") - return - } - - let salt - - if (!sender.commandStatus || !sender.commandStatus.salt) { - if (args.length > 1) { - salt = args[1] - } else { - reply("No password salt found. Did you enter your phone code already?") - return - } - } else { - salt = sender.commandStatus.salt - } - - const hash = makePasswordHash(salt, args[0]) - try { - await sender.checkPassword(hash) - reply(`Logged in successfully as ${sender.telegramPuppet.getDisplayName()}.`) - sender.commandStatus = undefined - } catch (err) { - reply(`**Login failed:** ${err}`) - if (err instanceof Error) { - reply(["```", err.stack, "```"].join("")) - console.error(err.stack) - } - } -} - -/* - * Login code send handler. - */ -commands.enterCode = async (sender, args, reply, { isManagement }) => { - if (!isManagement) { - reply("Logging in is considered a confidential action, and thus is only allowed in management rooms.") - return - } else if (args.length === 0) { - reply("**Usage:** `$cmdprefix `") - return - } - - try { - const data = await sender.signInToTelegram(args[0]) - if (data.status === "ok") { - reply(`Logged in successfully as ${sender.telegramPuppet.getDisplayName()}.`) - sender.commandStatus = undefined - } else if (data.status === "need-password") { - reply(`You have two-factor authentication enabled. Password hint: ${data.hint} -Enter your password using \`$cmdprefix \``) - sender.commandStatus = { - action: "Two-factor authentication", - next: commands.enterPassword, - salt: data.salt, - } - } else { - reply(`Unexpected sign in response, status=${data.status}`) - } - } catch (err) { - reply(`**Login failed:** ${err}`) - if (err instanceof Error) { - reply(["```", err.stack, "```"].join("")) - console.error(err.stack) - } - } -} - -/* - * Login code request handler. - */ -commands.login = async (sender, args, reply, { isManagement }) => { - if (!isManagement) { - reply("Logging in is considered a confidential action, and thus is only allowed in management rooms.") - return - } else if (args.length === 0) { - reply("**Usage:** `$cmdprefix login `") - return - } - - try { - /*const data = */ - await sender.sendTelegramCode(args[0]) - reply(`Login code sent to ${args[0]}.\nEnter the code using \`$cmdprefix \``) - sender.commandStatus = { - action: "Phone code authentication", - next: commands.enterCode, - } - } catch (err) { - reply(`**Failed to send code:** ${err}`) - if (err instanceof Error) { - reply(["```", err.stack, "```"].join("")) - console.error(err.stack) - } - } -} - -commands.register = async (sender, args, reply) => { - reply("Registration has not yet been implemented. Please use the official apps for now.") -} - -commands.logout = async (sender, args, reply) => { - try { - sender.logOutFromTelegram() - reply("Logged out successfully.") - } catch (err) { - reply(`**Failed to log out:** ${err}`) - if (err instanceof Error) { - reply(["```", err.stack, "```"].join("")) - console.error(err.stack) - } - } -} - -////////////////////////////// -// General command handlers // -////////////////////////////// - -commands.create = async (sender, args, reply, { app, roomID }) => { - if (args.length < 1 || (args[0] !== "group" && args[0] !== "channel")) { - reply("**Usage:** `$cmdprefix create `") - return - } else if (!sender._telegramPuppet) { - reply("This command requires you to be logged in.") - return - } else if (args[0] === "channel") { - reply("Creating channels is not yet supported.") - return - } - - if (args.length > 1) { - roomID = args[1] - } - - // TODO make sure that the AS bot is in the room. - - const title = await app.getRoomTitle(roomID) - if (!title) { - reply("Please set a room name before creating a Telegram chat.") - return - } - - let portal = await app.getPortalByRoomID(roomID) - if (portal) { - reply("This is already a portal room.") - return - } - - portal = new Portal(app, roomID) - try { - await portal.createTelegramChat(sender.telegramPuppet, title) - reply(`Telegram chat created. ID: ${portal.id}`) - if (app.managementRooms.includes(roomID)) { - app.managementRooms.splice(app.managementRooms.indexOf(roomID), 1) - } - } catch (err) { - reply(`Failed to create Telegram chat: ${err}`) - } -} - -commands.upgrade = async (sender, args, reply, { app, roomID }) => { - if (!sender._telegramPuppet) { - reply("This command requires you to be logged in.") - return - } - - const portal = await app.getPortalByRoomID(roomID) - if (!portal) { - reply("This is not a portal room.") - return - } - - await portal.upgradeTelegramChat(sender.telegramPuppet) -} - -commands.search = async (sender, args, reply, { app }) => { - if (args.length < 1) { - reply("**Usage:** `$cmdprefix search [-r|--remote] `") - return - } else if (!sender._telegramPuppet) { - reply("This command requires you to be logged in.") - return - } - const msg = [] - if (args[0] !== "-r" && args[0] !== "--remote") { - const contactResults = await sender.searchContacts(args.join(" ")) - if (contactResults.length > 0) { - msg.push("**Following results found from local contacts:**") - msg.push("") - for (const { match, contact } of contactResults) { - msg.push(`- ${contact.getDisplayName()}: ${contact.id} (${match}% match)`) - } - msg.push("") - msg.push("To force searching from Telegram servers, add `-r` before the search query.") - reply(msg.join("\n"), { allowHTML: true }) - return - } - } else { - args.shift() - msg.push("-r flag found: forcing remote search") - msg.push("") - } - const query = args.join(" ") - if (query.length < 5) { - reply("Failed to search server: Query is too short.") - return - } - const telegramResults = await sender.searchTelegram(query) - if (telegramResults.length > 0) { - msg.push("**Following results received from Telegram server:**") - for (const user of telegramResults) { - msg.push(`- ${user.getDisplayName()}: ${user.id}`) - } - } else { - msg.push("**No users found.**") - } - reply(msg.join("\n"), { allowHTML: true }) -} - -commands.pm = async (sender, args, reply, { app }) => { - if (args.length < 1) { - reply("**Usage:** `$cmdprefix pm `") - return - } else if (!sender._telegramPuppet) { - reply("This command requires you to be logged in.") - return - } - const user = await app.getTelegramUser(+args[0], { createIfNotFound: false }) - if (!user) { - reply("User info not saved. Try searching for the user first?") - return - } - const peer = user.toPeer(sender.telegramPuppet) - - const userInfo = await peer.getInfo(sender.telegramPuppet) - await user.updateInfo(sender.telegramPuppet, userInfo) - - const portal = await app.getPortalByPeer(peer) - await portal.createMatrixRoom(sender.telegramPuppet, { - invite: [sender.userID], - }) -} - -//////////////////////////// -// Debug command handlers // -//////////////////////////// - -commands.api = async (sender, args, reply, { app }) => { - if (!app.config.bridge.commands.allow_direct_api_calls) { - reply("Direct API calls are forbidden on this mautrix-telegram instance.") - return - } - const apiMethod = args.shift() - let apiArgs - try { - apiArgs = JSON.parse(args.join(" ")) - } catch (err) { - reply("Invalid API method parameters. Usage: $cmdprefix api ") - return - } - try { - reply(`Calling ${apiMethod} with the following arguments:\n${JSON.stringify(apiArgs, "", " ")}`) - const response = await sender.telegramPuppet.client(apiMethod, apiArgs) - reply(`API call successful. Response: - -

-  ${JSON.stringify(response, "", "  ")}
-
`, { allowHTML: true }) - } catch (err) { - reply(`API call errored. Response:\n${JSON.stringify(err, "", " ")}`) - } -} - -function timeout(promise, ms = 2500) { - return new Promise((resolve, reject) => { - promise.then(resolve, reject) - setTimeout(() => reject(new Error("API call response not received")), ms) - }) -} - -commands.ping = async (sender, args, reply) => { - try { - await timeout(sender.telegramPuppet.client("contacts.getContacts", {})) - reply("Connection seems OK.") - } catch (err) { - reply(`Not connected: ${err}`) - } -} - -module.exports = { - commands, - run, -} diff --git a/src/formatter.js b/src/formatter.js deleted file mode 100644 index 4a057f63..00000000 --- a/src/formatter.js +++ /dev/null @@ -1,360 +0,0 @@ -// mautrix-telegram - A Matrix-Telegram puppeting bridge -// Copyright (C) 2017 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -/** - * Utility functions to convert between Telegram and Matrix (HTML) formatting. - *

- * WARNING: This module contains headache-causing regular expressions and other duct tape. - * - * @module formatter - */ - -String.prototype.insert = function(at, str) { - return this.slice(0, at) + str + this.slice(at) -} - -/** - * Add a simple HTML tag to the given tag list. - * - * @param {Object[]} tags The tag list. - * @param {Object} entity The Telegram format entity. - * @param {number} entity.offset The index where the format entity starts. - * @param {number} entity.length The length of the format entity. - * @param {string} tag The HTML tag to add. - * @param {number} priority The tag priority to use when sorting tags at the same index. - * @private - */ -function addSimpleTag(tags, entity, tag, priority = 0) { - tags.push([entity.offset, `<${tag}>`, -priority]) - tags.push([entity.offset + entity.length, ``, priority]) -} - - -/** - * Add a HTML tag to the given tag list. - * - * @param {Object[]} tags The tag list. - * @param {Object} entity The Telegram format entity. - * @param {number} entity.offset The index where the format entity starts. - * @param {number} entity.length The length of the format entity. - * @param {string} tag The HTML tag to add. - * @param {string} attrs The HTML attributes to add to the tag. - * @param {number} priority The tag priority to use when sorting tags at the same index. - * @private - */ -function addTag(tags, entity, tag, attrs, priority = 0) { - tags.push([entity.offset, `<${tag} ${attrs}>`, -priority]) - tags.push([entity.offset + entity.length, ``, priority]) -} - -/** - * Convert a Telegram entity-formatted message to a Matrix HTML-formatted message. - *

- * WARNING: I am not responsible for possible severe headaches caused by reading any part of this function. - * - * @param {string} message The plaintext message. - * @param {Array} entities The Telegram formatting entities. - * @param {MautrixTelegram} app The app main class instance to use when reformatting mentions. - */ -function telegramToMatrix(message, entities, app) { - const tags = [] - // Decreasing priority counter used to ensure that formattings right next to eachother don't flip like this: - // *bold*_italic_ --> bolditalic - let pc = 9001 - - // Convert Telegram formatting entities into a weird custom indexed HTML tag format thingy. - for (const entity of entities) { - let url, tag, mxid - switch (entity._) { - case "messageEntityBold": - tag = tag || "strong" - case "messageEntityItalic": - tag = tag || "em" - case "messageEntityCode": - tag = tag || "code" - addSimpleTag(tags, entity, tag, --pc) - break - case "messageEntityPre": - pc-- - addSimpleTag(tags, entity, "pre", pc) - addTag(tags, entity, "code", `class="language-${entity.language}"`, pc + 1) - break - case "messageEntityBotCommand": - // TODO bridge bot commands differently? - message = `${message.substr(0, entity.offset)}!${message.substr(entity.offset + 1)}` - case "messageEntityHashtag": - addTag(tags, entity, "font", "color=\"blue\"", --pc) - break - case "messageEntityMentionName": - let user = app.matrixUsersByTelegramID.get(entity.user_id) - if (!user) { - // TODO this loop step should be made useless - for (const userByMXID of app.matrixUsersByID.values()) { - if (userByMXID.telegramUserID === entity.user_id) { - user = userByMXID - app.matrixUsersByTelegramID.set(userByMXID.telegramUserID, userByMXID) - break - } - } - } - mxid = user ? - user.userID : - app.getMXIDForTelegramUser(entity.user_id) - case "messageEntityMention": - if (!mxid) { - const username = message.substr(entity.offset + 1, entity.length - 1) - for (const userByMXID of app.matrixUsersByID.values()) { - if (userByMXID._telegramPuppet && userByMXID._telegramPuppet.data.username === username) { - mxid = userByMXID.userID - break - } - } - if (!mxid) { - for (const userByID of app.telegramUsersByID.values()) { - if (userByID.username === username) { - mxid = userByID.mxid - break - } - } - } - } - - if (!mxid) { - continue - } - addTag(tags, entity, "a", `href="https://matrix.to/#/${mxid}"`) - break - case "messageEntityEmail": - url = url || `mailto:${message.substr(entity.offset, entity.length)}` - case "messageEntityUrl": - url = url || message.substr(entity.offset, entity.length) - case "messageEntityTextUrl": - url = url || entity.url - addTag(tags, entity, "a", `href="${url}"`, --pc) - break - } - } - - // Sort tags in a mysterious way (it seems to work, don't touch it!). - // - // The important thing is that the tags are sorted last to first, - // so when replacing by index, the index doesn't need to be adapted. - tags.sort(([aIndex, , aPriority], [bIndex, , bPriority]) => bIndex - aIndex || aPriority - bPriority) - - // Insert tags into message - for (const [index, replacement] of tags) { - message = message.insert(index, replacement) - } - message = message.replace(/\n/g, "
\n") - return message -} - -// Formatting that is converted back to text -const linebreaks = /(\n)?/g -const paragraphs = /

([^]*?)<\/p>/g -const headers = /([^]*?)<\/h[0-6]>/g -const unorderedLists = /