Example #1
0
class CamBot:
    def __init__(self, agent: 'gphotos.GooglePhotosManager',
                 manager: vkmanager.VKManager):
        self._bot = Bot(conf.bot_token, proxy=conf.tele_proxy)
        self.session = self._bot.session
        self.loop = self._bot.loop
        self.menu_markup = Menu()
        self.init_handlers()
        self.agent = agent
        self.vk_manager = manager

    def init_handlers(self):
        self._bot.add_command(r'/mov (.+) (.+)', self.mov)
        self._bot.add_command(r'/push_vk (.+) (.+)', self.push_vk)
        self._bot.add_command(r'/check (.+) (.+)', self.check_album)
        self._bot.add_command(r'/full_check (.+)', self.full_check)
        self._bot.add_command(r'/clear (.+)', self.clear_command)
        self._bot.add_command(r'/reg', reg)
        self._bot.add_command(r'/ch', self.reg_channel)
        self._bot.add_command(r'/photo_reg', self.reg_photo_channel)
        self._bot.add_command(r'/menu', self.menu)
        self._bot.add_command(r'/all', self.img_all_cams)
        self._bot.add_command(r'/stats (.+)', self.stats_command)
        self._bot.add_command(r'/stats', self.stats_command)
        self._bot.add_command(r'/lstats (.+)', self.lstats_command)
        self._bot.add_command(r'/lstats', self.lstats_command)
        self._bot.add_command(r'/dbdata', self.db_data)
        self._bot.add_command(r'/daily', self.daily_movie_group_command)
        self._bot.add_command(r'/push_on', self.push_vk_on)
        self._bot.add_command(r'/push_off', self.push_vk_off)
        self._bot.add_callback(r'regular (.+)', regular)
        self._bot.add_callback(r'today (.+)', today)
        self._bot.add_callback(r'weekly (.+)', weekly)
        self._bot.add_callback(r'select (.+)', self.select)
        self._bot.add_callback(r'back', self.back)
        self._bot.add_callback(r'img (.+)', self.img_callback)
        self._bot.add_callback(r'choose_cam (.+)', self.choose_cam_callback)
        self._bot.add_callback(r'choose_photo_cam (.+)',
                               self.choose_photo_cam_callback)
        self._bot.add_callback(r'sync (.+)', self.sync_gphotos)
        self._bot.add_callback(r'gsnc (.+)', self.run_sync_gphotos)
        self._bot.add_callback(r'remove (.+)', self.remove_folder)
        self._bot.add_callback(r'post (.+) (.+)', self.post_photo)
        self._bot.add_callback(r'clear_cb (.+)', self.clear_callback)
        self._bot.add_callback(r'check_cb (.+)', self.full_check_callback)
        self._bot.callback(unhandled_callbacks)

    def stop(self):
        self._bot.stop()

    @ThreadSwitcherWithDB.optimized
    async def daily_stats(self):
        await self.stats_request(pendulum.yesterday(), self.notify_admins)

    @ThreadSwitcherWithDB.optimized
    async def daily_movie(self, cam: Cam):
        day = datetime.datetime.now() - datetime.timedelta(days=1)
        day = day.strftime('%d_%m_%Y')
        loop = asyncio.get_event_loop()
        with concurrent.futures.ThreadPoolExecutor() as pool:
            try:
                clip = await loop.run_in_executor(pool,
                                                  lambda: make_movie(cam, day))
            except FileNotFoundError as exc:
                logger.exception(exc)
                await self.notify_admins(
                    f'File {exc.filename} not found for daily movie {cam.name}: {day}'
                )
                return
            except Exception as exc:
                logger.exception(exc)
                await self.notify_admins(
                    f'Error during making daily movie for {cam.name}: {day}')
                return
        if cam.update_channel:
            async with db_in_thread():
                channels = db.query(Channel).filter(
                    Channel.cam == cam.name).all()
            for channel in channels:
                await send_video(Chat(self._bot, channel.chat_id), clip)
        await self.notify_admins(f'Daily movie for {cam.name}: {day} ready!')
        for chat in await self.admin_chats():
            await send_video(Chat(self._bot, chat.chat_id), clip)

    async def push_vk(self, chat, match):
        cam = await get_cam(match.group(1), chat)
        if not cam:
            return
        day = match.group(2)
        path = Path(conf.root_dir
                    ) / 'data' / cam.name / 'regular' / 'clips' / f'{day}.mp4'
        if not path.exists():
            await chat.send_text('Movie file does not exist!')
            return
        try:
            await self.vk_manager.new_post(cam.name, str(path),
                                           day.replace('_', ' '),
                                           day.replace('_', '/'))
        except vkmanager.VKManagerError as exc:
            logger.exception('Error during pushing video to vk')
            await chat.send_text(exc.detail)
        except Exception:
            logger.exception('Unhandled exception during pushing video to vk')
            await chat.send_text('Unhandled error!')
        await chat.send_text('Movie successfully published')

    async def mov(self, chat, match):
        """
        Make movie for specified cam and day. Example: /mov favcam 25_04_2019
        :param chat:
        :param match:
        :return:
        """
        cam = await get_cam(match.group(1), chat)
        if not cam:
            return
        day = match.group(2)
        loop = asyncio.get_event_loop()
        with concurrent.futures.ThreadPoolExecutor() as pool:
            try:
                clip = await loop.run_in_executor(pool,
                                                  lambda: make_movie(cam, day))
            except Exception:
                logger.exception('Error during movie request')
                await self.notify_admins(
                    f'Error during movie request {day} {cam.name}')
                return
        await self.notify_admins(f'Video ready. Uploading..')
        with open(clip.path, 'rb') as clip:
            await chat.send_video(clip)

    async def daily_movie_group(self):
        for cam in sorted(conf.cameras_list, key=lambda k: k.offset):
            if cam.render_daily:
                await self.daily_movie(cam)
        await self.daily_stats()

    async def daily_photo_group(self):
        for cam in conf.cameras_list:
            image = await CamHandler(cam,
                                     self._bot.session).get_img(regular=False)
            if not image:
                await self.notify_admins(
                    f'Error during image request for {cam.name}')
                continue
            path = image.original_path if cam.resize else image.path
            await self._post_photo(cam, path)

    async def daily_movie_group_command(self, chat, match):
        logger.info('Forced daily movie group command')
        await self.daily_movie_group()

    async def push_vk_on(self, chat: Chat, match):
        loop = asyncio.get_event_loop()
        cmd = f'systemctl --user start {conf.vk_service}'.split()
        try:
            await loop.run_in_executor(None, lambda: subprocess_call(cmd))
        except Exception:
            msg = 'Error during starting vk push service'
            logger.exception(msg)
            await chat.send_text(msg)
            return
        await chat.send_text('vk push service started')

    async def push_vk_off(self, chat: Chat, match):
        loop = asyncio.get_event_loop()
        cmd = f'systemctl --user stop {conf.vk_service}'.split()
        try:
            await loop.run_in_executor(None, lambda: subprocess_call(cmd))
        except Exception:
            msg = 'Error during stopping vk push service'
            logger.exception(msg)
            await chat.send_text(msg)
            return
        await chat.send_text('vk push service stopped')

    async def img_all_cams(self, chat: Chat, match):
        for cam in conf.cameras_list:
            await self.img_handler(chat, cam)

    async def img_handler(self, chat: Chat, cam):
        image = await CamHandler(cam, self._bot.session).get_img(regular=False)
        if not image:
            await chat.send_text(f'Error during image request for {cam.name}')
            return
        path = image.original_path if cam.resize else image.path
        markup = Markup([[
            InlineKeyboardButton(text='post',
                                 callback_data=f'post {cam.name} {path.name}')
        ]])
        with open(path, 'rb') as image:
            await chat.send_photo(image, reply_markup=markup.to_json())

    async def img_callback(self, chat, cq, match):
        await cq.answer()
        cam = await get_cam(match.group(1), chat)
        if not cam:
            return
        await self.img_handler(chat, cam)

    @ThreadSwitcherWithDB.optimized
    async def reg_channel(self, chat: Chat, match):
        async with db_in_thread():
            channel = db.query(Channel).filter(
                Channel.chat_id == chat.id).one_or_none()
        if channel:
            await self.notify_admins(f'Channel {chat.id} already registered!')
            return
        await chat.send_text('Choose cam for channel',
                             reply_markup=CamerasChannel().options.to_json())

    @ThreadSwitcherWithDB.optimized
    async def reg_photo_channel(self, chat: Chat, match):
        async with db_in_thread():
            channel = db.query(PhotoChannel).filter(
                PhotoChannel.chat_id == chat.id).one_or_none()
        if channel:
            await self.notify_admins(f'Channel {chat.id} already registered!')
            return
        await chat.send_text(
            'Choose cam for photo channel',
            reply_markup=CamerasChannel('choose_photo_cam').options.to_json())

    @ThreadSwitcherWithDB.optimized
    async def choose_cam_callback(self, chat, cq, match):
        cam = match.group(1)
        async with db_in_thread():
            channel = Channel(chat_id=chat.id, cam=cam)
            db.add(channel)
            db.commit()
        await cq.answer(text=f'Added channel for {cam}')
        await self.notify_admins(text=f'Added channel {chat.id} for {cam}')

    @ThreadSwitcherWithDB.optimized
    async def choose_photo_cam_callback(self, chat, cq, match):
        cam = match.group(1)
        async with db_in_thread():
            channel = PhotoChannel(chat_id=chat.id, cam=cam)
            db.add(channel)
            db.commit()
        await cq.answer(text=f'Added photo channel for {cam}')
        await self.notify_admins(
            text=f'Added photo channel {chat.id} for {cam}')

    async def post_photo(self, chat, cq, match):
        cam = match.group(1)
        photo = match.group(2)
        cam = conf.cameras[cam]
        path = Path(conf.root_dir) / 'data' / cam.name / 'imgs'
        if cam.resize:
            path /= 'original'
        path = path / '_'.join(photo.split('_')[:3]) / photo
        await self._post_photo(cam, path)
        await cq.answer()

    @ThreadSwitcherWithDB.optimized
    async def _post_photo(self, cam: Cam, photo: Path):
        async with db_in_thread():
            channels = db.query(PhotoChannel).filter(
                PhotoChannel.cam == cam.name).all()
        for channel in channels:
            chat = Chat(self._bot, channel.chat_id)
            with open(photo, 'rb') as ph:
                await chat.send_photo(ph)

    @ThreadSwitcherWithDB.optimized
    async def notify_admins(self, text, **options):
        async with db_in_thread():
            admins = db.query(Admin).all()
        for admin in admins:
            await self._bot.send_message(admin.chat_id, text, **options)

    @ThreadSwitcherWithDB.optimized
    async def admin_chats(self):
        async with db_in_thread():
            return db.query(Admin).all()

    async def menu(self, chat, match):
        await chat.send_text('Menu',
                             reply_markup=self.menu_markup.main_menu.to_json())

    async def select(self, chat: Chat, cq, match):
        await cq.answer()
        cam = match.group(1)
        await chat.edit_text(cq.src['message']['message_id'],
                             f'Camera: {cam}',
                             markup=dataclasses.asdict(
                                 self.menu_markup.cam_options[cam].markup))

    async def back(self, chat, cq, match):
        await cq.answer()
        await chat.edit_text(cq.src['message']['message_id'],
                             'Menu',
                             markup=dataclasses.asdict(
                                 self.menu_markup.main_menu))

    async def sync_gphotos(self, chat, cq, match):
        await cq.answer()
        cam = match.group(1)
        await chat.edit_text(cq.src['message']['message_id'],
                             f'Choose folder for {cam}',
                             markup=dataclasses.asdict(
                                 SyncFolders(cam).folders))

    async def run_sync_gphotos(self, chat, cq, match):
        _folder = match.group(1)
        folder = Path(conf.root_dir) / 'data' / _folder
        logger.debug(f'GOING TO SYNC FOLDER {folder}')
        await cq.answer(text=f'GOING TO SYNC FOLDER {folder}')
        await self.notify_admins(f'Started sync {folder}')
        try:
            await GooglePhotosManager().batch_upload(Path(folder))
        except Exception:
            logger.exception('Sync error!')
            await self.notify_admins(f'Error with {folder}!')
            return
        await self.notify_admins(f'{folder} successfully uploaded!')
        markup = Markup([[
            InlineKeyboardButton(text=f'{_folder}',
                                 callback_data=f'remove {_folder}')
        ]])
        await chat.send_text(f'Remove folder {folder.name}',
                             reply_markup=markup.to_json())

    async def remove_folder(self, chat, cq, match):
        await cq.answer(text='Removing folder..')
        folder = match.group(1)
        folder = Path(conf.root_dir) / 'data' / folder
        shutil.rmtree(folder)
        await chat.send_text('Successfully removed!')

    async def stats_command(self, chat: Chat, match):
        try:
            day = pendulum.from_format(match.group(1), 'DD_MM_YYYY')
        except IndexError:
            day = pendulum.today()
        await self.stats_request(day, chat.send_text)

    async def lstats_command(self, chat: Chat, match):
        try:
            day = pendulum.from_format(match.group(1), 'DD_MM_YYYY')
        except IndexError:
            day = pendulum.today()
        await self.stats_request(day, chat.send_text)

    @ThreadSwitcherWithDB.optimized
    async def db_data(self, chat: Chat, match):
        async with db_in_thread():
            md_data = db_data()
        await chat.send_text('\n'.join(md_data), parse_mode='Markdown')

    async def stats_request(self, day: pendulum.DateTime, send_command):
        logger.info(f'Getting stats info for {day}')
        try:
            markdown_result = await self.local_stats_handler(day)
        except Exception:
            logger.exception('Error during stats request')
            await send_command('Error during request stats')
            return
        day = day.format('DD_MM_YYYY')
        markup = Markup([[
            InlineKeyboardButton(text='check', callback_data=f'check_cb {day}')
        ], [
            InlineKeyboardButton(text='clear', callback_data=f'clear_cb {day}')
        ]])
        await send_command('\n'.join(markdown_result),
                           parse_mode='Markdown',
                           reply_markup=markup.to_json())

    async def stats_handler(self, day=None):
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(None, lambda: stats(day))
        album_stats = await self.agent.album_stats(day)
        markdown_result = [f'#stats *{day.format("DD/MM/YYYY")}*']
        for d in result['cameras']:
            stat = result['cameras'][d]
            count, size = stat['count'], convert_size(stat['size'])
            if count:
                avg = convert_size(stat['size'] / count)
            else:
                avg = 0
            media_count = album_stats[d]
            markdown_result.append(
                f'*{d}*: {count} - {media_count} - {size} - {avg} ')
        total = convert_size(result['total'])
        markdown_result.append(f'*total*: {total}')
        free = convert_size(result['free'])
        markdown_result.append(f'*free*: {free}')
        return markdown_result

    async def local_stats_handler(self, day=None):
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(None, lambda: stats(day))
        markdown_result = [f'#stats *{day.format("DD/MM/YYYY")}*']
        for d in result['cameras']:
            stat = result['cameras'][d]
            count, size = stat['count'], convert_size(stat['size'])
            if count:
                avg = convert_size(stat['size'] / count)
            else:
                avg = 0
            markdown_result.append(f'*{d}*: {count} - {size} - {avg} ')
        total = convert_size(result['total'])
        markdown_result.append(f'*total*: {total}')
        free = convert_size(result['free'])
        markdown_result.append(f'*free*: {free}')
        return markdown_result

    async def check_album(self, chat, match):
        cam = await get_cam(match.group(1), chat)
        if not cam:
            return
        day = match.group(2)
        await self.agent.check_album(cam, day)

    async def full_check_handler(self, chat, day):
        logger.info(f'Going to full check for {day}')
        for cam in conf.cameras_list:
            try:
                await self.agent.check_album(cam, day)
            except Exception:
                logger.exception(
                    f'Error during check and sync {cam.name} -- {day}')
                await chat.send_text(f'Error {cam.name} — {day}')
                continue
            await chat.send_text(f'Finished with {cam.name} — {day}')
        msg = f'Finished full check for {day}'
        logger.info(msg)
        await chat.send_text(msg)

    async def full_check(self, chat, match):
        day = match.group(1)
        await self.full_check_handler(chat, day)

    async def full_check_callback(self, chat, cq, match):
        day = match.group(1)
        await cq.answer(text=f'Running full check for {day}')
        await self.full_check_handler(chat, day)

    async def clear_handler(self, chat, day):
        logger.info(f'Going to clear for {day}')
        loop = asyncio.get_event_loop()
        for cam in conf.cameras_list:
            try:
                await loop.run_in_executor(None,
                                           lambda: clear_cam_storage(day, cam))
            except Exception:
                logger.exception(f'Error during clear {cam.name} -- {day}')
                await chat.send_text(f'Error {cam.name} — {day}')
                continue
            await chat.send_text(f'Finished with {cam.name} — {day}')
        logger.info(f'Finished clear for {day}')

    async def clear_command(self, chat, match):
        day = match.group(1)
        await self.clear_handler(chat, day)

    async def clear_callback(self, chat, cq, match):
        day = match.group(1)
        await cq.answer(text=f'Cleaning for {day}')
        await self.clear_handler(chat, day)
