merge bookmark @

This commit is contained in:
Goffi 2021-11-12 17:21:24 +01:00
commit 14d00a9dda
28 changed files with 327 additions and 201 deletions

View file

@ -7,6 +7,7 @@ tags
twistd.log
twistd.pid
bridge_constructor/generated
tests/e2e/report*
_trial_temp/
sat.egg-info
*.un~

View file

@ -100,7 +100,7 @@ v 0.8.0 « La Cecília » (NOT RELEASED YET):
- bulma theme becomes the default one for default site
- "tickets" have been renamed to more generic term "lists"
- (lists) common lists can be created from templates
- (photos) album can now be created or deleted from Libervia
- (photos) album can now be created or deleted from Libervia Web
- (photos/album) photos or videos can now be uploaded from Libervia
- (photos/album) a mobile friendly slideshow can be run
- (photos/album) affiliations can be modified with new invitations manager

View file

@ -16,7 +16,7 @@ USER libervia
RUN ./entrypoint.sh \
# we create the file sharing component which will autoconnect when backend is started
jp profile create file_sharing -j files.server1.test -p "" --xmpp-password test_e2e -C file_sharing -A && \
jp profile create file-sharing -j files.server1.test -p "" --xmpp-password test_e2e -C file-sharing -A && \
libervia-backend stop
RUN ./entrypoint.sh \

View file

@ -4,7 +4,7 @@ email_server = libervia-backend.test
email_port = 8025
email_admins_list = admin@server1.test
[component file_sharing]
[component file-sharing]
http_upload_public_facing_url = http://localhost:7777
http_upload_connection_type = http
http_upload_port = 7777

View file

@ -1,5 +1,5 @@
ARG REVISION=dev
FROM libervia/backend:${REVISION}
ARG REVISION
FROM libervia/backend:${REVISION:-dev}
LABEL maintainer="Goffi <tmp_dockerfiles@goffi.org>"
@ -58,7 +58,7 @@ RUN \
RUN ./entrypoint.sh \
# we create the file sharing component which will autoconnect when backend is started
li profile create file_sharing -j files.server1.test -p "" --xmpp-password test_e2e -C file_sharing -A && \
li profile create file-sharing -j files.server1.test -p "" --xmpp-password test_e2e -C file-sharing -A && \
libervia-backend stop
ENV LIBERVIA_TEST_REPORT_DIR=/reports

View file

@ -4,7 +4,7 @@ email_server = libervia-backend.test
email_port = 8025
email_admins_list = admin@server1.test
[component file_sharing]
[component file-sharing]
tls_certificate = /usr/share/libervia/certificates/server1.test.pem
tls_private_key = /usr/share/libervia/certificates/server1.test-key.pem
http_upload_public_facing_url = https://libervia-backend.test:8888

View file

@ -36,12 +36,12 @@ RUN python3 -m venv libervia_env && libervia_env/bin/pip install -U pip wheel &&
# local repos without it them cause troubles
hg clone https://repos.goffi.org/urwid-satext && ~/libervia_env/bin/pip install -e urwid-satext && \
mv urwid-satext/urwid_satext.egg-info ~/libervia_env/lib/python3.*/site-packages && \
hg clone https://repos.goffi.org/sat_tmp -u "${REVISION:-tip}" && ~/libervia_env/bin/pip install -e sat_tmp && \
hg clone https://repos.goffi.org/sat_tmp -u "${REVISION:-@}" && ~/libervia_env/bin/pip install -e sat_tmp && \
mv sat_tmp/sat_tmp.egg-info ~/libervia_env/lib/python3.*/site-packages && \
hg clone https://repos.goffi.org/libervia-templates && ~/libervia_env/bin/pip install -e libervia-templates && \
mv libervia-templates/libervia_templates.egg-info ~/libervia_env/lib/python3.*/site-packages && \
hg clone https://repos.goffi.org/libervia-media && \
hg clone https://repos.goffi.org/libervia-backend -u "${REVISION:-tip}" && ~/libervia_env/bin/pip install -e 'libervia-backend[SVG]' && \
hg clone https://repos.goffi.org/libervia-backend -u "${REVISION:-@}" && ~/libervia_env/bin/pip install -e 'libervia-backend[SVG]' && \
mv libervia-backend/libervia_backend.egg-info ~/libervia_env/lib/python3.*/site-packages && \
mkdir -p /home/libervia/.local/share/libervia

