zmoog / til

Today I Learned
MIT License
3 stars 0 forks source link

Research telegram message formats #15

Closed zmoog closed 1 year ago

zmoog commented 1 year ago

Posting text-base information to Telegram does not work great. I want to learn what message formats are currently supported by Telegram and its APIs.

zmoog commented 1 year ago

A complete Telegram Bot API reference is available at https://core.telegram.org/bots/api

zmoog commented 1 year ago

For the sendMessage method there are several formatting options:

The Bot API supports basic formatting for messages. You can use bold, italic, underlined, strikethrough, and spoiler text, as well as inline links and pre-formatted code in your bots' messages. Telegram clients will render them accordingly. You can specify text entities directly, or use markdown-style or HTML-style formatting.

zmoog commented 1 year ago

The sendMessage method has a parse_mode that currently supports two options:

The markdown mode is pretty strict and throws 400s very easily. The HTML one instead seems pretty tolerant.

zmoog commented 1 year ago

Is the markdown code block format supported? Let's give it a try!

zmoog commented 1 year ago
$ cat msg.md

> This are the last week stats:
>
> ```
>        Time Entries
> 
>   tags           Duration
>  ─────────────────────────
>   type:goal      19:53
>   type:meeting   9:00
>   type:sync      7:44
>   type:support   6:12
>  ─────────────────────────
>   Total          42:50
> 
> ```
> 

Sending the file content using the --parse-mode MarkdownV2 option:

$ tgm message send --chat-id 161035319 --parse-mode MarkdownV2 --text $msg
message-id: 698

And this is the final result in the Telegram web client:

Image

Pretty basic but readable.

zmoog commented 1 year ago

Unfortunately, with longer messages the result is not great:

Image

zmoog commented 1 year ago

I need custom formatting of text to fit the Telegram constraints.

Thinking about getting JSON documents and format them as needed using something link this https://pypi.org/project/jinja-cli/

zmoog commented 1 year ago

$ cat example.j2
{% for entry in entries %}
- {{ entry.description }}{% endfor %}

$ tgl --format json entries --project-id 178435728 list | jq > example.json

$ cat example.json
cat example.json
{
  "entries": [
    {
      "id": 2843982475,
      "workspace_id": 1815018,
      "user_id": 2621333,
      "project_id": 178435728,
      "task_id": null,
      "billable": false,
      "at": "2023-02-13T22:58:17+00:00",
      "description": "Azure SDK upgrade: review",
      "start": "2023-02-13T22:53:16+00:00",
      "stop": "2023-02-13T22:58:17+00:00",
      "duration": 301,
      "tags": [
        "type:support"
      ]
    },
    .... 

$ jinja -d example.json -f json example.j2

- Azure SDK upgrade: review
- ESF: research how to enable telemetry
- azure2: follow up to the questions
- azure2: follow up to the questions
- azure2: Add an entry to the Weekly Update
- Maurizio / Davide
- azure2: Add an entry to the Weekly Update
- Azure SDK upgrade: review
- azure2: Add an entry to the Weekly Update
- Azure SDK upgrade: review
- sync
- ESF: add sending tests
- azure2: send email
- sync
- Tamara / Maurizio (Cloud Monitoring Tours S1E4)
- sync
zmoog commented 1 year ago

I used a dict with an entries key instead of a list to make it easier for jinja-cli.

zmoog commented 1 year ago

jinja-cli assumes data is a dict.

If I want to use this tool I have to give it a dict.

zmoog commented 1 year ago

Another example with a patched version of toggl-track CLI that adds a --json-root option to wrap the result list in a dict:

$ tgl --format json --json-root entries entries --project-id 178435728 list | jinja -d - -f json example.j2

- Observability Demo Day
- sync
- Cloud Monitoring - Weekly
- ESF: prepare presentation
- ESF: collecting sample logs
- ESF: collecting sample logs
- sync
- ESF: prepare presentation
- ESF: collecting sample logs
- ESF: collecting sample logs
zmoog commented 1 year ago

Next steps:

With this changes, I would be able to write something similar:

$ tgl --format json \
    --json-root entries entries \
    --project-id 178435728 list | \
        jinja -d - -f json example.j2 | \
        tgm message send --chat-id 123 --parse-mode MarkdownV2 
zmoog commented 1 year ago

Doing some research on stdin at zmoog/public-notes#3

I am starting to thing that we should add a new --text-file option to read text content from a file, and use the stdin stream as a special case, something like what is described at https://stackoverflow.com/questions/59829848/supply-either-stdin-or-a-file-pathname-option-to-python-click-cli-script

import click
import sys

@click.group()
def cli():
    pass

@cli.command()
@click.option('--compose-file', 
              help='compose file to work with',
              type=click.File('r'),
              default=sys.stdin)
def secret_hash_ini(compose_file):
    with compose_file:
        data = compose_file.read()

    print(data)

if __name__ == '__main__':
    cli()
zmoog commented 1 year ago

I like this approach, it's quite flexible.

Here's one take on this that allows three different input sources:

  1. Directly as a text message using --text
  2. From a file or stdin using --text-file
  3. If I don't set neither, the text_file.read() will wait until the user has typed the message and hit CTRL + D

@message.command(name="send")
@click.option(
    "--text",
    help="Text to send. If not provided, text will be read from a file or stdin.",
    default=None,
)
@click.option(
    "--text-file", 
    help="Text file to read from. If not provided, stdin will be used.",
    type=click.File("r"),
    default=click.get_text_stream("stdin"),
)
@click.option(
    "--chat-id",
    required=True,
)
@click.option(
    '--parse-mode',
    type=click.Choice(
        ['HTML', 'MarkdownV2'],
        case_sensitive=False
    ),
    default=None,
)
@click.pass_context
def send(ctx: click.Context, text: str, text_file: typing.TextIO, chat_id: str, parse_mode: str):
    """Send a text message to a chat."""
    client = telegram.Client.from_envorinment(verbose=ctx.obj["verbose"])

    if text:
        message_text = text
    elif text_file and text_file.readable():
        message_text = text_file.read()
    else:
        raise Exception("No text or text file provided")

    resp = client.send(message_text, chat_id, parse_mode=parse_mode)

    message_id = resp.get("result", {}).get("message_id", "No message id found")
    click.echo(f"message-id: {message_id}")
zmoog commented 1 year ago

Here's and example:

CleanShot 2023-02-17 at 09 45 23