From 8abb94a88e40fde249b562a841a5b0398582717e Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Wed, 21 Jan 2015 18:12:25 -0200 Subject: #224 App is working without klein - migration to twisted "complete" --- service/pixelated/resources/__init__.py | 33 ++++++ .../pixelated/resources/attachments_resource.py | 63 ++++++++++ service/pixelated/resources/contacts_resource.py | 35 ++++++ service/pixelated/resources/features_resource.py | 32 ++++++ service/pixelated/resources/mail_resource.py | 64 +++++++++++ service/pixelated/resources/mails_resource.py | 128 +++++++++++++++++++++ service/pixelated/resources/root_resource.py | 46 ++++++++ service/pixelated/resources/sync_info_resource.py | 46 ++++++++ service/pixelated/resources/tags_resource.py | 38 ++++++ 9 files changed, 485 insertions(+) create mode 100644 service/pixelated/resources/__init__.py create mode 100644 service/pixelated/resources/attachments_resource.py create mode 100644 service/pixelated/resources/contacts_resource.py create mode 100644 service/pixelated/resources/features_resource.py create mode 100644 service/pixelated/resources/mail_resource.py create mode 100644 service/pixelated/resources/mails_resource.py create mode 100644 service/pixelated/resources/root_resource.py create mode 100644 service/pixelated/resources/sync_info_resource.py create mode 100644 service/pixelated/resources/tags_resource.py (limited to 'service/pixelated/resources') diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py new file mode 100644 index 00000000..a2e4c9d4 --- /dev/null +++ b/service/pixelated/resources/__init__.py @@ -0,0 +1,33 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + + +def respond_json(entity, request, status_code=200): + json_response = json.dumps(entity) + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + request.code = status_code + return json_response + + +def respond_json_deferred(entity, request, status_code=200): + json_response = json.dumps(entity) + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + request.code = status_code + request.write(json_response) + request.finish() + + +import json diff --git a/service/pixelated/resources/attachments_resource.py b/service/pixelated/resources/attachments_resource.py new file mode 100644 index 00000000..0ab214b9 --- /dev/null +++ b/service/pixelated/resources/attachments_resource.py @@ -0,0 +1,63 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +import io + +import re +from twisted.protocols.basic import FileSender +from twisted.python.log import err +from twisted.web.resource import Resource + + +class AttachmentResource(Resource): + def __init__(self, attachment_id, querier): + Resource.__init__(self) + self.attachment_id = attachment_id + self.querier = querier + + def render_GET(self, request): + encoding = request.args.get('encoding', [None])[0] + filename = request.args.get('filename', [self.attachment_id])[0] + attachment = self.querier.attachment(self.attachment_id, encoding) + + request.setHeader(b'Content-Type', b'application/force-download') + request.setHeader(b'Content-Disposition', bytes('attachment; filename=' + filename)) + bytes_io = io.BytesIO(attachment['content']) + d = FileSender().beginFileTransfer(bytes_io, request) + + def cb_finished(_): + bytes_io.close() + request.finish() + + d.addErrback(err).addCallback(cb_finished) + + return d + + def _extract_mimetype(self, content_type): + match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type) + return match.group(1) + + +class AttachmentsResource(Resource): + + isLeaf = True + + def __init__(self, querier): + Resource.__init__(self) + self.querier = querier + + def getChild(self, attachment_id, request): + return AttachmentResource(attachment_id, self.querier) diff --git a/service/pixelated/resources/contacts_resource.py b/service/pixelated/resources/contacts_resource.py new file mode 100644 index 00000000..94468a63 --- /dev/null +++ b/service/pixelated/resources/contacts_resource.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from pixelated.resources import respond_json_deferred +from twisted.internet.threads import deferToThread +from twisted.web.resource import Resource + + +class ContactsResource(Resource): + + isLeaf = True + + def __init__(self, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + + def render_GET(self, request): + query = request.args.get('q', [''])[0] + d = deferToThread(lambda: self._search_engine.contacts(query)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + + return d diff --git a/service/pixelated/resources/features_resource.py b/service/pixelated/resources/features_resource.py new file mode 100644 index 00000000..1784e463 --- /dev/null +++ b/service/pixelated/resources/features_resource.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from pixelated.resources import respond_json +import os +from twisted.web.resource import Resource + + +class FeaturesResource(Resource): + DISABLED_FEATURES = ['draftReply', 'encryptionStatus'] + + isLeaf = True + + def render_GET(self, request): + try: + disabled_features = {'logout': os.environ['DISPATCHER_LOGOUT_URL']} + except KeyError: + disabled_features = {} + return respond_json({'disabled_features': self.DISABLED_FEATURES, 'dispatcher_features': disabled_features}, request) diff --git a/service/pixelated/resources/mail_resource.py b/service/pixelated/resources/mail_resource.py new file mode 100644 index 00000000..03873ffb --- /dev/null +++ b/service/pixelated/resources/mail_resource.py @@ -0,0 +1,64 @@ +import json +from pixelated.resources import respond_json +from twisted.web.resource import Resource + + +class MailTags(Resource): + + isLeaf = True + + def __init__(self, mail_id, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + self._mail_id = mail_id + + def render_POST(self, request): + content_dict = json.loads(request.content.read()) + new_tags = map(lambda tag: tag.lower(), content_dict['newtags']) + try: + self._mail_service.update_tags(self._mail_id, new_tags) + mail = self._mail_service.mail(self._mail_id) + self._search_engine.index_mail(mail) + except ValueError as ve: + return respond_json(ve.message, request, 403) + return respond_json(mail.as_dict(), request) + + +class Mail(Resource): + + def __init__(self, mail_id, mail_service, search_engine): + Resource.__init__(self) + self.putChild('tags', MailTags(mail_id, mail_service, search_engine)) + + self._search_engine = search_engine + self._mail_id = mail_id + self._mail_service = mail_service + + def render_GET(self, request): + mail = self._mail_service.mail(self._mail_id) + return respond_json(mail.as_dict(), request) + + def render_DELETE(self, request): + self._delete_mail(self._mail_id) + return respond_json(None, request) + + def _delete_mail(self, mail_id): + mail = self._mail_service.mail(mail_id) + if mail.mailbox_name == 'TRASH': + self._mail_service.delete_permanent(mail_id) + self._search_engine.remove_from_index(mail_id) + else: + trashed_mail = self._mail_service.delete_mail(mail_id) + self._search_engine.index_mail(trashed_mail) + + +class MailResource(Resource): + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._mail_service = mail_service + self._search_engine = search_engine + + def getChild(self, mail_id, request): + return Mail(mail_id, self._mail_service, self._search_engine) diff --git a/service/pixelated/resources/mails_resource.py b/service/pixelated/resources/mails_resource.py new file mode 100644 index 00000000..75c73349 --- /dev/null +++ b/service/pixelated/resources/mails_resource.py @@ -0,0 +1,128 @@ +import json +from pixelated.adapter.model.mail import InputMail +from pixelated.resources import respond_json +from twisted.web.resource import Resource + + +def _format_exception(e): + exception_info = map(str, list(e.args)) + return '\n'.join(exception_info) + + +class MailsUnreadResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + + def render_POST(self, request): + content_dict = json.load(request.content) + idents = content_dict.get('idents') + for ident in idents: + mail = self._mail_service.mark_as_unread(ident) + self._search_engine.index_mail(mail) + return "" + + +class MailsReadResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + + def render_POST(self, request): + content_dict = json.load(request.content) + idents = content_dict.get('idents') + for ident in idents: + mail = self._mail_service.mark_as_read(ident) + self._search_engine.index_mail(mail) + return "" + + +class MailsDeleteResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._mail_service = mail_service + self._search_engine = search_engine + + def render_POST(self, request): + idents = json.loads(request.content.read())['idents'] + for ident in idents: + self._delete_mail(ident) + return respond_json(None, request) + + def _delete_mail(self, mail_id): + mail = self._mail_service.mail(mail_id) + if mail.mailbox_name == 'TRASH': + self._mail_service.delete_permanent(mail_id) + self._search_engine.remove_from_index(mail_id) + else: + trashed_mail = self._mail_service.delete_mail(mail_id) + self._search_engine.index_mail(trashed_mail) + + +class MailsResource(Resource): + + def __init__(self, search_engine, mail_service, draft_service): + Resource.__init__(self) + self.putChild('delete', MailsDeleteResource(mail_service, search_engine)) + self.putChild('read', MailsReadResource(mail_service, search_engine)) + self.putChild('unread', MailsUnreadResource(mail_service, search_engine)) + + self._draft_service = draft_service + self._mail_service = mail_service + self._search_engine = search_engine + + def render_GET(self, request): + mail_ids, total = self._search_engine.search(request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0]) + mails = self._mail_service.mails(mail_ids) + + response = { + "stats": { + "total": total, + }, + "mails": [mail.as_dict() for mail in mails] + } + + return json.dumps(response) + + def render_POST(self, request): + try: + content_dict = json.loads(request.content.read()) + _mail = InputMail.from_dict(content_dict) + draft_id = content_dict.get('ident') + if draft_id: + self._search_engine.remove_from_index(draft_id) + _mail = self._mail_service.send(draft_id, _mail) + self._search_engine.index_mail(_mail) + + return respond_json(_mail.as_dict(), request) + except Exception as error: + return respond_json({'message': _format_exception(error)}, request, status_code=422) + + def render_PUT(self, request): + content_dict = json.loads(request.content.read()) + _mail = InputMail.from_dict(content_dict) + draft_id = content_dict.get('ident') + + if draft_id: + if not self._mail_service.mail_exists(draft_id): + return respond_json("", request, status_code=422) + pixelated_mail = self._draft_service.update_draft(draft_id, _mail) + self._search_engine.remove_from_index(draft_id) + else: + pixelated_mail = self._draft_service.create_draft(_mail) + self._search_engine.index_mail(pixelated_mail) + return respond_json({'ident': pixelated_mail.ident}, request) + + + diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py new file mode 100644 index 00000000..326654eb --- /dev/null +++ b/service/pixelated/resources/root_resource.py @@ -0,0 +1,46 @@ +import os +from pixelated.resources.attachments_resource import AttachmentsResource +from pixelated.resources.contacts_resource import ContactsResource +from pixelated.resources.features_resource import FeaturesResource +from pixelated.resources.mail_resource import MailResource +from pixelated.resources.mails_resource import MailsResource +from pixelated.resources.sync_info_resource import SyncInfoResource +from pixelated.resources.tags_resource import TagsResource +from twisted.web.resource import Resource +from twisted.web.static import File + + +class RootResource(Resource): + + def __init__(self): + Resource.__init__(self) + self._static_folder = self._get_static_folder() + + def getChild(self, path, request): + if path == '': + return self + return Resource.getChild(self, path, request) + + def initialize(self, querier, search_engine, mail_service, draft_service): + self.putChild('assets', File(self._static_folder)) + self.putChild('attachments', AttachmentsResource(querier)) + self.putChild('contacts', ContactsResource(search_engine)) + self.putChild('features', FeaturesResource()) + self.putChild('sync_info', SyncInfoResource()) + self.putChild('tags', TagsResource(search_engine)) + self.putChild('mails', MailsResource(search_engine, mail_service, draft_service)) + self.putChild('mail', MailResource(mail_service, search_engine)) + + def _get_static_folder(self): + + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) + # this is a workaround for packaging + if not os.path.exists(static_folder): + static_folder = os.path.abspath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) + if not os.path.exists(static_folder): + static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + return static_folder + + def render_GET(self, request): + return open(os.path.join(self._static_folder, 'index.html')).read() \ No newline at end of file diff --git a/service/pixelated/resources/sync_info_resource.py b/service/pixelated/resources/sync_info_resource.py new file mode 100644 index 00000000..5aa94218 --- /dev/null +++ b/service/pixelated/resources/sync_info_resource.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . +from pixelated.resources import respond_json +from twisted.web.resource import Resource + + +class SyncInfoResource(Resource): + + isLeaf = True + + def __init__(self): + Resource.__init__(self) + self.current = 0 + self.total = 0 + + def _get_progress(self): + if self.total == 0: + return 0 + return self.current / float(self.total) + + def set_sync_info(self, soledad_sync_status): + self.current, self.total = map(int, soledad_sync_status.content.split('/')) + + def render_GET(self, request): + _sync_info = { + 'is_syncing': self.current != self.total, + 'count': { + 'current': self.current, + 'total': self.total, + 'progress': self._get_progress() + } + } + return respond_json(_sync_info, request) diff --git a/service/pixelated/resources/tags_resource.py b/service/pixelated/resources/tags_resource.py new file mode 100644 index 00000000..8a8ab81f --- /dev/null +++ b/service/pixelated/resources/tags_resource.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2014 ThoughtWorks, Inc. +# +# Pixelated is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pixelated is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Pixelated. If not, see . + +from pixelated.resources import respond_json_deferred +from twisted.internet.threads import deferToThread +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET + + +class TagsResource(Resource): + + isLeaf = True + + def __init__(self, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + + def render_GET(self, request): + query = request.args.get('q', [''])[0] + skip_default_tags = request.args.get('skipDefaultTags', [False])[0] + + d = deferToThread(lambda: self._search_engine.tags(query=query, skip_default_tags=skip_default_tags)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + + return NOT_DONE_YET -- cgit v1.2.3