Skip to content
Browse files

Open d3status demo app

  • Loading branch information...
0 parents commit 98aa44a153a0d4e848752c09c374df716787aef8 @felinx committed Oct 14, 2012
Showing with 1,656 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +201 −0 LICENSE
  3. +15 −0 README.md
  4. 0 d3status/__init__.py
  5. +91 −0 d3status/app.py
  6. +17 −0 d3status/consts.py
  7. 0 d3status/cron/__init__.py
  8. +103 −0 d3status/cron/d3_server_status.py
  9. +59 −0 d3status/d3status_apns_dev.pem
  10. +28 −0 d3status/db/__init__.py
  11. +59 −0 d3status/db/status.py
  12. +32 −0 d3status/db/subscribers.py
  13. +51 −0 d3status/exceptions.py
  14. +125 −0 d3status/handler.py
  15. 0 d3status/handlers/__init__.py
  16. +44 −0 d3status/handlers/status.py
  17. +6 −0 d3status/i18n/zh_CN.csv
  18. +6 −0 d3status/i18n/zh_TW.csv
  19. 0 d3status/libs/__init__.py
  20. +48 −0 d3status/libs/apnswrapper.py
  21. +86 −0 d3status/libs/importlib.py
  22. +32 −0 d3status/libs/loader.py
  23. +49 −0 d3status/libs/options.py
  24. +18 −0 d3status/libs/utils.py
  25. +147 −0 d3status/mail.py
  26. +55 −0 d3status/settings.py
  27. +2 −0 d3status/settings_local.py
  28. 0 d3status/tasks/__init__.py
  29. +48 −0 d3status/tasks/apns_tasks.py
  30. +17 −0 d3status/tasks/celeryconfig.py
  31. +15 −0 d3status/tasks/email_tasks.py
  32. +66 −0 d3status/tasks/status_tasks.py
  33. +68 −0 d3status/tasks/tasks.py
  34. +5 −0 d3status/templates/errors/400.html
  35. +15 −0 d3status/templates/errors/400_debug.html
  36. +32 −0 d3status/templates/errors/404.html
  37. +11 −0 d3status/templates/errors/404_debug.html
  38. +5 −0 d3status/templates/errors/500.html
  39. +15 −0 d3status/templates/errors/500_debug.html
  40. +7 −0 d3status/templates/errors/500_email.html
  41. +52 −0 d3status/urls.py
  42. +25 −0 schema.sql
