hyperboria/nexus/views/telegram/base_view_builder.py

390 lines
13 KiB
Python
Raw Normal View History

import datetime
import math
import re
from typing import Optional
from urllib.parse import quote
import numpy as np
from izihawa_nlptools.utils import (
despace_full,
escape_format,
)
from izihawa_types.datetime import CustomDatetime
from library.telegram.common import close_button
from nexus.translations import t
from telethon import Button
from .common import (
TooLongQueryError,
encode_query_to_deep_link,
)
def highlight_markdown(snippet):
markdowned = b''
start_from = 0
for highlight in snippet.highlights:
from_ = getattr(highlight, 'from')
to = highlight.to
markdowned += escape_format(snippet.fragment[start_from:from_])
markdowned += b'**'
markdowned += escape_format(snippet.fragment[from_:to])
markdowned += b'**'
start_from = to
markdowned += escape_format(snippet.fragment[start_from:])
markdowned = markdowned.decode()
markdowned = despace_full(re.sub(r'\n+', ' ', markdowned))
if markdowned[0].islower() or (markdowned[:2] == '**' and markdowned[2].islower()):
markdowned = '...' + markdowned
markdowned = markdowned + '...'
markdowned = f'__{markdowned}__'
return markdowned
class TextPart:
def __init__(self, part):
self.part = str(part)
self._bold = False
self._italic = False
self._brackets = False
self._escaped = False
self._clickable = False
self._unbreakable = False
def escaped(self):
self._escaped = True
return self
def bold(self):
self._bold = True
return self
def italic(self):
self._italic = True
return self
def within_brackets(self):
self._brackets = True
return self
def clickable(self):
self._clickable = True
return self
def unbreakable(self):
self._unbreakable = True
return self
def limits(self, limit, with_dots: bool = False):
if len(self.part) > limit:
if self._unbreakable:
self.part = ''
else:
self.part = self.part[:limit]
if with_dots:
self.part += '...'
def __add__(self, other):
self.part += str(other)
def __len__(self):
return len(self.part)
def __str__(self):
r = self.part
if not self._escaped:
r = escape_format(r)
if self._clickable:
r = f'`{r}`'
if self._brackets:
r = f'({r})'
if self._bold:
r = f'**{r}**'
if self._italic:
r = f'__{r}__'
return r
class BaseViewBuilder:
icon = ''
def __init__(self, document_holder, user_language):
self.document_holder = document_holder
self.user_language = user_language
self.last_limit_part = None
self.last_limit = None
self.parts = []
self.has_cover = False
def has_field(self, name):
return self.document_holder.has_field(name)
def add(self, el, bold=False, italic=False, escaped=False, clickable=False, lower=False, with_brackets=False, unbreakable=False,
eol: Optional[str] = ' '):
if el:
if not isinstance(el, TextPart):
el = TextPart(el if not lower else el.lower())
if bold:
el.bold()
if italic:
el.italic()
if escaped:
el.escaped()
if clickable:
el.clickable()
if with_brackets:
el.within_brackets()
if unbreakable:
el.unbreakable()
self.parts.append(el)
if eol is not None:
self.parts.append(TextPart(eol))
return self
def limits(self, limit=None, with_dots: bool = False):
if limit:
current_length = 0
for i, part in enumerate(self.parts):
current_length += len(part)
if current_length > limit:
part.limits(limit + len(part) - current_length, with_dots=with_dots)
if len(part) > 0:
self.parts = self.parts[:i + 1]
else:
self.parts = self.parts[:i]
return self
return self
def limited(self, limit):
self.last_limit_part = len(self.parts)
self.last_limit = limit
return self
def end_limited(self, with_dots: bool = False):
if self.last_limit is not None:
current_length = 0
for i, part in enumerate(self.parts[self.last_limit_part:]):
current_length += len(part)
if current_length > self.last_limit:
part.limits(current_length - self.last_limit, with_dots=with_dots)
if len(part) > 0:
self.parts = self.parts[:self.last_limit_part + i + 1]
else:
self.parts = self.parts[:self.last_limit_part + i]
return self
self.last_limit = None
self.last_limit_part = None
return self
def add_icon(self):
return self.add(self.icon)
def add_label(self, label_name, bold=True, lower=False):
return self.add(t(label_name, self.user_language) + ':', bold=bold, lower=lower)
def add_new_line(self, n: int = 1):
return self.add('\n' * n, eol=None)
def add_formatted_datetime(self):
if self.has_field('issued_at') and self.document_holder.issued_at != -62135596800:
dt = CustomDatetime(dt=np.datetime64(self.document_holder.issued_at, 's'))
try:
ct = datetime.date(dt.year, dt.month, 1)
if datetime.date.today() - datetime.timedelta(days=365) < ct:
self.add(TextPart(f'{dt.year}.{dt.month:02d}').within_brackets())
else:
self.add(TextPart(str(dt.year)).within_brackets())
except ValueError:
pass
return self
def add_downloads_count(self):
return self.add(f'{math.log1p(self.document_holder.downloads_count):.1f}')
def add_links(self, bot_name):
self.add_label("LINKS")
self.add(" - ".join(self.document_holder.generate_links(bot_name)), escaped=True)
return self
def add_tags(self, bot_name):
tag_links = self.document_holder.generate_tags_links(bot_name)
if not tag_links:
return self
self.add(tag_links[0], escaped=True, unbreakable=True)
for tag_link in tag_links[1:]:
self.add('- ' + tag_link, escaped=True, unbreakable=True)
return self
def add_authors(self, et_al=True, first_n_authors=1, on_newline=True):
et_al_suffix = (' et al' if et_al else '')
if self.document_holder.authors:
if on_newline:
self.add_new_line()
if len(self.document_holder.authors) > first_n_authors:
self.add('; '.join(self.document_holder.authors[:first_n_authors]) + et_al_suffix)
elif len(self.document_holder.authors) == 1:
if self.document_holder.authors[0].count(';') >= 1:
comma_authors = list(map(str.strip, self.document_holder.authors[0].split(';')))
if len(comma_authors) > first_n_authors:
self.add('; '.join(comma_authors[:first_n_authors]) + et_al_suffix)
else:
self.add('; '.join(comma_authors))
elif self.document_holder.authors[0].count(',') >= 1:
comma_authors = list(map(str.strip, self.document_holder.authors[0].split(',')))
if len(comma_authors) > first_n_authors:
self.add('; '.join(comma_authors[:first_n_authors]) + et_al_suffix)
else:
self.add('; '.join(comma_authors))
else:
self.add(self.document_holder.authors[0])
else:
self.add('; '.join(self.document_holder.authors))
return self
def add_doi(self, clickable=True, with_brackets=False, with_leading_pipe=False):
if self.document_holder.doi:
if with_leading_pipe:
self.add('|')
return self.add(self.document_holder.doi, clickable=clickable, with_brackets=with_brackets)
def add_doi_link(self, with_leading_pipe=False, on_newline=False, label=False, text=None, end_newline=False):
if self.document_holder.doi:
if on_newline:
self.add_new_line()
if with_leading_pipe:
self.add('|')
if label:
self.add('DOI:', bold=True)
escaped_doi = escape_format(self.document_holder.doi)
if text is None:
text = escaped_doi
self.add(f'[{text}](https://doi.org/{quote(escaped_doi)})', escaped=True)
if end_newline:
self.add_new_line()
return self
def add_references_counter(self, bot_name, with_leading_pipe=False):
if self.has_field('referenced_by_count') and self.document_holder.referenced_by_count:
if with_leading_pipe:
self.add('|')
text = f'🔗 {self.document_holder.referenced_by_count}'
if self.document_holder.doi:
try:
link = encode_query_to_deep_link(f'refs:{self.document_holder.doi}', bot_name=bot_name)
text = f'[{text}]({link})'
except TooLongQueryError:
pass
self.add(text, escaped=True)
return self
def add_short_description(self):
return (
self.add_icon()
.add_title()
.limits(250, with_dots=True)
.add_locator()
)
def add_view(self, bot_name):
return (
self.add_cover_url()
.add_icon()
.add_title()
.limits(400, with_dots=True)
.add_locator(first_n_authors=5)
.add_new_line(2)
.add_stats()
.add_links(bot_name=bot_name)
.add_new_line(2)
.add_description()
.limits(1500, with_dots=True)
.add_new_line(2)
.add_tags(bot_name=bot_name)
.limits(3000)
)
def add_title(self):
raise NotImplementedError()
def add_description(self):
raise NotImplementedError()
def add_locator(self, first_n_authors=1, markup=True):
raise NotImplementedError()
def add_filedata(self, with_leading_pipe=False):
raise NotImplementedError()
def add_stats(self, end_newline=True):
return self
def add_cover_url(self):
cover_url = self.document_holder.get_cover_url()
if cover_url:
self.has_cover = True
self.add(f'[]({cover_url})', escaped=True, unbreakable=True)
return self
def add_isbns(self, on_newline=False, label=False, end_newline=False):
if self.document_holder.isbns:
if on_newline:
self.add_new_line()
if label:
self.add('ISBN:', bold=True)
self.add(', '.join(self.document_holder.isbns[:2]))
if end_newline:
self.add_new_line()
return self
def build(self):
text = ''.join(map(str, self.parts)).strip()
text = re.sub('\n\n+', '\n\n', text)
return text
class BaseButtonsBuilder:
def __init__(self, document_holder, user_language):
self.document_holder = document_holder
self.user_language = user_language
self.buttons = [[]]
def add_back_button(self, back_command):
self.buttons[-1].append(
Button.inline(
text='⬅️',
data=back_command
)
)
return self
def add_download_button(self, session_id, position: int = 0):
# ⬇️ is a mark, Find+F over sources before replacing
self.buttons[-1].append(
Button.inline(
text=f'⬇️ GET',
data=self.document_holder.get_download_command(session_id=session_id, position=position),
)
)
return self
def add_remote_download_button(self, bot_name):
# ⬇️ is a mark, Find+F over sources before replacing
self.buttons[-1].append(
Button.url('', self.document_holder.get_deep_id_link(bot_name))
)
return self
def add_close_button(self, session_id):
self.buttons[-1].append(close_button(session_id))
return self
def add_new_line(self):
self.buttons.append([])
return self
def add_default_layout(self, bot_name, session_id, position: int = 0):
raise NotImplementedError()
def build(self):
return self.buttons