GitHub PullRequestReviewCommentEvent at 2014-03-26 10:52:24
Repository: vumi
Actor: jerith
Payload: {"comment":{"url":"https://api.github.com/repos/praekelt/vumi/pulls/comments/10971217","id":10971217,"diff_hunk":"@@ -0,0 +1,386 @@\n+# -*- test-case-name: vumi.transports.wechat.tests.test_wechat -*-\n+\n+import hashlib\n+import urllib\n+import json\n+from datetime import datetime\n+from functools import partial\n+\n+from twisted.internet import reactor\n+from twisted.internet.defer import inlineCallbacks, Deferred, returnValue\n+from twisted.web.resource import Resource\n+from twisted.web import http\n+from twisted.web.server import NOT_DONE_YET\n+\n+from vumi import log\n+from vumi.config import (\n+    ConfigText, ConfigServerEndpoint, ConfigDict, ConfigBool)\n+from vumi.transports import Transport\n+from vumi.transports.httprpc.httprpc import HttpRpcHealthResource\n+from vumi.transports.wechat.errors import WeChatException, WeChatApiException\n+from vumi.transports.wechat.message_types import (\n+    TextMessage, EventMessage, NewsMessage)\n+from vumi.transports.wechat.parser import WeChatXMLParser\n+from vumi.utils import build_web_site, http_request_full\n+from vumi.message import TransportUserMessage\n+from vumi.persist.txredis_manager import TxRedisManager\n+\n+\n+def is_verifiable(request):\n+    params = ['signature', 'timestamp', 'nonce']\n+    return all([(key in request.args) for key in params])\n+\n+\n+def http_ok(request):\n+    return 200 <= request.code < 300\n+\n+\n+def verify(token, request):\n+    signature = request.args['signature'][0]\n+    timestamp = request.args['timestamp'][0]\n+    nonce = request.args['nonce'][0]\n+\n+    hash_ = hashlib.sha1(''.join(sorted([timestamp, nonce, token])))\n+\n+    return hash_.hexdigest() == signature\n+\n+\n+class WeChatConfig(Transport.CONFIG_CLASS):\n+\n+    api_url = ConfigText(\n+        'The URL the WeChat API is accessible at.',\n+        default='https://api.wechat.com/cgi-bin/',\n+        required=False, static=True)\n+    auth_token = ConfigText(\n+        'This WeChat app\\'s auth token. '\n+        'Used for initial message authentication.',\n+        required=True, static=True)\n+    twisted_endpoint = ConfigServerEndpoint(\n+        'The endpoint to listen on.',\n+        required=True, static=True)\n+    web_path = ConfigText(\n+        \"The path to serve this resource on.\",\n+        default='/api/v1/wechat/', static=True)\n+    health_path = ConfigText(\n+        \"The path to serve the health resource on.\",\n+        default='/health/', static=True)\n+    redis_manager = ConfigDict('Parameters to connect to Redis with.',\n+                               default={}, required=False, static=True)\n+    wechat_appid = ConfigText(\n+        'The WeChat app_id. Issued by WeChat for developer accounts '\n+        'to allow push API access.', required=True, static=True)\n+    wechat_secret = ConfigText(\n+        'The WeChat secret. Issued by WeChat for developer accounts '\n+        'to allow push API access.', required=True, static=True)\n+    wechat_menu = ConfigDict(\n+        'The menu structure to create at boot.', required=False, static=True)\n+\n+\n+class WeChatResource(Resource):\n+\n+    isLeaf = True\n+\n+    def __init__(self, transport):\n+        Resource.__init__(self)\n+        self.transport = transport\n+        self.config = transport.get_static_config()\n+\n+    def render_GET(self, request):\n+        if is_verifiable(request) and verify(self.config.auth_token, request):\n+            return request.args['echostr'][0]\n+        request.setResponseCode(http.BAD_REQUEST)\n+        return ''\n+\n+    def render_POST(self, request):\n+        if not (is_verifiable(request)\n+                and verify(self.config.auth_token, request)):\n+            request.setResponseCode(http.BAD_REQUEST)\n+            return ''\n+\n+        d = Deferred()\n+        d.addCallback(self.handle_request)\n+        d.addCallback(self.transport.queue_request, request)\n+        d.addErrback(self.handle_error, request)\n+        reactor.callLater(0, d.callback, request)\n+        return NOT_DONE_YET\n+\n+    def handle_error(self, failure, request):\n+        if not failure.trap(WeChatException):\n+            raise failure\n+\n+        request.setResponseCode(http.BAD_REQUEST)\n+        request.write(failure.getErrorMessage())\n+        request.finish()\n+\n+    def handle_request(self, request):\n+        wc_msg = WeChatXMLParser.parse(request.content.read())\n+        return self.transport.handle_raw_inbound_message(wc_msg)\n+\n+\n+class WeChatTransport(Transport):\n+    \"\"\"\n+\n+    A Transport for the WeChat API.\n+\n+    Inbound Messaging\n+    ~~~~~~~~~~~~~~~~~\n+\n+    Supported Common Message types:\n+\n+        - Text Message\n+\n+    Supported Event Message types:\n+\n+        - Following / subscribe\n+        - Unfollowing / unsubscribe\n+        - Text Message (in response to Menu keypress events)\n+\n+    Outbound Messaging\n+    ~~~~~~~~~~~~~~~~~~\n+\n+    Supported Callback Message types:\n+\n+        - Text Message\n+\n+    \"\"\"\n+\n+    CONFIG_CLASS = WeChatConfig\n+    DEFAULT_MASK = 'default'\n+    MESSAGE_TYPES = [\n+        NewsMessage,\n+    ]\n+    DEFAULT_MESSAGE_TYPE = TextMessage\n+    # What key to store the `access_token` under in Redis\n+    ACCESS_TOKEN_KEY = 'access_token'\n+    # What key to store the `addr_mask` under in Redis\n+    ADDR_MASK_KEY = 'addr_mask'\n+\n+    @inlineCallbacks\n+    def setup_transport(self):\n+        config = self.get_static_config()\n+        self.request_queue = {}\n+        self.endpoint = config.twisted_endpoint\n+        self.resource = WeChatResource(self)\n+        self.factory = build_web_site({\n+            config.health_path: HttpRpcHealthResource(self),\n+            config.web_path: self.resource,\n+        })\n+\n+        self.redis = yield TxRedisManager.from_config(config.redis_manager)\n+        self.server = yield self.endpoint.listen(self.factory)\n+\n+        if config.wechat_menu:\n+            # not yielding because this shouldn't block startup\n+            d = self.get_access_token()\n+            d.addCallback(self.create_wechat_menu, config.wechat_menu)\n+\n+    @inlineCallbacks\n+    def create_wechat_menu(self, access_token, menu_structure):\n+        url = self.make_url('menu/create', {'access_token': access_token})\n+        response = yield http_request_full(\n+            url, method='POST', data=json.dumps(menu_structure),\n+            headers={'Content-Type': ['application/json']})\n+        if not http_ok(response):\n+            raise WeChatApiException(\n+                'Received HTTP code: %r when creating the menu.' % (\n+                    response.code,))\n+        data = json.loads(response.delivered_body)\n+        if data['errcode'] != 0:\n+            raise WeChatApiException(\n+                'Received errcode: %(errcode)s, errmsg: %(errmsg)s '\n+                'when creating WeChat Menu.' % data)\n+        log.info('WeChat Menu created succesfully.')\n+\n+    def mask_addr(self, to_addr, mask):\n+        return '@'.join([to_addr, mask])\n+\n+    def cache_addr_mask(self, mask):\n+        d = self.redis.set(self.ADDR_MASK_KEY, mask)","path":"vumi/transports/wechat/wechat.py","position":198,"original_position":198,"commit_id":"853b0501e5e818bf837c1ef3f464d7a8eb78d247","original_commit_id":"bca8e1572280b557d5d95df7411948cbc6c13cc3","user":{"login":"jerith","id":43731,"avatar_url":"https://avatars.githubusercontent.com/u/43731?","gravatar_id":"b7842ee0df3b024a6cad55bbe09c0d08","url":"https://api.github.com/users/jerith","html_url":"https://github.com/jerith","followers_url":"https://api.github.com/users/jerith/followers","following_url":"https://api.github.com/users/jerith/following{/other_user}","gists_url":"https://api.github.com/users/jerith/gists{/gist_id}","starred_url":"https://api.github.com/users/jerith/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jerith/subscriptions","organizations_url":"https://api.github.com/users/jerith/orgs","repos_url":"https://api.github.com/users/jerith/repos","events_url":"https://api.github.com/users/jerith/events{/privacy}","received_events_url":"https://api.github.com/users/jerith/received_events","type":"User","site_admin":false},"body":"Do we really need to cache this in perpetuity? We've been bitten by non-expiring keys before.","created_at":"2014-03-26T10:52:24Z","updated_at":"2014-03-26T10:52:24Z","html_url":"https://github.com/praekelt/vumi/pull/729#discussion_r10971217","pull_request_url":"https://api.github.com/repos/praekelt/vumi/pulls/729","_links":{"self":{"href":"https://api.github.com/repos/praekelt/vumi/pulls/comments/10971217"},"html":{"href":"https://github.com/praekelt/vumi/pull/729#discussion_r10971217"},"pull_request":{"href":"https://api.github.com/repos/praekelt/vumi/pulls/729"}}}}