#!/usr/bin/env python # coding: utf-8 import logging from collections import OrderedDict from functools import partial from itertools import chain, repeat import click import tornado.httpserver import tornado.ioloop import tornado.web from babel import Locale, UnknownLocaleError from prometheus_client import start_http_server from raven import Client import critics from .core import CriticApp from .i18n import get_locale logger = logging.getLogger('critics') @click.command() @click.option('--ios', multiple=True, help='ios app id, e.g. 122434343') @click.option('--ios-channel', multiple=True, help='Slack channel for ios notifications, optional') @click.option('--android', multiple=True, help='Android app name, e.g. "com.rovio.angrybirds"') @click.option('--android-channel', multiple=True, help='Slack channel for android notifications, optional') @click.option('--language', multiple=True, help='ISO 639-1 languages of review [default: system locale]') @click.option('--slack-webhook', help='Slack webhook absolute URL, required') @click.option('--parse-max-entries', default=10, help='Number of feed entries to look into [default: 10]') @click.option('--beat', default=300, help='Number of seconds between polling feed [default: 300]') @click.option('--verbose/--short', default=False) @click.option('--notify/--no-notify', default=True) @click.option('--persist/--no-persist', default=True) @click.option('--model', type=click.Path(), default='reviews.json') @click.option('--daemonize/--run-once', default=True) @click.option('--stats', default=9137, help='Port to serve prometheus stats [default: 9137]') @click.option('--sentry-dsn', help='DSN of Sentry instance to monitor exceptions') @click.option('--version', is_flag=True) def cli(**settings): """Notify about new reviews in AppStore and Google Play in slack. Launch command using supervisor or using screen/tmux/etc. Reviews are fetched for multiple apps and languages in --beat=300 interval. """ setup_logging(settings) settings = setup_languages(settings) channels = setup_channel_map(settings) app = CriticApp(**dict(settings, channels=channels)) if settings['sentry_dsn']: app.sentry_client = Client(settings['sentry_dsn']) logger.debug('Errors are reported to %s' % settings['sentry_dsn']) else: app.sentry_client = None if settings['version']: click.echo('Version %s' % critics.__version__) return if not (settings['ios'] or settings['android']): click.echo('Please choose either --ios or --android') return loop = tornado.ioloop.IOLoop.instance() if app.load_model(): logger.debug('Model loaded OK, not skipping notify on first run') notify = True else: notify = False if settings['ios']: logger.info('Tracking IOS apps: %s', ', '.join(settings['ios'])) itunes = tornado.ioloop.PeriodicCallback(partial(app.poll_store, 'ios'), 1000 * settings['beat'], loop) itunes.start() if settings['android']: logger.info('Tracking Android apps: %s', ', '.join(settings['android'])) google_play = tornado.ioloop.PeriodicCallback(partial(app.poll_store, 'android'), 1000 * settings['beat'], loop) google_play.start() echo_channel_map(channels) if settings['ios']: app.poll_store('ios', notify=notify) if settings['android']: app.poll_store('android', notify=notify) if settings['stats']: port = int(settings['stats']) logger.debug('Serving metrics server on port %s' % port) start_http_server(port) if settings['daemonize']: loop.start() def setup_logging(settings): handler = logging.StreamHandler() logger.addHandler(handler) if settings['verbose']: logger.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter('[%(asctime)s] %(message)s', "%Y-%m-%d %H:%M:%S")) else: logger.setLevel(logging.INFO) logger.propagate = False def setup_languages(settings): if not settings['language']: settings['language'] = [get_locale()[:2]] languages = [] language_names = [] for lang_code in settings['language']: try: language_names.append(Locale(lang_code).english_name) languages.append(lang_code) except UnknownLocaleError: raise click.ClickException('Unknown language code: %s' % lang_code) logger.info('Languages: %s', ', '.join(language_names)) return settings def setup_channel_map(settings): channel_map = OrderedDict() if not settings['slack_webhook']: return channel_map if settings['ios'] and settings['ios_channel']: channel_map['ios'] = {} ios_channels = chain(settings['ios_channel'], repeat(settings['ios_channel'][-1])) for app_id, channel in zip(settings['ios'], ios_channels): channel_map['ios'][app_id] = channel if settings['android'] and settings['android_channel']: channel_map['android'] = {} android_channels = chain(settings['android_channel'], repeat(settings['android_channel'][-1])) for app_id, channel in zip(settings['android'], android_channels): channel_map['android'][app_id] = channel return channel_map def echo_channel_map(channel_map): if not channel_map: return channel_output = '' for platform, app_mapping in channel_map.items(): for app_id, channel in app_mapping.items(): channel_output += ' {app_id} -> {channel}'.format( platform=platform, app_id=app_id, channel=channel) logger.info('Transport: slack channels:%s' % channel_output)