View file

@ -7,7 +7,6 @@ services:
depends_on:
# we need to depend on backend to get IP address of the container for conf
- backend
tmpfs: /var/lib/prosody
tmpfs: /var/log/prosody
networks:
default:

View file

@ -1,5 +1,5 @@
ARG REVISION=dev
FROM libervia/web:${REVISION}
ARG REVISION
FROM libervia/web:${REVISION:-dev}
LABEL maintainer="Goffi <tmp_dockerfiles@goffi.org>"

View file

@ -1,5 +1,5 @@
ARG REVISION=dev
FROM libervia/web:${REVISION}
ARG REVISION
FROM libervia/web:${REVISION:-dev}
LABEL maintainer="Goffi <tmp_dockerfiles@goffi.org>"

View file

@ -11,7 +11,7 @@ USER root
RUN apt-get install -y --no-install-recommends yarnpkg
WORKDIR /home/libervia
USER libervia
RUN cd /src && hg clone https://repos.goffi.org/libervia-web -u "${REVISION:-tip}" && \
RUN cd /src && hg clone https://repos.goffi.org/libervia-web -u "${REVISION:-@}" && \
~/libervia_env/bin/pip install -e libervia-web && \
mv libervia-web/libervia_web.egg-info ~/libervia_env/lib/python3.*/site-packages

View file

@ -16,7 +16,3 @@ COPY --chown=root:prosody prosody.cfg.lua /etc/prosody/prosody.cfg.lua
COPY --chown=root:tls-cert certificates/server1.test/cert.pem /usr/share/libervia/certificates/server1.test.pem
COPY --chown=root:tls-cert certificates/server1.test/key.pem /usr/share/libervia/certificates/server1.test-key.pem
# we add exec to handle properly signals, this is missing upstream
# FIXME: to be removed when new images are generated with
# https://github.com/prosody/prosody-docker/pull/65
RUN sed -i "s/^runuser -u prosody/exec \0/" /entrypoint.sh

View file

@ -1,15 +0,0 @@
#!/bin/bash
set -e
usermod -u "$(stat -c %u /var/lib/prosody/.)" prosody
if [[ "$1" != "prosody" ]]; then
exec prosodyctl "$@"
exit 0;
fi
if [ "$LOCAL" -a "$PASSWORD" -a "$DOMAIN" ] ; then
prosodyctl register "$LOCAL" "$DOMAIN" "$PASSWORD"
fi
exec runuser -u prosody -- "$@"

View file

@ -6,8 +6,8 @@ services:
depends_on:
# we need to depend on backend to get IP address of the container for conf
- backend
tmpfs: /var/lib/prosody
tmpfs: /var/log/prosody
tmpfs:
- /var/log/prosody
networks:
default:
aliases:

View file

