def describe_poi(poi: POI): deleted = '' if not poi.delete_reason else ' 🗑️' result = [f'<b>{h(poi.name)}</b>{deleted}'] if poi.description: result.append(h(poi.description)) part2 = [] if poi.hours: if poi.hours.is_24_7: part2.append('🌞 ' + tr('open_247')) elif poi.hours.is_open(): closes = poi.hours.next_change() open_now = '☀️ ' + tr('now_open', closes.strftime("%H:%M")) if (closes - datetime.now()).seconds <= 3600 * 2: opens = poi.hours.next_change(closes) open_now += ' ' + tr('next_open', day=relative_day(opens).capitalize(), hour=opens.strftime("%H:%M").lstrip("0")) part2.append(open_now) else: opens = poi.hours.next_change() part2.append('🌒 ' + tr('now_closed', day=relative_day(opens), hour=opens.strftime("%H:%M").lstrip("0"))) if poi.links and len(poi.links) > 1: part2.append('🌐 ' + tr('poi_links') + ': {}.'.format(', '.join([ '<a href="{}">{}</a>'.format(h(link[1]), h(link[0])) for link in poi.links ]))) if poi.house_name or poi.address_part: address = ', '.join([ s for s in (poi.house_name, uncap(poi.floor), uncap(poi.address_part)) if s ]) part2.append(f'🏠 {address}.') if poi.has_wifi is True: part2.append('📶 ' + tr('has_wifi')) if poi.accepts_cards is True: part2.append('💳 ' + tr('accepts_cards')) elif poi.accepts_cards is False: part2.append('💰 ' + tr('no_cards')) if poi.phones: part2.append('📞 {}.'.format(', '.join( [re.sub(r'[^0-9+]', '', phone) for phone in poi.phones]))) if part2: result.append('') result.extend(part2) if poi.comment: result.append('') result.append(poi.comment) return '\n'.join(result)
def describe_poi(poi: POI): deleted = '' if not poi.delete_reason else ' 🗑️' result = [f'<b>{h(poi.name)}</b>{deleted}'] if poi.description: result.append(h(poi.description)) part2 = [] if poi.hours: if poi.hours.is_24_7: part2.append('🌞 Открыто круглосуточно.') elif poi.hours.is_open(): closes = poi.hours.next_change() open_now = f'☀️ Открыто сегодня до {closes.strftime("%H:%M")}.' if (closes - datetime.now()).seconds <= 3600 * 2: opens = poi.hours.next_change(closes) open_now += (f' {relative_day(opens).capitalize()} работает ' f'с {opens.strftime("%H:%M").lstrip("0")}.') part2.append(open_now) else: opens = poi.hours.next_change() part2.append(f'🌒 Закрыто. Откроется {relative_day(opens)} ' f'в {opens.strftime("%H:%M").lstrip("0")}.') if poi.links and len(poi.links) > 1: part2.append('🌐 Ссылки: {}.'.format(', '.join([ '<a href="{}">{}</a>'.format(h(link[1]), h(link[0])) for link in poi.links ]))) if poi.house_name or poi.address_part: address = ', '.join([ s for s in (poi.house_name, uncap(poi.floor), uncap(poi.address_part)) if s ]) part2.append(f'🏠 {address}.') if poi.has_wifi is True: part2.append('📶 Есть бесплатный Wi-Fi.') if poi.accepts_cards is True: part2.append('💳 Можно оплатить картой.') elif poi.accepts_cards is False: part2.append('💰 Оплата только наличными.') if poi.phones: part2.append('📞 {}.'.format(', '.join( [re.sub(r'[^0-9+]', '', phone) for phone in poi.phones]))) if part2: result.append('') result.extend(part2) if poi.comment: result.append('') result.append(poi.comment) return '\n'.join(result)
def format(v, yes=None, no=None, null=None): if v is None or v == '': return f'<i>{null or tr(("editor", "unknown"))}</i>' if isinstance(v, str): return h(v) if isinstance(v, bool): return (yes or tr(('editor', 'bool_yes'))) if v else (no or tr( ('editor', 'bool_no'))) if isinstance(v, (int, float)): return v if isinstance(v, hoh.OHParser): return h(v.field) if isinstance(v, Location): return f'v.lat, v.lon' return str(v)
async def help(message: types.Message, state: FSMContext): await state.finish() msg = config.RESP['help'] stats = await db.get_stats() for k, v in stats.items(): msg = msg.replace('{' + k + '}', h(str(v))) await message.answer(msg, parse_mode=HTML, disable_web_page_preview=True, reply_markup=get_buttons())
async def print_audit(user: types.User): content = 'Последние операции:' last_audit = await db.get_last_audit(15) for a in last_audit: user_name = a.user_name if a.user_id == a.approved_by else str( a.user_id) if user_name is None: user_name = 'Администратор' line = f'{a.ts.strftime("%Y-%m-%d %H:%S")} {user_name}' if a.approved_by != a.user_id: line += f' (подтвердил {a.user_name})' if a.field == 'poi': line += ' создал' if not a.old_value else ' удалил' line += f' «{a.poi_name}» /poi{a.poi_id}' else: line += (f' изменил у «{a.poi_name}» /poi{a.poi_id} ' f'поле {a.field}: "{a.old_value}" → "{a.new_value}"') content += '\n\n' + h(line) + '.' await bot.send_message(user.id, content, disable_web_page_preview=True)
async def print_audit(user: types.User): content = tr(('admin', 'audit')) + ':' last_audit = await db.get_last_audit(15) for a in last_audit: user_name = a.user_name if a.user_id == a.approved_by else str( a.user_id) if user_name is None: user_name = tr(('admin', 'admin')) line = f'{a.ts.strftime("%Y-%m-%d %H:%S")} {user_name}' if a.approved_by != a.user_id: line += ' (' + tr(('admin', 'confirmed_by'), a.user_name) + ')' if a.field == 'poi': line += ' ' + tr( ('admin', 'created' if not a.old_value else 'deleted')) line += f' «{a.poi_name}» /poi{a.poi_id}' else: line += (' ' + tr(('admin', 'modified')) + f' «{a.poi_name}» /poi{a.poi_id} ' + tr( ('admin', 'field')) + f' {a.field}: "{a.old_value}" → "{a.new_value}"') content += '\n\n' + h(line) + '.' await bot.send_message(user.id, content, disable_web_page_preview=True)
async def print_next_queued(user: types.User): info = await get_user(user) if not info.is_moderator(): return False queue = await db.get_queue(1) if not queue: # This is done inside print_next_added() # await bot.send_message(user.id, tr(('queue', 'empty'))) await print_next_added(user) return True q = queue[0] poi = await db.get_poi_by_id(q.poi_id) if not poi: await bot.send_message(user.id, tr(('queue', 'poi_lost_del'))) await db.delete_queue(q) return True photo = None if q.field == 'message': content = tr(('queue', 'message'), user=h(q.user_name), name=h(poi.name)) content += f'\n\n{h(q.new_value)}' else: content = tr(('queue', 'field'), user=h(q.user_name), name=h(poi.name), field=q.field) content += '\n' nothing = '<i>' + tr(('queue', 'nothing')) + '</i>' vold = nothing if q.old_value is None else h(q.old_value) vnew = nothing if q.new_value is None else h(q.new_value) content += f'\n<b>{tr(("queue", "old"))}:</b> {vold}' content += f'\n<b>{tr(("queue", "new"))}:</b> {vnew}' if q.field in ('photo_in', 'photo_out') and q.new_value: photo = os.path.join(config.PHOTOS, q.new_value + '.jpg') if not os.path.exists(photo): photo = None kbd = types.InlineKeyboardMarkup(row_width=3) if q.field != 'message': kbd.insert( types.InlineKeyboardButton('🔍 ' + tr(('queue', 'look')), callback_data=MSG_CB.new(action='look', id=str(q.id)))) kbd.insert( types.InlineKeyboardButton('✅ ' + tr(('queue', 'apply')), callback_data=MSG_CB.new(action='apply', id=str(q.id)))) else: kbd.insert( types.InlineKeyboardButton('📝 ' + tr('edit_poi'), callback_data=POI_EDIT_CB.new( id=q.poi_id, d='0'))) kbd.insert( types.InlineKeyboardButton('❌ ' + tr(('queue', 'delete')), callback_data=MSG_CB.new(action='del', id=str(q.id)))) if not photo: await bot.send_message(user.id, content, parse_mode=HTML, reply_markup=kbd) else: await bot.send_photo(user.id, types.InputFile(photo), caption=content, parse_mode=HTML, reply_markup=kbd) return True
async def print_poi_list(user: types.User, query: str, pois: List[POI], full: bool = False, shuffle: bool = True, relative_to: Location = None, comment: str = None): max_buttons = 9 if not full else 20 location = (await get_user(user)).location or relative_to if shuffle: if location: pois.sort(key=lambda p: location.distance(p.location)) else: random.shuffle(pois) stars = await db.stars_for_poi_list(user.id, [p.id for p in pois]) if stars: pois.sort(key=lambda p: star_sort(stars.get(p.id)), reverse=True) pois.sort(key=lambda p: bool(p.hours) and not p.hours.is_open()) total_count = len(pois) all_ids = pack_ids([p.id for p in pois]) if total_count > max_buttons: pois = pois[:max_buttons if full else max_buttons - 1] # Build the message content = tr('poi_list', query) + '\n' for i, poi in enumerate(pois, 1): if poi.description: content += h(f'\n{i}. {poi.name} — {uncap(poi.description)}') else: content += h(f'\n{i}. {poi.name}') if poi.hours and not poi.hours.is_open(): content += ' 🌒' if total_count > max_buttons: if not full: content += '\n\n' + tr('poi_not_full', total_count=total_count) else: content += '\n\n' + tr('poi_too_many', total_count=total_count) if comment: content += '\n\n' + comment # Prepare the inline keyboard if len(pois) == 4: kbd_width = 2 else: kbd_width = 4 if len(pois) > 9 else 3 kbd = types.InlineKeyboardMarkup(row_width=kbd_width) for i, poi in enumerate(pois, 1): b_title = f'{i} {poi.name}' kbd.insert( types.InlineKeyboardButton( b_title, callback_data=POI_LIST_CB.new(id=poi.id))) if total_count > max_buttons and not full: try: callback_data = POI_FULL_CB.new(query=query[:55], ids=all_ids) except ValueError: # Too long callback_data = POI_FULL_CB.new(query=query[:55], ids='-') kbd.insert( types.InlineKeyboardButton(f'🔽 {config.MSG["all"]} {total_count}', callback_data=callback_data)) # Make a map and send the message map_file = get_map([poi.location for poi in pois], ref=location) if not map_file: await bot.send_message(user.id, content, parse_mode=HTML, reply_markup=kbd) else: await bot.send_photo(user.id, types.InputFile(map_file.name), caption=content, parse_mode=HTML, reply_markup=kbd) map_file.close()
async def print_poi(user: types.User, poi: POI, comment: str = None, buttons: bool = True): log_poi(poi) chat_id = user.id content = describe_poi(poi) if comment: content += '\n\n' + h(comment) # Prepare photos photos = [] photo_names = [] for photo in [poi.photo_in, poi.photo_out]: if photo: path = os.path.join(config.PHOTOS, photo + '.jpg') if os.path.exists(path): file_ids = await db.find_file_ids( {photo: os.path.getsize(path)}) if photo in file_ids: photos.append(file_ids[photo]) photo_names.append(None) else: photos.append(types.InputFile(path)) photo_names.append([photo, os.path.getsize(path)]) # Generate a map location = (await get_user(user)).location map_file = get_map([poi.location], location) if map_file: photos.append(types.InputFile(map_file.name)) photo_names.append(None) # Prepare the inline keyboard if poi.tag == 'building': kbd = await make_house_keyboard(user, poi) else: kbd = None if not buttons else await make_poi_keyboard(user, poi) # Send the message if not photos: msg = await bot.send_message(chat_id, content, parse_mode=HTML, reply_markup=kbd, disable_web_page_preview=True) elif len(photos) == 1: msg = await bot.send_photo(chat_id, photos[0], caption=content, parse_mode=HTML, reply_markup=kbd) else: media = types.MediaGroup() for i, photo in enumerate(photos): if not kbd and i == 0: photo = types.input_media.InputMediaPhoto(photo, caption=content, parse_mode=HTML) media.attach_photo(photo) if kbd: msg = await bot.send_media_group(chat_id, media=media) await bot.send_message(chat_id, content, parse_mode=HTML, reply_markup=kbd, disable_web_page_preview=True) else: msg = await bot.send_media_group(chat_id, media=media) if map_file: map_file.close() # Store file_ids for new photos if isinstance(msg, list): file_ids = [m.photo[-1].file_id for m in msg if m.photo] else: file_ids = [msg.photo[-1]] if msg.photo else [] for i, file_id in enumerate(file_ids): if photo_names[i]: await db.store_file_id(photo_names[i][0], photo_names[i][1], file_id)
async def print_next_queued(user: types.User): info = await get_user(user) if not info.is_moderator(): return False queue = await db.get_queue(1) if not queue: # This is done inside print_next_added() # await bot.send_message(user.id, config.MSG['queue']['empty']) await print_next_added(user) return True q = queue[0] poi = await db.get_poi_by_id(q.poi_id) if not poi: await bot.send_message(user.id, 'POI пропал, странно. Удаляю запись.') await db.delete_queue(q) return True photo = None if q.field == 'message': content = config.MSG['queue']['message'].format(user=h(q.user_name), name=h(poi.name)) content += f'\n\n{h(q.new_value)}' else: content = config.MSG['queue']['field'].format(user=h(q.user_name), name=h(poi.name), field=q.field) content += '\n' vold = '<i>ничего</i>' if q.old_value is None else h(q.old_value) vnew = '<i>ничего</i>' if q.new_value is None else h(q.new_value) content += f'\n<b>Сейчас:</b> {vold}' content += f'\n<b>Будет:</b> {vnew}' if q.field in ('photo_in', 'photo_out') and q.new_value: photo = os.path.join(config.PHOTOS, q.new_value + '.jpg') if not os.path.exists(photo): photo = None kbd = types.InlineKeyboardMarkup(row_width=3) if q.field != 'message': kbd.insert( types.InlineKeyboardButton(config.MSG['queue']['look'], callback_data=MSG_CB.new(action='look', id=str(q.id)))) kbd.insert( types.InlineKeyboardButton(config.MSG['queue']['apply'], callback_data=MSG_CB.new(action='apply', id=str(q.id)))) else: kbd.insert( types.InlineKeyboardButton('📝 Поправить', callback_data=POI_EDIT_CB.new( id=q.poi_id, d='0'))) kbd.insert( types.InlineKeyboardButton(config.MSG['queue']['delete'], callback_data=MSG_CB.new(action='del', id=str(q.id)))) if not photo: await bot.send_message(user.id, content, parse_mode=HTML, reply_markup=kbd) else: await bot.send_photo(user.id, types.InputFile(photo), caption=content, parse_mode=HTML, reply_markup=kbd) return True
async def print_edit_options(user: types.User, state: FSMContext, comment=None): poi = (await state.get_data())['poi'] lines = [] m = tr(('editor', 'panel')) lines.append(f'<b>{format(poi.name)}</b>') lines.append('') lines.append( f'/edesc <b>{m["desc"]}:</b> {format(poi.description, null=m["none"])}' ) lines.append(f'/ekey <b>{m["keywords"]}:</b> {format(poi.keywords)}') lines.append(f'/etag <b>{m["tag"]}:</b> {format(poi.tag)}') lines.append(f'/ehouse <b>{m["house"]}:</b> {format(poi.house_name)}') lines.append(f'/efloor <b>{m["floor"]}:</b> {format(poi.floor)}') lines.append(f'/eaddr <b>{m["addr"]}:</b> {format(poi.address_part)}') lines.append(f'/ehour <b>{m["hours"]}:</b> {format(poi.hours_src)}') lines.append(f'/eloc <b>{m["loc"]}:</b> ' '<a href="https://zverik.github.io/latlon/#18/' f'{poi.location.lat}/{poi.location.lon}">' f'{m["loc_browse"]}</a>') lines.append( f'/ephone <b>{m["phone"]}:</b> {format("; ".join(poi.phones))}') lines.append(f'/ewifi <b>{m["wifi"]}:</b> {format(poi.has_wifi)}') lines.append(f'/ecard <b>{m["card"]}:</b> {format(poi.accepts_cards)}') if poi.links: links = ', '.join( [f'<a href="{l[1]}">{h(l[0])}</a>' for l in poi.links]) else: links = f'<i>{m["none"]}</i>' lines.append(f'/elink <b>{m["links"]}:</b> {links}') lines.append( f'/ecom <b>{m["comment"]}:</b> {format(poi.comment, null=m["none"])}') if poi.photo_out and poi.photo_in: photos = m['photo_both'] elif poi.photo_out: photos = m['photo_out'] elif poi.photo_in: photos = m['photo_in'] else: photos = m['none'] lines.append(f'<b>{m["photo"]}:</b> {photos} ({m["photo_comment"]})') if poi.id: if poi.delete_reason: lines.append( f'<b>{m["deleted"]}:</b> {format(poi.delete_reason)}. ' f'{m["restore"]}: /undelete') else: lines.append(f'🗑️ {m["delete"]}: /delete') lines.append(f'✉️ {m["msg"]}: /msg') content = '\n'.join(lines) if comment is None: comment = tr(('new_poi', 'confirm2')) if comment: content += '\n\n' + h(comment) reply = await bot.send_message(user.id, content, parse_mode=HTML, reply_markup=new_keyboard(), disable_web_page_preview=True) await state.update_data(reply=reply.message_id)