From 7a7d66f1d8ec05c38d43718b710a64245c60d72e Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Sun, 15 Jul 2018 19:41:37 -0700 Subject: [PATCH 01/24] Modify .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dccaa0b..103448f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +__pycache__ *.pyc *~ *.DS_Store From 70917fcf93cabb54b703180b8cbf6774bbbadf5b Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Tue, 17 Jul 2018 16:27:47 -0700 Subject: [PATCH 02/24] Update quip.py with latest changes from quip repository --- python/quip.py | 245 +++++++++++++++++++++++++++++++------------------ 1 file changed, 157 insertions(+), 88 deletions(-) diff --git a/python/quip.py b/python/quip.py index 82b1c29..11f368a 100644 --- a/python/quip.py +++ b/python/quip.py @@ -35,15 +35,31 @@ import ssl import sys import time -try: - from urllib import urlencode - from urllib2 import HTTPError, Request, urlopen -except ImportError: - from urllib.error import HTTPError - from urllib.parse import urlencode - from urllib.request import Request, urlopen import xml.etree.cElementTree +PY3 = sys.version_info > (3,) + +if PY3: + import urllib.request, urllib.parse, urllib.error + Request = urllib.request.Request + urlencode = urllib.parse.urlencode + urlopen = urllib.request.urlopen + HTTPError = urllib.error.HTTPError + + iteritems = dict.items + +else: + import urllib + import urllib2 + + Request = urllib2.Request + urlencode = urllib.urlencode + urlopen = urllib2.urlopen + HTTPError = urllib2.HTTPError + + iteritems = dict.iteritems + + try: reload(sys) sys.setdefaultencoding('utf8') @@ -77,6 +93,13 @@ # https://cloud.google.com/appengine/docs/python/sockets/ssl_support +class QuipError(Exception): + def __init__(self, code, message, http_error): + Exception.__init__(self, "%d: %s" % (code, message)) + self.code = code + self.http_error = http_error + + class QuipClient(object): """A Quip API client""" # Edit operations @@ -95,7 +118,7 @@ class QuipClient(object): BLUE = range(5) def __init__(self, access_token=None, client_id=None, client_secret=None, - base_url=None, request_timeout=None, retry_rate_limit=False): + base_url=None, request_timeout=None): """Constructs a Quip API client. If `access_token` is given, all of the API methods in the client @@ -110,7 +133,6 @@ def __init__(self, access_token=None, client_id=None, client_secret=None, self.client_secret = client_secret self.base_url = base_url if base_url else "https://platform.quip.com" self.request_timeout = request_timeout if request_timeout else 10 - self.retry_rate_limit = retry_rate_limit def get_authorization_url(self, redirect_uri, state=None): """Returns the URL the user should be redirected to to sign in.""" @@ -144,6 +166,12 @@ def get_users(self, ids): """Returns a dictionary of users for the given IDs.""" return self._fetch_json("users/", post_data={"ids": ",".join(ids)}) + def update_user(self, user_id, picture_url=None): + return self._fetch_json("users/update", post_data={ + "user_id": user_id, + "picture_url": picture_url, + }) + def get_contacts(self): """Returns a list of the users in the authenticated user's contacts.""" return self._fetch_json("users/contacts") @@ -185,6 +213,10 @@ def remove_folder_members(self, folder_id, member_ids): "member_ids": ",".join(member_ids), }) + def get_teams(self): + """Returns the teams for the user corresponding to our access token.""" + return self._fetch_json("teams/current") + def get_messages(self, thread_id, max_created_usec=None, count=None): """Returns the most recent messages for the given thread. @@ -218,11 +250,11 @@ def get_threads(self, ids): """Returns a dictionary of threads for the given IDs.""" return self._fetch_json("threads/", post_data={"ids": ",".join(ids)}) - def get_recent_threads(self, max_updated_usec=None, count=None): + def get_recent_threads(self, max_updated_usec=None, count=None, **kwargs): """Returns the recently updated threads for a given user.""" return self._fetch_json( "threads/recent", max_updated_usec=max_updated_usec, - count=count) + count=count, **kwargs) def add_thread_members(self, thread_id, member_ids): """Adds the given folder or user IDs to the given thread.""" @@ -238,12 +270,29 @@ def remove_thread_members(self, thread_id, member_ids): "member_ids": ",".join(member_ids), }) + def pin_to_desktop(self, thread_id, **kwargs): + """Pins the given thread to desktop.""" + args = { + "thread_id": thread_id, + } + args.update(kwargs) + return self._fetch_json("threads/pin-to-desktop", post_data=args) + def move_thread(self, thread_id, source_folder_id, destination_folder_id): """Moves the given thread from the source folder to the destination one. """ self.add_thread_members(thread_id, [destination_folder_id]) self.remove_thread_members(thread_id, [source_folder_id]) + def new_chat(self, message, title=None, member_ids=[]): + """Creates a chat with the given title and members, and send the + initial message.""" + return self._fetch_json("threads/new-chat", post_data={ + "message": message, + "title": title, + "member_ids": ",".join(member_ids), + }) + def new_document(self, content, format="html", title=None, member_ids=[]): """Creates a new document from the given content. @@ -252,7 +301,7 @@ def new_document(self, content, format="html", title=None, member_ids=[]): client = quip.QuipClient(...) user = client.get_authenticated_user() - client.new_document(..., member_ids=[user["private_folder_id"]]) + client.new_document(..., member_ids=[user["archive_folder_id"]]) """ return self._fetch_json("threads/new-document", post_data={ @@ -272,9 +321,12 @@ def copy_document(self, id, title=None, member_ids=[]): old_thread["html"], title=title or old_thread["thread"]["title"], member_ids=member_ids) - def merge_comments(self, original_id, children_ids): + def merge_comments(self, original_id, children_ids, ignore_user_ids=[]): """Given an original document and a set of exact duplicates, copies all comments and messages on the duplicates to the original. + + Impersonates the commentors if the access token used has + permission, but does not add them to the thread. """ import re threads = self.get_threads(children_ids + [original_id]) @@ -287,7 +339,13 @@ def merge_comments(self, original_id, children_ids): parent_map = dict(zip(child_section_ids, original_section_ids)) messages = self.get_messages(thread_id) for message in reversed(messages): - kwargs = {} + if message["author_id"] in ignore_user_ids: + continue + kwargs = { + "user_id": message["author_id"], + "frame": "bubble", + "service_id": message["id"], + } if "parts" in message: kwargs["parts"] = json.dumps(message["parts"]) else: @@ -324,12 +382,16 @@ def edit_document(self, thread_id, content, operation=APPEND, format="html", `operation` is relative to another section of the document, you must also specify the `section_id`. """ + args = { "thread_id": thread_id, "content": content, "location": operation, "format": format, - "section_id": section_id, + # Since our cell ids in 10x contain ';', which is a valid cgi + # parameter separator, we are replacing them with '_' in 10x cell + # sections. This should be no op for all other sections. + "section_id": section_id.replace(";", "_") } args.update(kwargs) return self._fetch_json("threads/edit-document", post_data=args) @@ -350,13 +412,13 @@ def add_to_first_list(self, thread_id, *items, **kwargs): } args.update(kwargs) if "section_id" not in args: - first_list = self.get_first_list(thread_id) - if not first_list: - return None - args["section_id"] = self.get_last_list_item_id(first_list) - if not args["section_id"]: - # section_id = first_list.attrib["id"] - return None + first_list = self.get_first_list( + thread_id, kwargs.pop("document_html", None)) + if first_list: + args["section_id"] = self.get_last_list_item_id(first_list) + if not args.get("section_id"): + args["operation"] = self.APPEND + args["content"] = "\n\n".join([" * %s" % i for i in items]) return self.edit_document(**args) def add_to_spreadsheet(self, thread_id, *rows, **kwargs): @@ -373,12 +435,17 @@ def add_to_spreadsheet(self, thread_id, *rows, **kwargs): spreadsheet = self.get_named_spreadsheet(kwargs["name"], thread_id) else: spreadsheet = self.get_first_spreadsheet(thread_id) - section_id = self.get_last_row_item_id(spreadsheet) + if kwargs.get("add_to_top"): + section_id = self.get_first_row_item_id(spreadsheet) + operation = self.BEFORE_SECTION + else: + section_id = self.get_last_row_item_id(spreadsheet) + operation = self.AFTER_SECTION return self.edit_document( thread_id=thread_id, content=content, section_id=section_id, - operation=self.AFTER_SECTION) + operation=operation) def update_spreadsheet_row(self, thread_id, header, value, updates, **args): """Finds the row where the given header column is the given value, and @@ -397,7 +464,7 @@ def update_spreadsheet_row(self, thread_id, header, value, updates, **args): row = self.find_row_from_header(spreadsheet, header, value) if row: ids = self.get_row_ids(row) - for head, val in updates.iteritems(): + for head, val in iteritems(updates): index = self.get_index_of_header(headers, head) if not index or index >= len(ids) or not ids[index]: continue @@ -410,16 +477,25 @@ def update_spreadsheet_row(self, thread_id, header, value, updates, **args): **args) else: updates[header] = value - indexed_items = {} - extra_items = [] - for head, val in updates.iteritems(): - index = self.get_index_of_header( - headers, head, default=None) - if index is None or index in indexed_items: - extra_items.append(val) - else: - indexed_items[index] = val - cells = [] + response = self.add_spreadsheet_row( + thread_id, spreadsheet, updates, headers=headers, **args) + return response + + def add_spreadsheet_row( + self, thread_id, spreadsheet, updates, headers=None, **args): + if not headers: + headers = self.get_spreadsheet_header_items(spreadsheet) + indexed_items = {} + extra_items = [] + for head, val in iteritems(updates): + index = self.get_index_of_header( + headers, head, default=None) + if index is None or index in indexed_items: + extra_items.append(val) + else: + indexed_items[index] = val + cells = [] + if indexed_items: for i in range(max(indexed_items.keys()) + 1): if i in indexed_items: cells.append(indexed_items[i]) @@ -427,16 +503,16 @@ def update_spreadsheet_row(self, thread_id, header, value, updates, **args): cells.append(extra_items.pop(0)) else: cells.append("") - cells.extend(extra_items) - content = "%s" % "".join( - ["%s" % cell for cell in cells]) - section_id = self.get_last_row_item_id(spreadsheet) - response = self.edit_document( - thread_id=thread_id, - content=content, - section_id=section_id, - operation=self.AFTER_SECTION, - **args) + cells.extend(extra_items) + content = "%s" % "".join( + ["%s" % cell for cell in cells]) + section_id = self.get_last_row_item_id(spreadsheet) + response = self.edit_document( + thread_id=thread_id, + content=content, + section_id=section_id, + operation=self.AFTER_SECTION, + **args) return response def toggle_checkmark(self, thread_id, item, checked=True): @@ -535,6 +611,11 @@ def get_last_row_item_id(self, spreadsheet_tree): items = list(spreadsheet_tree.iter("tr")) return items[-1].attrib["id"] if items else None + def get_first_row_item_id(self, spreadsheet_tree): + """Returns the last row in the given spreadsheet `ElementTree`.""" + items = list(spreadsheet_tree.iter("tr")) + return items[1].attrib["id"] if items else None + def get_row_items(self, row_tree): """Returns the text of items in the given row `ElementTree`.""" return [(list(x.itertext()) or [None])[0] for x in row_tree] @@ -563,7 +644,7 @@ def get_index_of_header(self, header_items, header, default=0): if ord('A') < char < ord('Z'): return char - ord('A') + 1 else: - logging.warning("Could not find header, using first column") + pass return default def find_row_from_header(self, spreadsheet_tree, header, value): @@ -599,10 +680,20 @@ def parse_spreadsheet_contents(self, spreadsheet_tree): for i, cell in enumerate(row): if cell.tag != "td": continue - value["cells"][spreadsheet["headers"][i]] = { + data = { "id": cell.attrib.get("id"), - "content": list(cell.itertext())[0], } + images = list(cell.iter("img")) + if images: + data["content"] = images[0].attrib.get("src") + else: + data["content"] = list(cell.itertext())[0].replace( + u"\u200b", "") + style = cell.attrib.get("style") + if style and "background-color:#" in style: + sharp = style.find("#") + data["color"] = style[sharp+1:sharp+7] + value["cells"][spreadsheet["headers"][i]] = data if len(value["cells"]): spreadsheet["rows"].append(value) return spreadsheet @@ -632,19 +723,10 @@ def get_blob(self, thread_id, blob_id): except HTTPError as error: try: # Extract the developer-friendly error message from the response - message = json.loads(error.read())["error_description"] + message = json.loads(error.read().decode())["error_description"] except Exception: raise error - if (self.retry_rate_limit and error.code == 503 and - message == "Over Rate Limit"): - # Retry later. - reset_time = float(error.headers.get("X-RateLimit-Reset")) - delay = max(2, reset_time - time.time() + 1) - logging.warning("Rate Limit, delaying for %d seconds" % delay) - time.sleep(delay) - return self.get_blob(thread_id, blob_id) - else: - raise QuipError(error.code, message, error) + raise QuipError(error.code, message, error) def put_blob(self, thread_id, blob, name=None): """Uploads an image or other blob to the given Quip thread. Returns an @@ -673,45 +755,39 @@ def put_blob(self, thread_id, blob, name=None): raise error raise QuipError(error.response.status_code, message, error) - def new_websocket(self): - """Returns a websocket URL to connect to. The URL may expire if no - connection is initiated within 60 seconds.""" - return self._fetch_json("websockets/new") + def new_websocket(self, **kwargs): + """Gets a websocket URL to connect to. + """ + return self._fetch_json("websockets/new", **kwargs) def _fetch_json(self, path, post_data=None, **args): request = Request(url=self._url(path, **args)) if post_data: post_data = dict((k, v) for k, v in post_data.items() if v or isinstance(v, int)) - request.data = urlencode(self._clean(**post_data)).encode("utf-8") + request_data = urlencode(self._clean(**post_data)) + if PY3: + request.data = request_data.encode() + else: + request.data = request_data + if self.access_token: request.add_header("Authorization", "Bearer " + self.access_token) try: return json.loads( - urlopen(request, timeout=self.request_timeout).read().decode('utf-8')) + urlopen( + request, timeout=self.request_timeout).read().decode()) except HTTPError as error: try: # Extract the developer-friendly error message from the response - message = json.loads(error.read())["error_description"] + message = json.loads(error.read().decode())["error_description"] except Exception: raise error - if (self.retry_rate_limit and error.code == 503 and - message == "Over Rate Limit"): - # Retry later. - reset_time = float(error.headers.get("X-RateLimit-Reset")) - delay = max(2, reset_time - time.time() + 1) - logging.warning("Rate Limit, delaying for %d seconds", delay) - time.sleep(delay) - return self._fetch_json(path, post_data, **args) - else: - raise QuipError(error.code, message, error) + raise QuipError(error.code, message, error) def _clean(self, **args): - # We only expect ints or strings, but on Windows ints can become longs - return dict((k, str(v) if isinstance( - v, (int, float, complex)) else v.encode("utf-8")) - for k, v in args.items() if v or isinstance( - v, (int, float, complex))) + return dict((k, str(v) if isinstance(v, int) else v.encode("utf-8")) + for k, v in args.items() if v or isinstance(v, int)) def _url(self, path, **args): url = self.base_url + "/1/" + path @@ -719,10 +795,3 @@ def _url(self, path, **args): if args: url += "?" + urlencode(args) return url - - -class QuipError(Exception): - def __init__(self, code, message, http_error): - Exception.__init__(self, "%d: %s" % (code, message)) - self.code = code - self.http_error = http_error From 1ebbe0f60324d0ca27201d70a3be23b90aeb0969 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Tue, 17 Jul 2018 17:54:16 -0700 Subject: [PATCH 03/24] Tweaks to make websocket example work for python 2 & 3 --- samples/websocket/main.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/samples/websocket/main.py b/samples/websocket/main.py index 64a0a1e..ad71e7b 100755 --- a/samples/websocket/main.py +++ b/samples/websocket/main.py @@ -9,29 +9,34 @@ import argparse import json -import logging import quip -import thread +import sys import time import websocket +PY3 = sys.version_info > (3,) +if PY3: + import _thread as thread +else: + import thread HEARTBEAT_INTERVAL = 20 + def open_websocket(url): def on_message(ws, message): - print "message:" - print json.dumps(json.loads(message), indent=4) + print("message:") + print(json.dumps(json.loads(message), indent=4)) def on_error(ws, error): - print "error:" - print error + print("error:") + print(error) def on_close(ws): - print "### connection closed ###" + print("### connection closed ###") def on_open(ws): - print "### connection established ###" + print("### connection established ###") def run(*args): while True: @@ -40,7 +45,7 @@ def run(*args): thread.start_new_thread(run, ()) - websocket.enableTrace(True) + # websocket.enableTrace(True) ws = websocket.WebSocketApp( url, on_message=on_message, on_error=on_error, on_close=on_close) ws.on_open = on_open From 12254a4c201dc3510280fc4095f2b04bcdcb4b31 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Tue, 17 Jul 2018 16:29:15 -0700 Subject: [PATCH 04/24] Minor fix to enable python3 in baqup --- samples/baqup/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/baqup/main.py b/samples/baqup/main.py index 865456f..401bdc2 100755 --- a/samples/baqup/main.py +++ b/samples/baqup/main.py @@ -62,7 +62,7 @@ def main(): client = quip.QuipClient( access_token=args.access_token, base_url=args.quip_api_base_url, - retry_rate_limit=True, request_timeout=120) + request_timeout=120) output_directory = os.path.join( _normalize_path(args.output_directory), "baqup") _ensure_path_exists(output_directory) @@ -98,7 +98,7 @@ def _descend_into_folder(folder_id, processed_folder_ids, client, processed_folder_ids.add(folder_id) try: folder = client.get_folder(folder_id) - except quip.QuipError, e: + except quip.QuipError as e: if e.code == 403: logging.warning("%sSkipped over restricted folder %s.", " " * depth, folder_id) @@ -106,7 +106,7 @@ def _descend_into_folder(folder_id, processed_folder_ids, client, logging.warning("%sSkipped over folder %s due to unknown error %d.", " " * depth, folder_id, e.code) return - except urllib2.HTTPError, e: + except urllib2.HTTPError as e: logging.warning("%sSkipped over folder %s due to HTTP error %d.", " " * depth, folder_id, e.code) return From 71e415ae931577916f186f3c4ed411da690cc066 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Mon, 30 Jul 2018 11:27:52 -0700 Subject: [PATCH 05/24] Fix api failure in edit_document Fixes bug #41. --- python/quip.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/python/quip.py b/python/quip.py index 11f368a..92bb7e6 100644 --- a/python/quip.py +++ b/python/quip.py @@ -383,15 +383,17 @@ def edit_document(self, thread_id, content, operation=APPEND, format="html", also specify the `section_id`. """ + # Since our cell ids in 10x contain ';', which is a valid cgi + # parameter separator, we are replacing them with '_' in 10x cell + # sections. This should be no op for all other sections. + section_id = None if not section_id else section_id.replace(";", "_") + args = { "thread_id": thread_id, "content": content, "location": operation, "format": format, - # Since our cell ids in 10x contain ';', which is a valid cgi - # parameter separator, we are replacing them with '_' in 10x cell - # sections. This should be no op for all other sections. - "section_id": section_id.replace(";", "_") + "section_id": section_id } args.update(kwargs) return self._fetch_json("threads/edit-document", post_data=args) From 3df38db280978c2f503f62901444697733e09f49 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Thu, 2 Aug 2018 11:37:37 -0700 Subject: [PATCH 06/24] Add test.sh and auth.py --- trials/auth.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++ trials/test.sh | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 trials/auth.py create mode 100644 trials/test.sh diff --git a/trials/auth.py b/trials/auth.py new file mode 100644 index 0000000..2bf118c --- /dev/null +++ b/trials/auth.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +# Simple emulation of oauth codepath. Gets new tokens for client +# ex python3 auth.py --client_id='' --client_secret='' +import argparse +import json +import logging +import quip + +from http.server import BaseHTTPRequestHandler, HTTPServer +from queue import Queue +from threading import Thread +from urllib.parse import urlparse, parse_qs + +# use a queue to pass messages from the worker thread to the main thread +q = Queue() +httpd = None + + +# HTTPRequestHandler class +class OAuthServerHandler(BaseHTTPRequestHandler): + def do_GET(self): + url = urlparse(self.path) + query_args = parse_qs(url.query) + if "code" in query_args: + q.put(query_args["code"][0]) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(bytes("got it", "utf8")) + return + + +def run_http_server(): + global httpd + server_address = ('127.0.0.1', 8900) + httpd = HTTPServer(server_address, OAuthServerHandler) + print("Running http://%s:%s" % server_address) + httpd.serve_forever() + + +def start_server_in_thread(): + thrd = Thread(target=run_http_server) + thrd.start() + + +def main(): + # logging.getLogger().setLevel(logging.DEBUG) + + parser = argparse.ArgumentParser(description="Simulate oauth client") + + parser.add_argument("--client_id", + help="Client id to use in oauth call") + parser.add_argument("--client_secret", + help="Client secret to be used in oauth call") + parser.add_argument("--redirect_uri", + default='http://localhost:8900', + help="Client secret to be used in oauth call") + parser.add_argument("--quip_api_base_url", + default="http://platform.docker.qa:10000", + help="Alternative base URL for the Quip API. If none is provided, " + "http://platform.docker.qa:10000 will be used") + + args = parser.parse_args() + start_server_in_thread() + + client = quip.QuipClient( + client_id=args.client_id, + client_secret=args.client_secret, + base_url=args.quip_api_base_url, + request_timeout=120 + ) + + authorization_url = client.get_authorization_url(args.redirect_uri) + print('Authorize access using the following url: %s' % authorization_url) + + # Wait for auth code from http server + code = q.get() + token_info = client.get_access_token(args.redirect_uri, code) + print("token_info: %s" % json.dumps(token_info, indent=1)) + client.access_token = token_info['access_token'] + user = client.get_authenticated_user() + print('user: %s(%s)' % (user['name'], user['emails'][0])) + httpd.shutdown() + + +if __name__ == '__main__': + main() diff --git a/trials/test.sh b/trials/test.sh new file mode 100644 index 0000000..849b114 --- /dev/null +++ b/trials/test.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# request user (token owner): dwillhite@quip.com: dKWAEApvV4V +# quip.com: FTGAcAzRzdg +# files, JiraTest: ba4yA6bgL0fD, Things: WBAcArDaZGYD +set -x + +# this is api key, should work for root apis +at='ZEtXQU1BVmJjQkg=|1563288140|zwmDQ60jcM8k0R7kmJ/pAROsNaAOrCcp/meLucdIOBg=' + +curl http://platform.docker.qa:10000/1/threads/recent -H "Authorization: Bearer $at" | python -m json.tool # TESTED +curl http://platform.docker.qa:10000/1/threads/ba4yA6bgL0fD -H "Authorization: Bearer $at" | python -m json.tool # TESTED + +# curl http://platform.docker.qa:10000/1/oauth/login -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/oauth/access_token -H "Authorization: Bearer $at" | python -m json.tool +curl 'http://platform.docker.qa:10000/1/oauth/access_token' -d "grant_type=refresh_token&refresh_token=$ert&client_id=FTGAcAzRzdg-ce14b24e386e410ca0f21fa54e17c2bd&client_secret=7c8f4b2dbb124c66a682b4dedb607f09" +curl -s 'http://platform.docker.qa:10000/1/oauth/revoke?' -d "token=$rt&client_id=FTGAcAzRzdg-ce14b24e386e410ca0f21fa54e17c2bd&client_secret=7c8f4b2dbb124c66a682b4dedb607f09" +curl http://platform.docker.qa:10000/1/users/current -H "Authorization: Bearer $at" | python -m json.tool # TESTED +curl http://platform.docker.qa:10000/1/users/contacts -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/users/BREAEAINALE -H "Authorization: Bearer $at" | python -m json.tool # TESTED +curl http://platform.docker.qa:10000/1/users/brina@quip.com -H "Authorization: Bearer $at" | python -m json.tool # TESTED +curl http://platform.docker.qa:10000/1/users/?ids=dKWAEApvV4V,BREAEAINALE -H "Authorization: Bearer $at" | python -m json.tool # TESTED +# curl http://platform.docker.qa:10000/1/users/update -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/folders/UQNAOAMwq9d -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/folders/?ids=JAZAOAXMKK7,LLYAOA0KAUV -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/folders/new -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/folders/update -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/folders/add-members -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/folders/remove-members -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/teams/current -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/threads/recent -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/new-document -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/new-chat -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/edit-document -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/add-members -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/remove-members -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/pin-to-desktop -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/threads/search/ -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/threads/WBAcArDaZGYD -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/threads/?ids=KUfAAAacN4b,KJDAAA8hnTx -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/messages/KUfAAAacN4b -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/messages/new -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/blob/JcUAAAls35b/E8PFroUvsmXaby5WiJ1tcA -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/blob/KUfAAAacN4b -H "Authorization: Bearer $at" | python -m json.tool +curl -F 'blob=@/Users/dwillhite/Desktop/images/soccer-ball.jpeg' 'http://platform.docker.qa:10000/1/blob/WIdAAANyW8q' -H "Authorization: Bearer $pat" +# curl http://platform.docker.qa:10000/1/websockets/new -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/new -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/refresh -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/create -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/update -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/delete -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/ping -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/verify_code -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/twitter -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/integrations/email -H "Authorization: Bearer $at" | python -m json.tool + +# curl http://platform.docker.qa:10000/1/admin/threads/list -H "Authorization: Bearer $at" | python -m json.tool ## can't execute because of bigquery +# curl http://platform.docker.qa:10000/1/admin/threads/search -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/admin/threads/([a-zA-Z0-9:/]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool # Tested +# curl http://platform.docker.qa:10000/1/admin/messages/([a-zA-Z0-9]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/admin/threads/add-members -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/admin/threads/remove-members -H "Authorization: Bearer $at" | python -m json.tool +curl 'http://platform.docker.qa:10000/1/admin/threads/?ids=ba4yA6bgL0fD,WBAcArDaZGYD&company_id=FTGAcAzRzdg' -H "Authorization: Bearer $at" | python -m json.tool # TESTED +curl http://platform.docker.qa:10000/1/admin/users/list -X POST -H "Authorization: Bearer $at" -d "company_id=FTGAcAzRzdg" | python -m json.tool +curl http://platform.docker.qa:10000/1/admin/users/BREAEAINALE?company_id=FTGAcAzRzdg -H "Authorization: Bearer $at" | python -m json.tool +curl http://platform.docker.qa:10000/1/admin/users/brina@quip.com?company_id=FTGAcAzRzdg -H "Authorization: Bearer $at" | python -m json.tool ## doesn't work, i believe extra auth requires real user_id +# curl http://platform.docker.qa:10000/1/admin/threads/edit-document -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/admin/folders/([a-zA-Z0-9:/]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/admin/folders/([a-zA-Z0-9:/]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool +# curl http://platform.docker.qa:10000/1/admin/blob/([A-Za-z0-9]{11})/([A-Za-z0-9\-\_]{22}) +curl 'http://platform.docker.qa:10000/1/admin/blob/JcUAAAls35b/IranRc-Ao96onSHA9ViNqQ?name=IDcard.pdf&company_id=FTGAcAzRzdg' -H "Authorization: Bearer $at" > filename.txt \ No newline at end of file From 9458aff95da38efba9077c24897c57708e36f691 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Thu, 2 Aug 2018 11:38:58 -0700 Subject: [PATCH 07/24] Add link to quip.py --- trials/quip.py | 1 + 1 file changed, 1 insertion(+) create mode 120000 trials/quip.py diff --git a/trials/quip.py b/trials/quip.py new file mode 120000 index 0000000..c9466f5 --- /dev/null +++ b/trials/quip.py @@ -0,0 +1 @@ +../python/quip.py \ No newline at end of file From 9cda2dfb2d5987bdf4dc662d99101d81f7c68368 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Sat, 3 Nov 2018 09:15:46 -0700 Subject: [PATCH 08/24] Cleamup unnecessary files --- trials/auth.py | 88 -------------------------------------------------- trials/quip.py | 1 - trials/test.sh | 71 ---------------------------------------- 3 files changed, 160 deletions(-) delete mode 100644 trials/auth.py delete mode 120000 trials/quip.py delete mode 100644 trials/test.sh diff --git a/trials/auth.py b/trials/auth.py deleted file mode 100644 index 2bf118c..0000000 --- a/trials/auth.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/python - -# Simple emulation of oauth codepath. Gets new tokens for client -# ex python3 auth.py --client_id='' --client_secret='' -import argparse -import json -import logging -import quip - -from http.server import BaseHTTPRequestHandler, HTTPServer -from queue import Queue -from threading import Thread -from urllib.parse import urlparse, parse_qs - -# use a queue to pass messages from the worker thread to the main thread -q = Queue() -httpd = None - - -# HTTPRequestHandler class -class OAuthServerHandler(BaseHTTPRequestHandler): - def do_GET(self): - url = urlparse(self.path) - query_args = parse_qs(url.query) - if "code" in query_args: - q.put(query_args["code"][0]) - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(bytes("got it", "utf8")) - return - - -def run_http_server(): - global httpd - server_address = ('127.0.0.1', 8900) - httpd = HTTPServer(server_address, OAuthServerHandler) - print("Running http://%s:%s" % server_address) - httpd.serve_forever() - - -def start_server_in_thread(): - thrd = Thread(target=run_http_server) - thrd.start() - - -def main(): - # logging.getLogger().setLevel(logging.DEBUG) - - parser = argparse.ArgumentParser(description="Simulate oauth client") - - parser.add_argument("--client_id", - help="Client id to use in oauth call") - parser.add_argument("--client_secret", - help="Client secret to be used in oauth call") - parser.add_argument("--redirect_uri", - default='http://localhost:8900', - help="Client secret to be used in oauth call") - parser.add_argument("--quip_api_base_url", - default="http://platform.docker.qa:10000", - help="Alternative base URL for the Quip API. If none is provided, " - "http://platform.docker.qa:10000 will be used") - - args = parser.parse_args() - start_server_in_thread() - - client = quip.QuipClient( - client_id=args.client_id, - client_secret=args.client_secret, - base_url=args.quip_api_base_url, - request_timeout=120 - ) - - authorization_url = client.get_authorization_url(args.redirect_uri) - print('Authorize access using the following url: %s' % authorization_url) - - # Wait for auth code from http server - code = q.get() - token_info = client.get_access_token(args.redirect_uri, code) - print("token_info: %s" % json.dumps(token_info, indent=1)) - client.access_token = token_info['access_token'] - user = client.get_authenticated_user() - print('user: %s(%s)' % (user['name'], user['emails'][0])) - httpd.shutdown() - - -if __name__ == '__main__': - main() diff --git a/trials/quip.py b/trials/quip.py deleted file mode 120000 index c9466f5..0000000 --- a/trials/quip.py +++ /dev/null @@ -1 +0,0 @@ -../python/quip.py \ No newline at end of file diff --git a/trials/test.sh b/trials/test.sh deleted file mode 100644 index 849b114..0000000 --- a/trials/test.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# request user (token owner): dwillhite@quip.com: dKWAEApvV4V -# quip.com: FTGAcAzRzdg -# files, JiraTest: ba4yA6bgL0fD, Things: WBAcArDaZGYD -set -x - -# this is api key, should work for root apis -at='ZEtXQU1BVmJjQkg=|1563288140|zwmDQ60jcM8k0R7kmJ/pAROsNaAOrCcp/meLucdIOBg=' - -curl http://platform.docker.qa:10000/1/threads/recent -H "Authorization: Bearer $at" | python -m json.tool # TESTED -curl http://platform.docker.qa:10000/1/threads/ba4yA6bgL0fD -H "Authorization: Bearer $at" | python -m json.tool # TESTED - -# curl http://platform.docker.qa:10000/1/oauth/login -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/oauth/access_token -H "Authorization: Bearer $at" | python -m json.tool -curl 'http://platform.docker.qa:10000/1/oauth/access_token' -d "grant_type=refresh_token&refresh_token=$ert&client_id=FTGAcAzRzdg-ce14b24e386e410ca0f21fa54e17c2bd&client_secret=7c8f4b2dbb124c66a682b4dedb607f09" -curl -s 'http://platform.docker.qa:10000/1/oauth/revoke?' -d "token=$rt&client_id=FTGAcAzRzdg-ce14b24e386e410ca0f21fa54e17c2bd&client_secret=7c8f4b2dbb124c66a682b4dedb607f09" -curl http://platform.docker.qa:10000/1/users/current -H "Authorization: Bearer $at" | python -m json.tool # TESTED -curl http://platform.docker.qa:10000/1/users/contacts -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/users/BREAEAINALE -H "Authorization: Bearer $at" | python -m json.tool # TESTED -curl http://platform.docker.qa:10000/1/users/brina@quip.com -H "Authorization: Bearer $at" | python -m json.tool # TESTED -curl http://platform.docker.qa:10000/1/users/?ids=dKWAEApvV4V,BREAEAINALE -H "Authorization: Bearer $at" | python -m json.tool # TESTED -# curl http://platform.docker.qa:10000/1/users/update -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/folders/UQNAOAMwq9d -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/folders/?ids=JAZAOAXMKK7,LLYAOA0KAUV -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/folders/new -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/folders/update -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/folders/add-members -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/folders/remove-members -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/teams/current -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/threads/recent -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/new-document -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/new-chat -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/edit-document -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/add-members -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/remove-members -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/pin-to-desktop -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/threads/search/ -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/threads/WBAcArDaZGYD -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/threads/?ids=KUfAAAacN4b,KJDAAA8hnTx -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/messages/KUfAAAacN4b -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/messages/new -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/blob/JcUAAAls35b/E8PFroUvsmXaby5WiJ1tcA -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/blob/KUfAAAacN4b -H "Authorization: Bearer $at" | python -m json.tool -curl -F 'blob=@/Users/dwillhite/Desktop/images/soccer-ball.jpeg' 'http://platform.docker.qa:10000/1/blob/WIdAAANyW8q' -H "Authorization: Bearer $pat" -# curl http://platform.docker.qa:10000/1/websockets/new -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/new -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/refresh -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/create -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/update -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/delete -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/ping -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/verify_code -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/twitter -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/integrations/email -H "Authorization: Bearer $at" | python -m json.tool - -# curl http://platform.docker.qa:10000/1/admin/threads/list -H "Authorization: Bearer $at" | python -m json.tool ## can't execute because of bigquery -# curl http://platform.docker.qa:10000/1/admin/threads/search -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/admin/threads/([a-zA-Z0-9:/]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool # Tested -# curl http://platform.docker.qa:10000/1/admin/messages/([a-zA-Z0-9]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/admin/threads/add-members -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/admin/threads/remove-members -H "Authorization: Bearer $at" | python -m json.tool -curl 'http://platform.docker.qa:10000/1/admin/threads/?ids=ba4yA6bgL0fD,WBAcArDaZGYD&company_id=FTGAcAzRzdg' -H "Authorization: Bearer $at" | python -m json.tool # TESTED -curl http://platform.docker.qa:10000/1/admin/users/list -X POST -H "Authorization: Bearer $at" -d "company_id=FTGAcAzRzdg" | python -m json.tool -curl http://platform.docker.qa:10000/1/admin/users/BREAEAINALE?company_id=FTGAcAzRzdg -H "Authorization: Bearer $at" | python -m json.tool -curl http://platform.docker.qa:10000/1/admin/users/brina@quip.com?company_id=FTGAcAzRzdg -H "Authorization: Bearer $at" | python -m json.tool ## doesn't work, i believe extra auth requires real user_id -# curl http://platform.docker.qa:10000/1/admin/threads/edit-document -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/admin/folders/([a-zA-Z0-9:/]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/admin/folders/([a-zA-Z0-9:/]{11,12}) -H "Authorization: Bearer $at" | python -m json.tool -# curl http://platform.docker.qa:10000/1/admin/blob/([A-Za-z0-9]{11})/([A-Za-z0-9\-\_]{22}) -curl 'http://platform.docker.qa:10000/1/admin/blob/JcUAAAls35b/IranRc-Ao96onSHA9ViNqQ?name=IDcard.pdf&company_id=FTGAcAzRzdg' -H "Authorization: Bearer $at" > filename.txt \ No newline at end of file From ae58d16067b8ca69bb946d8aa2e456066079a2bd Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Thu, 24 Jan 2019 13:36:20 -0800 Subject: [PATCH 09/24] Add get_matching_threads() function to QuipClient --- python/quip.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/python/quip.py b/python/quip.py index 92bb7e6..f6ac6e8 100644 --- a/python/quip.py +++ b/python/quip.py @@ -40,7 +40,10 @@ PY3 = sys.version_info > (3,) if PY3: - import urllib.request, urllib.parse, urllib.error + import urllib.request + import urllib.parse + import urllib.error + Request = urllib.request.Request urlencode = urllib.parse.urlencode urlopen = urllib.request.urlopen @@ -104,18 +107,18 @@ class QuipClient(object): """A Quip API client""" # Edit operations APPEND, \ - PREPEND, \ - AFTER_SECTION, \ - BEFORE_SECTION, \ - REPLACE_SECTION, \ - DELETE_SECTION = range(6) + PREPEND, \ + AFTER_SECTION, \ + BEFORE_SECTION, \ + REPLACE_SECTION, \ + DELETE_SECTION = range(6) # Folder colors MANILA, \ - RED, \ - ORANGE, \ - GREEN, \ - BLUE = range(5) + RED, \ + ORANGE, \ + GREEN, \ + BLUE = range(5) def __init__(self, access_token=None, client_id=None, client_secret=None, base_url=None, request_timeout=None): @@ -256,6 +259,12 @@ def get_recent_threads(self, max_updated_usec=None, count=None, **kwargs): "threads/recent", max_updated_usec=max_updated_usec, count=count, **kwargs) + def get_matching_threads( + self, query, count=None, only_match_titles=False, **kwargs): + """Returns the recently updated threads for a given user.""" + return self._fetch_json("threads/search", query=query, count=count, + only_match_titles=False, **kwargs) + def add_thread_members(self, thread_id, member_ids): """Adds the given folder or user IDs to the given thread.""" return self._fetch_json("threads/add-members", post_data={ @@ -360,7 +369,7 @@ def merge_comments(self, original_id, children_ids, ignore_user_ids=[]): '= 0 and loc >= 0: - section_id = thread["html"][loc+4:loc+15] + section_id = thread["html"][loc + 4:loc + 15] if section_id and section_id in parent_map: kwargs["section_id"] = parent_map[section_id] if "files" in message: @@ -461,7 +470,10 @@ def update_spreadsheet_row(self, thread_id, header, value, updates, **args): """ response = None - spreadsheet = self.get_first_spreadsheet(thread_id) + if args.get("name"): + spreadsheet = self.get_named_spreadsheet(args["name"], thread_id) + else: + spreadsheet = self.get_first_spreadsheet(thread_id) headers = self.get_spreadsheet_header_items(spreadsheet) row = self.find_row_from_header(spreadsheet, header, value) if row: @@ -694,7 +706,7 @@ def parse_spreadsheet_contents(self, spreadsheet_tree): style = cell.attrib.get("style") if style and "background-color:#" in style: sharp = style.find("#") - data["color"] = style[sharp+1:sharp+7] + data["color"] = style[sharp + 1:sharp + 7] value["cells"][spreadsheet["headers"][i]] = data if len(value["cells"]): spreadsheet["rows"].append(value) From 0224cd73fd188131da099b1c6f73fc7f5cced26e Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Mon, 4 Feb 2019 13:53:17 -0800 Subject: [PATCH 10/24] Update quip client with new copy-document and delete endpoints --- python/quip.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/python/quip.py b/python/quip.py index f6ac6e8..3dbd0c5 100644 --- a/python/quip.py +++ b/python/quip.py @@ -272,6 +272,12 @@ def add_thread_members(self, thread_id, member_ids): "member_ids": ",".join(member_ids), }) + def delete_thread(self, thread_id): + """Deletes the thread with the given thread id or secret""" + return self._fetch_json("threads/delete", post_data={ + "thread_id": thread_id, + }) + def remove_thread_members(self, thread_id, member_ids): """Removes the given folder or user IDs from the given thread.""" return self._fetch_json("threads/remove-members", post_data={ @@ -320,15 +326,25 @@ def new_document(self, content, format="html", title=None, member_ids=[]): "member_ids": ",".join(member_ids), }) - def copy_document(self, id, title=None, member_ids=[]): - """Creates a new document from the given thread ID. - - To create it in a folder, include the folder ID in member_ids. + def copy_document(self, thread_id, folder_ids=None, member_ids=None, + title=None, values=None, **kwargs): + """Copies the given document, optionally replaces template variables + in the document with values in 'values' arg. The values argument + must be a dictionary that contains string keys and values that + are either strings, numbers or dictionaries. """ - old_thread = self.get_thread(id) - return self.new_document( - old_thread["html"], title=title or old_thread["thread"]["title"], - member_ids=member_ids) + + args = {"thread_id": thread_id} + if folder_ids: + args["folder_ids"]: ",".join(folder_ids) + if member_ids: + args["member_ids"]: ",".join(member_ids) + if title: + args["title"]: title + if values: + args["values"] = json.dumps(values) + args.update(kwargs) + return self._fetch_json("threads/copy-document", post_data=args) def merge_comments(self, original_id, children_ids, ignore_user_ids=[]): """Given an original document and a set of exact duplicates, copies From 592b3611a83ac4e1a25817ce39bf6c840ca1cfa8 Mon Sep 17 00:00:00 2001 From: Jeffrey Arnold Date: Mon, 4 Feb 2019 16:23:00 -0800 Subject: [PATCH 11/24] Fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7944b77..4546ee2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is the official repository for the [Quip Automation API](https://quip.com/a * [Quip Automation API Reference](https://quip.com/api/reference) -* [Get a Personal Automatoin API Access Token](https://quip.com/api/personal-token) +* [Get a Personal Automation API Access Token](https://quip.com/api/personal-token) ## Client Libraries From cfa97b941133bdca76b2ade89f8137750f7c9335 Mon Sep 17 00:00:00 2001 From: Dan Willhite Date: Tue, 5 Feb 2019 08:18:45 -0800 Subject: [PATCH 12/24] Fix typos in copy_document function --- python/quip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/quip.py b/python/quip.py index 3dbd0c5..ac35ecb 100644 --- a/python/quip.py +++ b/python/quip.py @@ -336,11 +336,11 @@ def copy_document(self, thread_id, folder_ids=None, member_ids=None, args = {"thread_id": thread_id} if folder_ids: - args["folder_ids"]: ",".join(folder_ids) + args["folder_ids"] = ",".join(folder_ids) if member_ids: - args["member_ids"]: ",".join(member_ids) + args["member_ids"] = ",".join(member_ids) if title: - args["title"]: title + args["title"] = title if values: args["values"] = json.dumps(values) args.update(kwargs) From 3c59c1f10e13793d54f496fd141a09752b7bd311 Mon Sep 17 00:00:00 2001 From: zimu Date: Mon, 1 Apr 2019 11:52:17 -0700 Subject: [PATCH 13/24] Remove `pin_to_desktop` in quip-api repo --- python/quip.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/python/quip.py b/python/quip.py index ac35ecb..da381df 100644 --- a/python/quip.py +++ b/python/quip.py @@ -285,14 +285,6 @@ def remove_thread_members(self, thread_id, member_ids): "member_ids": ",".join(member_ids), }) - def pin_to_desktop(self, thread_id, **kwargs): - """Pins the given thread to desktop.""" - args = { - "thread_id": thread_id, - } - args.update(kwargs) - return self._fetch_json("threads/pin-to-desktop", post_data=args) - def move_thread(self, thread_id, source_folder_id, destination_folder_id): """Moves the given thread from the source folder to the destination one. """ From 7cb10eab03ce083a714aacfa7598028c99ad5d79 Mon Sep 17 00:00:00 2001 From: Lindsey Simon Date: Wed, 9 Oct 2019 10:52:38 -0700 Subject: [PATCH 14/24] Update README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 4546ee2..c185268 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,8 @@ Quip Automation API =================== -## NOTE: If you're looking for the Quip Live Apps repository, it lives at https://github.com/quip/quip-apps - - This is the official repository for the [Quip Automation API](https://quip.com/api/). - * [Quip Automation API Reference](https://quip.com/api/reference) * [Get a Personal Automation API Access Token](https://quip.com/api/personal-token) From dfce1a8d7a951f0a8f353137197ec1f03f7f814d Mon Sep 17 00:00:00 2001 From: dengzb Date: Wed, 16 Oct 2019 23:51:00 -0700 Subject: [PATCH 15/24] Fix only_match_titles in search thread function --- python/quip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/quip.py b/python/quip.py index da381df..b39eafe 100644 --- a/python/quip.py +++ b/python/quip.py @@ -263,7 +263,7 @@ def get_matching_threads( self, query, count=None, only_match_titles=False, **kwargs): """Returns the recently updated threads for a given user.""" return self._fetch_json("threads/search", query=query, count=count, - only_match_titles=False, **kwargs) + only_match_titles=only_match_titles, **kwargs) def add_thread_members(self, thread_id, member_ids): """Adds the given folder or user IDs to the given thread.""" From e3f7260f36e092f82d7140df991dd2ba64744833 Mon Sep 17 00:00:00 2001 From: Edin Merdan Date: Sat, 22 Feb 2020 20:32:12 +0100 Subject: [PATCH 16/24] Python requirements.txt file added to the websocket example. The requirements file specifies python websocket client library that needs to be installed in order to run the websocket example. --- samples/websocket/requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 samples/websocket/requirements.txt diff --git a/samples/websocket/requirements.txt b/samples/websocket/requirements.txt new file mode 100644 index 0000000..965ad95 --- /dev/null +++ b/samples/websocket/requirements.txt @@ -0,0 +1 @@ +websocket-client==0.57.0 From ba0c6416e3e4e64ecb79d7b9d50df7438126bc8e Mon Sep 17 00:00:00 2001 From: Kevin Crain Date: Tue, 12 May 2020 15:54:31 -0700 Subject: [PATCH 17/24] Add events-api quaratine demo. --- samples/README.md | 1 + samples/events-api/README.md | 18 ++ samples/events-api/quarantine_demo.py | 230 ++++++++++++++++++++++++++ samples/events-api/requirements.txt | 1 + 4 files changed, 250 insertions(+) create mode 100644 samples/events-api/README.md create mode 100644 samples/events-api/quarantine_demo.py create mode 100644 samples/events-api/requirements.txt diff --git a/samples/README.md b/samples/README.md index a8ce4ae..2897d44 100644 --- a/samples/README.md +++ b/samples/README.md @@ -7,6 +7,7 @@ Quip API Sample Apps * [`webhooks`](webhooks): Inbound [Webhook](http://en.wikipedia.org/wiki/Webhook) support for posting GitHub, Crashlytics, PagerDuty and other service notifications to threads. _Python on App Engine_ * [`wordpress`](wordpress): Publish Quip documents to [WordPress](http://wordpress.org/). _Python on the command line_ * [`websocket`](websocket): Receive messages from Quip in real time. _Python on the command line_ +* [`events-api`](events-api): Examine, audit, and quaratine Quip content in real-time based on the Events API feed. _Python on the command line_ # Community Samples diff --git a/samples/events-api/README.md b/samples/events-api/README.md new file mode 100644 index 0000000..2eb5dab --- /dev/null +++ b/samples/events-api/README.md @@ -0,0 +1,18 @@ +# Examine, audit, and quaratine Quip content in real-time based on the Events API feed. + +A simple webserver to manage Events API cursors, audit messages, examine threads, and quarantine content. + +## Requirements +Uses the [tornado](https://pypi.org/project/tornado/) networking library to run a local server. + +``` +pip install -r requirements.txt +``` + +## Running + +``` +./quarantine_demo.py +``` + +Requires an admin [access token](https://quip.com/dev/automation) and Events API subscription. \ No newline at end of file diff --git a/samples/events-api/quarantine_demo.py b/samples/events-api/quarantine_demo.py new file mode 100644 index 0000000..f672dab --- /dev/null +++ b/samples/events-api/quarantine_demo.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +# +# pip install tornado +# +# Copyright 2019 Quip +# http://www.apache.org/licenses/LICENSE-2.0.html + +""" +Examples: + +Audit contents of newly created messages. + -Loop over all company events. + -Fetch and examine messages' contents, based on `create-message` events. + -Conditionally quarantine message contents based on admin-specified criteria. + +Quarantine sensitive threads if certain events, e.g. sharing, occur. + -Loop over all company events. + -Check if ids from `share-thread` events match an admin-specified list. + -Conditionally quarantine threads' contents. + +Examine thread metadata and content. + -Fetch a thread using its id. + +""" + +import json +import logging +import os.path +import re +import requests +import tornado.auth +import tornado.gen +import tornado.ioloop +import tornado.web +import tornado.httpclient + +from tornado.options import define, options + +define("port", type=int, default=8080) +define("admin_endpoint_base", default="https://platform.quip.com/1/admin") +define("company_id", default="<11_character_company_id") +# Must be for an admin. +define("access_token", + default="", +] + + +class Application(tornado.web.Application): + def __init__(self): + base_dir = os.path.dirname(__file__) + settings = { + "debug": True + } + tornado.web.Application.__init__(self, [ + tornado.web.url(r"/events/realtime", RealtimeHandler, name="realtime"), + tornado.web.url(r"/events/cursor", CursorHandler, name="cursor"), + tornado.web.url(r"/audit/message/(.*)", AuditMessageHandler, name="message"), + tornado.web.url(r"/threads/(.*)", GetThread, name="get-thread"), + ], **settings) + + +class EventsDemoHandler(tornado.web.RequestHandler): + def authorized_headers(self): + return { + 'Authorization': f"Bearer {options.access_token}", + } + + def fetch_new_cursor_json(self): + url = f"{options.admin_endpoint_base}/events/1/cursor/realtime/create" + params = {"company_id": options.company_id} + response = requests.request("GET", url, headers=self.authorized_headers(), params=params) + return response.json() + + def next_cursor(self): + try: + cursor = self.get_argument("cursor") + except tornado.web.MissingArgumentError as e: + cursor = self.fetch_new_cursor_json()["next_cursor"] + return cursor + + def quarantine_id(self, object_id): + url = f"{options.admin_endpoint_base}/quarantine" + params = { + "company_id": options.company_id, + "object_id": object_id + } + requests.request("POST", url, headers=self.authorized_headers(), params=params) + + def unquarantine_id(self, object_id): + url = f"{options.admin_endpoint_base}/quarantine" + params = { + "company_id": options.company_id, + "object_id": object_id + } + requests.request("DELETE", url, headers=self.authorized_headers(), params=params) + + +class CursorHandler(EventsDemoHandler): + @tornado.gen.coroutine + def get(self): + response_json = self.fetch_new_cursor_json() + pretty_response = json.dumps(response_json, sort_keys=True, indent=4) + self.write(f"
{pretty_response}
") + + +class RealtimeHandler(EventsDemoHandler): + @tornado.gen.coroutine + def get(self): + url = f"{options.admin_endpoint_base}/events/1/events/realtime/get" + params = { + "company_id": options.company_id, + "cursor": self.next_cursor() + } + response = requests.request("GET", url, headers=self.authorized_headers(), params=params) + raw_html_output = self.pretty_html_formatting(response) + if options.enable_audit_for_realtime: + for event in response.json()["events"]: + audit_event(self, event) + self.write(raw_html_output) + + def pretty_html_formatting(self, response): + response_json = response.json() + next_events_url = f"{self.reverse_url('realtime')}?cursor={response_json['next_cursor']}" + pretty_response = json.dumps(response_json, sort_keys=True, indent=4).replace( + '"event"', '"event"') + return f""" + Fetch Next Events +
+
+

