示例#1
0
def remove_account(credentials, talk_id):
    # アカウント連携を解除
    try:
        credentials.revoke(httplib2.Http())
    except client.TokenRevokeError:
        print('既にアカウント連携が削除されています')

    # DBから削除
    session = Session()
    with session.begin():
        talk = session.query(Talk).filter_by(talk_id=talk_id).one()
        session.delete(talk)
示例#2
0
 def oauth2callback():
     print(flask.session)
     session = Session()
     if 'talk_id' not in flask.session:
         return '不正なアクセスです。'
     talk_id = flask.session.pop('talk_id')
     auth_code = flask.request.args.get('code')
     credentials = flow.step2_exchange(auth_code)
     with session.begin():
         if session.query(Talk).filter(
                 Talk.talk_id == talk_id).one_or_none() is None:
             session.add(
                 Talk(talk_id=talk_id,
                      credential=credentials.to_json()))
             return 'あなたのLineとGoogleカレンダーが正常に紐付けられました。'
         else:
             return '既にグループにGoogleアカウントが紐付けられています'
示例#3
0
def get_credentials(talk_id):
    session = Session()
    with session.begin():
        talk = session.query(Talk).filter_by(talk_id=talk_id).one_or_none()
        if talk is None:
            return None
        credentials = client.OAuth2Credentials.from_json(talk.credential)
        if credentials.access_token_expired:
            print('認証の期限が切れています')
            http = credentials.authorize(httplib2.Http())
            try:
                credentials.refresh(http)
            except client.HttpAccessTokenRefreshError:
                print('リフレッシュエラーが起きました')
                session.delete(talk)
                print('ユーザーをDBから削除しました')
                return REFRESH_ERROR
            print('リフレッシュしました')
            talk.credential = credentials.to_json()
            print('新しい認証情報をDBに保存しました')
        return credentials
