async def check_floors(query: types.CallbackQuery, pois: List[POI], house: str = None): if not pois: kbd = types.InlineKeyboardMarkup().add( types.InlineKeyboardButton(tr('add_poi'), callback_data='new')) await bot.send_message(query.from_user.id, tr('no_poi_around'), reply_markup=kbd) return floors = set([p.floor for p in pois]) if len(floors) >= 2: khouse = '-' if house is None else house kbd = types.InlineKeyboardMarkup(row_width=3) for ifloor in floors: label = ifloor if ifloor is not None else tr( ('review', 'no_floor')) kbd.insert( types.InlineKeyboardButton(label, callback_data=FLOOR_CB.new( house=khouse, floor=ifloor or '-'))) kbd.insert( types.InlineKeyboardButton(tr(('review', 'all_floors')), callback_data=FLOOR_CB.new(house=khouse, floor='*'))) await bot.edit_message_reply_markup(query.from_user.id, query.message.message_id, reply_markup=kbd) else: # Just one floor, so doesn't matter await start_review(query.from_user, house)
async def print_edit_message(message: types.Message, state: FSMContext, attr: str, dash: bool = False, poi_attr: str = None, msg_attr: str = None, kbd=None, value='-', content: str = None): reply0 = None if value is not None: poi = (await state.get_data())['poi'] if value == '-': pvalue = getattr(poi, poi_attr or attr) else: pvalue = value(poi) if pvalue: reply0 = (await message.answer(str(pvalue))).message_id if not content: content = tr(('editor', msg_attr or attr)) if dash: content += ' ' + tr(('editor', 'dash')) if not kbd: kbd = cancel_attr_kbd() await delete_msg(message, state) reply = await message.answer(content, reply_markup=kbd, disable_web_page_preview=True) await EditState.attr.set() await state.update_data(attr=attr, reply=[reply.message_id, reply0])
async def edit_house(message: types.Message, state: FSMContext): poi = (await state.get_data())['poi'] houses = await db.get_houses() houses.sort(key=lambda h: poi.location.distance(h.location)) houses = houses[:3] # Prepare the map map_file = get_map([h.location for h in houses], ref=poi.location) # Prepare the keyboard kbd = types.InlineKeyboardMarkup(row_width=1) for i, house in enumerate(houses, 1): prefix = '✅ ' if house == poi.house else '' kbd.add( types.InlineKeyboardButton( f'{prefix} {i} {house.name}', callback_data=HOUSE_CB.new(hid=house.key))) kbd.add( types.InlineKeyboardButton(tr(('editor', 'cancel')), callback_data='cancel_attr')) # Finally send the reply await delete_msg(message, state) if map_file: await message.answer_photo(types.InputFile(map_file.name), caption=tr(('editor', 'house')), reply_markup=kbd) map_file.close() else: await message.answer(tr(('editor', 'house')), reply_markup=kbd)
async def message_info(message: types.Message): info = await get_user(message.from_user) if info.is_moderator(): await message.answer(tr('message_self')) return await message.answer(tr('message')) await MsgState.sending.set()
async def update_review(query: types.CallbackQuery, callback_data: Dict[str, str]): info = await get_user(query.from_user) if not info.review: await query.answer(tr(('review', 'no_review'))) return poi_id = int(callback_data['id']) review_record = [r for r in info.review if r[0] == poi_id] if not review_record: await query.answer(tr(('review', 'no_record'))) return if review_record[0][1]: # We have old updated, revert to it await db.set_updated(poi_id, review_record[0][1]) review_record[0][1] = None else: review_record[0][1] = await db.set_updated(poi_id) # Update keyboard pois = await db.get_poi_by_ids([r[0] for r in info.review]) kbd = await make_review_keyboard(pois) await bot.edit_message_reply_markup(query.from_user.id, query.message.message_id, reply_markup=kbd)
async def store_photo(query: types.CallbackQuery, callback_data: Dict[str, str], state: FSMContext): poi = (await state.get_data())['poi'] name = callback_data['name'] path = os.path.join(config.PHOTOS, name + '.jpg') if not os.path.exists(path): await query.answer(tr(('editor', 'photo_lost'))) return which = callback_data['which'] if which == 'out': poi.photo_out = name elif which == 'in': poi.photo_in = name elif which == 'unlink': if poi.photo_out == name: poi.photo_out = None elif poi.photo_in == name: poi.photo_in = None elif which == 'del': os.remove(path) await query.answer(tr(('editor', 'photo_deleted'))) else: await query.answer(tr(('editor', 'photo_forgot'))) await delete_msg(query) await state.set_data({'poi': poi}) await print_edit_options(query.from_user, state)
def edit_loc_kbd(poi): return types.InlineKeyboardMarkup().add( types.InlineKeyboardButton(tr(('editor', 'latlon')), url='https://zverik.github.io/latlon/#18/' f'{poi.location.lat}/{poi.location.lon}"'), types.InlineKeyboardButton(tr(('editor', 'cancel')), callback_data='cancel_attr'))
async def message_info_callback(query: types.CallbackQuery): info = await get_user(query.from_user) if info.is_moderator(): await query.answer(tr('message_self')) return await bot.send_message(query.from_user.id, tr('message')) await MsgState.sending.set()
async def process_queue(query: types.CallbackQuery, callback_data: Dict[str, str]): action = callback_data['action'] q = await db.get_queue_msg(int(callback_data['id'])) if not q: await query.answer(tr(('queue', 'missing'))) return if action == 'del': await db.delete_queue(q) await query.answer(tr(('queue', 'deleted'))) elif action == 'apply': await db.apply_queue(query.from_user.id, q) await query.answer(tr(('queue', 'applied'))) elif action == 'look': poi = await db.get_poi_by_id(q.poi_id) if not poi: await query.answer(tr(('queue', 'poi_lost_del'))) await db.delete_queue(q) else: await print_poi(query.from_user, poi, buttons=False) return else: await query.answer(f'Wrong queue action: "{action}"') await print_next_queued(query.from_user)
def location_keyboard(): return types.InlineKeyboardMarkup().add( types.InlineKeyboardButton( tr(('editor', 'latlon')), url='https://zverik.github.io/latlon/#16/53.9312/27.6525'), types.InlineKeyboardButton('❌ ' + tr('cancel'), callback_data='cancel'), )
async def new_poi(query: types.CallbackQuery): if config.MAINTENANCE: await bot.send_message(query.from_user.id, tr('maintenance')) return await EditState.name.set() await bot.send_message(query.from_user.id, tr(('new_poi', 'name')), reply_markup=cancel_keyboard())
async def validate_poi(query: types.CallbackQuery, callback_data: Dict[str, str]): poi = await db.get_poi_by_id(int(callback_data['id'])) if not poi: await query.answer(tr(('queue', 'poi_lost'))) return await db.validate_poi(poi.id) await query.answer(tr(('queue', 'validated_ok'))) await print_next_queued(query.from_user)
async def new_name(message: types.Message, state: FSMContext): name = message.text.strip() if len(name) < 3: await message.answer(tr(('new_poi', 'name_too_short'))) return await state.set_data({'name': name}) await EditState.location.set() await message.answer(tr(('new_poi', 'location')), reply_markup=location_keyboard())
def relative_day(next_day): days = (next_day.date() - datetime.now().date()).days if days < 1: opens_day = '' elif days == 1: opens_day = tr('tomorrow') else: opens_day = tr('relative_days')[next_day.weekday()] return opens_day
async def message_intro(message: types.Message, state: FSMContext): user = await get_user(message.from_user) if user.is_moderator(): await message.answer(tr(('editor', 'cant_message'))) return await delete_msg(message, state) reply = await message.answer(tr(('editor', 'message')), reply_markup=cancel_attr_kbd()) await EditState.message.set() await state.update_data(reply=reply.message_id)
async def process_reply(message: types.Message): info = await get_user(message.from_user) to = await get_user(message.reply_to_message.forward_from) if info.is_moderator(): # Notify other moderators that it has been replied # TODO: can we do it just once per user? await broadcast_str(tr('reply_sent', to.name), info.id, disable_notification=True) await bot.send_message(to.id, tr('do_reply')) await message.forward(to.id)
async def set_loc(message): location = Location(message.location.longitude, message.location.latitude) info = await get_user(message.from_user) info.location = location if info.is_moderator(): # Suggest review mode kbd = types.InlineKeyboardMarkup().add( types.InlineKeyboardButton(tr(('review', 'start')), callback_data='start_review')) else: kbd = get_buttons() await message.answer(tr('location'), reply_markup=kbd)
def boolean_kbd(attr: str): return types.InlineKeyboardMarkup().add( types.InlineKeyboardButton(tr(('editor', 'bool_true')), callback_data=BOOL_CB.new(attr=attr, value='true')), types.InlineKeyboardButton(tr(('editor', 'bool_false')), callback_data=BOOL_CB.new(attr=attr, value='false')), types.InlineKeyboardButton(tr(('editor', 'bool_none')), callback_data=BOOL_CB.new(attr=attr, value='null')), types.InlineKeyboardButton(tr(('editor', 'cancel')), callback_data='cancel_attr'))
async def new_cancel(query: types.CallbackQuery, state: FSMContext): await delete_msg(query, state) await state.finish() user = await get_user(query.from_user) if user.review: kbd = types.InlineKeyboardMarkup().add( types.InlineKeyboardButton('🗒️ ' + tr(('review', 'continue')), callback_data='continue_review')) else: kbd = get_buttons() await bot.send_message(query.from_user.id, tr(('new_poi', 'cancel')), reply_markup=kbd)
async def edit_links(message: types.Message, state: FSMContext): poi = (await state.get_data())['poi'] if poi.links: content = tr(('editor', 'links_have')) + '\n\n' content += '\n'.join([f'🔗 {h(l[0])}: {h(l[1])}' for l in poi.links]) else: content = tr(('editor', 'no_links')) content += '\n\n' + tr(('editor', 'links')) await print_edit_message(message, state, 'links', value=None, content=content)
async def new_keywords(message: types.Message, state: FSMContext): keywords = split_tokens(message.text) if not keywords: await message.answer(tr(('new_poi', 'no_keywords'))) return # Create a POI data = await state.get_data() poi = POI(name=data['name'], location=Location(lat=data['lat'], lon=data['lon']), keywords=' '.join(keywords)) await state.set_data({'poi': poi}) await EditState.confirm.set() await print_edit_options(message.from_user, state, tr(('new_poi', 'confirm')))
async def new_location(message: types.Message, state: FSMContext): loc = parse_location(message) if not loc: await message.answer(tr(('new_poi', 'no_location')), reply_markup=location_keyboard()) return if not valid_location(loc): await message.answer(tr(('new_poi', 'location_out')), reply_markup=location_keyboard()) return await state.update_data(lon=loc.lon, lat=loc.lat) await EditState.keywords.set() await message.answer(tr(('new_poi', 'keywords')), reply_markup=cancel_keyboard())
async def simlar_poi(query: types.CallbackQuery, callback_data: Dict[str, str], state: FSMContext): poi = await db.get_poi_by_id(int(callback_data['id'])) if not poi or not poi.tag: await query.answer(tr('query_fail')) else: pois = await db.get_poi_by_tag(poi.tag) if len(pois) == 1: await query.answer(tr('no_similar')) else: tag_names = config.TAGS['tags'].get(poi.tag) pquery = poi.tag if not tag_names else tag_names[0] await PoiState.poi_list.set() await state.set_data({'query': pquery, 'poi': [p.id for p in pois]}) await print_poi_list(query.from_user, pquery, pois, relative_to=poi.location)
async def undelete_poi(message: types.Message, state: FSMContext): user = await get_user(message.from_user) if not user.is_moderator(): await message.answer(tr(('editor', 'cant_restore'))) return poi = (await state.get_data())['poi'] await db.restore_poi(message.from_user.id, poi) await state.finish() if user.review: kbd = types.InlineKeyboardMarkup().add( types.InlineKeyboardButton('🗒️ ' + tr(('review', 'continue')), callback_data='continue_review')) else: kbd = get_buttons() await message.answer(tr(('editor', 'restored')), reply_markup=kbd)
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 add_mod(message: types.Message, state: FSMContext): if message.from_user.id != config.ADMIN: raise SkipHandler if not message.is_forward(): await message.answer(tr(('admin', 'forward'))) return await state.finish() me = await get_user(message.from_user) new_user = await get_user(message.forward_from) if new_user.is_moderator(): await message.answer(tr(('admin', 'mod_already'))) return await db.add_user_to_role(new_user, 'moderator', me) forget_user(new_user.id) await message.answer(tr(('admin', 'mod_added'), new_user.name)) await bot.send_message(new_user.id, tr(('admin', 'mod_you')))
async def in_house_callback(query: types.CallbackQuery, callback_data: Dict[str, str], state: FSMContext): house = callback_data['house'] floor = callback_data['floor'] data = await db.get_poi_by_key(house) pois = await db.get_poi_by_house(house, None if floor == '-' else floor) if floor == '-' and len(pois) > 9: floors = await db.get_floors_by_house(house) if len(floors) >= 2 and None not in floors: # We have floors - add another selection kbd = types.InlineKeyboardMarkup(row_width=3) for ifloor in floors: kbd.insert(types.InlineKeyboardButton( ifloor, callback_data=POI_HOUSE_CB.new(house=house, floor=ifloor))) await bot.edit_message_reply_markup( query.from_user.id, query.message.message_id, reply_markup=kbd) return if not pois: await query.answer(tr('no_poi_in_house')) elif len(pois) == 1: await PoiState.poi.set() await state.set_data({'poi': pois[0].id}) await print_poi(query.from_user, pois[0]) else: await PoiState.poi_list.set() await state.set_data({'query': query, 'poi': [p.id for p in pois]}) await print_poi_list(query.from_user, data.name, pois, True)
async def print_next_added(user: types.User): info = await get_user(user) if not info.is_moderator(): return False poi = await db.get_next_unchecked() if not poi: await bot.send_message(user.id, tr(('queue', 'empty'))) return True await print_poi(user, poi) content = tr(('queue', 'new_poi')) kbd = types.InlineKeyboardMarkup().add( types.InlineKeyboardButton( '✔️ ' + tr(('queue', 'validated')), callback_data=POI_VALIDATE_CB.new(id=str(poi.id)))) await bot.send_message(user.id, content, reply_markup=kbd)
def parse_hours(s): def norm_hour(h): if not h: return None if len(h) < 4: h += ':00' return h.replace('.', ':').rjust(5, '0') if s in ('24', '24/7'): return '24/7' HOURS_WEEK = {tr(('editor', 'hours_abbr'))[i]: DOW[i] for i in range(7)} HOURS_WEEK.update({DOW[i].lower(): DOW[i] for i in range(7)}) parts = [] for part in s.split(','): m = RE_HOURS.match(part.strip().lower()) if not m: raise ValueError(part) wd = 'Mo-Su' if not m.group(1) else HOURS_WEEK[m.group(1)] if m.group(2): wd += '-' + HOURS_WEEK[m.group(2)] h1 = norm_hour(m.group(3)) h2 = norm_hour(m.group(4)) l1 = norm_hour(m.group(5)) l2 = norm_hour(m.group(6)) if l1: wd += f' {h1}-{l1},{l2}-{h2}' else: wd += f' {h1}-{h2}' parts.append(wd) return '; '.join(parts)
async def delete_poi_prompt(message: types.Message, state: FSMContext): poi = (await state.get_data())['poi'] if poi.delete_reason: user = await get_user(message.from_user) if not user.is_moderator(): await message.answer(tr(('editor', 'delete_twice'))) else: await db.delete_poi_forever(user.id, poi) await state.finish() await message.answer(tr(('editor', 'deleted2')), reply_markup=get_buttons()) else: await message.answer(tr(('editor', 'delete')), reply_markup=cancel_attr_kbd()) await EditState.attr.set() await state.update_data(attr='delete')