Raw JSON response:

+
{pretty_response}
+ """ + + +def audit_event(handler, event): + if event["event"] == "share-thread": + if event["thread_id"] in SHARE_DENYLIST: + handler.quarantine_id(event["thread_id"]) + elif event["event"] == "create-message": + audit_message(handler, event["message_id"]) + + +def audit_message_summary(handler, message_id): + url = f"{options.admin_endpoint_base}/message/{message_id}" + params = { + "company_id": options.company_id + } + response = requests.request("GET", url, headers=handler.authorized_headers(), params=params) + unredacted_message_text = response.json()[0]["text"] + + denied_words = set() + for word in DENYLIST_WORDS: + if word in unredacted_message_text: + denied_words.add(word) + + denied_patterns = set() + for name, pattern in DENYLIST_PATTERNS.items(): + if re.search(pattern, unredacted_message_text): + denied_patterns.add(name) + + is_disallowed = bool(denied_words or denied_patterns) + + audit_summary = f"Message {message_id}

Audit: {'FAILED' if is_disallowed else 'PASSED'}." + if denied_words: + audit_summary += f"

Denylist words: {denied_words}" + if denied_patterns: + audit_summary += f"

Denylist patterns: {denied_patterns}" + + return audit_summary + + +def audit_message(handler, message_id): + audit_summary = audit_message_summary(handler, message_id) + # Crude, but hey it's just a demo... + if "FAILED" in audit_summary: + handler.quarantine_id(message_id) + + +class AuditMessageHandler(EventsDemoHandler): + @tornado.gen.coroutine + def get(self, message_id): + audit_summary = audit_message_summary(self, message_id) + audit_message(self, message_id) + self.write(audit_summary) + + +class GetThread(EventsDemoHandler): + @tornado.gen.coroutine + def get(self, thread_id): + url = f"{options.admin_endpoint_base}/threads/{thread_id}" + params = { + "company_id": options.company_id + } + response = requests.request("GET", url, headers=self.authorized_headers(), params=params) + response_json = response.json() + + # Field contains raw html that formats oddly. + thread_html = response_json["html"] + del response_json["html"] + + pretty_response = json.dumps(response_json, sort_keys=True, indent=4) + output = f"
{pretty_response}
" + output += f"

Thread's HTML (extracted out of above):

{thread_html}" + self.write(output) + + +def main(): + tornado.options.parse_command_line() + Application().listen(options.port) + logging.info("Running at http://localhost:%d", options.port) + tornado.ioloop.IOLoop.instance().start() + + +if __name__ == "__main__": + main() diff --git a/samples/events-api/requirements.txt b/samples/events-api/requirements.txt new file mode 100644 index 0000000..46690a7 --- /dev/null +++ b/samples/events-api/requirements.txt @@ -0,0 +1 @@ +tornado==5.1.1 From 430b8eb59864a004df1d04b24bf3c96f0ec45b82 Mon Sep 17 00:00:00 2001 From: Kevin Crain Date: Tue, 12 May 2020 15:56:37 -0700 Subject: [PATCH 18/24] Add hyperlink to Admin API documentation for Events API. --- samples/events-api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/events-api/README.md b/samples/events-api/README.md index 2eb5dab..8ab64f3 100644 --- a/samples/events-api/README.md +++ b/samples/events-api/README.md @@ -1,6 +1,6 @@ # Examine, audit, and quaratine Quip content in real-time based on the Events API feed. -A simple webserver to manage Events API cursors, audit messages, examine threads, and quarantine content. +A simple webserver to manage [Events API](https://quip.com/dev/admin/documentation#events-requires-subscription) cursors, audit messages, examine threads, and quarantine content. ## Requirements Uses the [tornado](https://pypi.org/project/tornado/) networking library to run a local server. From 0d05606a52b0df550d106b9e2df02f5c2f29c2aa Mon Sep 17 00:00:00 2001 From: Kevin Crain Date: Tue, 12 May 2020 15:59:17 -0700 Subject: [PATCH 19/24] nit: hyperlink and misspelling --- samples/README.md | 2 +- samples/events-api/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/README.md b/samples/README.md index 2897d44..325052a 100644 --- a/samples/README.md +++ b/samples/README.md @@ -7,7 +7,7 @@ Quip API Sample Apps * [`webhooks`](webhooks): Inbound [Webhook](http://en.wikipedia.org/wiki/Webhook) support for posting GitHub, Crashlytics, PagerDuty and other service notifications to threads. _Python on App Engine_ * [`wordpress`](wordpress): Publish Quip documents to [WordPress](http://wordpress.org/). _Python on the command line_ * [`websocket`](websocket): Receive messages from Quip in real time. _Python on the command line_ -* [`events-api`](events-api): Examine, audit, and quaratine Quip content in real-time based on the Events API feed. _Python on the command line_ +* [`events-api`](events-api): Examine, audit, and quarantine Quip content in real-time based on the Events API feed. _Python on the command line_ # Community Samples diff --git a/samples/events-api/README.md b/samples/events-api/README.md index 8ab64f3..ee1bdbb 100644 --- a/samples/events-api/README.md +++ b/samples/events-api/README.md @@ -1,4 +1,4 @@ -# Examine, audit, and quaratine Quip content in real-time based on the Events API feed. +# Examine, audit, and quarantine Quip content in real-time based on the Events API feed. A simple webserver to manage [Events API](https://quip.com/dev/admin/documentation#events-requires-subscription) cursors, audit messages, examine threads, and quarantine content. @@ -15,4 +15,4 @@ pip install -r requirements.txt ./quarantine_demo.py ``` -Requires an admin [access token](https://quip.com/dev/automation) and Events API subscription. \ No newline at end of file +Requires an admin [access token](https://quip.com/dev/automation) and [Events API](https://quip.com/dev/admin/documentation#events-requires-subscription) subscription. \ No newline at end of file From 69550a75af9e9d30065bc681f00613cae57d4540 Mon Sep 17 00:00:00 2001 From: kenichi-ando Date: Sat, 13 Jun 2020 07:01:34 +0900 Subject: [PATCH 20/24] Add community link in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c185268..98a7ee1 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This is the official repository for the [Quip Automation API](https://quip.com/a * [Ruby](https://github.com/jacamat/quip-ruby) (from [@jacamat](https://github.com/jacamat)) * [Go](https://github.com/mduvall/go-quip) (from [@mduvall](https://github.com/mduvall)) * [Elixir](https://github.com/mmartinson/e_quip) (from [@mmartinson](https://github.com/mmartinson)) +* [Java](https://github.com/kenichi-ando/quip-api-java) (from [@kenichi-ando](https://github.com/kenichi-ando)) ## Sample Apps From 3f2dd391527a7e161fac4855a205aabf4b94f594 Mon Sep 17 00:00:00 2001 From: Doug Colaizzo Date: Sun, 18 Oct 2020 12:05:54 -0700 Subject: [PATCH 21/24] Add support for 'MessageType' and corresponding 'messageType' parameter in 'getMessages' method See 'message_type' in https://quip.com/dev/automation/documentation#get-recent-messages --- nodejs/quip.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/nodejs/quip.js b/nodejs/quip.js index 38dfe26..72bebbf 100644 --- a/nodejs/quip.js +++ b/nodejs/quip.js @@ -42,6 +42,15 @@ var Operation = { DELETE_SECTION: 5 }; +/** + * Message Type + * @enum {string} + */ +var MessageType = { + MESSAGE: 'message', + EDIT: 'edit' +}; + /** * A Quip API client. * @@ -224,13 +233,15 @@ Client.prototype.removeFolderMembers = function(options, callback) { /** * @param {{threadId: string, * maxUpdatedUsec: (number|undefined), - * count: (number|undefined)}} options + * count: (number|undefined) + * messageType: (MessageType|undefined)}} options * @param {function(Error, Object)} callback */ Client.prototype.getMessages = function(options, callback) { this.call_('messages/' + options.threadId + '?' + querystring.stringify({ 'max_updated_usec': options.maxUpdatedUsec, - 'count': options.count + 'count': options.count, + 'message_type': options.messageType }), callback); }; @@ -418,5 +429,6 @@ ClientError.prototype = Object.create(Error.prototype); exports.Color = Color; exports.Operation = Operation; +exports.MessageType = MessageType; exports.Client = Client; exports.ClientError = ClientError; From d5cc8661db1609c7f0a8939ab812c51285cb88de Mon Sep 17 00:00:00 2001 From: Tong Yin Date: Wed, 11 Nov 2020 14:01:29 -0800 Subject: [PATCH 22/24] Remove sample project mail2quip We are sunsetting share@quip feature, and considering mail2quip is no longer supported, hence removing this sample project. --- README.md | 1 - samples/mail2quip/README.md | 23 ----- samples/mail2quip/app.py | 137 ------------------------- samples/mail2quip/app.yaml | 26 ----- samples/mail2quip/quip.py | 1 - samples/mail2quip/static/mail2quip.css | 6 -- samples/mail2quip/templates/index.html | 28 ----- 7 files changed, 222 deletions(-) delete mode 100644 samples/mail2quip/README.md delete mode 100644 samples/mail2quip/app.py delete mode 100644 samples/mail2quip/app.yaml delete mode 120000 samples/mail2quip/quip.py delete mode 100644 samples/mail2quip/static/mail2quip.css delete mode 100644 samples/mail2quip/templates/index.html diff --git a/README.md b/README.md index 98a7ee1..147b1f2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ This is the official repository for the [Quip Automation API](https://quip.com/a ### Official * [`baqup`](samples/baqup): Exports all of your Quip folders, documents and messages to a local directory. _Python on the command line_ -* [`mail2quip`](samples/mail2quip): Create Quip messages by email. [Sample server](http://mail2quip.appspot.com/). _Python on App Engine_ * [`twitterbot`](samples/twitterbot): Post Twitter messages to a Quip thread via the Twitter streaming API. _Python on the command line_ * [`webhooks`](samples/webhooks): Inbound [Webhook](http://en.wikipedia.org/wiki/Webhook) support for posting GitHub, Crashlytics, PagerDuty and other service notifications to threads. _Python on App Engine_ * [`wordpress`](samples/wordpress): Publish Quip documents to [WordPress](http://wordpress.org/). _Python on the command line_ diff --git a/samples/mail2quip/README.md b/samples/mail2quip/README.md deleted file mode 100644 index 64a214a..0000000 --- a/samples/mail2quip/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Mail2Quip - -Simple (state-less) server that creates Quip messages from emails. Sample server [running here](http://mail2quip.appspot.com/). - -## Caveats - -API access tokens are embedded in the email. If you generate a new access token, you will need to start using a new email address. - -## Running Locally - -First, install the App Engine SDK from https://developers.google.com/appengine/downloads and open it to install the symlinks. Then run: - -``` -PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python dev_appserver.py ./ -``` - -The App Engine admin console has a tool for testing incoming emails without deploying. - -## Deploying to App Engine - -``` -appcfg.py --oauth2 --no_cookies update ./ -``` diff --git a/samples/mail2quip/app.py b/samples/mail2quip/app.py deleted file mode 100644 index ee1bcb7..0000000 --- a/samples/mail2quip/app.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright 2014 Quip -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Inbound email handler that creates Quip messages from emails. - -This is a sample app for the Quip API - https://quip.com/api/. -""" - -import logging -import jinja2 -import os -import quip -import urllib -import webapp2 - -from google.appengine.api import files -from google.appengine.ext import blobstore -from google.appengine.ext.webapp import blobstore_handlers -from google.appengine.ext.webapp.mail_handlers import InboundMailHandler - -JINJA_ENVIRONMENT = jinja2.Environment( - loader=jinja2.FileSystemLoader(os.path.dirname(__file__)), - extensions=["jinja2.ext.autoescape"], - autoescape=True) - -class Home(webapp2.RequestHandler): - def get(self): - template = JINJA_ENVIRONMENT.get_template( - "templates/index.html") - self.response.write(template.render()) - - -class MailHandler(InboundMailHandler): - def receive(self, message): - addresses = message.to.split(',') - thread_id = "" - token = "" - client = None - for address in addresses: - try: - (to, domain) = address.split('@') - (thread_id, token) = to.split('+', 1) - except: - pass - if len(thread_id) in [11,12] and len(token) > 0: - client = quip.QuipClient(access_token=token, request_timeout=30) - try: - client.get_thread(thread_id) - break - except Exception as e: - client = None - logging.exception(e) - client = quip.QuipClient(access_token=to, request_timeout=30) - try: - client.get_authenticated_user() - thread_id = "" - break - except Exception as e: - client = None - logging.exception(e) - if not client: - logging.error("Could not find token in %r", addresses) - self.abort(404) - text = None - for content_type, body in message.bodies("text/plain"): - text = body.decode() - # Strip some common signature patterns - for pattern in ["\n----", "\nIFTTT"]: - if pattern in text: - text = text[:text.find(pattern)] - if len(text) > 0: - break - html = None - for content_type, body in message.bodies("text/html"): - html = body.decode() - if len(html) > 0: - break - attachments = [] - if hasattr(message, "attachments"): - for filename, attachment in message.attachments: - try: - blob = files.blobstore.create( - _blobinfo_uploaded_filename=filename) - with files.open(blob, 'a') as f: - f.write(attachment.decode()) - files.finalize(blob) - host = self.request.host_url.replace("http:", "https:") - attachments.append("%s/attach/%s" % ( - host, files.blobstore.get_blob_key(blob))) - except Exception: - pass - message_id = None - if "message-id" in message.original: - message_id = message.original["message-id"] - if thread_id: - # Post a message - args = { - "silent": "silent" in message.subject, - } - if attachments: - args["attachments"] = ",".join(attachments) - if message_id: - args["service_id"] = message_id - client.new_message(thread_id, text, **args) - else: - # Create a thread from the message body - thread = client.new_document( - html or text, format="html" if html else "markdown", - title=message.subject) - if attachments: - client.new_message( - thread["thread"]["id"], attachments=",".join(attachments)) - - -class AttachmentHandler(blobstore_handlers.BlobstoreDownloadHandler): - def get(self, resource): - resource = str(urllib.unquote(resource)) - blob_info = blobstore.BlobInfo.get(resource) - self.send_blob(blob_info) - - -application = webapp2.WSGIApplication([ - ('/', Home), - ('/attach/([^/]+)?', AttachmentHandler), - MailHandler.mapping(), -], debug=True) diff --git a/samples/mail2quip/app.yaml b/samples/mail2quip/app.yaml deleted file mode 100644 index 5ecd03e..0000000 --- a/samples/mail2quip/app.yaml +++ /dev/null @@ -1,26 +0,0 @@ -application: mail2quip -version: 1 -runtime: python27 -api_version: 1 -threadsafe: true - -inbound_services: -- mail - -handlers: -- url: /static - static_dir: static - -- url: /admin/.* - script: google.appengine.ext.admin.application - login: admin - -- url: /.* - script: app.application - secure: always - -libraries: -- name: webapp2 - version: latest -- name: jinja2 - version: latest diff --git a/samples/mail2quip/quip.py b/samples/mail2quip/quip.py deleted file mode 120000 index 6ca5967..0000000 --- a/samples/mail2quip/quip.py +++ /dev/null @@ -1 +0,0 @@ -../../python/quip.py \ No newline at end of file diff --git a/samples/mail2quip/static/mail2quip.css b/samples/mail2quip/static/mail2quip.css deleted file mode 100644 index f738ae7..0000000 --- a/samples/mail2quip/static/mail2quip.css +++ /dev/null @@ -1,6 +0,0 @@ -body { - font-family: Helvetica, Arial, sans-serif; - font-size: 14px; - padding: 20px; - line-height: 1.4; -} diff --git a/samples/mail2quip/templates/index.html b/samples/mail2quip/templates/index.html deleted file mode 100644 index a6b2b57..0000000 --- a/samples/mail2quip/templates/index.html +++ /dev/null @@ -1,28 +0,0 @@ - -{% autoescape true %} - - - Mail2Quip - email to Quip gateway - - - -