@ -1,22 +1,18 @@
attrs==21.2.0
Automat==20.2.0
Babel==2.9.1
cairocffi==1.2.0
CairoSVG==2.5.2
certifi==2021.5.30
cffi==1.14.5
chardet==4.0.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.7
constantly==15.1.0
cryptography==3.4.7
cssselect2==0.4.1
dbus-python==1.2.16
defusedxml==0.7.1
cryptography==3.4.8
dbus-python==1.2.18
DoubleRatchet==0.7.0
html2text==2020.1.16
hyperlink==21.0.0
idna==2.10
idna==3.3
incremental==21.3.0
Jinja2==3.0.1
Jinja2==3.0.2
langid==1.1.6
lxml==4.6.3
Markdown==3.3.4
@ -24,38 +20,36 @@ MarkupSafe==2.0.1
miniupnpc==2.0.2
mutagen==1.45.1
netifaces==0.11.0
numpy==1.20.3
numpy==1.21.3
OMEMO==0.12.0
omemo-backend-signal==0.2.6
Pillow==8.2.0
progressbar2==3.53.1
protobuf==3.17.3
Pillow==8.4.0
progressbar2==3.53.3
protobuf==3.19.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycairo==1.20.1
pycparser==2.20
pycrypto==2.6.1
Pygments==2.9.0
PyGObject==3.40.1
Pygments==2.10.0
PyGObject==3.40.0
PyNaCl==1.4.0
pyOpenSSL==20.0.1
python-dateutil==2.8.1
python-dateutil==2.8.2
python-potr==1.0.2
python-utils==2.5.6
pytz==2021.1
pytz==2021.3
pyxdg==0.27
PyYAML==5.4.1
requests==2.25.1
requests==2.26.0
service-identity==21.1.0
setuptools-scm==6.0.1
shortuuid==1.0.1
six==1.16.0
tinycss2==1.1.0
treq==21.5.0
Twisted==21.2.0
urllib3==1.26.5
typing-extensions==3.10.0.2
urllib3==1.26.7
urwid==2.1.2
webencodings==0.5.1
wokkel==18.0.0
X3DH==0.5.9
XEdDSA==0.4.7

View file

@ -303,7 +303,11 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
self.streamInitialized()
def _finish_connection(self, __):
self.conn_deferred.callback(None)
if self.conn_deferred.called:
# can happen in case of forced disconnection by server
log.debug(f"{self} has already been connected")
else:
self.conn_deferred.callback(None)
def streamInitialized(self):
"""Called after _authd"""

View file

@ -545,7 +545,7 @@ class FileSharing:
self._f.openFileWrite(
client, file_tmp_path, transfer_data, file_data, stream_object
)
return False, defer.succeed(True)
return False, True
async def _retrieveFiles(
self, client, session, content_data, content_name, file_data, file_elt

View file

@ -428,6 +428,7 @@ class Events(object):
)
else:
timestamp, data = self._parseEventElt(event_elt)
data["interest_id"] = item["id"]
events.append((timestamp, data))
defer.returnValue(events)

View file

