File size: 4,756 Bytes
61d6e2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e79f82
 
 
 
 
 
61d6e2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Button model: rows of {'t': text, 'u': url} (URL buttons) or {'t', 'raw'} (other bots' buttons, kept as-is)."""
from __future__ import annotations

import re
from typing import Optional

MAX_ROW = 8
MAX_TOTAL = 100
_URL_RX = re.compile(r"(?:https?://|tg://|t\.me/|telegram\.me/)\S+", re.I)


def norm_url(u: str) -> Optional[str]:
    u = (u or "").strip()
    if re.match(r"(?i)^(t\.me|telegram\.me)/", u):
        u = "https://" + u
    if not re.match(r"(?i)^(https?://|tg://)\S+$", u) or len(u) > 2000:
        return None
    return u


def parse_buttons_text(text: str) -> list:
    """One row per line, 'Text - link'. Several buttons in a row: separate them with ' | '."""
    rows: list = []
    total = 0
    for line in (text or "").strip().splitlines():
        line = line.strip()
        if not line:
            continue
        row = []
        for part in re.split(r"\s+\|\s+|\s*&&\s*", line):
            part = part.strip()
            if not part:
                continue
            found = list(_URL_RX.finditer(part))
            if not found:
                raise ValueError(f"No link found in: {part}")
            m = found[-1]
            label = part[: m.start()].strip().rstrip("-–—:|=").strip()
            if not label:
                raise ValueError(f"No button text before the link in: {part}")
            url = norm_url(m.group(0))
            if not url:
                raise ValueError(f"That link doesn't look valid: {m.group(0)}")
            row.append({"t": label, "u": url})
        if len(row) > MAX_ROW:
            raise ValueError(f"At most {MAX_ROW} buttons fit in one row.")
        if row:
            rows.append(row)
            total += len(row)
    if total > MAX_TOTAL:
        raise ValueError(f"At most {MAX_TOTAL} buttons per post.")
    return rows


def copy_rows(rows: list) -> list:
    return [[dict(b) for b in row] for row in (rows or [])]


def count(rows: list) -> int:
    return sum(len(r) for r in rows or [])


def missing_links(rows: list) -> int:
    return sum(1 for r in rows or [] for b in r if not b.get("u") and not b.get("raw"))


def link_positions(rows: list) -> list:
    """(row, column) of every button that has a link of its own. Other bots' buttons (reactions...) are left out:
    they can't be edited."""
    return [(r, c) for r, row in enumerate(rows or []) for c, b in enumerate(row) if b.get("u") and not b.get("raw")]


def valid(rows: list, r: int, c: int) -> bool:
    return 0 <= r < len(rows) and 0 <= c < len(rows[r])


def add_button(rows: list, btn: dict, new_row: bool = True) -> list:
    rows = copy_rows(rows)
    if new_row or not rows or len(rows[-1]) >= MAX_ROW:
        rows.append([btn])
    else:
        rows[-1].append(btn)
    return rows


def set_field(rows: list, r: int, c: int, **fields) -> list:
    rows = copy_rows(rows)
    if valid(rows, r, c):
        rows[r][c].update(fields)
    return rows


def delete_button(rows: list, r: int, c: int) -> list:
    rows = copy_rows(rows)
    if valid(rows, r, c):
        rows[r].pop(c)
        rows = [row for row in rows if row]
    return rows


def move_button(rows: list, r: int, c: int, direction: str) -> tuple:
    """direction: l | r | u | d. Returns (rows, new_r, new_c)."""
    rows = copy_rows(rows)
    if not valid(rows, r, c):
        return rows, r, c
    b = rows[r][c]
    if direction == "l":
        if c > 0:
            rows[r][c - 1], rows[r][c] = rows[r][c], rows[r][c - 1]
            c -= 1
    elif direction == "r":
        if c < len(rows[r]) - 1:
            rows[r][c + 1], rows[r][c] = rows[r][c], rows[r][c + 1]
            c += 1
    elif direction == "u":
        if r > 0 and len(rows[r - 1]) < MAX_ROW:
            rows[r].pop(c)
            pos = min(c, len(rows[r - 1]))
            rows[r - 1].insert(pos, b)
            nr, nc = r - 1, pos
            if not rows[r]:
                rows.pop(r)
            r, c = nr, nc
    elif direction == "d":
        if r < len(rows) - 1:
            if len(rows[r + 1]) < MAX_ROW:
                rows[r].pop(c)
                pos = min(c, len(rows[r + 1]))
                rows[r + 1].insert(pos, b)
                nr, nc = r + 1, pos
                if not rows[r]:
                    rows.pop(r)
                    nr -= 1
                r, c = nr, nc
        elif len(rows[r]) > 1:  # last row with company: split off into a new row below
            rows[r].pop(c)
            rows.append([b])
            r, c = len(rows) - 1, 0
    return rows, r, c


def grid_label(b: dict) -> str:
    t = (b.get("t") or "").strip() or "…"
    if len(t) > 26:
        t = t[:25] + "…"
    if b.get("raw"):
        return "🔒 " + t
    if not b.get("u"):
        return "⚠️ " + t
    return t