Refactoring telegram loader
Labels: name: area: doc loader, name: auto:improvement

### Feature request

The current Telegram loader is not very flexible. For example:

```
async for message in client.iter_messages(self.chat_entity):
```

Here are the arguments available in the api

```
    def iter_messages(
          self: 'TelegramClient',
          entity: 'hints.EntityLike',
          limit: float = None,
          *,
          offset_date: 'hints.DateLike' = None,
          offset_id: int = 0,
          max_id: int = 0,
          min_id: int = 0,
          add_offset: int = 0,
          search: str = None,
          filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None,
          from_user: 'hints.EntityLike' = None,
          wait_time: float = None,
          ids: 'typing.Union[int, typing.Sequence[int]]' = None,
          reverse: bool = False,
          reply_to: int = None,
          scheduled: bool = False
  ) -> 'typing.Union[_MessagesIter, _IDsIter]':
      """
      Iterator over the messages for the given chat.

      The default order is from newest to oldest, but this
      behaviour can be changed with the `reverse` parameter.

      If either `search`, `filter` or `from_user` are provided,
      :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`.

      .. note::

          Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to
          be around 30 seconds per 10 requests, therefore a sleep of 1
          second is the default for this limit (or above).

      Arguments
          entity (`entity`):
              The entity from whom to retrieve the message history.

              It may be `None` to perform a global search, or
              to get messages by their ID from no particular chat.
              Note that some of the offsets will not work if this
              is the case.

              Note that if you want to perform a global search,
              you **must** set a non-empty `search` string, a `filter`.
              or `from_user`.

          limit (`int` | `None`, optional):
              Number of messages to be retrieved. Due to limitations with
              the API retrieving more than 3000 messages will take longer
              than half a minute (or even more based on previous calls).

              The limit may also be `None`, which would eventually return
              the whole history.

          offset_date (`datetime`):
              Offset date (messages *previous* to this date will be
              retrieved). Exclusive.

          offset_id (`int`):
              Offset message ID (only messages *previous* to the given
              ID will be retrieved). Exclusive.

          max_id (`int`):
              All the messages with a higher (newer) ID or equal to this will
              be excluded.

          min_id (`int`):
              All the messages with a lower (older) ID or equal to this will
              be excluded.

          add_offset (`int`):
              Additional message offset (all of the specified offsets +
              this offset = older messages).

          search (`str`):
              The string to be used as a search query.

          filter (:tl:`MessagesFilter` | `type`):
              The filter to use when returning messages. For instance,
              :tl:`InputMessagesFilterPhotos` would yield only messages
              containing photos.

          from_user (`entity`):
              Only messages from this entity will be returned.

          wait_time (`int`):
              Wait time (in seconds) between different
              :tl:`GetHistoryRequest`. Use this parameter to avoid hitting
              the ``FloodWaitError`` as needed. If left to `None`, it will
              default to 1 second only if the limit is higher than 3000.

              If the ``ids`` parameter is used, this time will default
              to 10 seconds only if the amount of IDs is higher than 300.

          ids (`int`, `list`):
              A single integer ID (or several IDs) for the message that
              should be returned. This parameter takes precedence over
              the rest (which will be ignored if this is set). This can
              for instance be used to get the message with ID 123 from
              a channel. Note that if the message doesn't exist, `None`
              will appear in its place, so that zipping the list of IDs
              with the messages can match one-to-one.

              .. note::

                  At the time of writing, Telegram will **not** return
                  :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that
                  failed (i.e. the message is not replying to any, or is
                  replying to a deleted message). This means that it is
                  **not** possible to match messages one-by-one, so be
                  careful if you use non-integers in this parameter.

          reverse (`bool`, optional):
              If set to `True`, the messages will be returned in reverse
              order (from oldest to newest, instead of the default newest
              to oldest). This also means that the meaning of `offset_id`
              and `offset_date` parameters is reversed, although they will
              still be exclusive. `min_id` becomes equivalent to `offset_id`
              instead of being `max_id` as well since messages are returned
              in ascending order.

              You cannot use this if both `entity` and `ids` are `None`.

          reply_to (`int`, optional):
              If set to a message ID, the messages that reply to this ID
              will be returned. This feature is also known as comments in
              posts of broadcast channels, or viewing threads in groups.

              This feature can only be used in broadcast channels and their
              linked megagroups. Using it in a chat or private conversation
              will result in ``telethon.errors.PeerIdInvalidError`` to occur.

              When using this parameter, the ``filter`` and ``search``
              parameters have no effect, since Telegram's API doesn't
              support searching messages in replies.

              .. note::

                  This feature is used to get replies to a message in the
                  *discussion* group. If the same broadcast channel sends
                  a message and replies to it itself, that reply will not
                  be included in the results.

          scheduled (`bool`, optional):
              If set to `True`, messages which are scheduled will be returned.
              All other parameter will be ignored for this, except `entity`.

      Yields
          Instances of `Message <telethon.tl.custom.message.Message>`.

      Example
          .. code-block:: python

              # From most-recent to oldest
              async for message in client.iter_messages(chat):
                  print(message.id, message.text)

              # From oldest to most-recent
              async for message in client.iter_messages(chat, reverse=True):
                  print(message.id, message.text)

              # Filter by sender
              async for message in client.iter_messages(chat, from_user='me'):
                  print(message.text)

              # Server-side search with fuzzy text
              async for message in client.iter_messages(chat, search='hello'):
                  print(message.id)

              # Filter by message type:
              from telethon.tl.types import InputMessagesFilterPhotos
              async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos):
                  print(message.photo)

              # Getting comments from a post in a channel:
              async for message in client.iter_messages(channel, reply_to=123):
                  print(message.chat.title, message.text)
      """
```

Of particular interest to me is the ability to specify `offset_date`, `limit`, and `reverse`. Other users may have other needs. I understand that the `BaseLoader` load method signature doesn't allow for any arguments. That leaves two options (perhaps there is a third way?):

1. Provide something like `**telegram_kwargs` in the TelegramApiChatLoader loader constructor, e.g. 

```
class TelegramChatApiLoader(BaseLoader):
    """Loader that loads Telegram chat json directory dump."""

    def __init__(
        self,
        chat_entity: Optional[EntityLike] = None,
        api_id: Optional[int] = None,
        api_hash: Optional[str] = None,
        username: Optional[str] = None,
        file_path: str = "telegram_data.json",
        **telegram_kwargs # <---- add this new argument
    ):
  ```
2. Alternatively, refactor the `fetch_data_from_telegram` method and extract `async for message in client.iter_messages(self.chat_entity):` into a separate method that can be overriden by child classes, e.g.:

```
async def fetch_data_from_telegram(self) -> None:
        """Fetch data from Telegram API and save it as a JSON file."""
        from telethon.sync import TelegramClient

        data = []
        async with TelegramClient(self.username, self.api_id, self.api_hash) as client:
            # change this line to call a local method
            async for message in self.iter_messages(client, self.chat_entity):
                is_reply = message.reply_to is not None
                reply_to_id = message.reply_to.reply_to_msg_id if is_reply else None
                data.append(
                    {
                        "sender_id": message.sender_id,
                        "text": message.text,
                        "date": message.date.isoformat(),
                        "message.id": message.id,
                        "is_reply": is_reply,
                        "reply_to_id": reply_to_id,
                    }
                )

        with open(self.file_path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)
            
    **# Add this new method**
    def iter_messages(client: TelegramClient, chat_entity: Optional[EntityLike]):
        return client.iter_messages(self.chat_entity, self._offset, self._limit, ...):
            
            ```
            
A child class can then override the constructor and the `iter_message` method.
My current approach is to override the entire `fetch_data_from_telegram` method. This is problematic since I am duplicating code that may change in future versions of langchain
      
     

### Motivation

My use case is pretty simple, return all message starting from a certain date. As I understand the current implementation, the entire history is returned.

### Your contribution

I'm happy to submit a PR if one of the abovementioned approaches is approved.