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)
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()