1 .gitignore
@@ -0,0 +1 @@
+*.pyc
201 LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
15 README.md
@@ -0,0 +1,15 @@
+# D3Status
+
+Diablo3 server status notification APP, a RESTful API demo powered by Tornado.
+
+## Requirement(Python packages)
+
+ - Tornado
+ - libxml, require libxslt1-dev(libxslt) and libxml2
+ - pyquery
+ - python-dateutil
+ - celery
+ - apns([https://github.com/simonwhitaker/PyAPNs][1])
+
+
+ [1]: https://github.com/simonwhitaker/PyAPNs
0 d3status/__init__.py
No changes.
91 d3status/app.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+
+import os
+import platform
+import sys
+
+if platform.system() == "Linux":
+ os.environ["PYTHON_EGG_CACHE"] = "/tmp/egg"
+_root = os.path.dirname(os.path.abspath(__file__))
+# append tasks directory for celeryconfig.py
+sys.path.append(os.path.join(_root, "tasks"))
+# chdir to current directory
+# workaround for d3status-redis27 server which raise exception(celeryd use os.getcwd())
+# when using supervisor to run app.py
+os.chdir(_root)
+
+from tornado import web
+from tornado.ioloop import IOLoop
+from tornado.httpserver import HTTPServer
+from tornado.options import options
+from tornado.database import Connection
+
+try:
+ import d3status
+except ImportError:
+ import sys
+ sys.path.append(os.path.join(_root, ".."))
+
+from d3status.libs.options import parse_options
+
+
+class Application(web.Application):
+ def __init__(self):
+ from d3status.urls import handlers, ui_modules
+ from d3status.db import Model
+
+ settings = dict(debug=options.debug,
+ template_path=os.path.join(os.path.dirname(__file__),
+ "templates"),
+ static_path=os.path.join(os.path.dirname(__file__),
+ "static"),
+ login_url=options.login_url,
+ xsrf_cookies=options.xsrf_cookies,
+ cookie_secret=options.cookie_secret,
+ ui_modules=ui_modules,
+ #autoescape=None,
+ )
+
+ # d3status db connection
+ self.db = Connection(host=options.mysql["host"] + ":" +
+ options.mysql["port"],
+ database=options.mysql["database"],
+ user=options.mysql["user"],
+ password=options.mysql["password"],
+ )
+
+ Model.setup_dbs({"db": self.db})
+
+ super(Application, self).__init__(handlers, **settings)
+
+ def reverse_api(self, request):
+ """Returns a URL name for a request"""
+ handlers = self._get_host_handlers(request)
+
+ for spec in handlers:
+ match = spec.regex.match(request.path)
+ if match:
+ return spec.name
+
+ return None
+
+
+def main():
+ parse_options()
+
+ http_server = HTTPServer(Application(),
+ xheaders=True)
+ http_server.bind(int(options.port), "127.0.0.1") # listen local only
+ http_server.start(1)
+
+ IOLoop.instance().start()
+
+if __name__ == '__main__':
+ main()
17 d3status/consts.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jul 1, 2012
+#
+
+CATEGORY_AMERICAS = "Americas"
+CATEGORY_EUROPE = "Europe"
+CATEGORY_ASIA = "Asia"
+CATEGORYS = (CATEGORY_AMERICAS, CATEGORY_EUROPE, CATEGORY_ASIA)
+
+SUBSCRIBE_STATUS_ON = "on"
+SUBSCRIBE_STATUS_OFF = "off"
+
+LOCALES = ("en", "zh_CN", "zh_TW")
0 d3status/cron/__init__.py
No changes.
103 d3status/cron/d3_server_status.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jul 2, 2012
+#
+
+import os
+import platform
+import sys
+import logging
+from pyquery import PyQuery as pq
+from lxml import etree
+
+from tornado.httpclient import HTTPRequest, HTTPClient
+from tornado.options import options
+
+_dir = os.path.dirname(os.path.abspath(__file__))
+_root = os.path.join(_dir, "..")
+# append tasks directory for celeryconfig.py
+sys.path.append(os.path.join(_root, "tasks"))
+
+try:
+ # tornado process
+ import d3status
+except ImportError:
+ # celeryd process runtime env
+ if platform.system() == "Linux":
+ os.environ["PYTHON_EGG_CACHE"] = "/tmp/egg"
+ sys.path.append(os.path.join(_root, ".."))
+
+from tornado.options import options
+from tornado.database import Connection
+
+from d3status.libs.options import parse_options
+parse_options()
+
+from d3status.db import Model
+from d3status.db import load_model
+from d3status.mail import send_email
+from d3status.tasks import status_tasks
+
+# db connection
+db = Connection(host=options.mysql["host"] + ":" +
+ options.mysql["port"],
+ database=options.mysql["database"],
+ user=options.mysql["user"],
+ password=options.mysql["password"],
+ )
+
+Model.setup_dbs({"db": db})
+
+
+def update_server_status():
+ url = options.d3_server_status_url
+ req = HTTPRequest(url=url)
+
+ client = HTTPClient()
+ response = client.fetch(req)
+ if response.code == 200:
+ status = _parse_server_status(response.body)
+ changed_status = load_model("status").update_status(status)
+ if changed_status:
+ status_tasks.status_notification_task.delay(changed_status)
+ else:
+ err = "GET_D3_SERVER_STAUTS_ERROR: %s\n%s" (response.code, response)
+ logging.error(err)
+
+ # send email
+ subject = "[%s]Get D3 server status error" % options.sitename
+ body = err
+ if options.send_error_email:
+ send_email(options.email_from, options.admins, subject, body)
+
+
+def _parse_server_status(body):
+ status = {}
+
+ q = pq(etree.fromstring(body))
+ boxes = q(".box") # category box
+ for box in boxes:
+ box_q = pq(etree.fromstring(etree.tostring(box)))
+ category = box_q(".category")[0].text.strip()
+ status[category] = {}
+ servers = box_q(".server")
+ for server in servers:
+ server_q = pq(etree.fromstring(etree.tostring(server)))
+ server_name = server_q(".server-name")[0].text.strip().replace(" ", "")
+ if server_name:
+ status_icon = server_q(".status-icon")[0]
+ class_ = status_icon.get("class")
+ if class_:
+ st = 0
+ if "up" in class_:
+ st = 1
+ status[category][server_name] = st
+
+ return status
+
+
+if __name__ == "__main__":
+ update_server_status()
59 d3status/d3status_apns_dev.pem
@@ -0,0 +1,59 @@
+-----BEGIN CERTIFICATE-----
+MIIFdzCCBF+gAwIBAgIIDGJ7998UXlkwDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
+ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
+aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
+HhcNMTIwNzA1MDMzNzIwWhcNMTMwNzA1MDMzNzIwWjB3MSQwIgYKCZImiZPyLGQB
+AQwUY29tLmppYW5namguZDNzdGF0dXMxQjBABgNVBAMMOUFwcGxlIERldmVsb3Bt
+ZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uamlhbmdqaC5kM3N0YXR1czELMAkG
+A1UEBhMCQ04wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0+ehCIORY
+DZRdezvMI5In1hI3qf+Aw8oJ+Lwc0hYfGys15mYHQJ8INSFDxGBn+PdJbPypbccw
+sPHrR5AKXj59ujHebTUeKLPmxUyVf7B/FVi18veTbgpzuXjldNv6q+3NCS3tszHK
+9E3QJU2P5jFDp6ZS65cSA9bprK6jXzhnAq6Na2j74LFPx9usORAErUBa0L4Lyz1g
+Ak0cEv5ObLL+utlhGOuNKlfa6Ixrr1hbYrur1fwm0DEexJkvX1nv9uibccWBI5ZG
+vHLj70KNnBoV3qLxZWhzq8vtdhhpA0r/r67HjvfbaOYA+MTFN9X01uVcB7MTeXTD
++5GHJZ3T+qQbAgMBAAGjggHlMIIB4TAdBgNVHQ4EFgQU/DeCU4EKRFWd5vIVEt7D
+1pr8S5cwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBSIJxcJqbYYYIvs67r2R1nFUlSj
+tzCCAQ8GA1UdIASCAQYwggECMIH/BgkqhkiG92NkBQEwgfEwgcMGCCsGAQUFBwIC
+MIG2DIGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFydHkg
+YXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVuIGFwcGxpY2FibGUgc3RhbmRh
+cmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBwb2xp
+Y3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ugc3RhdGVtZW50cy4wKQYIKwYB
+BQUHAgEWHWh0dHA6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvME0GA1UdHwRGMEQw
+QqBAoD6GPGh0dHA6Ly9kZXZlbG9wZXIuYXBwbGUuY29tL2NlcnRpZmljYXRpb25h
+dXRob3JpdHkvd3dkcmNhLmNybDALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
+BQUHAwIwEAYKKoZIhvdjZAYDAQQCBQAwDQYJKoZIhvcNAQEFBQADggEBAALmTG7V
+FDi/5tYaMN2PJfHSsdq/iAlDbUnO1H2411himHrOHYV3d6Bhte2ad7IBj74T2fUr
+GpypzTSZwJ6zxVjOg2o050j8XDEkpcVcMQzp43Wwqz7FBWwck/3J8qeZJCs3VRzC
+WSGmNSFLnKuloxh2eKq+BEAh+ImffuklzJuJ2kaL18XEpjisrKNV2kgefwGWUUkD
+bwTHZkvPDMWvlxvMj2aUtsq6xbyjVtwa0pz5DBncb7G15yz4U5GHl6433T+wj5hn
+q3ziRCUV3BIcU89iwS8ufjCmxPlfQKmLK0DqQNj/iU6OajVRitYF05F2B7BvtCEk
+J8QFPPNPrFTRYs8=
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAtPnoQiDkWA2UXXs7zCOSJ9YSN6n/gMPKCfi8HNIWHxsrNeZm
+B0CfCDUhQ8RgZ/j3SWz8qW3HMLDx60eQCl4+fbox3m01Hiiz5sVMlX+wfxVYtfL3
+k24Kc7l45XTb+qvtzQkt7bMxyvRN0CVNj+YxQ6emUuuXEgPW6ayuo184ZwKujWto
+++CxT8fbrDkQBK1AWtC+C8s9YAJNHBL+Tmyy/rrZYRjrjSpX2uiMa69YW2K7q9X8
+JtAxHsSZL19Z7/bom3HFgSOWRrxy4+9CjZwaFd6i8WVoc6vL7XYYaQNK/6+ux473
+22jmAPjExTfV9NblXAezE3l0w/uRhyWd0/qkGwIDAQABAoIBAQCFeKIw0y1VO36m
+ixKI9dr/AkShRQEpKDzDqeyinNavzkvKDshEpQYk+Xo1DonDZLyMLJMTKeF0Kavh
+x/+Vhfg0pbPNxWEdgwtbMTvQLkIvF8E7P5wT4V5YBvwAznTGpTJWu2RiIHyioBcb
+YceYTz8aFHT4RsQ+BxLjd/W8i6d/YB9qiDaQHlAe1UsFD8qDiszZa4XzeHy3IgYo
+JFZ6UCY2jL6i1cOJclqYo9b3mCAJ/04BobdWK+IzA3iiKW2GipiijTxMC+1fLFMw
+h++QGY7/tc7SM46DS+6pyhFX0Ib60lK3a9IOGca9zDhENsv2usU9ZmzdLIrjC52g
+yGInyv7RAoGBANt/ouNJ462gruWp63am4klOHcVeNGIMbtOCJnVeVq9qaNt3eYuF
+IK5RBYH3Fb8/BL5axovwHLGLwzTjWa0m3dGPzbIZc1DHuf2XU/8rTY/mQhbuKFgX
+Qoxj8YAe9R3KWHJPvCZHoqOv++xRmL+l/ShUUGxgpm5t3D+dsWNF8az1AoGBANMS
+UayPH01yf5CsxXM2zcUlH97OCH87ujebM/3hcXXPZGKyOtiSpazT6HzaEGvV+vzb
+Lu/JBpvU3RS8NjwWVKMscq/bMWcXVoLq0SN05u9SITMgiWHj9MzOa9f+9mxIQ3N1
+G97XDRxinq8zS50bHFNtlF74GEVFd7GFmBojsmLPAoGAd7VOgqLjlufROtPG5Pjy
+5IPD3MYZz3d0YcnTa6M9p4FjGn44PY0nW6o4VL8KjzixP5eGPP+AxpuwpIFxgOt3
+gjjpN76Fk4K5vsHvP3TAYkBzvsm4GwLkemhvZy57A/o87mrp8/6RhrANtr5xjePb
+A0moatLzMbqcqd04xyl4OpkCgYEAojeyDazxodQdtlMSbTnxa1Lc65/tZ9u/gn0F
+uFlLmf+KZ1ATad9K9UjnpQzzEe2iuDK8IA2fxqQSRZ1hEU0YP1Ap1H3huhl1o6hU
+k6uE0OmOGn0nGNTZj44V1CtfuFjRfirDAMDGkso4qu4Bbv0nB/dv0I1cGeEJ3KWQ
+AIbu0oECgYEAtHFq9F+HwqfBcAhC8J48UGIDxWmvS/ZJeAPDKZoJMlZ2B++k883T
+e/b95T5cdenYkcBFFE0aouDicw20+3zEtx0DxT2vIqhQ/SMj2aoP1k8DGDVBxn+h
+GxMvIBE7wPHfdnmCaqXW56yM9t1aAbWPX/21XqDSKgrVQFhRkjLfAoQ=
+-----END RSA PRIVATE KEY-----
28 d3status/db/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+from d3status.libs.loader import load
+
+load_model = load("d3status.db", "Model")
+
+
+class Model(object):
+ _dbs = {}
+
+ @classmethod
+ def setup_dbs(cls, dbs):
+ cls._dbs = dbs
+
+ @property
+ def dbs(self):
+ return self.dbs
+
+ # legacy support
+ @property
+ def db(self):
+ return self._dbs.get("db", None)
59 d3status/db/status.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+from d3status.db import Model
+
+
+class StatusModel(Model):
+ def get_status(self):
+ status = {"status": {"items": []}}
+ items = status["status"]["items"]
+ categorys = {}
+
+ rows = self.db.query("select * from status")
+ for row in rows:
+ categorys.setdefault(row.category, {})[row.service] = row.status
+
+ for category, services in categorys.iteritems():
+ items.append({"category": category,
+ "services": services})
+
+ if rows:
+ status["count"] = len(items)
+ return status
+ else:
+ return {}
+
+ def update_status(self, status):
+ changed_status = {}
+
+ old_status = self.get_status()
+ old_status_ = {}
+ if old_status:
+ for item in old_status["status"]["items"]:
+ old_status_[item["category"]] = item["services"]
+
+ for category, services in status.iteritems():
+ for name, st in services.iteritems():
+ old_st = old_status_[category].get(name, None)
+ if old_st is not None and old_st != st:
+ changed_status.setdefault(category, {})[name] = st
+ self._update_status(category, name, st)
+
+ return changed_status
+
+ def _update_status(self, category, server_name, status):
+ row = self.db.get("select * from status where category=%s and service=%s",
+ category, server_name)
+ if not row:
+ self.db.execute("insert into status (category, service, status) "
+ "values (%s, %s, %s)",
+ category, server_name, status)
+ else:
+ self.db.execute("update status set status=%s where id=%s",
+ status, row.id)
32 d3status/db/subscribers.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+from d3status.db import Model
+from d3status import consts
+
+
+class SubscribersModel(Model):
+ def subscribe(self, token, categorys, locale="en"):
+ row = self.db.get("select * from subscribers where token=%s", token)
+ if not row:
+ sql = "insert into subscribers (token, categorys, status, locale) " \
+ "values (%s, %s, %s, %s)"
+ self.db.execute(sql, token, categorys, consts.SUBSCRIBE_STATUS_ON,
+ locale)
+ else:
+ sql = "update subscribers set categorys=%s, locale=%s where token=%s"
+ self.db.execute(sql, categorys, locale, token)
+
+ def unsubscribe(self, token):
+ sql = "update subscribers set status=%s where token=%s"
+ self.db.execute(sql, consts.SUBSCRIBE_STATUS_OFF, token)
+
+ def get_subscribers(self, limit=200, offset=0):
+ return self.db.query("select * from subscribers where status='on' "
+ "limit %s offset %s",
+ limit, offset)
51 d3status/exceptions.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+from tornado import escape
+from tornado.web import HTTPError
+
+
+class HTTPAPIError(HTTPError):
+ """API error handling exception
+
+ API server always returns formatted JSON to client even there is
+ an internal server error.
+ """
+ def __init__(self, status_code=400, error_detail="", error_type="",
+ notification="", response="", log_message=None, *args):
+
+ super(HTTPAPIError, self).__init__(int(status_code), log_message, *args)
+
+ self.error_type = error_type if error_type else \
+ _error_types.get(self.status_code, "unknow_error")
+ self.error_detail = error_detail
+ self.notification = {"message": notification} if notification else {}
+ self.response = response if response else {}
+
+ def __str__(self):
+ err = {"meta": {"code": self.status_code, "errorType": self.error_type}}
+ self._set_err(err, ["notification", "response"])
+
+ if self.error_detail:
+ err["meta"]["errorDetail"] = self.error_detail
+
+ return escape.json_encode(err)
+
+ def _set_err(self, err, names):
+ for name in names:
+ v = getattr(self, name)
+ if v:
+ err[name] = v
+
+
+_error_types = {400: "param_error",
+ 401: "invalid_auth",
+ 403: "not_authorized",
+ 404: "endpoint_error",
+ 405: "method_not_allowed",
+ 500: "server_error"}
125 d3status/handler.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+import traceback
+import logging
+
+from tornado import escape
+from tornado.options import options
+from tornado.web import RequestHandler as BaseRequestHandler, HTTPError
+from d3status import exceptions
+from d3status.tasks import email_tasks
+
+
+class BaseHandler(BaseRequestHandler):
+ def get(self, *args, **kwargs):
+ # enable GET request when enable delegate get to post
+ if options.app_get_to_post:
+ self.post(*args, **kwargs)
+ else:
+ raise exceptions.HTTPAPIError(405)
+
+ def prepare(self):
+ self.traffic_control()
+ pass
+
+ def traffic_control(self):
+ # traffic control hooks for api call etc
+ self.log_apicall()
+ pass
+
+ def log_apicall(self):
+ pass
+
+
+class RequestHandler(BaseHandler):
+ pass
+
+
+class APIHandler(BaseHandler):
+ def get_current_user(self):
+ pass
+
+ def finish(self, chunk=None, notification=None):
+ if chunk is None:
+ chunk = {}
+
+ if isinstance(chunk, dict):
+ chunk = {"meta": {"code": 200}, "response": chunk}
+
+ if notification:
+ chunk["notification"] = {"message": notification}
+
+ callback = escape.utf8(self.get_argument("callback", None))
+ if callback:
+ self.set_header("Content-Type", "application/x-javascript")
+
+ if isinstance(chunk, dict):
+ chunk = escape.json_encode(chunk)
+
+ self._write_buffer = [callback, "(", chunk, ")"] if chunk else []
+ super(APIHandler, self).finish()
+ else:
+ self.set_header("Content-Type", "application/json; charset=UTF-8")
+ super(APIHandler, self).finish(chunk)
+
+ def write_error(self, status_code, **kwargs):
+ """Override to implement custom error pages."""
+ debug = self.settings.get("debug", False)
+ try:
+ exc_info = kwargs.pop('exc_info')
+ e = exc_info[1]
+
+ if isinstance(e, exceptions.HTTPAPIError):
+ pass
+ elif isinstance(e, HTTPError):
+ e = exceptions.HTTPAPIError(e.status_code)
+ else:
+ e = exceptions.HTTPAPIError(500)
+
+ exception = "".join([ln for ln in traceback.format_exception(*exc_info)])
+
+ if status_code == 500 and not debug:
+ self._send_error_email(exception)
+
+ if debug:
+ e.response["exception"] = exception
+
+ self.clear()
+ self.set_status(200) # always return 200 OK for API errors
+ self.set_header("Content-Type", "application/json; charset=UTF-8")
+ self.finish(str(e))
+ except Exception:
+ logging.error(traceback.format_exc())
+ return super(APIHandler, self).write_error(status_code, **kwargs)
+
+ def _send_error_email(self, exception):
+ try:
+ # send email
+ subject = "[%s]Internal Server Error" % options.sitename
+ body = self.render_string("errors/500_email.html",
+ exception=exception)
+ if options.send_error_email:
+ email_tasks.send_email_task.delay(options.email_from,
+ options.admins, subject, body)
+ except Exception:
+ logging.error(traceback.format_exc())
+
+
+class ErrorHandler(RequestHandler):
+ """Default 404: Not Found handler."""
+ def prepare(self):
+ super(ErrorHandler, self).prepare()
+ raise HTTPError(404)
+
+
+class APIErrorHandler(APIHandler):
+ """Default API 404: Not Found handler."""
+ def prepare(self):
+ super(APIErrorHandler, self).prepare()
+ raise exceptions.HTTPAPIError(404)
0 d3status/handlers/__init__.py
No changes.
44 d3status/handlers/status.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+from d3status.handler import APIHandler
+from d3status.db import load_model
+from d3status import consts
+from d3status.tasks import status_tasks
+
+
+class StatusIndexHandler(APIHandler):
+ def get(self):
+ self.finish(load_model('status').get_status())
+
+
+class StatusSubscribeHandler(APIHandler):
+ def post(self):
+ token = self.get_argument("deviceToken", "")
+ categorys = self.get_argument("categorys", "").split(",")
+ categorys = [c for c in categorys if c in consts.CATEGORYS]
+ categorys = ",".join(categorys)
+ locale = self.get_argument("locale", "en")
+ if locale not in consts.LOCALES:
+ locale = "en"
+
+ if token:
+ load_model("subscribers").subscribe(token, categorys, locale)
+
+
+class StatusUnsubscribeHandler(APIHandler):
+ def post(self):
+ token = self.get_argument("deviceToken", "")
+ if token:
+ load_model("subscribers").unsubscribe(token)
+
+
+handlers = [(r"/status", StatusIndexHandler),
+ (r"/status/subscribe", StatusSubscribeHandler),
+ (r"/status/unsubscribe", StatusUnsubscribeHandler),
+ ]
6 d3status/i18n/zh_CN.csv
@@ -0,0 +1,6 @@
+"Americas", "美服"
+"Europe", "欧服"
+"Asia", "台服"
+"Diablo3 %s server status has changed to %s","暗黑破坏神%s%s"
+"Available", "恢复正常"
+"Unavailable", "现在维护"
6 d3status/i18n/zh_TW.csv
@@ -0,0 +1,6 @@
+"Americas", "美服"
+"Europe", "歐服"
+"Asia", "台服"
+"Diablo3 %s server status has changed to %s","暗黑破壞神%s%s"
+"Available", "恢復正常"
+"Unavailable", "現在維護"
0 d3status/libs/__init__.py
No changes.
48 d3status/libs/apnswrapper.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+import time
+import traceback
+import logging
+from apns import APNs, Payload
+
+_ignored_content_keys = ("message",) # maybe more keys later
+
+
+class APNsWrapper(APNs):
+ def __init__(self, use_sandbox=False, cert_file=None, key_file=None):
+ super(APNsWrapper, self).__init__(use_sandbox, cert_file, key_file)
+ self._payloads = []
+
+ def append(self, token, notification, alert=None, badge=None, sound=None):
+ if not alert:
+ alert = notification.get("message", None)
+
+ if alert and isinstance(alert, dict):
+ alert = alert.get("message", None)
+
+ for key in _ignored_content_keys:
+ try:
+ del notification[key]
+ except KeyError:
+ pass
+
+ payload = Payload(alert, badge, sound, custom=notification)
+ self._payloads.append((token, payload))
+
+ def flush(self):
+ if self._payloads:
+ for token, payload in self._payloads:
+ try:
+ self.gateway_server.write(self.gateway_server._get_notification(token, payload))
+ except:
+ logging.error(traceback.format_exc())
+ # trigger reconnect
+ self._gateway_connection = None
+
+ self._payloads = []
86 d3status/libs/importlib.py
@@ -0,0 +1,86 @@
+# License for code in this file that was taken from Python 2.7.
+
+# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+# --------------------------------------------
+#
+# 1. This LICENSE AGREEMENT is between the Python Software Foundation
+# ("PSF"), and the Individual or Organization ("Licensee") accessing and
+# otherwise using this software ("Python") in source or binary form and
+# its associated documentation.
+#
+# 2. Subject to the terms and conditions of this License Agreement, PSF
+# hereby grants Licensee a nonexclusive, royalty-free, world-wide
+# license to reproduce, analyze, test, perform and/or display publicly,
+# prepare derivative works, distribute, and otherwise use Python
+# alone or in any derivative version, provided, however, that PSF's
+# License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
+# 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation;
+# All Rights Reserved" are retained in Python alone or in any derivative
+# version prepared by Licensee.
+#
+# 3. In the event Licensee prepares a derivative work that is based on
+# or incorporates Python or any part thereof, and wants to make
+# the derivative work available to others as provided herein, then
+# Licensee hereby agrees to include in any such work a brief summary of
+# the changes made to Python.
+#
+# 4. PSF is making Python available to Licensee on an "AS IS"
+# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+# INFRINGE ANY THIRD PARTY RIGHTS.
+#
+# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+#
+# 6. This License Agreement will automatically terminate upon a material
+# breach of its terms and conditions.
+#
+# 7. Nothing in this License Agreement shall be deemed to create any
+# relationship of agency, partnership, or joint venture between PSF and
+# Licensee. This License Agreement does not grant permission to use PSF
+# trademarks or trade name in a trademark sense to endorse or promote
+# products or services of Licensee, or any third party.
+#
+# 8. By copying, installing or otherwise using Python, Licensee
+# agrees to be bound by the terms and conditions of this License
+# Agreement.
+import sys
+
+
+def _resolve_name(name, package, level):
+ """Return the absolute name of the module to be imported."""
+ if not hasattr(package, 'rindex'):
+ raise ValueError("'package' not set to a string")
+ dot = len(package)
+ for x in xrange(level, 1, -1):
+ try:
+ dot = package.rindex('.', 0, dot)
+ except ValueError:
+ raise ValueError("attempted relative import beyond top-level "
+ "package")
+ return "%s.%s" % (package[:dot], name)
+
+
+def import_module(name, package=None):
+ """Import a module.
+
+ The 'package' argument is required when performing a relative import. It
+ specifies the package to use as the anchor point from which to resolve the
+ relative import to an absolute import.
+
+ """
+ if name.startswith('.'):
+ if not package:
+ raise TypeError("relative imports require the 'package' argument")
+ level = 0
+ for character in name:
+ if character != '.':
+ break
+ level += 1
+ name = _resolve_name(name[level:], package, level)
+ __import__(name)
+ return sys.modules[name]
32 d3status/libs/loader.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+import importlib
+
+_module_instances = {}
+
+
+def load(root_module, suffix):
+ def load_(name):
+ name = name.lower()
+ key = "%s.%s" % (root_module, name)
+ if key not in _module_instances:
+ try:
+ module = importlib.import_module(".%s" % name, root_module)
+ except ImportError:
+ module = importlib.import_module(".%s" % name[:-1], root_module)
+
+ # load("breeze.db", "users", "Model") will return UsersModel class obj
+ cls = getattr(module,
+ "%s%s%s%s" % (name[0].upper(), name[1:],
+ suffix[0].upper(), suffix[1:]))
+ _module_instances[key] = cls()
+
+ return _module_instances[key]
+
+ return load_
49 d3status/libs/options.py
@@ -0,0 +1,49 @@
+## -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+import logging
+import os
+
+from tornado.options import parse_command_line, options, define
+
+
+def parse_config_file(path):
+ """Rewrite tornado default parse_config_file.
+
+ Parses and loads the Python config file at the given path.
+
+ This version allow customize new options which are not defined before
+ from a configuration file.
+ """
+ config = {}
+ execfile(path, config, config)
+ for name in config:
+ if name in options:
+ options[name].set(config[name])
+ else:
+ define(name, config[name])
+
+
+def parse_options():
+ _root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
+ _settings = os.path.join(_root, "settings.py")
+ _settings_local = os.path.join(_root, "settings_local.py")
+
+ try:
+ parse_config_file(_settings)
+ logging.info("Using settings.py as default settings.")
+ except Exception, e:
+ logging.error("No any default settings, are you sure? Exception: %s" % e)
+
+ try:
+ parse_config_file(_settings_local)
+ logging.info("Override some settings with local settings.")
+ except Exception, e:
+ logging.error("No local settings. Exception: %s" % e)
+
+ parse_command_line()
18 d3status/libs/utils.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+import os
+import mimetypes
+
+
+def find_modules(modules_dir):
+ try:
+ return [f[:-3] for f in os.listdir(modules_dir)
+ if not f.startswith('_') and f.endswith('.py')]
+ except OSError:
+ return []
147 d3status/mail.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+import re
+import logging
+import smtplib
+import time
+from datetime import datetime, timedelta
+from email import encoders
+from email.mime.multipart import MIMEMultipart
+from email.mime.base import MIMEBase
+from email.mime.text import MIMEText
+from email.utils import COMMASPACE
+from email.utils import formatdate
+
+from tornado.escape import utf8
+from tornado.options import options
+
+__all__ = ("send_email", "EmailAddress")
+
+# borrow email re pattern from django
+_email_re = re.compile(
+ r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
+ r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
+ r')@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain
+
+
+def send_email(fr, to, subject, body, html=None, attachments=[]):
+ """Send an email.
+
+ If an HTML string is given, a mulitpart message will be generated with
+ plain text and HTML parts. Attachments can be added by providing as a
+ list of (filename, data) tuples.
+ """
+ # convert EmailAddress to pure string
+ if isinstance(fr, EmailAddress):
+ fr = str(fr)
+ else:
+ fr = utf8(fr)
+ to = [utf8(t) for t in to]
+
+ if html:
+ # Multipart HTML and plain text
+ message = MIMEMultipart("alternative")
+ message.attach(MIMEText(body, "plain"))
+ message.attach(MIMEText(html, "html"))
+ else:
+ # Plain text
+ message = MIMEText(body)
+ if attachments:
+ part = message
+ message = MIMEMultipart("mixed")
+ message.attach(part)
+ for filename, data in attachments:
+ part = MIMEBase("application", "octet-stream")
+ part.set_payload(data)
+ encoders.encode_base64(part)
+ part.add_header("Content-Disposition", "attachment",
+ filename=filename)
+ message.attach(part)
+
+ message["Date"] = formatdate(time.time())
+ message["From"] = fr
+ message["To"] = COMMASPACE.join(to)
+ message["Subject"] = utf8(subject)
+
+ _get_session().send_mail(fr, to, utf8(message.as_string()))
+
+
+class EmailAddress(object):
+ def __init__(self, addr, name=""):
+ assert _email_re.match(addr), "Email address(%s) is invalid." % addr
+
+ self.addr = addr
+ if name:
+ self.name = name
+ else:
+ self.name = addr.split("@")[0]
+
+ def __str__(self):
+ return '%s <%s>' % (utf8(self.name), utf8(self.addr))
+
+
+class _SMTPSession(object):
+ def __init__(self, host, user='', password='', duration=30, tls=False):
+ self.host = host
+ self.user = user
+ self.password = password
+ self.duration = duration
+ self.tls = tls
+ self.session = None
+ self.deadline = datetime.now()
+ self.renew()
+
+ def send_mail(self, fr, to, message):
+ if self.timeout:
+ self.renew()
+
+ try:
+ self.session.sendmail(fr, to, message)
+ except Exception, e:
+ err = "Send email from %s to %s failed!\n Exception: %s!" \
+ % (fr, to, e)
+ logging.error(err)
+ self.renew()
+
+ @property
+ def timeout(self):
+ if datetime.now() < self.deadline:
+ return False
+ else:
+ return True
+
+ def renew(self):
+ try:
+ if self.session:
+ self.session.quit()
+ except Exception:
+ pass
+
+ self.session = smtplib.SMTP(self.host)
+ if self.user and self.password:
+ if self.tls:
+ self.session.starttls()
+
+ self.session.login(self.user, self.password)
+
+ self.deadline = datetime.now() + timedelta(seconds=self.duration * 60)
+
+
+def _get_session():
+ global _session
+ if _session is None:
+ _session = _SMTPSession(options.smtp['host'],
+ options.smtp['user'],
+ options.smtp['password'],
+ options.smtp['duration'],
+ options.smtp['tls'])
+
+ return _session
+
+_session = None
55 d3status/settings.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+"""Project settings"""
+
+import platform
+import os
+
+# can't use __file__ directly here because it's parsed by tornado.options
+import d3status
+root_dir = os.path.dirname(os.path.abspath(d3status.__file__))
+
+if platform.node() == "FELINX": # FELINX is the hosting server name.
+ debug = False
+else:
+ debug = True
+
+loglevel = "INFO" # for celeryd
+port = 8888
+
+d3_server_status_url = "http://us.battle.net/d3/en/status"
+
+sitename = "D3 Status"
+domain = "api.feilong.me"
+home_url = "http://%s/d3" % domain
+login_url = "http://%s/login" % home_url
+app_url_prefix = "/d3/v1"
+email_from = "%s <noreply@%s>" % (sitename, domain)
+admins = ("Felinx <felinx.lee@gmail.com>",)
+send_error_email = True
+cookie_secret = "d1d87395-8272-4749-b2f2-dcabd3903a1c"
+xsrf_cookies = False
+
+# Apple push notification settings
+apns_sandbox = debug
+apns_certificate = "d3status_apns_dev.pem"
+apns_certificate_key = None
+
+mysql = {"host": "localhost",
+ "port": "3306",
+ "database": "d3status",
+ "user": "felinx",
+ "password": "felinx"
+ }
+
+smtp = {"host": "localhost",
+ "user": "",
+ "password": "",
+ "duration": 30,
+ "tls": False
+ }
2 d3status/settings_local.py
@@ -0,0 +1,2 @@
+debug = True
+apns_sandbox = True
0 d3status/tasks/__init__.py
No changes.
48 d3status/tasks/apns_tasks.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on May 30, 2012
+#
+
+import os
+
+from celery.task import task
+from tornado.options import options
+
+from d3status.libs.apnswrapper import APNsWrapper
+
+_root = os.path.join(os.path.dirname(__file__), "..")
+_apns = None
+
+
+@task
+def apns_push_task(tokens, notification, alert=None, badge=None, sound=None):
+ apns_push(tokens, notification, alert, badge, sound)
+
+
+def apns_push(tokens, notification, alert=None, badge=None, sound=None):
+ _setup_apns()
+ if isinstance(tokens, basestring):
+ tokens = [tokens, ]
+
+ for token in tokens:
+ _apns.append(token, notification, alert, badge, sound)
+
+ _apns.flush()
+
+
+def _setup_apns():
+ global _apns
+
+ if not _apns:
+ cert_file = os.path.join(_root, options.apns_certificate)
+ if not options.apns_certificate_key:
+ key_file = None
+ else:
+ key_file = os.path.join(_root, options.apns_certificate_key)
+
+ _apns = APNsWrapper(use_sandbox=options.apns_sandbox,
+ cert_file=cert_file,
+ key_file=key_file)
17 d3status/tasks/celeryconfig.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+
+
+CELERY_IMPORTS = ("tasks", )
+
+CELERY_RESULT_BACKEND = "redis"
+CELERY_REDIS_HOST = "localhost"
+CELERY_REDIS_PORT = 6379
+CELERY_REDIS_DB = 0
+
+BROKER_URL = "redis://%s:%s/%s" % (CELERY_REDIS_HOST, CELERY_REDIS_PORT,
+ CELERY_REDIS_DB)
15 d3status/tasks/email_tasks.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+from celery.task import task
+from d3status.mail import send_email
+
+
+@task
+def send_email_task(fr, to, subject, body, html=None, attachments=[]):
+ send_email(fr, to, subject, body, html, attachments)
66 d3status/tasks/status_tasks.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jul 2, 2012
+#
+
+import os
+import tornado.locale
+
+from celery.task import task
+from tornado.options import options
+from d3status.db import load_model
+from d3status.tasks import apns_tasks
+
+
+@task
+def status_notification_task(changed_status):
+ status_notifciation(changed_status)
+
+
+def status_notifciation(changed_status):
+ notifications = {}
+ for category, services in changed_status.iteritems():
+ for name, st in services.iteritems():
+ # just push notification about game server now
+ if name == "GameServer":
+ notifications[category] = st
+
+ for category, st in notifications.iteritems():
+ status = "Available" if st else "Unavailable"
+
+ offset = 0
+ limit = 200
+ while True:
+ subscribers = load_model("subscribers").get_subscribers(limit, offset)
+ if not subscribers:
+ break
+
+ for subscribe in subscribers:
+ if category in subscribe.categorys:
+ alert = _trans_alert("Diablo3 %s server status has changed to %s",
+ category, status, subscribe.locale)
+ apns_tasks.apns_push_task.delay(subscribe.token, {},
+ alert=alert, badge=1,
+ sound="default")
+ offset += len(subscribers)
+
+
+def _trans(s, locale):
+ locale = tornado.locale.get(locale)
+ s = locale.translate(s).strip("\"")
+
+ return s
+
+
+def _trans_alert(alert, category, status, locale):
+ def _(s):
+ return _trans(s, locale)
+
+ return _(alert) % (_(category), _(status))
+
+
+_i18n_dir = os.path.join(os.path.join(os.path.dirname(__file__), ".."), 'i18n')
+tornado.locale.load_translations(_i18n_dir)
68 d3status/tasks/tasks.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+"""Celery tasks center
+
+Setup env for celery tasks and import them.
+"""
+
+import os
+import platform
+import sys
+
+_dir = os.path.dirname(os.path.abspath(__file__))
+_root = os.path.join(_dir, "..")
+
+try:
+ # tornado process
+ import d3status
+except ImportError:
+ # celeryd process runtime env
+ if platform.system() == "Linux":
+ os.environ["PYTHON_EGG_CACHE"] = "/tmp/egg"
+ sys.path.append(os.path.join(_root, ".."))
+ # append current directory for celeryconfig.py
+ sys.path.append(_dir)
+
+ from tornado.options import options
+ from tornado.database import Connection
+
+ from d3status.libs.options import parse_options
+ parse_options()
+
+ from d3status.db import Model
+
+ # db connection
+ db = Connection(host=options.mysql["host"] + ":" +
+ options.mysql["port"],
+ database=options.mysql["database"],
+ user=options.mysql["user"],
+ password=options.mysql["password"],
+ )
+
+ Model.setup_dbs({"db": db})
+
+
+from d3status.libs.importlib import import_module
+from d3status.libs.utils import find_modules
+
+
+def _load_tasks():
+ _current_module = sys.modules[__name__]
+ for m in find_modules(os.path.dirname(__file__)):
+ if m.endswith("_tasks"): # xxx_tasks.py
+ try:
+ mod = import_module("." + m, package="d3status.tasks")
+ for func in dir(mod):
+ if func.endswith("_task"):
+ setattr(_current_module, func, getattr(mod, func))
+ except ImportError:
+ pass
+
+_load_tasks()
+
5 d3status/templates/errors/400.html
@@ -0,0 +1,5 @@
+{% extends ../base.html %}
+
+{% block body %}
+ 400 Bad Request
+{% end %}
15 d3status/templates/errors/400_debug.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en-us" xml:lang="en-us">
+ <head>
+ <title>400 Bad Request</title>
+ </head>
+ <body>
+ 400 Bad Request
+ <br/><br/>
+ URL:{{request.full_url()}}
+ <br/><br/>
+ Exeception:
+ <br/>
+ {% raw str(exception).replace("\n", "<br/>") %}
+ </body>
+</html>
32 d3status/templates/errors/404.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Page Not Found :(</title>
+ <style>
+ ::-moz-selection { background: #b3d4fc; text-shadow: none; }
+ ::selection { background: #b3d4fc; text-shadow: none; }
+ html { padding: 30px 10px; font-size: 20px; line-height: 1.4; color: #737373; background: #f0f0f0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ html, input { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
+ body { max-width: 500px; _width: 500px; padding: 30px 20px 50px; border: 1px solid #b3b3b3; border-radius: 4px; margin: 0 auto; box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff; background: #fcfcfc; }
+ h1 { margin: 0 10px; font-size: 50px; text-align: center; }
+ h1 span { color: #bbb; }
+ h3 { margin: 1.5em 0 0.5em; }
+ p { margin: 1em 0; }
+ ul { padding: 0 0 0 40px; margin: 1em 0; }
+ .container { max-width: 380px; _width: 380px; margin: 0 auto; }
+ /* google search */
+ #goog-fixurl ul { list-style: none; padding: 0; margin: 0; }
+ #goog-fixurl form { margin: 0; }
+ #goog-wm-qt, #goog-wm-sb { border: 1px solid #bbb; font-size: 16px; line-height: normal; vertical-align: top; color: #444; border-radius: 2px; }
+ #goog-wm-qt { width: 220px; height: 20px; padding: 5px; margin: 5px 10px 0 0; box-shadow: inset 0 1px 1px #ccc; }
+ #goog-wm-sb { display: inline-block; height: 32px; padding: 0 10px; margin: 5px 0 0; white-space: nowrap; cursor: pointer; background-color: #f5f5f5; background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1); background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1); background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1); background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1); -webkit-appearance: none; -moz-appearance: none; appearance: none; *overflow: visible; *display: inline; *zoom: 1; }
+ #goog-wm-sb:hover, #goog-wm-sb:focus { border-color: #aaa; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); background-color: #f8f8f8; }
+ #goog-wm-qt:hover, #goog-wm-qt:focus { border-color: #105cb6; outline: 0; color: #222; }
+ input::-moz-focus-inner { padding: 0; border: 0; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>Not found <span>:(</span></h1>
+ </div>
11 d3status/templates/errors/404_debug.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en-us" xml:lang="en-us">
+ <head>
+ <title>404 Page Not Find</title>
+ </head>
+ <body>
+ 404 Page Not Find
+ <br/><br/>
+ URL:{{request.full_url()}}
+ </body>
+</html>
5 d3status/templates/errors/500.html
@@ -0,0 +1,5 @@
+{% extends ../base.html %}
+
+{% block body %}
+ 500 Server Error
+{% end %}
15 d3status/templates/errors/500_debug.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en-us" xml:lang="en-us">
+ <head>
+ <title>500 Internal Server Error</title>
+ </head>
+ <body>
+ 500 Internal Server Error
+ <br/><br/>
+ URL:{{request.full_url()}}
+ <br/><br/>
+ Exeception:
+ <br/>
+ {% raw str(exception).replace("\n", "<br/>") %}
+ </body>
+</html>
7 d3status/templates/errors/500_email.html
@@ -0,0 +1,7 @@
+500 Internal Server Error
+
+URL:
+{{request.full_url()}}
+
+Exeception:
+{% raw str(exception) %}
52 d3status/urls.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012 feilong.me. All rights reserved.
+#
+# @author: Felinx Lee <felinx.lee@gmail.com>
+# Created on Jun 30, 2012
+#
+
+try:
+ import importlib
+except:
+ from d3status.libs import importlib
+
+from tornado.options import options
+from tornado.web import url
+from d3status.handler import APIErrorHandler
+
+handlers = []
+ui_modules = {}
+
+# the module names in handlers folder
+handler_names = ["status", ]
+
+
+def _generate_handler_patterns(root_module, handler_names, prefix=options.app_url_prefix):
+ for name in handler_names:
+ module = importlib.import_module(".%s" % name, root_module)
+ module_hanlders = getattr(module, "handlers", None)
+ if module_hanlders:
+ _handlers = []
+ for handler in module_hanlders:
+ try:
+ patten = r"%s%s" % (prefix, handler[0])
+ if len(handler) == 2:
+ _handlers.append((patten,
+ handler[1]))
+ elif len(handler) == 3:
+ _handlers.append(url(patten,
+ handler[1],
+ name=handler[2])
+ )
+ else:
+ pass
+ except IndexError:
+ pass
+
+ handlers.extend(_handlers)
+
+_generate_handler_patterns("d3status.handlers", handler_names)
+
+# Override Tornado default ErrorHandler
+handlers.append((r".*", APIErrorHandler))
25 schema.sql
@@ -0,0 +1,25 @@
+
+create database if not exists `d3status`;
+
+USE `d3status`;
+
+DROP TABLE IF EXISTS `status`;
+CREATE TABLE `status` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `category` varchar(45) NOT NULL,
+ `service` varchar(45) NOT NULL,
+ `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1 - up, 0 -down',
+ `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `subscribers`;
+CREATE TABLE `subscribers` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `token` varchar(100) NOT NULL,
+ `categorys` varchar(45) NOT NULL COMMENT '''Americas,Europe,Asia''',
+ `locale` enum('en','zh_CN','zh_TW') NOT NULL DEFAULT 'en',
+ `status` enum('on','off') NOT NULL DEFAULT 'on',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `token_UNIQUE` (`token`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;

0 comments on commit 98aa44a

Please sign in to comment.
Something went wrong with that request. Please try again.