Compare commits
21 Commits
f2de3d9584
...
72809d024e
Author | SHA1 | Date | |
---|---|---|---|
72809d024e | |||
|
59b928d4f7 | ||
|
12902d126c | ||
|
c096e239e7 | ||
|
abbe00d70f | ||
|
e3ea5f8051 | ||
|
c656956365 | ||
|
bab99443b6 | ||
|
5959e6e35a | ||
|
6a4b8d9199 | ||
|
473205007f | ||
|
e99b7584d2 | ||
|
294bd887ba | ||
|
8eb1afb91c | ||
|
160670bea3 | ||
|
022df56c9e | ||
|
7d6fba5ac7 | ||
|
4de8e87f06 | ||
|
8521182a09 | ||
|
456525aa25 | ||
|
0e6e55a920 |
|
@ -6,6 +6,7 @@ The format is based on
|
||||||
and this project adheres to
|
and this project adheres to
|
||||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
- [0.7.5 (2024-03-07)](#075-2024-03-07)
|
||||||
- [0.7.4 (2024-01-04)](#074-2024-01-04)
|
- [0.7.4 (2024-01-04)](#074-2024-01-04)
|
||||||
- [0.7.3 (2022-01-31)](#073-2022-01-31)
|
- [0.7.3 (2022-01-31)](#073-2022-01-31)
|
||||||
- [0.7.2 (2021-07-26)](#072-2021-07-26)
|
- [0.7.2 (2021-07-26)](#072-2021-07-26)
|
||||||
|
@ -25,6 +26,30 @@ and this project adheres to
|
||||||
- [0.4.0 (2020-03-21)](#040-2020-03-21)
|
- [0.4.0 (2020-03-21)](#040-2020-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.7.5 (2024-03-07)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added emoji processing, for example `:eyes:`
|
||||||
|
|
||||||
|
- Added /react command, to add (emoji) reactions
|
||||||
|
|
||||||
|
- Added displaying of spoiler tags
|
||||||
|
|
||||||
|
- Added /spoiler and /unspoiler commands
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Now using Mistune 3
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed profile picture flickering in account settings UI
|
||||||
|
|
||||||
|
- Fixed overlaying tabs glitch in many UIs
|
||||||
|
|
||||||
|
- Fixed some issues when running on OpenBSD
|
||||||
|
|
||||||
## 0.7.4 (2024-01-04)
|
## 0.7.4 (2024-01-04)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -25,6 +25,7 @@ but compiling on Windows and macOS should be possible with the right tools.
|
||||||
- [Common issues](#common-issues)
|
- [Common issues](#common-issues)
|
||||||
- [cffi version mismatch](#cffi-version-mismatch)
|
- [cffi version mismatch](#cffi-version-mismatch)
|
||||||
- [Type XYZ unavailable](#type-xyz-unavailable)
|
- [Type XYZ unavailable](#type-xyz-unavailable)
|
||||||
|
- [libimagequant.so.0: cannot open shared object file: No such file or directory](#libimagequantso0-cannot-open-shared-object-file-no-such-file-or-directory)
|
||||||
|
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
@ -379,3 +380,12 @@ sudo chmod 755 /usr/lib/qt5/qml/io/thp/pyotherside/*.so
|
||||||
|
|
||||||
Note that the Qt lib path might be `/usr/lib/qt/` instead of `/usr/lib/qt5/`,
|
Note that the Qt lib path might be `/usr/lib/qt/` instead of `/usr/lib/qt5/`,
|
||||||
depending on the distro.
|
depending on the distro.
|
||||||
|
|
||||||
|
#### libimagequant.so.0: cannot open shared object file: No such file or directory
|
||||||
|
|
||||||
|
Solution from [here](https://stackoverflow.com/questions/77499381/libimagequant-so-0-cannot-open-shared-object-file-no-such-file-or-directory) works.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ln /lib/libimagequant.so.0.4 /lib/libimagequant.so.0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
<provides>
|
<provides>
|
||||||
<binary>moment</binary>
|
<binary>moment</binary>
|
||||||
</provides>
|
</provides>
|
||||||
|
<requires>
|
||||||
|
<display_length compare="ge">360</display_length>
|
||||||
|
</requires>
|
||||||
<project_license>LGPL-3.0-or-later</project_license>
|
<project_license>LGPL-3.0-or-later</project_license>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
|
@ -56,6 +59,7 @@
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<metadata_license>FSFAP</metadata_license>
|
<metadata_license>FSFAP</metadata_license>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.7.5" date="2024-03-07"/>
|
||||||
<release version="0.7.4" date="2024-01-04"/>
|
<release version="0.7.4" date="2024-01-04"/>
|
||||||
<release version="0.7.3" date="2022-01-31"/>
|
<release version="0.7.3" date="2022-01-31"/>
|
||||||
<release version="0.7.2" date="2021-07-26"/>
|
<release version="0.7.2" date="2021-07-26"/>
|
||||||
|
@ -74,8 +78,4 @@
|
||||||
<release version="0.4.1" date="2020-03-23"/>
|
<release version="0.4.1" date="2020-03-23"/>
|
||||||
<release version="0.4.0" date="2020-03-21"/>
|
<release version="0.4.0" date="2020-03-21"/>
|
||||||
</releases>
|
</releases>
|
||||||
<custom>
|
|
||||||
<value key="Purism::form_factor">workstation</value>
|
|
||||||
<value key="Purism::form_factor">mobile</value>
|
|
||||||
</custom>
|
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -4,10 +4,10 @@ appdirs >= 1.4.4, < 2
|
||||||
cairosvg >= 2.4.2, < 3
|
cairosvg >= 2.4.2, < 3
|
||||||
emoji >= 2.0, < 3.0
|
emoji >= 2.0, < 3.0
|
||||||
filetype >= 1.0.7, < 2
|
filetype >= 1.0.7, < 2
|
||||||
html_sanitizer >= 1.9.1, < 2
|
html_sanitizer >= 1.9.1, < 3
|
||||||
lxml >= 4.5.1, < 5
|
lxml >= 4.5.1, < 6
|
||||||
mistune >= 2.0.0, < 3.0
|
mistune >= 2.0.0, < 4.0
|
||||||
pymediainfo >= 4.2.1, < 5
|
pymediainfo >= 4.2.1, < 7
|
||||||
plyer >= 1.4.3, < 2
|
plyer >= 1.4.3, < 2
|
||||||
sortedcontainers >= 2.2.2, < 3
|
sortedcontainers >= 2.2.2, < 3
|
||||||
watchgod >= 0.7, < 0.8
|
watchgod >= 0.7, < 0.8
|
||||||
|
|
|
@ -16,4 +16,4 @@ documentation in the following modules first:
|
||||||
__app_name__ = "moment"
|
__app_name__ = "moment"
|
||||||
__display_name__ = "Moment"
|
__display_name__ = "Moment"
|
||||||
__reverse_dns__ = "xyz.mx-moment"
|
__reverse_dns__ = "xyz.mx-moment"
|
||||||
__version__ = "0.7.4"
|
__version__ = "0.7.5"
|
|
@ -20,7 +20,16 @@ import aiohttp
|
||||||
import nio
|
import nio
|
||||||
import plyer
|
import plyer
|
||||||
import pyotherside
|
import pyotherside
|
||||||
import simpleaudio
|
|
||||||
|
has_simpleaudio = True
|
||||||
|
try:
|
||||||
|
import simpleaudio
|
||||||
|
except ImportError as e:
|
||||||
|
trace = traceback.format_exc().rstrip()
|
||||||
|
log.error("Importing simpleaudio failed\n%s", trace)
|
||||||
|
has_simpleaudio = False
|
||||||
|
|
||||||
|
|
||||||
from appdirs import AppDirs
|
from appdirs import AppDirs
|
||||||
from nio.client.async_client import client_session
|
from nio.client.async_client import client_session
|
||||||
|
|
||||||
|
@ -590,6 +599,13 @@ class Backend:
|
||||||
|
|
||||||
|
|
||||||
async def sound_notify(self) -> None:
|
async def sound_notify(self) -> None:
|
||||||
|
|
||||||
|
if not has_simpleaudio:
|
||||||
|
if self.audio_working:
|
||||||
|
log.error("Playing audio not supported as python-simpleaudio is not installed")
|
||||||
|
self.audio_working = False
|
||||||
|
return
|
||||||
|
|
||||||
path = self.settings.Notifications.default_sound
|
path = self.settings.Notifications.default_sound
|
||||||
path = str(Path(path).expanduser())
|
path = str(Path(path).expanduser())
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from urllib.parse import unquote
|
||||||
|
|
||||||
import html_sanitizer.sanitizer as sanitizer
|
import html_sanitizer.sanitizer as sanitizer
|
||||||
import lxml.html # nosec
|
import lxml.html # nosec
|
||||||
|
import emoji
|
||||||
import mistune
|
import mistune
|
||||||
import nio
|
import nio
|
||||||
from html_sanitizer.sanitizer import Sanitizer
|
from html_sanitizer.sanitizer import Sanitizer
|
||||||
|
@ -40,7 +41,10 @@ def plugin_matrix(md):
|
||||||
# Mark colour as high priority as otherwise e.g. <red>(hi) matches the
|
# Mark colour as high priority as otherwise e.g. <red>(hi) matches the
|
||||||
# inline_html rule instead of the colour rule.
|
# inline_html rule instead of the colour rule.
|
||||||
md.inline.rules.insert(1, "colour")
|
md.inline.rules.insert(1, "colour")
|
||||||
|
if mistune.__version__.startswith("2."): # v2
|
||||||
md.inline.register_rule("colour", colour, parse_colour)
|
md.inline.register_rule("colour", colour, parse_colour)
|
||||||
|
else:
|
||||||
|
md.inline.register("colour", colour, parse_colour)
|
||||||
|
|
||||||
if md.renderer.NAME == "html":
|
if md.renderer.NAME == "html":
|
||||||
md.renderer.register("colour", render_html_colour)
|
md.renderer.register("colour", render_html_colour)
|
||||||
|
@ -215,6 +219,14 @@ class HTMLProcessor:
|
||||||
if not outgoing:
|
if not outgoing:
|
||||||
self._matrix_to_links_add_classes(a_tag)
|
self._matrix_to_links_add_classes(a_tag)
|
||||||
|
|
||||||
|
for node in tree.iterdescendants():
|
||||||
|
if node.tag != "code" and node.text:
|
||||||
|
node.text = emoji.emojize(
|
||||||
|
node.text, language="alias", variant="emoji_type")
|
||||||
|
if node.getparent() and node.getparent().tag != "code" and node.tail:
|
||||||
|
node.tail = emoji.emojize(
|
||||||
|
node.tail, language="alias", variant="emoji_type")
|
||||||
|
|
||||||
html = etree.tostring(tree, encoding="utf-8", method="html").decode()
|
html = etree.tostring(tree, encoding="utf-8", method="html").decode()
|
||||||
html = sanit.sanitize(html).rstrip("\n")
|
html = sanit.sanitize(html).rstrip("\n")
|
||||||
|
|
||||||
|
@ -223,6 +235,27 @@ class HTMLProcessor:
|
||||||
|
|
||||||
# Client-side modifications
|
# Client-side modifications
|
||||||
|
|
||||||
|
# re-parsing, will sanitize again but allowing style
|
||||||
|
tree = etree.fromstring(
|
||||||
|
html, parser=etree.HTMLParser(encoding="utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
for span_tag in tree.iterfind(".//span[@data-mx-spoiler]"):
|
||||||
|
# if there are sub-elements, their styles also need to be set or
|
||||||
|
# background-color doesn't seem to apply
|
||||||
|
for tag in span_tag.iter():
|
||||||
|
tag.set(
|
||||||
|
"style",
|
||||||
|
"color: black !important; background-color: black !important;"
|
||||||
|
+ (tag.get("style") or "")
|
||||||
|
)
|
||||||
|
|
||||||
|
html = etree.tostring(tree, encoding="utf-8", method="html").decode()
|
||||||
|
|
||||||
|
html = Sanitizer(self.sanitize_settings(
|
||||||
|
inline, outgoing, mentions, extra_attributes={"style"}
|
||||||
|
)).sanitize(html).rstrip("\n")
|
||||||
|
|
||||||
html = self.quote_regex.sub(r'\1<span class="quote">\2</span>\3', html)
|
html = self.quote_regex.sub(r'\1<span class="quote">\2</span>\3', html)
|
||||||
|
|
||||||
if not inline:
|
if not inline:
|
||||||
|
@ -238,6 +271,7 @@ class HTMLProcessor:
|
||||||
inline: bool = False,
|
inline: bool = False,
|
||||||
outgoing: bool = False,
|
outgoing: bool = False,
|
||||||
display_name_mentions: Optional[Dict[str, str]] = None,
|
display_name_mentions: Optional[Dict[str, str]] = None,
|
||||||
|
extra_attributes: set = set(),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Return an html_sanitizer configuration."""
|
"""Return an html_sanitizer configuration."""
|
||||||
|
|
||||||
|
@ -250,16 +284,21 @@ class HTMLProcessor:
|
||||||
"font": {"color"},
|
"font": {"color"},
|
||||||
"a": {"href", "class", "data-mention"},
|
"a": {"href", "class", "data-mention"},
|
||||||
"code": {"class"},
|
"code": {"class"},
|
||||||
|
"span": {"data-mx-spoiler", "data-mx-color"},
|
||||||
}
|
}
|
||||||
attributes = {**inlines_attributes, **{
|
attributes = {**inlines_attributes, **{
|
||||||
"ol": {"start"},
|
"ol": {"start"},
|
||||||
"hr": {"width"},
|
"hr": {"width"},
|
||||||
"span": {"data-mx-color"},
|
|
||||||
"img": {
|
"img": {
|
||||||
"data-mx-emote", "src", "alt", "title", "width", "height",
|
"data-mx-emote", "src", "alt", "title", "width", "height",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
for key in inlines_attributes:
|
||||||
|
inlines_attributes[key] |= extra_attributes
|
||||||
|
for key in attributes:
|
||||||
|
attributes[key] |= extra_attributes
|
||||||
|
|
||||||
username_link_regexes = [re.compile(r) for r in [
|
username_link_regexes = [re.compile(r) for r in [
|
||||||
rf"(?<!\w)(?P<body>{re.escape(name or user_id)})(?!\w)(?P<host>)"
|
rf"(?<!\w)(?P<body>{re.escape(name or user_id)})(?!\w)(?P<host>)"
|
||||||
for user_id, name in (display_name_mentions or {}).items()
|
for user_id, name in (display_name_mentions or {}).items()
|
||||||
|
@ -272,7 +311,7 @@ class HTMLProcessor:
|
||||||
"separate": {"a"} if inline else {
|
"separate": {"a"} if inline else {
|
||||||
"a", "p", "li", "table", "tr", "th", "td", "br", "hr", "img",
|
"a", "p", "li", "table", "tr", "th", "td", "br", "hr", "img",
|
||||||
},
|
},
|
||||||
"whitespace": {},
|
"whitespace": set(),
|
||||||
"keep_typographic_whitespace": True,
|
"keep_typographic_whitespace": True,
|
||||||
"add_nofollow": False,
|
"add_nofollow": False,
|
||||||
"autolink": {
|
"autolink": {
|
||||||
|
|
|
@ -151,7 +151,6 @@ class MatrixClient(nio.AsyncClient):
|
||||||
"m.call.*",
|
"m.call.*",
|
||||||
"m.room.third_party_invite",
|
"m.room.third_party_invite",
|
||||||
"m.room.tombstone",
|
"m.room.tombstone",
|
||||||
"m.reaction",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -179,6 +178,24 @@ class MatrixClient(nio.AsyncClient):
|
||||||
if host in ("127.0.0.1", "localhost", "::1"):
|
if host in ("127.0.0.1", "localhost", "::1"):
|
||||||
proxy = None
|
proxy = None
|
||||||
|
|
||||||
|
# self is passed in explicitly, so that it will also be passed to
|
||||||
|
# handlers in the user config
|
||||||
|
self.cmd_handler_map = {
|
||||||
|
"me ": MatrixClient.handle_cmd_emote,
|
||||||
|
"react ": MatrixClient.handle_cmd_react,
|
||||||
|
"spoiler ": MatrixClient.handle_cmd_spoiler,
|
||||||
|
"unspoiler": MatrixClient.handle_cmd_unspoiler,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cmd_handler_map = {
|
||||||
|
**self.cmd_handler_map,
|
||||||
|
**backend.settings.Commands.get_cmd_handler_map(),
|
||||||
|
}
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
# make sure we don't break older configs
|
||||||
|
pass
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
homeserver = homeserver,
|
homeserver = homeserver,
|
||||||
user = user,
|
user = user,
|
||||||
|
@ -238,8 +255,6 @@ class MatrixClient(nio.AsyncClient):
|
||||||
# {room_id: event}
|
# {room_id: event}
|
||||||
self.power_level_events: Dict[str, nio.PowerLevelsEvent] = {}
|
self.power_level_events: Dict[str, nio.PowerLevelsEvent] = {}
|
||||||
|
|
||||||
self.invalid_disconnecting: bool = False
|
|
||||||
|
|
||||||
self.nio_callbacks = NioCallbacks(self)
|
self.nio_callbacks = NioCallbacks(self)
|
||||||
|
|
||||||
|
|
||||||
|
@ -609,7 +624,6 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
return {**self.invited_rooms, **self.rooms}
|
return {**self.invited_rooms, **self.rooms}
|
||||||
|
|
||||||
|
|
||||||
async def send_text(
|
async def send_text(
|
||||||
self,
|
self,
|
||||||
room_id: str,
|
room_id: str,
|
||||||
|
@ -617,22 +631,46 @@ class MatrixClient(nio.AsyncClient):
|
||||||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
reply_to_event_id: Optional[str] = None,
|
reply_to_event_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a markdown `m.text` or `m.notice` (with `/me`) message ."""
|
if text.startswith("//") or text.startswith(r"\/"):
|
||||||
|
await self._send_text(
|
||||||
|
room_id, text[1:], display_name_mentions, reply_to_event_id)
|
||||||
|
elif text.startswith("/"):
|
||||||
|
for k, v in self.cmd_handler_map.items():
|
||||||
|
if text.startswith("/" + k):
|
||||||
|
await v(self, room_id, text[len("/" + k):],
|
||||||
|
display_name_mentions, reply_to_event_id)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id,
|
||||||
|
r"That command was not recognised. "
|
||||||
|
r"To send a message starting with /, use //",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self._send_text(
|
||||||
|
room_id, text, display_name_mentions, reply_to_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_text(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
text: str,
|
||||||
|
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
override_to_html: Optional[str] = None,
|
||||||
|
override_echo_body: Optional[str] = None,
|
||||||
|
emote: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Send a markdown `m.text` or `m.emote` message ."""
|
||||||
|
|
||||||
from_md = partial(
|
from_md = partial(
|
||||||
HTML.from_markdown, display_name_mentions=display_name_mentions,
|
HTML.from_markdown, display_name_mentions=display_name_mentions,
|
||||||
)
|
)
|
||||||
|
|
||||||
escape = False
|
|
||||||
if text.startswith("//") or text.startswith(r"\/"):
|
|
||||||
escape = True
|
|
||||||
text = text[1:]
|
|
||||||
|
|
||||||
content: Dict[str, Any]
|
content: Dict[str, Any]
|
||||||
|
|
||||||
if text.startswith("/me ") and not escape:
|
if emote:
|
||||||
event_type = nio.RoomMessageEmote
|
event_type = nio.RoomMessageEmote
|
||||||
text = text[len("/me "):]
|
|
||||||
content = {"body": text, "msgtype": "m.emote"}
|
content = {"body": text, "msgtype": "m.emote"}
|
||||||
to_html = from_md(text, inline=True, outgoing=True)
|
to_html = from_md(text, inline=True, outgoing=True)
|
||||||
echo_body = from_md(text, inline=True)
|
echo_body = from_md(text, inline=True)
|
||||||
|
@ -642,6 +680,12 @@ class MatrixClient(nio.AsyncClient):
|
||||||
to_html = from_md(text, outgoing=True)
|
to_html = from_md(text, outgoing=True)
|
||||||
echo_body = from_md(text)
|
echo_body = from_md(text)
|
||||||
|
|
||||||
|
# override_echo_body will not be effective if it is a reply.
|
||||||
|
# echo_body is only shown before the event is received back from the
|
||||||
|
# server, so this is fine if not ideal
|
||||||
|
to_html = override_to_html or to_html
|
||||||
|
echo_body = override_echo_body or echo_body
|
||||||
|
|
||||||
if to_html not in (html.escape(text), f"<p>{html.escape(text)}</p>"):
|
if to_html not in (html.escape(text), f"<p>{html.escape(text)}</p>"):
|
||||||
content["format"] = "org.matrix.custom.html"
|
content["format"] = "org.matrix.custom.html"
|
||||||
content["formatted_body"] = to_html
|
content["formatted_body"] = to_html
|
||||||
|
@ -697,6 +741,171 @@ class MatrixClient(nio.AsyncClient):
|
||||||
await self.pause_while_offline()
|
await self.pause_while_offline()
|
||||||
await self._send_message(room_id, content, tx_id)
|
await self._send_message(room_id, content, tx_id)
|
||||||
|
|
||||||
|
async def handle_cmd_emote(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
text: str,
|
||||||
|
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
):
|
||||||
|
await self._send_text(
|
||||||
|
room_id,
|
||||||
|
text,
|
||||||
|
display_name_mentions,
|
||||||
|
reply_to_event_id,
|
||||||
|
emote=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cmd_react(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
text: str,
|
||||||
|
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
if reply_to_event_id is None or reply_to_event_id == "":
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id, "Please reply to a message to react to it")
|
||||||
|
else:
|
||||||
|
reaction = emoji.emojize(
|
||||||
|
text, language="alias", variant="emoji_type")
|
||||||
|
await self.send_reaction(room_id, reaction, reply_to_event_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cmd_spoiler(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
text: str,
|
||||||
|
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
from_md = partial(
|
||||||
|
HTML.from_markdown, display_name_mentions=display_name_mentions,
|
||||||
|
)
|
||||||
|
|
||||||
|
to_html = from_md(text, outgoing=True)
|
||||||
|
echo_body = from_md(text)
|
||||||
|
|
||||||
|
if to_html.startswith("<p>") and to_html.endswith("</p>"):
|
||||||
|
# we want to make sure the <span> is inside the <p>, otherwise the
|
||||||
|
# black bar will be too wide
|
||||||
|
inner_html = to_html[len('<p>'):-len('</p>')]
|
||||||
|
to_html = f"<p><span data-mx-spoiler>{inner_html}</span></p>"
|
||||||
|
else:
|
||||||
|
to_html = f"<span data-mx-spoiler>{to_html}</span>"
|
||||||
|
|
||||||
|
if echo_body.startswith("<p>") and echo_body.endswith("</p>"):
|
||||||
|
inner_html = echo_body[len('<p>'):-len('</p>')]
|
||||||
|
echo_body = f"<p><span data-mx-spoiler>{inner_html}</span></p>"
|
||||||
|
else:
|
||||||
|
echo_body = f"<span data-mx-spoiler>{echo_body}</span>"
|
||||||
|
|
||||||
|
await self._send_text(
|
||||||
|
room_id,
|
||||||
|
text,
|
||||||
|
display_name_mentions,
|
||||||
|
reply_to_event_id,
|
||||||
|
override_to_html = to_html,
|
||||||
|
override_echo_body = echo_body,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_cmd_unspoiler(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
text: str,
|
||||||
|
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
if reply_to_event_id is None or reply_to_event_id == "":
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id,
|
||||||
|
"Please reply to a message with /unspoiler to unspoiler it",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
spoiler_event: Event = \
|
||||||
|
self.models[self.user_id, room_id, "events"][reply_to_event_id]
|
||||||
|
|
||||||
|
# get formatted_body, fallback to body,
|
||||||
|
spoiler = getattr(spoiler_event.source, "formatted_body", None) \
|
||||||
|
or getattr(spoiler_event.source, "body", "")
|
||||||
|
|
||||||
|
unspoiler = re.sub(
|
||||||
|
r"<span[^>]+data-mx-spoiler[^>]*>(.*?)</?span>", r"\1",
|
||||||
|
spoiler,
|
||||||
|
)
|
||||||
|
await self.send_fake_notice(room_id, unspoiler)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_reaction(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
key: str,
|
||||||
|
reacts_to: str,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# local event id in model isn't necessarily the actual event id
|
||||||
|
reacts_to_event = self.models[
|
||||||
|
self.user_id, room_id, "events"][reacts_to]
|
||||||
|
|
||||||
|
reacts_to_event_id = reacts_to_event.event_id
|
||||||
|
|
||||||
|
if self.user_id in reacts_to_event.reactions.get(key, {}).get('users', []):
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id,
|
||||||
|
"Can't send the same reaction more than once",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif reacts_to_event_id.startswith("echo-"):
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id,
|
||||||
|
"Can't react to that, it's not a real event",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
item_uuid = uuid4()
|
||||||
|
|
||||||
|
content: Dict[str, Any] = {
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
"event_id": reacts_to_event_id,
|
||||||
|
"key": key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_id = uuid4()
|
||||||
|
content[f"{__reverse_dns__}.transaction_id"] = str(tx_id)
|
||||||
|
|
||||||
|
await self.pause_while_offline()
|
||||||
|
try:
|
||||||
|
await self._send_message(
|
||||||
|
room_id, content, item_uuid, message_type = "m.reaction")
|
||||||
|
except MatrixError as err:
|
||||||
|
if err.m_code == "M_DUPLICATE_ANNOTATION":
|
||||||
|
# potentially possible if the new reaction is
|
||||||
|
# sent before the existing reaction is loaded
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id,
|
||||||
|
"Can't send the same reaction more than once",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if err.m_code == "M_UNKNOWN":
|
||||||
|
await self.send_fake_notice(
|
||||||
|
room_id,
|
||||||
|
"Failed to send reaction. Has the event you are reacting to fully sent yet?",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
# only update the UI after the reaction is sent, to not be misleading
|
||||||
|
await self._register_reaction(
|
||||||
|
self.all_rooms[room_id],
|
||||||
|
reacts_to_event_id,
|
||||||
|
key,
|
||||||
|
self.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
async def toggle_pause_transfer(
|
async def toggle_pause_transfer(
|
||||||
self, room_id: str, uuid: Union[str, UUID],
|
self, room_id: str, uuid: Union[str, UUID],
|
||||||
|
@ -748,7 +957,6 @@ class MatrixClient(nio.AsyncClient):
|
||||||
reply_to_event_id: Optional[str] = None,
|
reply_to_event_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a `m.file`, `m.image`, `m.audio` or `m.video` message.
|
"""Send a `m.file`, `m.image`, `m.audio` or `m.video` message.
|
||||||
|
|
||||||
The Matrix client-server API states that media messages can't have a
|
The Matrix client-server API states that media messages can't have a
|
||||||
reply attached.
|
reply attached.
|
||||||
Thus, if a `reply_to_event_id` is passed, we send a pseudo-reply as two
|
Thus, if a `reply_to_event_id` is passed, we send a pseudo-reply as two
|
||||||
|
@ -1022,6 +1230,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
room_id: str,
|
room_id: str,
|
||||||
transaction_id: UUID,
|
transaction_id: UUID,
|
||||||
event_type: Type[nio.Event],
|
event_type: Type[nio.Event],
|
||||||
|
fake_event: bool = False,
|
||||||
**event_fields,
|
**event_fields,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a local model `Event` while waiting for the server.
|
"""Register a local model `Event` while waiting for the server.
|
||||||
|
@ -1049,13 +1258,13 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
event = Event(
|
event = Event(
|
||||||
id = f"echo-{transaction_id}",
|
id = f"echo-{transaction_id}",
|
||||||
event_id = "",
|
event_id = f"echo-{transaction_id}" if fake_event else "",
|
||||||
event_type = event_type,
|
event_type = event_type,
|
||||||
date = datetime.now(),
|
date = datetime.now(),
|
||||||
sender_id = self.user_id,
|
sender_id = self.user_id,
|
||||||
sender_name = our_info.display_name,
|
sender_name = our_info.display_name,
|
||||||
sender_avatar = our_info.avatar_url,
|
sender_avatar = our_info.avatar_url,
|
||||||
is_local_echo = True,
|
is_local_echo = not fake_event,
|
||||||
links = Event.parse_links(content),
|
links = Event.parse_links(content),
|
||||||
**event_fields,
|
**event_fields,
|
||||||
)
|
)
|
||||||
|
@ -1067,9 +1276,18 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
await self.set_room_last_event(room_id, event)
|
await self.set_room_last_event(room_id, event)
|
||||||
|
|
||||||
|
async def send_fake_notice(self, room_id, msg):
|
||||||
|
await self._local_echo(
|
||||||
|
room_id,
|
||||||
|
uuid4(),
|
||||||
|
nio.RoomMessageNotice,
|
||||||
|
content = msg,
|
||||||
|
fake_event = True,
|
||||||
|
)
|
||||||
|
|
||||||
async def _send_message(
|
async def _send_message(
|
||||||
self, room_id: str, content: dict, transaction_id: UUID,
|
self, room_id: str, content: dict, transaction_id: UUID,
|
||||||
|
message_type: str = "m.room.message",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a message event with `content` dict to a room."""
|
"""Send a message event with `content` dict to a room."""
|
||||||
|
|
||||||
|
@ -1079,7 +1297,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
async with self.backend.send_locks[room_id]:
|
async with self.backend.send_locks[room_id]:
|
||||||
await self.room_send(
|
await self.room_send(
|
||||||
room_id = room_id,
|
room_id = room_id,
|
||||||
message_type = "m.room.message",
|
message_type = message_type,
|
||||||
content = content,
|
content = content,
|
||||||
ignore_unverified_devices = True,
|
ignore_unverified_devices = True,
|
||||||
)
|
)
|
||||||
|
@ -2434,11 +2652,21 @@ class MatrixClient(nio.AsyncClient):
|
||||||
ev: nio.ReactionEvent,
|
ev: nio.ReactionEvent,
|
||||||
event_id: str = "",
|
event_id: str = "",
|
||||||
**fields,
|
**fields,
|
||||||
) -> Event:
|
) -> None:
|
||||||
|
await self._register_reaction(room, ev.reacts_to, ev.key, ev.sender)
|
||||||
|
await self.register_nio_event(
|
||||||
|
room, ev, event_id, type_specifier=TypeSpecifier.Reaction,
|
||||||
|
content=ev.key, hidden=True, **fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _register_reaction(
|
||||||
|
self,
|
||||||
|
room: nio.MatrixRoom,
|
||||||
|
reacts_to: str,
|
||||||
|
key: str,
|
||||||
|
sender: str,
|
||||||
|
) -> None:
|
||||||
"""Register/update a reaction."""
|
"""Register/update a reaction."""
|
||||||
reacts_to = ev.reacts_to
|
|
||||||
key = ev.key
|
|
||||||
sender = ev.sender
|
|
||||||
|
|
||||||
model = self.models[self.user_id, room.room_id, "events"]
|
model = self.models[self.user_id, room.room_id, "events"]
|
||||||
reacts_to_event = model.get(reacts_to)
|
reacts_to_event = model.get(reacts_to)
|
||||||
|
@ -2451,7 +2679,8 @@ class MatrixClient(nio.AsyncClient):
|
||||||
if reacts_to_event:
|
if reacts_to_event:
|
||||||
reactions = reacts_to_event.reactions
|
reactions = reacts_to_event.reactions
|
||||||
if key not in reactions:
|
if key not in reactions:
|
||||||
reactions[key] = {"hint": emoji.demojize(key), "users": []}
|
reactions[key] = {
|
||||||
|
"hint": emoji.demojize(key, language="alias"), "users": []}
|
||||||
if sender not in reactions[key]["users"]:
|
if sender not in reactions[key]["users"]:
|
||||||
reactions[key]["users"].append(sender)
|
reactions[key]["users"].append(sender)
|
||||||
reacts_to_event.set_fields(reactions=reactions)
|
reacts_to_event.set_fields(reactions=reactions)
|
||||||
|
@ -2467,11 +2696,6 @@ class MatrixClient(nio.AsyncClient):
|
||||||
if sender not in registry[reacts_to][key]:
|
if sender not in registry[reacts_to][key]:
|
||||||
registry[reacts_to][key].append(sender)
|
registry[reacts_to][key].append(sender)
|
||||||
|
|
||||||
await self.register_nio_event(
|
|
||||||
room, ev, event_id, type_specifier=TypeSpecifier.Reaction,
|
|
||||||
content=key, hidden=True, **fields,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def register_message_replacement(
|
async def register_message_replacement(
|
||||||
self,
|
self,
|
||||||
room: nio.MatrixRoom,
|
room: nio.MatrixRoom,
|
||||||
|
@ -2689,8 +2913,8 @@ class MatrixClient(nio.AsyncClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
sender = item.sender_name or item.sender_id
|
sender = item.sender_name or item.sender_id
|
||||||
is_linux = platform.system() == "Linux"
|
is_nux = platform.system() == "Linux" or "BSD" in platform.system()
|
||||||
use_html = is_linux and self.backend.settings.Notifications.use_html
|
use_html = is_nux and self.backend.settings.Notifications.use_html
|
||||||
content = item.inline_content if use_html else item.plain_content
|
content = item.inline_content if use_html else item.plain_content
|
||||||
|
|
||||||
if isinstance(ev, nio.RoomMessageEmote) and use_html:
|
if isinstance(ev, nio.RoomMessageEmote) and use_html:
|
||||||
|
|
|
@ -541,3 +541,36 @@ class Keys:
|
||||||
|
|
||||||
# Sign out checked sessions if any, else sign out all sessions.
|
# Sign out checked sessions if any, else sign out all sessions.
|
||||||
sign_out = ["Alt+S", "Delete"]
|
sign_out = ["Alt+S", "Delete"]
|
||||||
|
|
||||||
|
class Commands:
|
||||||
|
# If you are an advanced user, here you can define new /commands
|
||||||
|
#
|
||||||
|
# get_cmd_handler_map should return a dictionary of the form
|
||||||
|
# {
|
||||||
|
# "command": command_handling_function,
|
||||||
|
# "another_command": ...
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# each command_handling_function should have the signature:
|
||||||
|
#
|
||||||
|
# async def command_handling_function(
|
||||||
|
# matrix_client: moment.backend.MatrixClient,
|
||||||
|
# room_id: str,
|
||||||
|
# text: str, # text after the end of /command
|
||||||
|
# display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||||
|
# reply_to_event_id: Optional[str] = None,
|
||||||
|
# ) -> None:
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
|
||||||
|
def get_cmd_handler_map(self):
|
||||||
|
return {
|
||||||
|
# "rot13 ": self.handle_cmd_rot13,
|
||||||
|
}
|
||||||
|
|
||||||
|
# @staticmethod
|
||||||
|
# def handle_cmd_rot13(matrix_client, room_id, text, display_name_mentions, reply_to_event_id):
|
||||||
|
# import codecs
|
||||||
|
# matrix_client._send_text(
|
||||||
|
# room_id, codecs.encode(text, 'rot_13'), display_name_mentions, reply_to_event_id
|
||||||
|
# )
|
|
@ -367,7 +367,7 @@ int main(int argc, char *argv[]) {
|
||||||
QApplication::setOrganizationName("moment");
|
QApplication::setOrganizationName("moment");
|
||||||
QApplication::setApplicationName("moment");
|
QApplication::setApplicationName("moment");
|
||||||
QApplication::setApplicationDisplayName("Moment");
|
QApplication::setApplicationDisplayName("Moment");
|
||||||
QApplication::setApplicationVersion("0.7.4");
|
QApplication::setApplicationVersion("0.7.5");
|
||||||
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
|
||||||
// app needs to be constructed before attempting to migrate
|
// app needs to be constructed before attempting to migrate
|
||||||
|
|
Loading…
Reference in New Issue
Block a user