Mail 2 Quip

-

- Do you have emailed alerts you'd like to appear in Quip? IFTTT - or Zapier recipes that should go to Quip? Mail2Quip allows you - to create new messages or threads by email. -

-
    -
  1. Get a token for - the user you want to post as.
  2. -
  3. Send an email: -
      -
    • To create a new thread: <token>@mail2quip.appspotmail.com
    • -
    • To send a message to an existing thread: <thread_id>+<token>@mail2quip.appspotmail.com
    • -
    -
  4. -
  5. See your message or thread posted on Quip.
  6. -
- - -{% endautoescape %} From 19f3b32a05ed092a70dc2c616e214aaff8a06de2 Mon Sep 17 00:00:00 2001 From: Patrick Linehan Date: Thu, 4 Mar 2021 17:11:58 -0800 Subject: [PATCH 23/24] remove outdated sample app. the app describes itself as "DEPRECATED and NOT maintained anymore since this feature is built-in in Quip." --- README.md | 4 ---- samples/README.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/README.md b/README.md index 147b1f2..ef0a224 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,3 @@ This is the official repository for the [Quip Automation API](https://quip.com/a * [`webhooks`](samples/webhooks): Inbound [Webhook](http://en.wikipedia.org/wiki/Webhook) support for posting GitHub, Crashlytics, PagerDuty and other service notifications to threads. _Python on App Engine_ * [`wordpress`](samples/wordpress): Publish Quip documents to [WordPress](http://wordpress.org/). _Python on the command line_ * [`websocket`](samples/websocket): Receive messages from Quip in real time. _Python on the command line_ - -### Community - -* [`quiptree`](https://github.com/kwent/quiptree): Browser extension to display Quip folders and files in tree format. _Javascript on Browser Extension Engine_ diff --git a/samples/README.md b/samples/README.md index 325052a..6c313ec 100644 --- a/samples/README.md +++ b/samples/README.md @@ -8,7 +8,3 @@ Quip API Sample Apps * [`wordpress`](wordpress): Publish Quip documents to [WordPress](http://wordpress.org/). _Python on the command line_ * [`websocket`](websocket): Receive messages from Quip in real time. _Python on the command line_ * [`events-api`](events-api): Examine, audit, and quarantine Quip content in real-time based on the Events API feed. _Python on the command line_ - -# Community Samples - -* [`quiptree`](https://github.com/kwent/quiptree): Browser extension to display Quip folders and files in tree format. _Javascript on Browser Extension Engine_ From c5160e86ee44fa2f569b186e3cb5d06f540a5190 Mon Sep 17 00:00:00 2001 From: Tripti Agarwal Date: Tue, 1 Apr 2025 11:22:31 +0530 Subject: [PATCH 24/24] enable-checkpoint --- slack.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 slack.json diff --git a/slack.json b/slack.json new file mode 100644 index 0000000..53e2783 --- /dev/null +++ b/slack.json @@ -0,0 +1,8 @@ +{ + "checkpoint": { + "checkpoint_ready_check": false, + "notifications": { + "dm": false + } + } + } \ No newline at end of file