def handle_edited_message(self, update: telegram.update.Update, context: CallbackContext): # NOTE: work in progress # TODO: finish implement editing old Messages using edited_messages context.bot.send_chat_action(chat_id=update.effective_message.chat_id, action=ChatAction.TYPING) print(json.dumps(update.to_dict(), indent=2)) message_identifier = get_message_identifier(update) msg = Message.objects.get(source_id=message_identifier) msg_text = clean_message_text(self.get_message_text(update)) if msg_text == "": # Do nothing if message was empty # TODO: delete original message in this case? return msg.text = msg_text msg.json = json.dumps(update.to_dict()) msg.update_record() msg.save() res_msg = [] if hasattr(msg, "record"): rec: Record = msg.record kw_str = rec.name words = [_("Edit:"), rec.type] + create_message( kw_str, rec) + [rec.time.strftime(" @%H:%M")] res_msg.append(" ".join(words)) else: res_msg.append(_("Message.record doesn't exist.") + " 🧐") context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(res_msg))
def handle_reply(self, update: telegram.update.Update, context: CallbackContext): """ Handle replies to earlier Messages. Doesn't create and save a Record object. :param update: telegram Update object :param context: telegram CallbackContext """ res_msg = [] updict = update.to_dict() print(json.dumps(updict, indent=2)) msg = self.get_message(updict) reply_message_identifier = format_message_identifier( msg["reply_to_message"]["chat"]["id"], msg["reply_to_message"]["message_id"]) try: reply_to_msg = Message.objects.get( source_id=reply_message_identifier) new_msg = self.create_logbook_message(update, save=False) new_msg.reply_message = reply_to_msg new_msg.save() res_msg.append(_("Thanks for the reply!")) except Message.DoesNotExist: res_msg.append( _("Replied message doesn't exist, perhaps it is sent by Bot or it has been deleted?" )) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(res_msg))
def choice_cmd(self, update: telegram.update.Update, context: CallbackContext): self.setenv() print(json.dumps(update.to_dict(), indent=2)) msg = [] count = 10 words = update.message.text.split() words.pop(0) # Remove /search if len(words) == 0: msg.append(_("Add search string after search command")) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg)) return query = Q() for w in words: query = query & Q(description__icontains=w) records = Record.objects.filter(query).order_by("-time") if records: reply_text = _( "Found some records. Choose one of these or cancel:") options = options_for_records(records, count) options.insert(0, [_("Cancel"), ""]) reply_markup = create_buttons_edit(options) update.message.reply_text(reply_text, reply_markup=reply_markup) return else: msg.append( _("No records for search string %(kw_str)s found.") % {"kw_str": " ".join(words)}) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg), parse_mode=telegram.ParseMode.HTML)
def set_media_group_id(self, update: telegram.update.Update, msg_text: str) -> str: """ If media_group_id is present, this Update is part of multi-file post. Store msg_text for future use. :param update: Telegram Update :param msg_text: text or caption from Update :return: """ updict = update.to_dict() msg = self.get_message(updict) # First Update has media_group_id and text/caption present if "media_group_id" in msg: if msg_text != "": self.media_group_ids[msg["media_group_id"]] = { "text": msg_text, "time": time.time(), } elif msg["media_group_id"] in self.media_group_ids: msg_text = self.media_group_ids[msg["media_group_id"]]["text"] self.media_group_ids[ msg["media_group_id"]]["time"] = time.time() # Clean up over 60 seconds old media_groups for k in list(self.media_group_ids.keys()): if self.media_group_ids[k]["time"] + 60 < time.time(): self.media_group_ids.pop(k) return msg_text
def search_cmd(self, update: telegram.update.Update, context: CallbackContext): self.setenv() print(json.dumps(update.to_dict(), indent=2)) msg = [] count = 10 words = update.message.text.split() words.pop(0) # Remove /search if len(words) == 0: msg.append(_("Add search string after search command")) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg)) return search_str = words.pop(0) records = Record.objects.filter( description__icontains=search_str).order_by("-time") if records: last_dstr = "" for r in records[:count]: dstr = r.time.astimezone(pytz.timezone( self.timezone)).strftime("%d.%m.%Y") tstr = r.time.astimezone(pytz.timezone( self.timezone)).strftime("%H:%M") if last_dstr != dstr: msg.append("<code>{}</code>".format(dstr)) last_dstr = dstr msg.append("<b>{}</b> {}".format( tstr, " ".join(create_message(r.name, r)))) else: msg.append( _("No records for search string %(kw_str)s found.") % {"kw_str": search_str}) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg), parse_mode=telegram.ParseMode.HTML)
def handle_media_group_id(self, update: telegram.update.Update) -> Optional[str]: """ If media_group_id is present, this Update is part of multi-file post. It is not guaranteed that multiple Updates are received in order. :param update: Telegram Update :return: """ cache_time = 600 # seconds updict = update.to_dict() msg = self.get_message(updict) mgid = msg.get("media_group_id") if mgid: if mgid not in self.media_group_ids: self.media_group_ids[mgid] = { "time": time.time(), "updates": [update], } else: self.media_group_ids[mgid]["updates"].append(update) # Clean up over `cache_time` seconds old media_groups for k in list(self.media_group_ids.keys()): if self.media_group_ids[k]["time"] + cache_time < time.time(): self.media_group_ids.pop(k) return mgid
def get_file_data(self, update: telegram.update.Update, context: CallbackContext): """ Get data of certain attachment types from Telegram API. "photo" is a special case, because it may contain several instances in a list. :param update: Telegram Update object :param context: :return: audio (telegram.Audio, optional) document (telegram.Document, optional) animation (telegram.Animation, optional) game (telegram.Game, optional) photo (List[telegram.PhotoSize], optional) sticker (telegram.Sticker, optional) video (telegram.Video, optional) voice (telegram.Voice, optional) video_note (telegram.VideoNote, optional) """ _file = {} # contains all available data of file attachment if update.message.photo: # Loop over photo (List) logging.info("photo found") max_width = biggest = 0 for i in range(len(update.message.photo) ): # Pick largest image and store only it if max_width < update.message.photo[i]["width"]: max_width = update.message.photo[i]["width"] biggest = i _file["type"] = "photo" _file["id"] = update.message.photo[biggest]["file_id"] for _ttype in ["audio", "document", "video", "voice", "video_note"]: # Other document types if update.message[_ttype]: doc = update.to_dict()["message"][ _ttype] # shortcut to message logging.info(f"{_ttype} found") _file["type"] = _ttype _file["mime_type"] = doc["mime_type"] _file["id"] = doc["file_id"] if doc.get("file_name"): _file["name"] = doc["file_name"] break if "id" in _file: url = f"https://api.telegram.org/bot{self.token}/getFile?file_id={_file['id']}" res = requests.get(url) filedata = res.json() if filedata["ok"]: file_path = filedata["result"]["file_path"] _file[ "url"] = f"https://api.telegram.org/file/bot{self.token}/{file_path}" if "name" not in _file: _file["name"] = os.path.basename(file_path) if "mime_type" not in _file: _file["mime_type"] = mimetypes.types_map.get( os.path.splitext(_file["name"])[1], "") return _file else: logging.warning(filedata)
def handle_edited_message(self, update: telegram.update.Update, context: CallbackContext): context.bot.send_chat_action(chat_id=update.effective_message.chat_id, action=ChatAction.TYPING) print(json.dumps(update.to_dict(), indent=2)) res_msg = ["Thank you for editing a message."] t = self.get_message_text(update) print(f"EDITED {t}") context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(res_msg))
def get_message_identifier(update: telegram.update.Update) -> Optional[str]: """ Find out chat id and message_id from message. :param update: Telegram Update :return: message identifier """ updict = update.to_dict() for msg_key in ["message", "edited_message"]: if msg_key in updict: message = updict[msg_key] return format_message_identifier(message["chat"]["id"], message["message_id"])
def show_cmd(self, update: telegram.update.Update, context: CallbackContext): self.setenv() print(json.dumps(update.to_dict(), indent=2)) msg = [] count = 10 words = update.message.text.split() words.pop(0) # Remove /show if len(words) == 0: categories = ", ".join([ k[0] for k in Category.objects.values_list("type").order_by("type") ]) msg.append( _("Add keyword after show command, e.g. one of %(categories)s") % {"categories": categories}) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg)) return kw_str = words.pop(0) categories = Category.objects.filter(type__iexact=kw_str) if len(categories) == 1: records = Record.objects.filter( category=categories[0]).order_by("-time") else: records = Record.objects.filter(name=kw_str).order_by("-time") if len(words) > 0: try: count = int(words[0]) except ValueError: pass if records: last_dstr = "" for r in records[:count]: dstr = r.time.astimezone(pytz.timezone( self.timezone)).strftime("%d.%m.%Y") tstr = r.time.astimezone(pytz.timezone( self.timezone)).strftime("%H:%M") if last_dstr != dstr: msg.append("<code>{}</code>".format(dstr)) last_dstr = dstr msg.append("<b>{}</b> {}".format( tstr, " ".join(create_message(r.name, r)))) else: msg.append( _("No records for keyword %(kw_str)s found.") % {"kw_str": kw_str}) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg), parse_mode=telegram.ParseMode.HTML)
def get_message_text(self, update: telegram.update.Update): """ Find out text or caption from message. :param update: Telegram Update :return: message text """ msg = self.get_message(update.to_dict()) if "text" in msg: msg_text = msg["text"] elif "caption" in msg: msg_text = msg["caption"] else: msg_text = "" return msg_text
def set_cmd(self, update: telegram.update.Update, context: CallbackContext): self.setenv() print(json.dumps(update.to_dict(), indent=2)) words = update.message.text.split() words.pop(0) # Remove /set if len(words) == 0: msg_lines = [ _("Add some of these after /set:"), "- tz, timezone", "- lang, language", ] context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg_lines)) return if len(words) == 1: msg_lines = [ _("Empty command"), _("You should add value for command too"), ] context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(msg_lines)) return cmd = words[0] val = words[1] if cmd in ["tz", "timezone"]: tz = [s for s in pytz.all_timezones if val.lower() in s.lower()] if len(tz) == 1: self.timezone = tz[0] msg = _("Set timezone to %(tz)s") % {"tz": self.timezone} context.bot.send_message(chat_id=update.effective_chat.id, text=msg) else: msg = _("Found many") + ":\n" + "\n- ".join(tz) context.bot.send_message(chat_id=update.effective_chat.id, text=msg) if cmd in ["lang", "language"]: self.language = val self.setenv() msg = _("Language set to %(lang)s") % {"lang": val} context.bot.send_message(chat_id=update.effective_chat.id, text=msg) else: msg = _("Unknown setting") context.bot.send_message(chat_id=update.effective_chat.id, text=msg)
def get_user(self, update: telegram.update.Update) -> User: """ Return Django User object corresponding Telegram user. If User doesn't exist, it will be created. Also missing user Profile will be created. :param update: telegram Update object :return: User object """ msg = self.get_message(update.to_dict()) username = msg["from"]["username"] user, created = User.objects.get_or_create(username=username) if hasattr( user, "profile" ) is False: # Create a Profile for User, if it doesn't exist Profile.objects.create(user=user, timezone=self.timezone) if created: logging.info(f"Created new user {username}") return user
def create_logbook_message(self, update: telegram.update.Update, save: bool = True) -> Message: message_identifier = get_message_identifier(update) updict = update.to_dict() msg_text = self.get_message_text(update) msg_text = clean_message_text(self.get_message_text(update)) timestamp = update.message.date msg = Message( text=msg_text, source="TG", source_id=message_identifier, user=self.get_user(update), time=timestamp, json=json.dumps(updict), ) if save: msg.save() return msg
def handle_location(self, update: telegram.update.Update, context: CallbackContext): """ Handle location updates. Doesn't create and save Message or Record objects. :param update: telegram Update object :param context: telegram CallbackContext """ res_msg = [] updict = update.to_dict() print(json.dumps(updict, indent=2)) msg = self.get_message(updict) loc = msg["location"] trk_src, created = Tracksource.objects.get_or_create( name="Logbook Telegram Bot", slug="logbookbot") if "edit_date" in msg: epoch = msg["edit_date"] else: epoch = msg["date"] res_msg.append( _("Activated location tracking. Location updates will be silently saved into database." )) ts = pytz.UTC.localize(datetime.datetime.utcfromtimestamp(epoch)) trk_pnt = Trackpoint.objects.create( user=self.get_user(update), tracksource=trk_src, lat=loc["latitude"], lon=loc["longitude"], hacc=loc.get("horizontal_accuracy"), course=loc.get("heading"), time=ts, geometry=Point(loc["longitude"], loc["latitude"]), ) if res_msg: context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(res_msg))
def handle_message(self, update: telegram.update.Update, context: CallbackContext): context.bot.send_chat_action(chat_id=update.effective_message.chat_id, action=ChatAction.TYPING) lb_message = self.create_logbook_message(update) res_msg = [] updict = update.to_dict() message = self.get_message(updict) msg_text = lb_message.text print(json.dumps(updict, indent=2)) logging.info(f"Message time: {lb_message.time}, text: {msg_text}") media_group_id = self.handle_media_group_id(update) if msg_text == "" and media_group_id is None: # Do nothing if message was empty logging.info("Ignore empty message") return # If this is not the first multi file update, there should be existing Message stored if media_group_id in self.media_group_ids: if "msg" in self.media_group_ids[media_group_id]: msg = self.media_group_ids[message["media_group_id"]]["msg"] res_msg += self.save_remaining_updates(msg, media_group_id, context) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(res_msg)) return if msg_text == "": return if media_group_id in self.media_group_ids: self.media_group_ids[media_group_id]["msg"] = lb_message else: # Save file rf = self.download_files(lb_message, update, context) if rf: res_msg.append(f"Saved attachment to {rf.file.name}.") rec: Record = lb_message.update_record() if rec is None and msg_text != "": kw_str = sanitize_keyword(msg_text.split()[0]) kws = Category.objects.filter(keywords__contains=[kw_str]) if kws.count() == 1: # keyword found, requested latest records? reply_text = _( "Category '%(kw_str)s' found. Choose one of these or cancel:" ) % { "kw_str": kw_str } records = Record.objects.filter(name=kw_str).order_by("-time") options = options_for_records(records, 5) options.insert(0, [_("Cancel"), ""]) reply_markup = create_buttons_edit(options) else: reply_text = _( "Category '%(kw_str)s' not found. Choose one of these or cancel:" ) % { "kw_str": kw_str } options = [[k.type] for k in Category.objects.order_by("type")] for o in options: o.append(f"newkw:{o[0]}:{kw_str}") options.insert(0, [_("Cancel"), "Cancel query"]) reply_markup = create_buttons(options) update.message.reply_text(reply_text, reply_markup=reply_markup) return else: kw_str = sanitize_keyword(msg_text.split()[0]) time_str = rec.time.astimezone(pytz.timezone( self.timezone)).strftime(" @%H:%M") res_msg.append(" ".join(create_message(kw_str, rec)) + time_str) if rec.abv is not None and rec.volume is not None: res_msg.append("BAC after latest alcohol: {:.2f}‰".format( calculate_bac())) res_msg += self.save_remaining_updates(lb_message, media_group_id, context) context.bot.send_message(chat_id=update.effective_chat.id, text="\n".join(res_msg))