@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import shortuuid
from typing import Optional
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber import error
@ -131,6 +132,97 @@ class InvitationsPlugin(object):
kwargs[key] = str(value)
return defer.ensureDeferred(self.create(**kwargs))
async def getExistingInvitation(self, email: Optional[str]) -> Optional[dict]:
"""Retrieve existing invitation with given email
@param email: check if any invitation exist with this email
@return: first found invitation, or None if nothing found
"""
# FIXME: This method is highly inefficient, it get all invitations and check them
# one by one, this is just a temporary way to avoid creating creating new accounts
# for an existing email. A better way will be available with Libervia 0.9.
# TODO: use a better way to check existing invitations
if email is None:
return None
all_invitations = await self.invitations.all()
for id_, invitation in all_invitations.items():
if invitation.get("email") == email:
invitation[KEY_ID] = id_
return invitation
async def _createAccountAndProfile(
self,
id_: str,
kwargs: dict,
extra: dict
) -> None:
"""Create XMPP account and Libervia profile for guest"""
## XMPP account creation
password = kwargs.pop('password', None)
if password is None:
password = utils.generatePassword()
assert password
# XXX: password is here saved in clear in database
# it is needed for invitation as the same password is used for profile
# and SàT need to be able to automatically open the profile with the uuid
# FIXME: we could add an extra encryption key which would be used with the
# uuid when the invitee is connecting (e.g. with URL). This key would
# not be saved and could be used to encrypt profile password.
extra[KEY_PASSWORD] = password
jid_ = kwargs.pop('jid_', None)
if not jid_:
domain = self.host.memory.getConfig(None, 'xmpp_domain')
if not domain:
# TODO: fallback to profile's domain
raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
domain=domain)
jid_ = jid.JID(jid_)
extra[KEY_JID] = jid_.full()
if jid_.user:
# we don't register account if there is no user as anonymous login is then
# used
try:
await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
except error.StanzaError as e:
prefix = jid_.user
idx = 0
while e.condition == 'conflict':
if idx >= SUFFIX_MAX:
raise exceptions.ConflictError(_("Can't create XMPP account"))
jid_.user = prefix + '_' + str(idx)
log.info(_("requested jid already exists, trying with {}".format(
jid_.full())))
try:
await self.host.plugins['XEP-0077'].registerNewAccount(
jid_,
password
)
except error.StanzaError:
idx += 1
else:
break
if e.condition != 'conflict':
raise e
log.info(_("account {jid_} created").format(jid_=jid_.full()))
## profile creation
extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(
uuid=id_
)
# profile creation should not fail as we generate unique name ourselves
await self.host.memory.createProfile(guest_profile, password)
await self.host.memory.startSession(password, guest_profile)
await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
profile_key=guest_profile)
await self.host.memory.setParam("Password", password, "Connection",
profile_key=guest_profile)
async def create(self, **kwargs):
r"""Create an invitation
@ -201,6 +293,13 @@ class InvitationsPlugin(object):
self.checkExtra(extra)
email = kwargs.pop('email', None)
existing = await self.getExistingInvitation(email)
if existing is not None:
log.info(f"There is already an invitation for {email!r}")
extra.update(existing)
del extra[KEY_ID]
emails_extra = kwargs.pop('emails_extra', [])
if not email and emails_extra:
raise ValueError(
@ -214,67 +313,18 @@ class InvitationsPlugin(object):
## uuid
log.info(_("creating an invitation"))
id_ = str(shortuuid.uuid())
id_ = existing[KEY_ID] if existing else str(shortuuid.uuid())
## XMPP account creation
password = kwargs.pop('password', None)
if password is None:
password = utils.generatePassword()
assert password
# XXX: password is here saved in clear in database
# it is needed for invitation as the same password is used for profile
# and SàT need to be able to automatically open the profile with the uuid
# FIXME: we could add an extra encryption key which would be used with the uuid
# when the invitee is connecting (e.g. with URL). This key would not be
# saved and could be used to encrypt profile password.
extra[KEY_PASSWORD] = password
if existing is None:
await self._createAccountAndProfile(id_, kwargs, extra)
jid_ = kwargs.pop('jid_', None)
if not jid_:
domain = self.host.memory.getConfig(None, 'xmpp_domain')
if not domain:
# TODO: fallback to profile's domain
raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
domain=domain)
jid_ = jid.JID(jid_)
if jid_.user:
# we don't register account if there is no user as anonymous login is then
# used
try:
await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
except error.StanzaError as e:
prefix = jid_.user
idx = 0
while e.condition == 'conflict':
if idx >= SUFFIX_MAX:
raise exceptions.ConflictError(_("Can't create XMPP account"))
jid_.user = prefix + '_' + str(idx)
log.info(_("requested jid already exists, trying with {}".format(
jid_.full())))
try:
await self.host.plugins['XEP-0077'].registerNewAccount(jid_,
password)
except error.StanzaError:
idx += 1
else:
break
if e.condition != 'conflict':
raise e
profile = kwargs.pop('profile', None)
guest_profile = extra[KEY_GUEST_PROFILE]
jid_ = jid.JID(extra[KEY_JID])
log.info(_("account {jid_} created").format(jid_=jid_.full()))
## profile creation
extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
# profile creation should not fail as we generate unique name ourselves
await self.host.memory.createProfile(guest_profile, password)
await self.host.memory.startSession(password, guest_profile)
await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
profile_key=guest_profile)
await self.host.memory.setParam("Password", password, "Connection",
profile_key=guest_profile)
## identity
name = kwargs.pop('name', None)
password = extra[KEY_PASSWORD]
if name is not None:
extra['name'] = name
try:
@ -306,7 +356,6 @@ class InvitationsPlugin(object):
else:
format_args['name'] = name
profile = kwargs.pop('profile', None)
if profile is None:
format_args['profile'] = ''
else:
@ -344,8 +393,6 @@ class InvitationsPlugin(object):
if kwargs:
log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
extra[KEY_JID] = jid_.full()
## extra data saving
self.invitations[id_] = extra

View file

@ -17,16 +17,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from html import escape
from functools import partial
from sat.core.i18n import _, D_
from sat.core.constants import Const as C
from sat.core.log import getLogger
from html import escape
import re
from typing import Set
from twisted.internet import defer
from twisted.internet.threads import deferToThread
from sat.core import exceptions
from sat.core.constants import Const as C
from sat.core.i18n import D_, _
from sat.core.log import getLogger
from sat.tools import xml_tools
try:
@ -115,7 +117,15 @@ VOID_ELEMENTS = (
"track",
"wbr")
SAFE_ATTRS = html.defs.safe_attrs.union(("style", "poster", "controls"))
SAFE_ATTRS = html.defs.safe_attrs.union({"style", "poster", "controls"}) - {"id"}
SAFE_CLASSES = {
# those classes are used for code highlighting
"bp", "c", "ch", "cm", "cp", "cpf", "cs", "dl", "err", "fm", "gd", "ge", "get", "gh",
"gi", "go", "gp", "gr", "gs", "gt", "gu", "highlight", "hll", "il", "k", "kc", "kd",
"kn", "kp", "kr", "kt", "m", "mb", "mf", "mh", "mi", "mo", "na", "nb", "nc", "nd",
"ne", "nf", "ni", "nl", "nn", "no", "nt", "nv", "o", "ow", "s", "sa", "sb", "sc",
"sd", "se", "sh", "si", "sr", "ss", "sx", "vc", "vg", "vi", "vm", "w", "write",
}
STYLES_VALUES_REGEX = (
r"^("
+ "|".join(
@ -237,7 +247,8 @@ class TextSyntaxes(object):
except ImportError:
log.warning("markdown or html2text not found, can't use Markdown syntax")
log.info(
"You can download/install them from https://pythonhosted.org/Markdown/ and https://github.com/Alir3z4/html2text/"
"You can download/install them from https://pythonhosted.org/Markdown/ "
"and https://github.com/Alir3z4/html2text/"
)
host.bridge.addMethod(
"syntaxConvert",
@ -288,14 +299,14 @@ class TextSyntaxes(object):
)
return failure
def cleanStyle(self, styles):
def cleanStyle(self, styles_raw: str) -> str:
""""Clean unsafe CSS styles
Remove styles not in the whitelist, or where the value doesn't match the regex
@param styles_raw(unicode): CSS styles
@return (unicode): cleaned styles
@param styles_raw: CSS styles
@return: cleaned styles
"""
styles = styles.split(";")
styles: List[str] = styles_raw.split(";")
cleaned_styles = []
for style in styles:
try:
@ -315,6 +326,14 @@ class TextSyntaxes(object):
["%s: %s" % (key_, value_) for key_, value_ in cleaned_styles]
)
def cleanClasses(self, classes_raw: str) -> str:
"""Remove any non whitelisted class
@param classes_raw: classes set on an element
@return: remaining classes (can be empty string)
"""
return " ".join(SAFE_CLASSES.intersection(classes_raw.split()))
def cleanXHTML(self, xhtml):
"""Clean XHTML text by removing potentially dangerous/malicious parts
@ -341,6 +360,8 @@ class TextSyntaxes(object):
xhtml_elt = cleaner.clean_html(xhtml_elt)
for elt in xhtml_elt.xpath("//*[@style]"):
elt.set("style", self.cleanStyle(elt.get("style")))
for elt in xhtml_elt.xpath("//*[@class]"):
elt.set("class", self.cleanClasses(elt.get("class")))
# we remove self-closing elements for non-void elements
for element in xhtml_elt.iter(tag=etree.Element):
if not element.text:

