diff --git a/conftest.py b/conftest.py index f1c11a74..72129fe8 100644 --- a/conftest.py +++ b/conftest.py @@ -20,6 +20,7 @@ def pytest_configure(config): 'sentry_plugins.gitlab', 'sentry_plugins.pagerduty', 'sentry_plugins.pivotal', 'sentry_plugins.pushover', 'sentry_plugins.jira', 'sentry_plugins.segment', 'sentry_plugins.sessionstack', 'sentry_plugins.slack', 'sentry_plugins.victorops', + 'sentry_plugins.itunesconnect' ) # TODO(dcramer): we need a PluginAPITestCase that can do register/unregister @@ -31,6 +32,7 @@ def pytest_configure(config): from sentry_plugins.gitlab.plugin import GitLabPlugin from sentry_plugins.heroku.plugin import HerokuPlugin from sentry_plugins.hipchat_ac.plugin import HipchatPlugin + from sentry_plugins.itunesconnect.plugin import ItunesConnectPlugin from sentry_plugins.jira.plugin import JiraPlugin from sentry_plugins.pagerduty.plugin import PagerDutyPlugin from sentry_plugins.pivotal.plugin import PivotalPlugin @@ -46,6 +48,7 @@ def pytest_configure(config): plugins.register(GitLabPlugin) plugins.register(HerokuPlugin) plugins.register(HipchatPlugin) + plugins.register(ItunesConnectPlugin) plugins.register(JiraPlugin) plugins.register(PagerDutyPlugin) plugins.register(PivotalPlugin) diff --git a/setup.py b/setup.py index 5a7e78f0..e43376cf 100644 --- a/setup.py +++ b/setup.py @@ -105,6 +105,7 @@ def run(self): 'gitlab = sentry_plugins.gitlab', 'heroku = sentry_plugins.heroku', 'hipchat_ac = sentry_plugins.hipchat_ac', + 'itunesconnect = sentry_plugins.itunesconnect', 'jira = sentry_plugins.jira', 'jira_ac = sentry_plugins.jira_ac', 'pagerduty = sentry_plugins.pagerduty', @@ -123,6 +124,7 @@ def run(self): 'gitlab = sentry_plugins.gitlab.plugin:GitLabPlugin', 'heroku = sentry_plugins.heroku.plugin:HerokuPlugin', 'hipchat_ac = sentry_plugins.hipchat_ac.plugin:HipchatPlugin', + 'itunesconnect = sentry_plugins.itunesconnect.plugin:ItunesConnectPlugin', 'jira = sentry_plugins.jira.plugin:JiraPlugin', 'jira_ac = sentry_plugins.jira_ac.plugin:JiraACPlugin', 'pagerduty = sentry_plugins.pagerduty.plugin:PagerDutyPlugin', diff --git a/src/sentry_plugins/itunesconnect/__init__.py b/src/sentry_plugins/itunesconnect/__init__.py new file mode 100644 index 00000000..c3961685 --- /dev/null +++ b/src/sentry_plugins/itunesconnect/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import diff --git a/src/sentry_plugins/itunesconnect/client.py b/src/sentry_plugins/itunesconnect/client.py new file mode 100644 index 00000000..2d709570 --- /dev/null +++ b/src/sentry_plugins/itunesconnect/client.py @@ -0,0 +1,228 @@ +from __future__ import absolute_import + +import re + +from six.moves.urllib.parse import urljoin +from requests.utils import dict_from_cookiejar, add_dict_to_cookiejar +from sentry.http import SafeSession + +BASE_URL = 'https://itunesconnect.apple.com/' +API_BASE = urljoin(BASE_URL, 'WebObjects/iTunesConnect.woa/') +ISK_JS_URL = urljoin(BASE_URL, 'itc/static-resources/controllers/login_cntrl.js') +USER_DETAILS_URL = urljoin(API_BASE, 'ra/user/detail') + +APPLEID_BASE_URL = 'https://idmsa.apple.com/' +LOGIN_URL = urljoin(APPLEID_BASE_URL, 'appleauth/auth/signin') + +_isk_re = re.compile(r'itcServiceKey\s+=\s+["\'](.*)["\']') + + +class ItcError(Exception): + pass + + +class ItunesConnectClient(object): + + def __init__(self): + self._reset() + + def _reset(self): + self._session = SafeSession() + self._service_key = None + self._user_details = None + self._scnt = None + self._session_id = None + self.authenticated = False + + def logout(self): + self._reset() + + def login(self, email=None, password=None): + if email is None or password is None: + return + + if self.authenticated: + # we don't want to login again + return + + self._get_service_key() + + rv = self._session.post(LOGIN_URL, json={ + 'accountName': email, + 'password': password, + 'rememberMe': False, + }, headers={ + 'X-Requested-With': 'XMLHttpRequest', + 'X-Apple-Widget-Key': self._service_key + }) + + self._session_id = rv.headers.get('X-Apple-Id-Session-Id', None) + self._scnt = rv.headers.get('scnt', None) + + # This is necessary because it sets some further cookies + rv = self._session.get(urljoin(API_BASE, 'wa')) + rv.raise_for_status() + + # If we reach this we are authenticated + self.authenticated = True + + def _get_service_key(self): + if self._service_key is not None: + return self._service_key + rv = self._session.get(ISK_JS_URL) + match = _isk_re.search(rv.text) + if match is not None: + self._service_key = match.group(1) + return self._service_key + raise ItcError('Could not find service key') + + def _request_headers(self): + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Apple-Widget-Key': self._service_key, + 'X-Apple-Id-Session-Id': self._session_id, + 'scnt': self._scnt + } + + @classmethod + def from_json(cls, data): + """Creates an Itc object from json.""" + rv = cls() + + val = data.get('service_key') + if val is not None: + rv._service_key = val + + val = data.get('session_id') + if val is not None: + rv._session_id = val + + val = data.get('scnt') + if val is not None: + rv._scnt = val + + val = data.get('authenticated') + if val is not None: + rv.authenticated = val + + val = data.get('cookies') + if val: + add_dict_to_cookiejar(rv._session.cookies, val) + + return rv + + def to_json(self, ensure_user_details=True): + """Converts an ITC into a JSON object for caching.""" + return { + 'service_key': self._service_key, + 'scnt': self._scnt, + 'session_id': self._session_id, + 'authenticated': self.authenticated, + 'cookies': dict_from_cookiejar(self._session.cookies), + } + + def get_user_details(self): + """Returns the user details. If they were not loaded yet this + triggers a refresh. + """ + if self._user_details is None: + self.refresh_user_details() + return self._user_details + + def refresh_user_details(self): + """Refreshes the user details.""" + rv = self._session.get(USER_DETAILS_URL, headers=self._request_headers()) + rv.raise_for_status() + data = rv.json()['data'] + + self._user_details = { + 'apps': self._list_apps(), + 'session': { + 'ds_id': data['sessionToken']['dsId'] + }, + 'email': data['userName'], + 'name': data['displayName'], + 'user_id': data['userId'], + } + + def iter_apps(self): + """Iterates over all apps the user has access to.""" + return self.get_user_details()['apps'] + + def iter_app_builds(self, app): + """Given an app ID, this iterates over all the builds that exist + for it. + """ + for platform in app['platforms']: + rv = self._session.get(urljoin( + API_BASE, 'ra/apps/%s/buildHistory?platform=%s' % ( + app['id'], platform))) + + rv.raise_for_status() + + trains = rv.json()['data']['trains'] + for train in trains: + if train.get('items') is None: + app_builds = self._fetch_build_history_with_train(app, train.get('versionString'), platform) + for app_build in app_builds: + yield { + 'app_id': app['id'], + 'platform': app_build['platform'], + 'version': train['versionString'], + 'build_id': app_build['buildVersion'], + } + else: + for item in train.get('items') or (): + yield { + 'app_id': app['id'], + 'platform': platform, + 'version': train['versionString'], + 'build_id': item['buildVersion'], + } + + def _fetch_build_history_with_train(self, app, version, platform): + rv = self._session.get(urljoin( + API_BASE, 'ra/apps/%s/trains/%s/buildHistory?platform=%s' % ( + app['id'], version, platform))) + rv.raise_for_status() + return rv.json().get('data', {}).get('items', []) + + def get_dsym_url(self, app_id, platform, version, build_id): + """Looks up the dsym URL for a given build""" + rv = self._session.get(urljoin( + API_BASE, 'ra/apps/%s/platforms/%s/trains/%s/builds/%s/details' % ( + app_id, platform, version, build_id))) + rv.raise_for_status() + return rv.json()['data']['dsymurl'] + + def _list_apps(self): + rv = self._session.get(urljoin( + API_BASE, 'ra/apps/manageyourapps/summary/v2')) + rv.raise_for_status() + apps = rv.json()['data']['summaries'] + + rv = [] + for app in apps: + platforms = set() + for x in app['versionSets']: + if x['type'] == 'APP': + platforms.add(x['platformString']) + rv.append({ + 'id': app['adamId'], + 'icon_url': app['iconUrl'], + 'bundle_id': app['bundleId'], + 'name': app['name'], + 'platforms': sorted(platforms), + }) + + return rv + + def close(self): + self._session.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + self.close() diff --git a/src/sentry_plugins/itunesconnect/endpoints/__init__.py b/src/sentry_plugins/itunesconnect/endpoints/__init__.py new file mode 100644 index 00000000..c3961685 --- /dev/null +++ b/src/sentry_plugins/itunesconnect/endpoints/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import diff --git a/src/sentry_plugins/itunesconnect/endpoints/itunesconnect.py b/src/sentry_plugins/itunesconnect/endpoints/itunesconnect.py new file mode 100644 index 00000000..e5281e4c --- /dev/null +++ b/src/sentry_plugins/itunesconnect/endpoints/itunesconnect.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import + +from sentry.utils import json +from sentry.plugins.endpoints import PluginProjectEndpoint + + +class ItunesConnectAppSyncEndpoint(PluginProjectEndpoint): + + def post(self, request, project, *args, **kwargs): + client = self.plugin.get_client(project=project) + app_id = json.loads(request.body.decode('utf-8')).get('app_id') + apps = set(client.apps_to_sync) + if app_id in apps: + apps.remove(app_id) + else: + apps.add(app_id) + client.apps_to_sync = list(apps) + client.save() + return self.respond({ + 'result': {'apps': client.get_apps()} + }) + + +class ItunesConnectTestConfigEndpoint(PluginProjectEndpoint): + + def get(self, request, project, *args, **kwargs): + client = self.plugin.get_client(project=project) + return self.respond({ + 'result': {'apps': client.get_apps()} + }) + + def post(self, request, project, *args, **kwargs): + client = self.plugin.get_client(project=project) + result = None + try: + api_client = self.plugin.get_api_client(project=project) + api_client = self.plugin.login(project=project, api_client=api_client) + result = api_client.get_user_details() + except Exception as exc: + error = True + message = 'There was an error connecting to iTunes Connect.' + exception = repr(exc) + self.plugin.reset_client(project=project) + else: + error = False + message = 'No errors returned' + exception = None + client.apps = result.get('apps', {}) + client.save() + result['apps'] = client.get_apps() + self.plugin.store_client(project=project, api_client=api_client) + + return self.respond({ + 'message': message, + 'result': result, + 'error': error, + 'exception': exception, + }) diff --git a/src/sentry_plugins/itunesconnect/migrations/0001_initial.py b/src/sentry_plugins/itunesconnect/migrations/0001_initial.py new file mode 100644 index 00000000..a4cec344 --- /dev/null +++ b/src/sentry_plugins/itunesconnect/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Client' + db.create_table(u'itunesconnect_client', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('project', self.gf('sentry.db.models.fields.foreignkey.FlexibleForeignKey')(to=orm['sentry.Project'], unique=True)), + ('apps', self.gf('sentry.db.models.fields.encrypted.EncryptedJsonField')(default={})), + ('itc_client', self.gf('sentry.db.models.fields.encrypted.EncryptedJsonField')(default={})), + ('apps_to_sync', self.gf('jsonfield.fields.JSONField')(default={})), + ('last_updated', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + )) + db.send_create_signal('itunesconnect', ['Client']) + + + def backwards(self, orm): + # Deleting model 'Client' + db.delete_table(u'itunesconnect_client') + + + models = { + 'itunesconnect.client': { + 'Meta': {'object_name': 'Client'}, + 'apps': ('sentry.db.models.fields.encrypted.EncryptedJsonField', [], {'default': '{}'}), + 'apps_to_sync': ('jsonfield.fields.JSONField', [], {'default': '{}'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'itc_client': ('sentry.db.models.fields.encrypted.EncryptedJsonField', [], {'default': '{}'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'project': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'to': "orm['sentry.Project']", 'unique': 'True'}) + }, + 'sentry.organization': { + 'Meta': {'object_name': 'Organization'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_role': ('django.db.models.fields.CharField', [], {'default': "'member'", 'max_length': '32'}), + 'flags': ('django.db.models.fields.BigIntegerField', [], {'default': '1'}), + 'id': ('sentry.db.models.fields.bounded.BoundedBigAutoField', [], {'primary_key': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'org_memberships'", 'symmetrical': 'False', 'through': "orm['sentry.OrganizationMember']", 'to': "orm['sentry.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}), + 'status': ('sentry.db.models.fields.bounded.BoundedPositiveIntegerField', [], {'default': '0'}) + }, + 'sentry.organizationmember': { + 'Meta': {'unique_together': "(('organization', 'user'), ('organization', 'email'))", 'object_name': 'OrganizationMember'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'flags': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'has_global_access': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('sentry.db.models.fields.bounded.BoundedBigAutoField', [], {'primary_key': 'True'}), + 'organization': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'related_name': "'member_set'", 'to': "orm['sentry.Organization']"}), + 'role': ('django.db.models.fields.CharField', [], {'default': "'member'", 'max_length': '32'}), + 'teams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['sentry.Team']", 'symmetrical': 'False', 'through': "orm['sentry.OrganizationMemberTeam']", 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'type': ('sentry.db.models.fields.bounded.BoundedPositiveIntegerField', [], {'default': '50', 'blank': 'True'}), + 'user': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'blank': 'True', 'related_name': "'sentry_orgmember_set'", 'null': 'True', 'to': "orm['sentry.User']"}) + }, + 'sentry.organizationmemberteam': { + 'Meta': {'unique_together': "(('team', 'organizationmember'),)", 'object_name': 'OrganizationMemberTeam', 'db_table': "'sentry_organizationmember_teams'"}, + 'id': ('sentry.db.models.fields.bounded.BoundedAutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'organizationmember': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'to': "orm['sentry.OrganizationMember']"}), + 'team': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'to': "orm['sentry.Team']"}) + }, + 'sentry.project': { + 'Meta': {'unique_together': "(('team', 'slug'), ('organization', 'slug'))", 'object_name': 'Project'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'first_event': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'flags': ('django.db.models.fields.BigIntegerField', [], {'default': '0', 'null': 'True'}), + 'forced_color': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'id': ('sentry.db.models.fields.bounded.BoundedBigAutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'organization': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'to': "orm['sentry.Organization']"}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'null': 'True'}), + 'status': ('sentry.db.models.fields.bounded.BoundedPositiveIntegerField', [], {'default': '0', 'db_index': 'True'}), + 'team': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'to': "orm['sentry.Team']"}) + }, + 'sentry.team': { + 'Meta': {'unique_together': "(('organization', 'slug'),)", 'object_name': 'Team'}, + 'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True'}), + 'id': ('sentry.db.models.fields.bounded.BoundedBigAutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'organization': ('sentry.db.models.fields.foreignkey.FlexibleForeignKey', [], {'to': "orm['sentry.Organization']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'status': ('sentry.db.models.fields.bounded.BoundedPositiveIntegerField', [], {'default': '0'}) + }, + 'sentry.user': { + 'Meta': {'object_name': 'User', 'db_table': "'auth_user'"}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'id': ('sentry.db.models.fields.bounded.BoundedAutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_managed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_password_expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_password_change': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'db_column': "'first_name'", 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'session_nonce': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}) + } + } + + complete_apps = ['itunesconnect'] \ No newline at end of file diff --git a/src/sentry_plugins/itunesconnect/migrations/__init__.py b/src/sentry_plugins/itunesconnect/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/sentry_plugins/itunesconnect/models.py b/src/sentry_plugins/itunesconnect/models.py new file mode 100644 index 00000000..207b046b --- /dev/null +++ b/src/sentry_plugins/itunesconnect/models.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import + +from jsonfield import JSONField +from django.db import models +from django.utils import timezone + +from sentry.db.models import ( + BaseModel, BaseManager, FlexibleForeignKey, EncryptedJsonField +) + + +class Client(BaseModel): + __core__ = False + + objects = BaseManager() + project = FlexibleForeignKey('sentry.Project', unique=True) + apps = EncryptedJsonField() + itc_client = EncryptedJsonField() + apps_to_sync = JSONField() + last_updated = models.DateTimeField(default=timezone.now) + + class Meta: + app_label = 'itunesconnect' + + def get_apps(self): + rv = [] + for app in self.apps: + app['active'] = self.is_app_active(app.get('id', None)) + rv.append(app) + return rv + + def is_app_active(self, app_id): + return app_id in self.apps_to_sync diff --git a/src/sentry_plugins/itunesconnect/plugin.py b/src/sentry_plugins/itunesconnect/plugin.py new file mode 100644 index 00000000..00a17e02 --- /dev/null +++ b/src/sentry_plugins/itunesconnect/plugin.py @@ -0,0 +1,135 @@ +from __future__ import absolute_import + +from datetime import timedelta + +from sentry.plugins.base.v1 import Plugin +from sentry.plugins.base.configuration import react_plugin_config +from sentry_plugins.utils import get_secret_field_config +from sentry.exceptions import PluginError + +from sentry_plugins.base import CorePluginMixin +from .client import ItunesConnectClient +from .endpoints.itunesconnect import ( + ItunesConnectTestConfigEndpoint, ItunesConnectAppSyncEndpoint +) +from .models import Client + + +class ItunesConnectPlugin(CorePluginMixin, Plugin): + description = 'iTunes Connect Debug symbols sync service.' + slug = 'itunesconnect' + title = 'iTunes Connect' + conf_title = title + conf_key = 'itunesconnect' + + asset_key = 'itunesconnect' + assets = [ + 'dist/itunesconnect.js', + ] + + def configure(self, project, request): + return react_plugin_config(self, project, request) + + def get_plugin_type(self): + return 'task-runner' + + def can_enable_for_projects(self): + return True + + def has_project_conf(self): + return True + + def set_option(self, key, value, project=None, user=None): + super(Plugin, self).set_option(key, value, project, user) + if key != 'enabled' and project: + self.reset_client(project) + + def reset_options(self, project=None, user=None): + super(Plugin, self).reset_options(project, user) + self.reset_client(project=project, full_reset=True) + + def is_configured(self, project, **kwargs): + return all((self.get_option(k, project) for k in ('email', 'password'))) + + def get_client(self, project): + client, _ = Client.objects.get_or_create( + project=project + ) + return client + + def get_api_client(self, project): + try: + stored_client, _ = Client.objects.get_or_create( + project=project + ) + if (stored_client.itc_client and + stored_client.itc_client.get('authenticated')): + return ItunesConnectClient.from_json(stored_client.itc_client) + return ItunesConnectClient() + except Exception as exc: + raise PluginError(exc) + + def reset_client(self, project, full_reset=False): + client, _ = Client.objects.get_or_create( + project=project + ) + client.itc_client = {} + if full_reset: + client.apps = {} + client.apps_to_sync = {} + client.save() + + def store_client(self, project, api_client): + itc_client, _ = Client.objects.get_or_create( + project=project + ) + itc_client.itc_client = api_client.to_json(ensure_user_details=False) + itc_client.save() + return itc_client + + def get_project_urls(self): + return [ + (r'^test-config/', ItunesConnectTestConfigEndpoint.as_view(plugin=self)), + (r'^sync-app/', ItunesConnectAppSyncEndpoint.as_view(plugin=self)) + ] + + def login(self, project, api_client): + api_client.login( + email=self.get_option('email', project), + password=self.get_option('password', project) + ) + return api_client + + def get_config(self, project, **kwargs): + password = self.get_option('password', project) + secret_field = get_secret_field_config(password, + 'Enter your iTunes Connect password.') + secret_field.update({ + 'name': 'password', + 'label': 'Password', + 'help': 'Enter the password of the iTunes Connect user.' + }) + + return [{ + 'name': 'email', + 'label': 'Email', + 'type': 'email', + 'required': True, + 'help': 'Enter the email of the iTunes Connect user.' + }, secret_field] + + def get_cron_schedule(self): + # 'schedule': timedelta(minutes=15), + return {'sync-dsyms-from-itunes-connect': { + 'task': 'sentry.tasks.sync_dsyms_from_itunes_connect', + 'schedule': timedelta(seconds=30), + 'options': { + 'expires': 300, + }, + }} + + def get_worker_imports(self): + return ['sentry_plugins.itunesconnect.tasks.itunesconnect'] + + def get_worker_queues(self): + return ['itunesconnect'] diff --git a/src/sentry_plugins/itunesconnect/static/itunesconnect/components/settings.jsx b/src/sentry_plugins/itunesconnect/static/itunesconnect/components/settings.jsx new file mode 100644 index 00000000..e6c8b326 --- /dev/null +++ b/src/sentry_plugins/itunesconnect/static/itunesconnect/components/settings.jsx @@ -0,0 +1,198 @@ +import React from 'react'; +import {i18n, IndicatorStore, plugins, Switch} from 'sentry'; + +class Settings extends plugins.BasePlugin.DefaultSettings { + constructor(props) { + super(props); + + this.syncAccount = this.syncAccount.bind(this); + this.handleLoading = this.handleLoading.bind(this); + this.finishedLoading = this.finishedLoading.bind(this); + this.fetchData = this.fetchData.bind(this); + + Object.assign(this.state, { + loading: false, + sessionExpired: false + }); + } + + handleLoading() { + if (this.state.loading !== false) { + return true; + } + this.setState({ + loading: true, + }); + return false; + } + + finishedLoading(loadingIndicator) { + if (loadingIndicator) IndicatorStore.remove(loadingIndicator); + this.setState({ + loading: false, + }); + } + + fetchData() { + if (this.props.plugin.enabled === false) return; + super.fetchData(); + this.api.request(`${this.getPluginEndpoint()}test-config/`, { + success: (data) => { + this.setState({ + testResults: data, + sessionExpired: data.sessionExpired + }); + this.syncAccount(); + } + }); + } + + syncAccount() { + if (this.handleLoading()) return; + let loadingIndicator = IndicatorStore.add(i18n.t('Syncing account...')); + + this.api.request(`${this.getPluginEndpoint()}test-config/`, { + method: 'POST', + success: (data) => { + this.setState({ + testResults: data + }); + }, + error: (error) => { + this.setState({ + testResults: { + error: true, + message: 'An unknown error occurred while loading this integration.', + }, + }); + }, + complete: () => this.finishedLoading(loadingIndicator) + }); + } + + activateApp(appID) { + if (this.handleLoading()) return; + + this.api.request(`${this.getPluginEndpoint()}sync-app/`, { + method: 'POST', + data: { + app_id: appID, + }, + success: (data) => { + this.setState({ + testResults: data, + }); + }, + complete: () => this.finishedLoading() + }); + } + + renderApp(app) { + return ( +
{this.state.testResults.exception}
+