示例#4
0
class PostBackEventHandler:
    def __init__(self, handler):
        self.handler = handler
        self.cases = []
        self.preexe_cases = []
        self.session = Session()

        @self._add_case(template_id='ExitConfirm_yes', preexe=True)
        def exit_confirm(event, data, credentials):
            try:
                talk_id = self._get_talk_id(event)
                line_bot_api.reply_message(
                    event.reply_token,
                    StickerSendMessage(package_id="2", sticker_id="42"))
                if credentials is not None:
                    api_manager.remove_account(credentials, talk_id)
                if event.source.type == 'group':
                    line_bot_api.leave_group(event.source.group_id)
                else:
                    line_bot_api.leave_room(event.source.room_id)
            except LineBotApiError as e:
                print(e)

        @self._add_case(template_id='ExitConfirm_no', preexe=True)
        def exit_confirm_no(event, data, credentials):
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text="退出をキャンセルしました。"))

        @self._add_case(template_id='AccountRemoveConfirm_yes')
        def account_remove(event, data, credentials, _):
            talk_id = self._get_talk_id(event)
            api_manager.remove_account(credentials, talk_id)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text='アカウント連携を解除しました。'))

        @self._add_case(template_id='AccountRemoveConfirm_no')
        def account_remove_no(event, data, credentials, service):
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text="アカウント連携解除をキャンセルしました。"))

        @self._add_case(template_id='EventCreateButtons_#create-calendar')
        def calendar_create(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            created_datetime = datetime.strptime(data[1], '%m/%d')
            current_year = datetime.now(jst).year
            created_date = date(
                current_year if is_over_now(created_datetime) else
                current_year + 1, created_datetime.month, created_datetime.day)
            title = 'Smart Scheduleからの予定'
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
            try:
                calendar_event = api_manager.create_event(
                    service, talk.calendar_id, created_date, title)
            except client.HttpAccessTokenRefreshError:
                with self.session.begin():
                    self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '{}月{}日の予定を作成しました\n{}'.format(
                created_date.month, created_date.day,
                calendar_event.get('htmlLink'))
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(template_id='GroupMenuButtons_#member')
        def member_display(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            reply_text = '登録されているメンバー一覧'
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
            user_names = sorted(
                {free_day.user_name
                 for free_day in talk.free_days})
            for user_name in user_names:
                reply_text += '\n'
                reply_text += user_name
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(template_id='GroupMenuButtons_#adjust')
        def schedule_adjust(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()

            reply_text = "空いてる日を入力してください\n例:橋本 1/1 1/2 1/3 1/4\n\n※日程調整を終了する際は「end」と入力してください\n--------------------------------"
            if len(talk.free_days) == 0:
                reply_text += '\n\n現在、空いている日は登録されていません'
            else:
                reply_text += '\n\n現在の空いている日\n'
                dates = [free_day.date for free_day in talk.free_days]
                date_count_dict = OrderedDict(
                    sorted(Counter(dates).items(), key=lambda x: x[0]))
                for d, count in date_count_dict.items():
                    reply_text += '\n{}/{} {}票'.format(d.month, d.day, count)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(template_id='GroupMenuButtons_#g-calender')
        def schedule_display(event, data, credentials, service):
            post_carousel(event.reply_token)

        @self._add_case(template_id='#keyword_search')
        def keyword_search(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
                talk.keyword_flag = True
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text="キーワードを入力してください\n例:バイト、研究室"))

        @self._add_case(template_id='#up to n days_schedule')
        def up_to_n_days_schedule(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
                talk.up_to_day_flag = True
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text="何日後までの予定を表示しますか?\n例:5"))

        @self._add_case(template_id='#date_schedule')
        def date_schedule(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
                talk.date_flag = True
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text="取得したい予定の日付を入力してください\n例:4/1"))

        @self._add_case(template_id='#today_schedule')
        def today_schedule(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
            days = 0
            try:
                events = api_manager.get_events_after_n_days(
                    service, talk.calendar_id, days)
            except client.HttpAccessTokenRefreshError:
                with self.session.begin():
                    self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '今日の予定'
            reply_text = generate_message_from_events(events, reply_text)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(template_id='#tomorrow_schedule')
        def tomorrow_schedule(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
            days = 1
            try:
                events = api_manager.get_events_after_n_days(
                    service, talk.calendar_id, days)
            except client.HttpAccessTokenRefreshError:
                with self.session.begin():
                    self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '明日の予定'
            reply_text = generate_message_from_events(events, reply_text)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(template_id='#7days_schedule')
        def seven_days_schedule(event, data, credentials, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter_by(
                    talk_id=talk_id).one()
            days = 7
            try:
                events = api_manager.get_n_days_events(service,
                                                       talk.calendar_id, days)
            except client.HttpAccessTokenRefreshError:
                with self.session.begin():
                    self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '1週間後までの予定'
            reply_text = generate_message_from_events(events, reply_text)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self.handler.add(PostbackEvent)
        def handle(event):
            print("postbackevent: {}".format(event))
            session = Session()
            data = event.postback.data.split(',')
            print(data)
            print(data[1])
            pre_time = datetime.strptime(data[-1], '%Y-%m-%d %H:%M:%S')
            compare = datetime.now() - pre_time
            print(compare)

            if compare.total_seconds() > int(line_env['time_out_seconds']):
                line_bot_api.reply_message(
                    event.reply_token,
                    TextSendMessage(text="タイムアウトです。\nもう一度最初からやり直してください"))
                return

            talk_id = self._get_talk_id(event)
            credentials = api_manager.get_credentials(talk_id)
            if credentials == REFRESH_ERROR:
                reply_refresh_error_message(event)
                return

            for func, template_id in self.preexe_cases:
                if data[0] == template_id:
                    func(event, data, credentials)
                    return

            service = api_manager.build_service(credentials)
            if service is None:
                reply_google_auth_message(event)
                return

            for func, template_id in self.cases:
                if data[0] == template_id:
                    func(event, data, credentials, service)
                    return

    def _add_case(self, template_id=None, preexe=False):
        """A decorator that is used to register a function executing 
        in case of matching given filtering rules.

        :param template_id: the template data rule as string.
        :param preexe: the boolean indicating whether function is pre-executed.
        """
        def wrapper(func):
            if preexe:
                self.preexe_cases.append((func, template_id))
            else:
                self.cases.append((func, template_id))
            return func

        return wrapper

    @staticmethod
    def _get_talk_id(event):
        if event.source.type == 'user':
            talk_id = event.source.user_id
        elif event.source.type == 'group':
            talk_id = event.source.group_id
        elif event.source.type == 'room':
            talk_id = event.source.room_id
        else:
            raise Exception('invalid `event.source`')
        return talk_id

    @classmethod
    def _get_service(cls, event):
        talk_id = cls._get_talk_id(event)
        # google calendar api のcredentialをDBから取得する
        credentials = api_manager.get_credentials(talk_id)
        if credentials == REFRESH_ERROR or credentials is None:
            return credentials
        service = api_manager.build_service(credentials)
        return service
class MessageEventHandler:
    message_event_messages = messages['text_messages']['message_event']

    def __init__(self, handler):
        self.handler = handler
        self.cases = []
        self.preexe_cases = []
        self.flag_cases = []
        self.session = Session()

        @self._add_case(text='exit', type=['group', 'room'], preexe=True)
        def exit(event):
            time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            confirm_message = TemplateSendMessage(
                alt_text='Confirm template',
                template=ExitConfirm(time,
                                     messages['templates']['exit_confirm']))
            line_bot_api.reply_message(event.reply_token, confirm_message)

        @self._add_case(text='help', type='user', preexe=True)
        def user_help(event):
            reply_text = self.message_event_messages['user_help_message']
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(text='help', type=['group', 'room'], preexe=True)
        def group_help(event):
            reply_text = self.message_event_messages['group_help_message']
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(text=['schedule', '予定'])
        def schedule(event, service):
            post_carousel(event.reply_token)

        @self._add_case(type=['group', 'room'],
                        pattern=r'(ss|smart[\s_-]?schedule|スマートスケジュール)$',
                        pattern_flags=re.IGNORECASE)
        def group_menu(event, service):
            time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            # グループのメニューを表示する
            buttons_template_message = TemplateSendMessage(
                alt_text='Button template',
                template=GroupMenuButtons(
                    time, messages['templates']['group_menu_buttons']))
            line_bot_api.reply_message(event.reply_token,
                                       buttons_template_message)

        @self._add_case(text='select')
        def select(event, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter(
                    Talk.talk_id == talk_id).one()
                talk.calendar_select_flag = True
                try:
                    calendar_list = api_manager.get_calendar_list(service)
                except client.HttpAccessTokenRefreshError:
                    self.session.delete(talk)
                    reply_invalid_credential_error_message(event)
                    return
            reply_text = 'Google Calendar で確認できるカレンダーの一覧です。\n 文字を入力してカレンダーを選択してください'
            for item in calendar_list['items']:
                reply_text += '\n- {}'.format(item['summary'])
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_case(text='logout')
        def logout(event, service):
            time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            confirm_message = TemplateSendMessage(
                alt_text='Confirm template',
                template=AccountRemoveConfirm(
                    time, messages['templates']['account_remove_confirm']))
            line_bot_api.reply_message(event.reply_token, confirm_message)

        @self._add_case(
            pattern=
            r'[^\s\d]\S*(?:\s(?:[1-9]|1[0-2])/(?:[1-9]|[1-2][0-9]|3[0-1]))+$')
        def add_free_day(event, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter(
                    Talk.talk_id == talk_id).one()
            split_message = event.message.text.split()
            name = split_message[0]
            day_strs = split_message[1:]
            datetimes = [
                datetime.strptime(day_str, '%m/%d') for day_str in day_strs
            ]
            # TODO 無効な日にち(31を指定したが、その月の31日が存在しない場合など)のエラーハンドリングやメッセージを実装する(ValueError)
            current_year = datetime.now(jst).year
            dates = [
                date(current_year if is_over_now(dt) else current_year + 1,
                     dt.month, dt.day) for dt in datetimes
            ]
            free_days = [FreeDay(date, name, talk.id) for date in dates]
            with self.session.begin():
                # TODO 既にDBにある日を登録しようとしたときのエラーハンドリング及びメッセージの実装(sqlalchemy.exc.IntegrityError)
                self.session.add_all(free_days)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text='空いている日を保存しました'))

        @self._add_case(text='end')
        def end(event, service):
            talk_id = self._get_talk_id(event)
            with self.session.begin():
                talk = self.session.query(Talk).filter(
                    Talk.talk_id == talk_id).one()

            if len(talk.free_days) == 0:
                # TODO 登録されている空いている日は無いことを知らせるメッセージが欲しい
                print('空いている日が登録されてないぞー')
                return
            time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            reply_text = '空いている日'
            dates = [free_day.date for free_day in talk.free_days]
            date_count_dict = OrderedDict(
                sorted(Counter(dates).items(), key=lambda x: x[0]))
            for i, dic in enumerate(date_count_dict.items()):
                d, count = dic
                if i % 3 == 0:
                    reply_text += '\n'
                else:
                    reply_text += ', '
                reply_text += '{}/{} {}人'.format(d.month, d.day, count)

            best_date_count = max(date_count_dict.values())
            best_dates = [
                k for k, v in date_count_dict.items() if v == best_date_count
            ]
            print(len(reply_text))
            if len(reply_text) >= 100:
                reply_text = reply_text[:100]

            buttons_template_message = TemplateSendMessage(
                alt_text='Button template',
                # TODO ボタンテンプレートが4つしか受け付けないので4つしか選べない
                template=EventCreateButtons(
                    time, messages['templates']['event_create_buttons'],
                    reply_text, best_dates[:4]))
            line_bot_api.reply_message(event.reply_token,
                                       buttons_template_message)
            with self.session.begin():
                for free_day in talk.free_days:
                    self.session.delete(free_day)

        @self._add_flag_case(flag='up_to_day_flag')
        def up_to_day(event, service, talk):
            talk.up_to_day_flag = False
            try:
                days = int(event.message.text)
            except ValueError:
                # TODO 数字ではないメッセージが送られてきたときのメッセージを送る
                return
            try:
                events = api_manager.get_n_days_events(service,
                                                       talk.calendar_id, days)
            except client.HttpAccessTokenRefreshError:
                self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '{}日後までの予定'.format(days)
            reply_text = generate_message_from_events(events, reply_text)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_flag_case(flag='date_flag')
        def day_by_date(event, service, talk):
            talk.date_flag = False
            try:
                current_year = datetime.now(jst).year
                dt = datetime.strptime(event.message.text, '%m/%d')
                specified_date = date(
                    current_year if is_over_now(dt) else current_year + 1,
                    dt.month, dt.day)
            except ValueError:
                # TODO 不適当なメッセージが送られてきたときのメッセージを送る
                return
            try:
                events = api_manager.get_events_by_date(
                    service, talk.calendar_id, specified_date)
            except client.HttpAccessTokenRefreshError:
                self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '{} の予定'.format(event.message.text)
            reply_text = generate_message_from_events(events, reply_text)
            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_flag_case(flag='keyword_flag')
        def keyword(event, service, talk):
            talk.keyword_flag = False
            keyword = event.message.text
            try:
                events = api_manager.get_events_by_title(
                    service, talk.calendar_id, keyword)
            except client.HttpAccessTokenRefreshError:
                self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            reply_text = '{}の検索結果'.format(keyword)
            reply_text = generate_message_from_events(events, reply_text)
            if len(reply_text) >= 1900:
                reply_text = reply_text[:1900]

            line_bot_api.reply_message(event.reply_token,
                                       TextSendMessage(text=reply_text))

        @self._add_flag_case(flag='calendar_select_flag')
        def calendar_select(event, service, talk):
            try:
                calendar_list = api_manager.get_calendar_list(service)
            except client.HttpAccessTokenRefreshError:
                self.session.delete(talk)
                reply_invalid_credential_error_message(event)
                return
            summaries = [item['summary'] for item in calendar_list['items']]
            if event.message.text in summaries:
                talk.calendar_select_flag = False
                calendar_id = [
                    item['id'] for item in calendar_list['items']
                    if item['summary'] == event.message.text
                ][0]
                talk.calendar_id = calendar_id
                reply_text = 'カレンダーを {} に設定しました'.format(event.message.text)
                line_bot_api.reply_message(event.reply_token,
                                           TextSendMessage(text=reply_text))
            else:
                talk.calendar_select_flag = False
                reply_text = '{} はカレンダーには存在しません'.format(event.message.text)
                line_bot_api.reply_message(event.reply_token,
                                           TextSendMessage(text=reply_text))

        @self.handler.add(MessageEvent, message=TextMessage)
        def handle(event):
            print(event)
            for func, text, type, pattern, pattern_flags in self.preexe_cases:
                if self._validate(event, text, type, pattern, pattern_flags):
                    func(event)
                    return

            service = self._get_service(event)
            # リフレッシュエラーが起きた場合、手動でアカウント連携を解除するように促すメッセージを送る
            if service == REFRESH_ERROR:
                reply_refresh_error_message(event)
                return
            # DBに登録されていない場合、認証URLをリプライする
            if service is None:
                reply_google_auth_message(event)
                return

            for func, flag in self.flag_cases:
                talk_id = self._get_talk_id(event)
                # TODO トランザクション開始とコミットのタイミングこれじゃまずい
                self.session.begin()
                talk = self.session.query(Talk).filter(
                    Talk.talk_id == talk_id).one()
                try:
                    if getattr(talk, flag):
                        func(event, service, talk)
                        self.session.commit()
                        return
                finally:
                    self.session.close()

            for func, text, type, pattern, pattern_flags in self.cases:
                if self._validate(event, text, type, pattern, pattern_flags):
                    func(event, service)
                    return

    def _add_case(self,
                  text=None,
                  type=None,
                  pattern=None,
                  pattern_flags=0,
                  preexe=False):
        """A decorator that is used to register a function executing 
        in case of matching given filtering rules.

        :param text: the user message text rule as string or its list
        :param type: the type of `linebot.models.sources` as string or its list.
        :param pattern: the pattern matching user message text. 
                        it is evaluated by `re.match` function.
        :param pattern_flags: the pattern flags given as an argument 
                              of `re.match` function.
        :param preexe: the boolean indicating whether function is pre-executed.
        """
        def wrapper(func):
            if preexe:
                self.preexe_cases.append(
                    (func, text, type, pattern, pattern_flags))
            else:
                self.cases.append((func, text, type, pattern, pattern_flags))
            return func

        return wrapper

    def _add_flag_case(self, flag):
        """A decorator that is used to register a function executing 
        in case of erecting given flags.

        :param flag: the flag of `smart_schedule.models.Talk` as string
        """
        def wrapper(func):
            self.flag_cases.append((func, flag))
            return func

        return wrapper

    @classmethod
    def _validate(cls, event, text, type, pattern, pattern_flags):
        if text is not None:
            if not cls._validate_text(event.message.text, text):
                return False
        if type is not None:
            if not cls._validate_text(event.source.type, type):
                return False
        if pattern is not None:
            if not re.match(pattern, event.message.text, pattern_flags):
                return False
        return True

    @staticmethod
    def _validate_text(actual, expected):
        is_valid = False
        if isinstance(expected, list):
            for text in expected:
                if actual == text:
                    is_valid = True
                    break
        if isinstance(expected, str):
            if actual == expected:
                is_valid = True
        return is_valid

    @staticmethod
    def _get_talk_id(event):
        if event.source.type == 'user':
            talk_id = event.source.user_id
        elif event.source.type == 'group':
            talk_id = event.source.group_id
        elif event.source.type == 'room':
            talk_id = event.source.room_id
        else:
            raise Exception('invalid `event.source`')
        return talk_id

    @classmethod
    def _get_service(cls, event):
        talk_id = cls._get_talk_id(event)
        # google calendar api のcredentialをDBから取得する
        credentials = api_manager.get_credentials(talk_id)
        if credentials == REFRESH_ERROR or credentials is None:
            return credentials
        service = api_manager.build_service(credentials)
        return service