View file

@ -45,10 +45,7 @@ except:
"Missing module pillow, please download/install it from https://python-pillow.github.io"
)
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
from wokkel.subprotocols import XMPPHandler
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
IMPORT_NAME = "XEP-0054"
@ -109,7 +106,7 @@ class XEP_0054(object):
NS_VCARD, client.profile)
await client._xep_0054_avatar_hashes.load()
def savePhoto(self, client, photo_elt, entity_jid):
def savePhoto(self, client, photo_elt, entity):
"""Parse a <PHOTO> photo_elt and save the picture"""
# XXX: this method is launched in a separate thread
try:
@ -131,7 +128,7 @@ class XEP_0054(object):
raise Failure(exceptions.NotFound())
if not buf:
log.warning("empty avatar for {jid}".format(jid=entity_jid.full()))
log.warning("empty avatar for {jid}".format(jid=entity.full()))
raise Failure(exceptions.NotFound())
log.debug(_("Decoding binary"))
@ -140,7 +137,7 @@ class XEP_0054(object):
if mime_type is None:
log.debug(
f"no media type found specified for {entity_jid}'s avatar, trying to "
f"no media type found specified for {entity}'s avatar, trying to "
f"guess")
try:
@ -149,7 +146,7 @@ class XEP_0054(object):
log.warning(f"Can't open avatar buffer: {e}")
if mime_type is None:
msg = f"Can't find media type for {entity_jid}'s avatar"
msg = f"Can't find media type for {entity}'s avatar"
log.warning(msg)
raise Failure(exceptions.DataError(msg))
@ -435,19 +432,6 @@ class XEP_0054_handler(XMPPHandler):
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
return []
def _checkAvatarHash(self, client, entity, given_hash):
"""Check that hash in cache (i.e. computed hash) is the same as given one"""
# XXX: if they differ, the avatar will be requested on each connection
# TODO: try to avoid re-requesting avatar in this case
computed_hash = client._xep_0054_avatar_hashes[entity.full()]
if computed_hash != given_hash:
log.warning(
"computed hash differs from given hash for {entity}:\n"
"computed: {computed}\ngiven: {given}".format(
entity=entity, computed=computed_hash, given=given_hash
)
)
async def update(self, presence):
"""Called on <presence/> stanza with vcard data
@ -468,19 +452,19 @@ class XEP_0054_handler(XMPPHandler):
except StopIteration:
return
new_hash = str(photo_elt).strip()
if new_hash == HASH_SHA1_EMPTY:
new_hash = ""
given_hash = str(photo_elt).strip()
if given_hash == HASH_SHA1_EMPTY:
given_hash = ""
hashes_cache = client._xep_0054_avatar_hashes
old_hash = hashes_cache.get(entity_jid.full())
if old_hash == new_hash:
if old_hash == given_hash:
# no change, we can return…
if new_hash:
if given_hash:
# …but we double check that avatar is in cache
avatar_cache = self.host.common_cache.getMetadata(new_hash)
avatar_cache = self.host.common_cache.getMetadata(given_hash)
if avatar_cache is None:
log.debug(
f"Avatar for [{entity_jid}] is known but not in cache, we get "
@ -492,19 +476,19 @@ class XEP_0054_handler(XMPPHandler):
log.debug(f"avatar for {entity_jid} is already in cache")
return
if new_hash is None:
if given_hash is None:
# XXX: we use empty string to indicate that there is no avatar
new_hash = ""
given_hash = ""
await hashes_cache.aset(entity_jid.full(), new_hash)
await hashes_cache.aset(entity_jid.full(), given_hash)
if not new_hash:
if not given_hash:
await self.plugin_parent._i.update(
client, IMPORT_NAME, "avatar", None, entity_jid)
# the avatar has been removed, no need to go further
return
avatar_cache = self.host.common_cache.getMetadata(new_hash)
avatar_cache = self.host.common_cache.getMetadata(given_hash)
if avatar_cache is not None:
log.debug(
f"New avatar found for [{entity_jid}], it's already in cache, we use it"
@ -516,7 +500,7 @@ class XEP_0054_handler(XMPPHandler):
'path': avatar_cache['path'],
'filename': avatar_cache['filename'],
'media_type': avatar_cache['mime_type'],
'cache_uid': new_hash,
'cache_uid': given_hash,
},
entity_jid
)
@ -528,7 +512,14 @@ class XEP_0054_handler(XMPPHandler):
if vcard is None:
log.warning(f"Unexpected empty vCard for {entity_jid}")
return
await self._checkAvatarHash(client, entity_jid, new_hash)
computed_hash = client._xep_0054_avatar_hashes[entity_jid.full()]
if computed_hash != given_hash:
log.warning(
"computed hash differs from given hash for {entity}:\n"
"computed: {computed}\ngiven: {given}".format(
entity=entity_jid, computed=computed_hash, given=given_hash
)
)
def _update(self, presence):
defer.ensureDeferred(self.update(presence))

View file

@ -73,7 +73,10 @@ class XEP_0264(object):
SIZE_MEDIUM = (640, 640)
SIZE_BIG = (1280, 1280)
SIZE_FULL_SCREEN = (2560, 2560)
SIZES = (SIZE_SMALL, SIZE_MEDIUM, SIZE_FULL_SCREEN)
# FIXME: SIZE_FULL_SCREEN is currently discarded as the resulting files are too big
# for BoB
# TODO: use an other mechanism than BoB for bigger files
SIZES = (SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG)
def __init__(self, host):
log.info(_("Plugin XEP_0264 initialization"))

View file

@ -1428,13 +1428,12 @@ class OMEMO:
header_elt = encrypted_elt.addElement('header')
header_elt['sid'] = str(encryption_data['sid'])
for to_jid in to_jids:
bare_jid_s = to_jid.userhost()
for rid, data in encryption_data['keys'][bare_jid_s].items():
for key_data in encryption_data['keys'].values():
for rid, data in key_data.items():
key_elt = header_elt.addElement(
'key',
content=b64enc(data['data']))
content=b64enc(data['data'])
)
key_elt['rid'] = str(rid)
if data['pre_key']:
key_elt['prekey'] = 'true'

View file

@ -121,33 +121,65 @@ class LiberviaCli:
return config.getConfig(self.sat_conf, section, name, default=default)
def guess_background(self):
if not sys.stdin.isatty() or not sys.stdout.isatty():
return 'dark'
stdin_fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(stdin_fd)
# cf. https://unix.stackexchange.com/a/245568 (thanks!)
try:
tty.setraw(sys.stdin.fileno())
# we request background color
sys.stdout.write("\033]11;?\a")
sys.stdout.flush()
expected = "\033]11;rgb:"
for c in expected:
ch = sys.stdin.read(1)
if ch != c:
# background id is not supported, we default to "dark"
# TODO: log something?
return 'dark'
red, green, blue = [int(c, 16)/65535 for c in sys.stdin.read(14).split('/')]
# '\a' is the last character
sys.stdin.read(1)
finally:
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
# for VTE based terminals
vte_version = int(os.getenv("VTE_VERSION", 0))
except ValueError:
vte_version = 0
lum = utils.per_luminance(red, green, blue)
if lum <= 0.5:
return 'dark'
color_fg_bg = os.getenv("COLORFGBG")
if ((sys.stdin.isatty() and sys.stdout.isatty()
and (
# XTerm
os.getenv("XTERM_VERSION")
# Konsole
or os.getenv("KONSOLE_VERSION")
# All VTE based terminals
or vte_version >= 3502
))):
# ANSI escape sequence
stdin_fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(stdin_fd)
try:
tty.setraw(sys.stdin.fileno())
# we request background color
sys.stdout.write("\033]11;?\a")
sys.stdout.flush()
expected = "\033]11;rgb:"
for c in expected:
ch = sys.stdin.read(1)
if ch != c:
# background id is not supported, we default to "dark"
# TODO: log something?
return 'dark'
red, green, blue = [
int(c, 16)/65535 for c in sys.stdin.read(14).split('/')
]
# '\a' is the last character
sys.stdin.read(1)
finally:
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
lum = utils.per_luminance(red, green, blue)
if lum <= 0.5:
return 'dark'
else:
return 'light'
elif color_fg_bg:
# no luck with ANSI escape sequence, we try COLORFGBG environment variable
try:
bg = int(color_fg_bg.split(";")[-1])
except ValueError:
return "dark"
if bg in list(range(7)) + [8]:
return "dark"
else:
return "light"
else:
return 'light'
# no autodetection method found
return "dark"
def set_color_theme(self):
background = self.get_config('background', default='auto')
@ -159,6 +191,7 @@ class LiberviaCli:
'your settings in libervia.conf').format(
background=repr(background)
))
self.background = background
if background == 'light':
C.A_HEADER = A.FG_MAGENTA
C.A_SUBHEADER = A.BOLD + A.FG_RED

View file

@ -199,6 +199,7 @@ class Theme(base.CommandBase):
pass
async def start(self):
print(f"background currently used: {A.BOLD}{self.host.background}{A.RESET}\n")
for attr in dir(C):
if not attr.startswith("A_"):
continue

View file

@ -41,7 +41,7 @@ install_requires = [
'pygments < 3',
'pygobject < 3.40.1',
'pyopenssl < 21.0.0',
'python-dateutil < 3',
'python-dateutil >= 2.8.1, < 3',
'python-potr < 1.1',
'pyxdg < 0.30',
'sat_tmp == 0.9.*',

View file

@ -21,7 +21,7 @@ import re
import pytest
from helium import (
go_to, write, press, click, drag_file, find_all, wait_until, S, Text, Link, Button,
get_driver, ENTER
select, scroll_down, get_driver, ENTER
)
@ -178,3 +178,54 @@ class TestPhotos:
click("test album")
wait_until(lambda: not S("#loading_screen").exists())
assert len(find_all(S("img.is-photo-thumbnail"))) == 2
class TestLists:
TEST_GENERIC_LIST_URL = (
"https://libervia-web.test:8443/lists/view/account1@server1.test/"
"fdp%2Fsubmitted%2Forg.salut-a-toi.tickets%3A0_test-generic-list"
)
@pytest.mark.dependency(name="create_generic_list")
def test_user_can_create_generic(self, log_in_account1):
go_to("https://libervia-web.test:8443/lists/view")
click("create a list")
tickets_btn = S("//span[text()='Tickets']")
click(tickets_btn)
write("test-generic-list", "name of the list")
click("create list")
wait_until(Text("Success").exists)
@pytest.mark.dependency(
name="create_generic_list_item", depends=["create_generic_list"]
)
def test_user_can_create_generic_list_item(self):
go_to(self.TEST_GENERIC_LIST_URL)
click("create")
write("test item", "title")
write("label 1, label 2", "labels")
select("type", "feature request")
write("this is a test body", "body")
scroll_down(600)
click("create list item")
wait_until(Text("Success").exists)
assert Link("create").exists()
assert Link("manage invitations").exists()
item = Link("test item")
assert item.exists()
labels_elt = item.web_element.find_element_by_class_name("xmlui_field__labels")
labels = [t.text for t in labels_elt.find_elements_by_tag_name("span")]
assert labels == ["label 1", "label 2"]
@pytest.mark.dependency(depends=["create_generic_list_item"])
def test_list_item_can_be_displayed(self):
go_to(self.TEST_GENERIC_LIST_URL)
item = Link("test item")
# FIXME: we can't click on the item created above with Selenium, looks like a
# Selenium bug, to be checked
go_to(item.href)
item_title = Text("test item")
assert item_title.exists()
item_body = Text("this is a test body")
assert item_body.exists()

View file

@ -226,7 +226,7 @@ def use_e2e_env():
docker_compose.up("-d")
p = docker_compose.exec(
"-T", "--workdir", "/src/libervia-backend/tests", "backend",
"--workdir", "/src/libervia-backend/tests", "backend",
"pytest", "-o", "cache_dir=/tmp", *sys.argv[1:], color="yes",
_in=sys.stdin, _out=live_out, _out_bufsize=0, _err=live_err, _err_bufsize=0,
_bg=True