Example #2
0
class TeleGate(object):
    def __init__(self):
        self.ids = PickleDict('ids')
        self.members = PickleDict('members')
        self.ignored = PickleDict('ignored')
        self.cooldown = defaultdict(lambda: 0)
        self.dialogs = {}
        self.tripmap = {}
        self.bot = Bot(api_token=config.token, default_in_groups=True)
        for content_type in ['photo', 'video', 'audio', 'voice', 'document', 'sticker']:
            self.bot.handle(content_type)(self.handle_chat)
        self.bot.default(self.handle_chat)
        # self.bot.command(r'/set(icon|name|region|cooldown) (.+)')(self.set_user_prefs)
        # self.bot.command('help')(self.help)
        self.bot.command('setup')(self.setup)
        self.bot.command('start')(self.setup)
        self.bot.callback(r"setup-(\w+)")(self.setup_button_clicked)

    async def get_trip_flags(self):
        async with self.bot.session.get(f'https://{config.url}/js/tripflags.js') as s:
            data = await s.text()
            for l in data.splitlines():
                if l.startswith('flags_hover_strings'):
                    self.tripmap[l.split('"')[1]] = l.split('"')[3]

    async def post(self, body, name="Anonymous", convo="General", trip="", file="", country=None):
        if trip:
            name = '{}#{}'.format(name, trip)
        data = {
            'chat': config.board,
            'name': name,
            'trip': trip or '',
            'body': body,
            'convo': convo,
        }
        if country:
            data['country'] = country
        if file:
            data['image'] = open(file, 'rb')
        await self.bot.session.post(
                f'https://{config.url}/chat/{config.board}',
                data=data, cookies={'password_livechan': config.password_livechan}
            )

    async def get_posts(self, last_count=0, limit=30):
        params = {'count': last_count, 'limit': limit}
        async with self.bot.session.get(f'https://{config.url}/last/{config.board}', params=params) as s:
            data = await s.json()
            data.reverse()
            return data

    def send_gif(self, chat, animation, caption="", **options):
        return self.bot.api_call(
            "sendAnimation",
            chat_id=str(chat.id),
            animation=animation,
            caption=caption,
            **options
        )

    def get_member(self, chat):
        default = Member(chat.message['from']['first_name'], config.default_trip, None, config.default_cooldown)
        return Member(*self.members.get(chat.message['from']['id'], default))

    async def updater(self):
        last_count = 0
        post_data = {'count': 0}
        while True:
            try:
                await asyncio.sleep(config.poll_interval)
                group = self.bot.group(config.group_id)
                data = await self.get_posts(last_count)
                for post_data in data:
                    if post_data['identifier'] in self.ignored:
                        continue
                    if post_data['convo'] == 'General' and post_data['count'] not in self.ids:
                        res = None
                        country2 = emoji_flags.get_flag(post_data['country'].split('-')[0])
                        if '-' in post_data['country']:
                            country2 = '{}-{}'.format(country2, post_data['country'].split('-')[1])
                        body = '{} {} {} {}:\n{}'.format(post_data['count'], post_data['name'],
                                                         self.tripmap.get(post_data.get('trip'), ''), country2,
                                                         post_data['body'])
                        image = post_data.get('image')
                        reply_to = self.ids.get(int(post_data['body'].lstrip('>').split()[0]) if post_data['body'].startswith('>>') else None)
                        reply_to = {'reply_to_message_id':str(reply_to)} if reply_to else {}
                        if image:
                            image = 'https://{}{}'.format(config.url, image.split('public', 1)[1])
                            filename = image.split('/')[-1]
                            body = body[:1024]
                            async with self.bot.session.get(image) as f:
                                if f.status == 200:
                                    data = await f.read()
                                    with open('tmp/{}'.format(filename), 'wb') as f:
                                        f.write(data)
                                else:
                                    data = None
                            if data:
                                with open('tmp/{}'.format(filename), 'rb') as f:
                                    ext = os.path.splitext(image)[1]
                                    if ext in ['.png', '.jpg']:
                                        res = await group.send_photo(f, caption=body, **reply_to)
                                    elif ext in ['.gif']:
                                        res = await self.send_gif(group, f, caption=body, **reply_to)

                                    elif ext in ['.mp4']:
                                        res = await group.send_video(f, caption=body, **reply_to)
                                    elif ext in ['.mp3', '.ogg']:
                                        res = await group.send_audio(f, caption=body)
                                    elif ext == '.webm':
                                        body += f'\nhttps://{config.url}/tmp/uploads/' + filename
                                        res = await group.send_text(body, **reply_to)
                                os.unlink('tmp/{}'.format(filename))
                            else:
                                res = await group.send_text(body, **reply_to)
                        elif post_data['body']:
                            res = await group.send_text(body, **reply_to)

                        for st in re.findall(r'\[st\]([\w\d\-\.]+)\[\/st\]', body):
                            path = 'stickers/{}.png'.format(st)
                            if not os.path.exists(path):
                                async with self.bot.session.get(f'https://{config.url}/images/stickers/{st}.png') as f:
                                    if f.status == 200:
                                        data = await f.read()
                                        with open(path, 'wb') as f:
                                            f.write(data)
                                        with open(path, 'rb') as f:
                                            res2 = await group.send_photo(f)
                                            if not res:
                                                res = res2
                        if res:
                            self.ids[post_data['count']] = res['result']['message_id']
                            self.ids[res['result']['message_id']] = post_data['count']
            except Exception as e:
                traceback.print_exc()
            last_count = post_data['count']

    async def handle_chat(self, chat, image):
        if not chat.is_group():
            if chat.message['from']['id'] in self.dialogs:
                await self.setup(chat, image)
            return
        else:
            if chat.message['from']['id'] not in self.members:
                await self.setup(chat, image)
        if type(image) == list:
            image = image[-1]
        if 'file_id' in image:
            cq = chat.message
            text = chat.message.get('caption', '')
        else:
            cq = image
            text = cq['text']
        if 'reply_to_message' in cq:
            id = cq['reply_to_message']['message_id']
            if id in self.ids:
                text = '>>{}\n{}'.format(self.ids[id], text)
        id = image.get('file_id')
        if id:
            info = await self.bot.get_file(id)

            path = 'tmp/{}'.format(info['file_path'].split('/')[-1])
            if path.endswith('.oga'): path = path.replace('.oga', '.ogg')
            async with self.bot.download_file(info['file_path']) as res:
                data = await res.read()
                open(path, 'wb').write(data)
            if path.endswith('.webp'):
                newpath = path.replace('.webp', '.png')
                os.system('convert {} {}'.format(path, newpath)) # requires imagemagick
                path = newpath
            elif path.endswith('.tgs'):
                newpath = 'stickers/{}.gif'.format(image['file_id'])
                if not os.path.exists(newpath):
                    import tgs
                    from tgs.exporters import gif
                    a=tgs.parsers.tgs.parse_tgs(path)

                    with open(newpath, 'wb') as f:
                        gif.export_gif(a, f)
                os.unlink(path)
                path = newpath
        else:
            path = None
        member = self.get_member(chat)
        if not (time() > self.cooldown[cq['from']['id']] + member.cooldown_limit):
            return
        self.cooldown[cq['from']['id']] = time()
        await self.post(text, name=member.name, trip=member.trip, country=member.country, file=path)
        if path and path.startswith('tmp/'):
            os.unlink(path)
        await chat.delete_message(cq['message_id'])

    async def setup(self, chat, match):
        member = self.get_member(chat)
        id = chat.message['from']['id']
        if chat.is_group():
            chat = self.bot.private(chat.message['from']['id'])
        if id in self.dialogs:
            setattr(member, {'name': 'name', 'icon': 'trip', 'region': 'country'}[self.dialogs[id]], chat.message['text'])
            if member.trip == 'none':
                member.trip = None
            self.members[chat.message['from']['id']] = member
            del self.dialogs[id]

        buttons = []
        for button in ['name', 'icon', 'region']:
            buttons.append({
                "type": "InlineKeyboardButton",
                "text": "Set {}".format(button),
                "callback_data": "setup-{}".format(button),
            })
        markup = {
            "type": "InlineKeyboardMarkup",
            "inline_keyboard": [buttons]
        }
        chat.send_text(f"Name: {member.name}\nIcon: {member.trip}\nRegion: {member.country}", reply_markup=json.dumps(markup))

    def setup_button_clicked(self, chat, cq, match):
        if chat.is_group():
            chat = self.bot.private(chat.message['from']['id'])
        id = chat.message['chat']['id']
        param = match.group(1)
        self.dialogs[id] = param
        example = {
            'name': 'Kot',
            'icon': 'plkot; none for no icon',
            'region': 'PL-77 or RU-47',
        }[param]
        chat.send_text('Send your {}(for example: {})'.format(param, example))

    def run(self):
        loop = asyncio.get_event_loop()
        loop.create_task(self.updater())
        loop.create_task(self.get_trip_flags())
        self.bot.run()