Global reformatting with black.

This commit is contained in:
Goffi 2025-09-13 10:46:10 +02:00
commit 96bf3af324
68 changed files with 557 additions and 561 deletions

View file

@ -4,16 +4,13 @@
def process_docstring(app, what, name, obj, options, lines): def process_docstring(app, what, name, obj, options, lines):
lines[:] = [ lines[:] = [l.replace("@param", ":param").replace("@raise", ":raises") for l in lines]
l.replace("@param", ":param").replace("@raise", ":raises")
for l in lines
]
def setup(app): def setup(app):
app.connect("autodoc-process-docstring", process_docstring) app.connect("autodoc-process-docstring", process_docstring)
return { return {
'version': '0.1', "version": "0.1",
'parallel_read_safe': True, "parallel_read_safe": True,
'parallel_write_safe': True, "parallel_write_safe": True,
} }

View file

@ -1,4 +1,3 @@
# #
# Configuration file for the Sphinx documentation builder. # Configuration file for the Sphinx documentation builder.
# #
@ -24,9 +23,9 @@ sys.path.insert(0, os.path.abspath("./_ext"))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'Libervia Backend' project = "Libervia Backend"
copyright = '2019-2023 Libervia' copyright = "2019-2023 Libervia"
author = 'Libervia team' author = "Libervia team"
version = __version__ version = __version__
@ -60,22 +59,19 @@ rst_prolog = """
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = ["sphinx.ext.autodoc", "docstring"]
"sphinx.ext.autodoc",
"docstring"
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['.templates'] templates_path = [".templates"]
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -87,7 +83,7 @@ language = "en"
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path. # This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [u'.build', 'Thumbs.db', '.DS_Store'] exclude_patterns = [".build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = None pygments_style = None
@ -98,7 +94,7 @@ pygments_style = None
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
html_theme = 'alabaster' html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@ -109,7 +105,7 @@ html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['.static'] html_static_path = [".static"]
# Custom sidebar templates, must be a dictionary that maps document names # Custom sidebar templates, must be a dictionary that maps document names
# to template names. # to template names.
@ -125,7 +121,7 @@ html_static_path = ['.static']
# -- Options for HTMLHelp output --------------------------------------------- # -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'SalutToidoc' htmlhelp_basename = "SalutToidoc"
# -- Options for LaTeX output ------------------------------------------------ # -- Options for LaTeX output ------------------------------------------------
@ -134,15 +130,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
@ -152,8 +145,7 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'SalutToi.tex', u'Libervia Documentation', (master_doc, "SalutToi.tex", "Libervia Documentation", "Libervia team", "manual"),
u'Libervia team', 'manual'),
] ]
@ -161,10 +153,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, "saluttoi", "Libervia Documentation", [author], 1)]
(master_doc, 'saluttoi', u'Libervia Documentation',
[author], 1)
]
# -- Options for Texinfo output ---------------------------------------------- # -- Options for Texinfo output ----------------------------------------------
@ -173,9 +162,15 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'SalutToi', u'Libervia Documentation', (
author, 'SalutToi', 'One line description of project.', master_doc,
'Miscellaneous'), "SalutToi",
"Libervia Documentation",
author,
"SalutToi",
"One line description of project.",
"Miscellaneous",
),
] ]
@ -194,7 +189,7 @@ epub_title = project
# epub_uid = '' # epub_uid = ''
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ["search.html"]
# -- Extension configuration ------------------------------------------------- # -- Extension configuration -------------------------------------------------

View file

@ -47,4 +47,5 @@ class Global:
def storage(self) -> "Storage": def storage(self) -> "Storage":
return self.host.memory.storage return self.host.memory.storage
G = Global() G = Global()

View file

@ -46,7 +46,9 @@ class EncryptionSession(TypedDict):
# Incomplete types built through observation rather than code inspection. # Incomplete types built through observation rather than code inspection.
MessageDataExtra = TypedDict( MessageDataExtra = TypedDict(
"MessageDataExtra", {"encrypted": bool, "origin_id": str, "headers": dict[str, Any]}, total=False "MessageDataExtra",
{"encrypted": bool, "origin_id": str, "headers": dict[str, Any]},
total=False,
) )
@ -135,7 +137,7 @@ class SatXMPPEntity(ABC):
def generate_message_xml( def generate_message_xml(
self, self,
data: MessageData, data: MessageData,
post_xml_treatments: defer.Deferred|None = None, post_xml_treatments: defer.Deferred | None = None,
) -> MessageData: ) -> MessageData:
"""Generate <message/> stanza from message data """Generate <message/> stanza from message data
@ -177,7 +179,7 @@ class SatXMPPEntity(ABC):
def send(self, obj: domish.Element) -> None: def send(self, obj: domish.Element) -> None:
# We need to call super() due to mulitple inheritance: Wokkel XMPPClient or # We need to call super() due to mulitple inheritance: Wokkel XMPPClient or
# Component's `send` needs to be called. # Component's `send` needs to be called.
super().send(obj) # type: ignore super().send(obj) # type: ignore
@abstractmethod @abstractmethod
async def send_message_data(self, mess_data: MessageData) -> MessageData: async def send_message_data(self, mess_data: MessageData) -> MessageData:

View file

@ -515,7 +515,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
xs.add_hook(C.STREAM_HOOK_SEND, hook) xs.add_hook(C.STREAM_HOOK_SEND, hook)
super(SatXMPPEntity, self)._connected(xs) super(SatXMPPEntity, self)._connected(xs)
def disconnect_profile(self, reason: failure.Failure|None) -> None: def disconnect_profile(self, reason: failure.Failure | None) -> None:
if self._connected_d is not None: if self._connected_d is not None:
self.host_app.bridge.disconnected( self.host_app.bridge.disconnected(
self.profile self.profile
@ -616,11 +616,11 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
return iq_elt return iq_elt
def sendError( def sendError(
self, self,
iq_elt: domish.Element, iq_elt: domish.Element,
condition: str, condition: str,
text: str|None = None, text: str | None = None,
appCondition: str|None = None appCondition: str | None = None,
) -> None: ) -> None:
"""Send error stanza build from iq_elt """Send error stanza build from iq_elt
@ -1355,17 +1355,19 @@ class SatMessageProtocol(xmppim.MessageProtocol):
message = {} message = {}
subject = {} subject = {}
extra = {} extra = {}
data = MessageData({ data = MessageData(
"from": from_jid, {
"to": jid.JID(message_elt["to"]), "from": from_jid,
"uid": message_elt.getAttribute( "to": jid.JID(message_elt["to"]),
"uid", str(uuid.uuid4()) "uid": message_elt.getAttribute(
), # XXX: uid is not a standard attribute but may be added by plugins "uid", str(uuid.uuid4())
"message": message, ), # XXX: uid is not a standard attribute but may be added by plugins
"subject": subject, "message": message,
"type": message_elt.getAttribute("type", "normal"), "subject": subject,
"extra": extra, "type": message_elt.getAttribute("type", "normal"),
}) "extra": extra,
}
)
try: try:
message_id = extra["message_id"] = message_elt["id"] message_id = extra["message_id"] = message_elt["id"]

View file

@ -207,9 +207,9 @@ class Discovery(object):
def get_infos( def get_infos(
self, self,
client: SatXMPPEntity, client: SatXMPPEntity,
jid_: jid.JID|None=None, jid_: jid.JID | None = None,
node: str="", node: str = "",
use_cache: bool=True use_cache: bool = True,
) -> defer.Deferred[disco.DiscoInfo]: ) -> defer.Deferred[disco.DiscoInfo]:
"""get disco infos from jid_, filling capability hash if needed """get disco infos from jid_, filling capability hash if needed

View file

@ -82,8 +82,8 @@ class EncryptionHandler:
plg_instance, plg_instance,
name: str, name: str,
namespace: str, namespace: str,
priority: int=0, priority: int = 0,
directed: bool=False directed: bool = False,
) -> None: ) -> None:
"""Register a plugin handling an encryption algorithm """Register a plugin handling an encryption algorithm

View file

@ -1672,7 +1672,7 @@ class Memory:
access: dict | None = None, access: dict | None = None,
extra: dict | None = None, extra: dict | None = None,
peer_jid: Any | None = None, peer_jid: Any | None = None,
perms_to_check: tuple[str]|None = (C.ACCESS_PERM_WRITE,), perms_to_check: tuple[str] | None = (C.ACCESS_PERM_WRITE,),
) -> None: ) -> None:
"""Set a file metadata. """Set a file metadata.

View file

@ -5,52 +5,47 @@ Revises: fe3a02cb4bec
Create Date: 2025-05-02 11:20:11.555189 Create Date: 2025-05-02 11:20:11.555189
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '29f85e56b6d4' revision = "29f85e56b6d4"
down_revision = 'fe3a02cb4bec' down_revision = "fe3a02cb4bec"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
with op.batch_alter_table('history', schema=None) as batch_op: with op.batch_alter_table("history", schema=None) as batch_op:
batch_op.alter_column('extra', batch_op.alter_column(
existing_type=sa.TEXT(), "extra", existing_type=sa.TEXT(), type_=sa.JSON(), existing_nullable=True
type_=sa.JSON(), )
existing_nullable=True)
with op.batch_alter_table('private_gen_bin', schema=None) as batch_op: with op.batch_alter_table("private_gen_bin", schema=None) as batch_op:
batch_op.alter_column('value', batch_op.alter_column(
existing_type=sa.TEXT(), "value", existing_type=sa.TEXT(), type_=sa.JSON(), existing_nullable=True
type_=sa.JSON(), )
existing_nullable=True)
with op.batch_alter_table('private_ind_bin', schema=None) as batch_op: with op.batch_alter_table("private_ind_bin", schema=None) as batch_op:
batch_op.alter_column('value', batch_op.alter_column(
existing_type=sa.TEXT(), "value", existing_type=sa.TEXT(), type_=sa.JSON(), existing_nullable=True
type_=sa.JSON(), )
existing_nullable=True)
def downgrade(): def downgrade():
with op.batch_alter_table('private_ind_bin', schema=None) as batch_op: with op.batch_alter_table("private_ind_bin", schema=None) as batch_op:
batch_op.alter_column('value', batch_op.alter_column(
existing_type=sa.JSON(), "value", existing_type=sa.JSON(), type_=sa.TEXT(), existing_nullable=True
type_=sa.TEXT(), )
existing_nullable=True)
with op.batch_alter_table('private_gen_bin', schema=None) as batch_op: with op.batch_alter_table("private_gen_bin", schema=None) as batch_op:
batch_op.alter_column('value', batch_op.alter_column(
existing_type=sa.JSON(), "value", existing_type=sa.JSON(), type_=sa.TEXT(), existing_nullable=True
type_=sa.TEXT(), )
existing_nullable=True)
with op.batch_alter_table('history', schema=None) as batch_op: with op.batch_alter_table("history", schema=None) as batch_op:
batch_op.alter_column('extra', batch_op.alter_column(
existing_type=sa.JSON(), "extra", existing_type=sa.JSON(), type_=sa.TEXT(), existing_nullable=True
type_=sa.TEXT(), )
existing_nullable=True)

View file

@ -5,29 +5,34 @@ Revises: 29f85e56b6d4
Create Date: 2025-05-02 11:21:54.023587 Create Date: 2025-05-02 11:21:54.023587
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.sql import false from sqlalchemy.sql import false
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '6af2d8f6be76' revision = "6af2d8f6be76"
down_revision = '29f85e56b6d4' down_revision = "29f85e56b6d4"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
with op.batch_alter_table('thread', schema=None) as batch_op: with op.batch_alter_table("thread", schema=None) as batch_op:
batch_op.add_column(sa.Column('is_retroactive', sa.Boolean(), server_default=false(), nullable=False, comment="Indicates if thread ID was added after the original message was sent (e.g. to link a parent message which didn't have a thread ID to a thread).")) batch_op.add_column(
batch_op.alter_column('thread_id', sa.Column(
existing_type=sa.TEXT(), "is_retroactive",
nullable=False) sa.Boolean(),
server_default=false(),
nullable=False,
comment="Indicates if thread ID was added after the original message was sent (e.g. to link a parent message which didn't have a thread ID to a thread).",
)
)
batch_op.alter_column("thread_id", existing_type=sa.TEXT(), nullable=False)
def downgrade(): def downgrade():
with op.batch_alter_table('thread', schema=None) as batch_op: with op.batch_alter_table("thread", schema=None) as batch_op:
batch_op.alter_column('thread_id', batch_op.alter_column("thread_id", existing_type=sa.TEXT(), nullable=True)
existing_type=sa.TEXT(), batch_op.drop_column("is_retroactive")
nullable=True)
batch_op.drop_column('is_retroactive')

View file

@ -5,36 +5,74 @@ Revises: 6af2d8f6be76
Create Date: 2025-08-02 12:03:19.726497 Create Date: 2025-08-02 12:03:19.726497
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from libervia.backend.memory.sqla_mapping import JID from libervia.backend.memory.sqla_mapping import JID
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '8db042adb973' revision = "8db042adb973"
down_revision = '6af2d8f6be76' down_revision = "6af2d8f6be76"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
op.create_table('pubsub_affiliations', op.create_table(
sa.Column('id', sa.Integer(), nullable=False), "pubsub_affiliations",
sa.Column('node_id', sa.Integer(), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('entity', JID(), nullable=False), sa.Column("node_id", sa.Integer(), nullable=False),
sa.Column('affiliation', sa.Enum('outcast', 'member', 'publisher', 'owner', name='affiliation', create_constraint=True), nullable=False), sa.Column("entity", JID(), nullable=False),
sa.ForeignKeyConstraint(['node_id'], ['pubsub_nodes.id'], name=op.f('fk_pubsub_affiliations_node_id_pubsub_nodes'), ondelete='CASCADE'), sa.Column(
sa.PrimaryKeyConstraint('id', name=op.f('pk_pubsub_affiliations')), "affiliation",
sa.UniqueConstraint('node_id', 'entity', name=op.f('uq_pubsub_affiliations_node_id')) sa.Enum(
"outcast",
"member",
"publisher",
"owner",
name="affiliation",
create_constraint=True,
),
nullable=False,
),
sa.ForeignKeyConstraint(
["node_id"],
["pubsub_nodes.id"],
name=op.f("fk_pubsub_affiliations_node_id_pubsub_nodes"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_pubsub_affiliations")),
sa.UniqueConstraint(
"node_id", "entity", name=op.f("uq_pubsub_affiliations_node_id")
),
) )
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op: with op.batch_alter_table("pubsub_nodes", schema=None) as batch_op:
batch_op.add_column(sa.Column('access_model', sa.Enum('open', 'whitelist', name='accessmodel', create_constraint=True), nullable=True)) batch_op.add_column(
batch_op.add_column(sa.Column('publish_model', sa.Enum('publishers', 'subscribers', 'open', name='publishmodel', create_constraint=True), nullable=True)) sa.Column(
"access_model",
sa.Enum("open", "whitelist", name="accessmodel", create_constraint=True),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"publish_model",
sa.Enum(
"publishers",
"subscribers",
"open",
name="publishmodel",
create_constraint=True,
),
nullable=True,
)
)
def downgrade(): def downgrade():
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op: with op.batch_alter_table("pubsub_nodes", schema=None) as batch_op:
batch_op.drop_column('publish_model') batch_op.drop_column("publish_model")
batch_op.drop_column('access_model') batch_op.drop_column("access_model")
op.drop_table('pubsub_affiliations') op.drop_table("pubsub_affiliations")

View file

@ -5,28 +5,45 @@ Revises: 8db042adb973
Create Date: 2025-08-16 16:47:48.242973 Create Date: 2025-08-16 16:47:48.242973
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'ece2a6d7e4bb' revision = "ece2a6d7e4bb"
down_revision = '8db042adb973' down_revision = "8db042adb973"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op: with op.batch_alter_table("pubsub_nodes", schema=None) as batch_op:
batch_op.add_column(sa.Column('linked_node_id', sa.Integer(), nullable=True)) batch_op.add_column(sa.Column("linked_node_id", sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('parent_node_id', sa.Integer(), nullable=True)) batch_op.add_column(sa.Column("parent_node_id", sa.Integer(), nullable=True))
batch_op.create_foreign_key(batch_op.f('fk_pubsub_nodes_parent_node_id_pubsub_nodes'), 'pubsub_nodes', ['parent_node_id'], ['id'], ondelete='CASCADE') batch_op.create_foreign_key(
batch_op.create_foreign_key(batch_op.f('fk_pubsub_nodes_linked_node_id_pubsub_nodes'), 'pubsub_nodes', ['linked_node_id'], ['id'], ondelete='CASCADE') batch_op.f("fk_pubsub_nodes_parent_node_id_pubsub_nodes"),
"pubsub_nodes",
["parent_node_id"],
["id"],
ondelete="CASCADE",
)
batch_op.create_foreign_key(
batch_op.f("fk_pubsub_nodes_linked_node_id_pubsub_nodes"),
"pubsub_nodes",
["linked_node_id"],
["id"],
ondelete="CASCADE",
)
def downgrade(): def downgrade():
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op: with op.batch_alter_table("pubsub_nodes", schema=None) as batch_op:
batch_op.drop_constraint(batch_op.f('fk_pubsub_nodes_linked_node_id_pubsub_nodes'), type_='foreignkey') batch_op.drop_constraint(
batch_op.drop_constraint(batch_op.f('fk_pubsub_nodes_parent_node_id_pubsub_nodes'), type_='foreignkey') batch_op.f("fk_pubsub_nodes_linked_node_id_pubsub_nodes"), type_="foreignkey"
batch_op.drop_column('parent_node_id') )
batch_op.drop_column('linked_node_id') batch_op.drop_constraint(
batch_op.f("fk_pubsub_nodes_parent_node_id_pubsub_nodes"), type_="foreignkey"
)
batch_op.drop_column("parent_node_id")
batch_op.drop_column("linked_node_id")

View file

@ -500,7 +500,7 @@ class Storage:
@aio @aio
async def get_history_from_xmpp_id( async def get_history_from_xmpp_id(
self, client: SatXMPPEntity, message_id: str, message_type: str|None self, client: SatXMPPEntity, message_id: str, message_type: str | None
) -> History | None: ) -> History | None:
"""Retrieves history row that match a specific message ID. """Retrieves history row that match a specific message ID.
@ -520,12 +520,11 @@ class Storage:
) )
else: else:
query = select(History).where( query = select(History).where(
History.profile_id == profile_id, History.origin_id == message_id History.profile_id == profile_id, History.origin_id == message_id
) )
query = ( query = (
query query.outerjoin(History.messages)
.outerjoin(History.messages)
.outerjoin(History.subjects) .outerjoin(History.subjects)
.outerjoin(History.thread) .outerjoin(History.thread)
.options( .options(
@ -540,7 +539,6 @@ class Storage:
return history return history
@aio @aio
async def history_get( async def history_get(
self, self,
@ -676,9 +674,7 @@ class Storage:
# FIXME: "json_each" works with SQLite, but when PostGreSQL support will # FIXME: "json_each" works with SQLite, but when PostGreSQL support will
# be added, "json_array_elements" may have to be used with it. # be added, "json_array_elements" may have to be used with it.
subquery = func.json_each(History.extra["keywords"]).table_valued("value") subquery = func.json_each(History.extra["keywords"]).table_valued("value")
stmt = stmt.where( stmt = stmt.where(exists().where(subquery.c.value == keyword))
exists().where(subquery.c.value == keyword)
)
if "origin" in filters: if "origin" in filters:
stmt = stmt.where( stmt = stmt.where(
History.extra["origin"].as_string() == filters["origin"] History.extra["origin"].as_string() == filters["origin"]
@ -752,7 +748,7 @@ class Storage:
self, self,
message_uid: str, message_uid: str,
thread_id: str, thread_id: str,
parent_id: str|None, parent_id: str | None,
is_retroactive: bool, is_retroactive: bool,
) -> None: ) -> None:
"""Add a thread to a message. """Add a thread to a message.
@ -1180,8 +1176,8 @@ class Storage:
with_subscriptions: bool = False, with_subscriptions: bool = False,
with_affiliations: bool = False, with_affiliations: bool = False,
create: bool = False, create: bool = False,
create_kwargs: dict|None = None, create_kwargs: dict | None = None,
) -> PubsubNode|None: ) -> PubsubNode | None:
"""Retrieve a PubsubNode from DB """Retrieve a PubsubNode from DB
@param service: service hosting the node @param service: service hosting the node
@ -1238,9 +1234,9 @@ class Storage:
@aio @aio
async def get_pubsub_nodes( async def get_pubsub_nodes(
self, self,
client: SatXMPPEntity|None, client: SatXMPPEntity | None,
service: jid.JID|None, service: jid.JID | None,
node_name: str|None = None, node_name: str | None = None,
linked_node: bool = False, linked_node: bool = False,
depth: int = 0, depth: int = 0,
with_items: bool = False, with_items: bool = False,
@ -1279,7 +1275,7 @@ class Storage:
if depth > 0: if depth > 0:
options = [ options = [
selectinload(PubsubNode.child_nodes), selectinload(PubsubNode.child_nodes),
joinedload(PubsubNode.parent_node) joinedload(PubsubNode.parent_node),
] ]
else: else:
options = [] options = []
@ -1323,24 +1319,21 @@ class Storage:
base_node_ids = [node.id for node in base_nodes] base_node_ids = [node.id for node in base_nodes]
# Create recursive CTE to find child base nodes. # Create recursive CTE to find child base nodes.
hierarchy = select( hierarchy = (
PubsubNode.id, select(PubsubNode.id, literal(0).label("depth"))
literal(0).label('depth') .filter(
).filter( PubsubNode.id.in_(base_node_ids),
PubsubNode.id.in_(base_node_ids), PubsubNode.linked_node_id.is_(None),
PubsubNode.linked_node_id.is_(None) )
).cte(name="hierarchy", recursive=True) .cte(name="hierarchy", recursive=True)
)
# Recursive part: get direct children that are also base nodes. # Recursive part: get direct children that are also base nodes.
children = ( children = (
select( select(PubsubNode.id, hierarchy.c.depth + 1)
PubsubNode.id,
hierarchy.c.depth + 1
)
.join(hierarchy, PubsubNode.parent_node_id == hierarchy.c.id) .join(hierarchy, PubsubNode.parent_node_id == hierarchy.c.id)
.filter( .filter(
hierarchy.c.depth < depth, hierarchy.c.depth < depth, PubsubNode.linked_node_id.is_(None)
PubsubNode.linked_node_id.is_(None)
) )
) )
@ -1348,11 +1341,8 @@ class Storage:
full_hierarchy = hierarchy.union_all(children) full_hierarchy = hierarchy.union_all(children)
# Final query for all base nodes in the hierarchy. # Final query for all base nodes in the hierarchy.
hierarchy_query = ( hierarchy_query = select(PubsubNode).filter(
select(PubsubNode) PubsubNode.id.in_(select(full_hierarchy.c.id))
.filter(PubsubNode.id.in_(
select(full_hierarchy.c.id)
))
) )
# Apply the same eager loading options. # Apply the same eager loading options.
@ -1377,18 +1367,18 @@ class Storage:
client: SatXMPPEntity, client: SatXMPPEntity,
service: jid.JID, service: jid.JID,
name: str, name: str,
access_model: AccessModel|None = None, access_model: AccessModel | None = None,
publish_model: PublishModel|None = None, publish_model: PublishModel | None = None,
analyser: Optional[str] = None, analyser: Optional[str] = None,
type_: Optional[str] = None, type_: Optional[str] = None,
subtype: Optional[str] = None, subtype: Optional[str] = None,
subscribed: bool = False, subscribed: bool = False,
extra: dict|None = None, extra: dict | None = None,
items: list[PubsubItem]|None = None, items: list[PubsubItem] | None = None,
affiliations: list[PubsubAffiliation]|None = None, affiliations: list[PubsubAffiliation] | None = None,
subscriptions: list[PubsubSub]|None = None, subscriptions: list[PubsubSub] | None = None,
parent_node: PubsubNode|None = None, parent_node: PubsubNode | None = None,
linked_node: PubsubNode|None = None linked_node: PubsubNode | None = None,
) -> PubsubNode: ) -> PubsubNode:
node = PubsubNode( node = PubsubNode(
profile_id=self.profiles[client.profile], profile_id=self.profiles[client.profile],
@ -1400,12 +1390,12 @@ class Storage:
analyser=analyser, analyser=analyser,
type_=type_, type_=type_,
subtype=subtype, subtype=subtype,
extra = extra, extra=extra,
items = items or [], items=items or [],
affiliations = affiliations or [], affiliations=affiliations or [],
subscriptions = subscriptions or [], subscriptions=subscriptions or [],
parent_node = parent_node, parent_node=parent_node,
linked_node = linked_node linked_node=linked_node,
) )
async with self.session() as session: async with self.session() as session:
async with session.begin(): async with session.begin():

View file

@ -462,7 +462,7 @@ class Thread(Base):
comment=( comment=(
"Indicates if thread ID was added after the original message was sent (e.g. " "Indicates if thread ID was added after the original message was sent (e.g. "
"to link a parent message which didn't have a thread ID to a thread)." "to link a parent message which didn't have a thread ID to a thread)."
) ),
) )
history = relationship("History", uselist=False, back_populates="thread") history = relationship("History", uselist=False, back_populates="thread")
@ -653,18 +653,24 @@ class PubsubNode(Base):
subtype = Column(Text, nullable=True) subtype = Column(Text, nullable=True)
extra = Column(JSON) extra = Column(JSON)
linked_node_id = Column(Integer, ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=True) linked_node_id = Column(
parent_node_id = Column(Integer, ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=True) Integer, ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=True
)
parent_node_id = Column(
Integer, ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=True
)
items = relationship("PubsubItem", back_populates="node", passive_deletes=True) items = relationship("PubsubItem", back_populates="node", passive_deletes=True)
affiliations = relationship("PubsubAffiliation", back_populates="node", passive_deletes=True) affiliations = relationship(
"PubsubAffiliation", back_populates="node", passive_deletes=True
)
subscriptions = relationship("PubsubSub", back_populates="node", passive_deletes=True) subscriptions = relationship("PubsubSub", back_populates="node", passive_deletes=True)
linked_node = relationship( linked_node = relationship(
"PubsubNode", "PubsubNode",
remote_side=[id], remote_side=[id],
foreign_keys=[linked_node_id], foreign_keys=[linked_node_id],
back_populates="linking_nodes" back_populates="linking_nodes",
) )
linking_nodes = relationship( linking_nodes = relationship(
"PubsubNode", "PubsubNode",
@ -676,7 +682,7 @@ class PubsubNode(Base):
"PubsubNode", "PubsubNode",
remote_side=[id], remote_side=[id],
foreign_keys=[parent_node_id], foreign_keys=[parent_node_id],
back_populates="child_nodes" back_populates="child_nodes",
) )
child_nodes = relationship( child_nodes = relationship(
"PubsubNode", "PubsubNode",
@ -693,10 +699,10 @@ class PubsubAffiliation(Base):
User by components managing a pubsub service. User by components managing a pubsub service.
""" """
__tablename__ = "pubsub_affiliations" __tablename__ = "pubsub_affiliations"
__table_args__ = (UniqueConstraint("node_id", "entity"),) __table_args__ = (UniqueConstraint("node_id", "entity"),)
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
node_id = Column(ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=False) node_id = Column(ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=False)
entity = Column(JID, nullable=False) entity = Column(JID, nullable=False)

View file

@ -286,9 +286,7 @@ class APPubsubService(rsm.PubSubService):
client, requestor_actor_id, ap_account client, requestor_actor_id, ap_account
) )
if "base64" not in avatar_data: if "base64" not in avatar_data:
await threads.deferToThread( await threads.deferToThread(self._blocking_b64encode_avatar, avatar_data)
self._blocking_b64encode_avatar, avatar_data
)
else: else:
if len(itemIdentifiers) > 1: if len(itemIdentifiers) > 1:
# only a single item ID is supported # only a single item ID is supported

View file

@ -52,7 +52,13 @@ from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger from libervia.backend.core.log import getLogger
from libervia.backend.memory.persistent import LazyPersistentBinaryDict from libervia.backend.memory.persistent import LazyPersistentBinaryDict
from libervia.backend.memory.sqla import select from libervia.backend.memory.sqla import select
from libervia.backend.memory.sqla_mapping import AccessModel, Affiliation, PrivateIndBin, PublishModel, PubsubAffiliation from libervia.backend.memory.sqla_mapping import (
AccessModel,
Affiliation,
PrivateIndBin,
PublishModel,
PubsubAffiliation,
)
from libervia.backend.models.core import MessageData from libervia.backend.models.core import MessageData
from libervia.backend.plugins.plugin_comp_email_gateway.pubsub_service import ( from libervia.backend.plugins.plugin_comp_email_gateway.pubsub_service import (
EmailGWPubsubService, EmailGWPubsubService,
@ -96,8 +102,17 @@ PLUGIN_INFO = {
C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT, C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT,
C.PI_PROTOCOLS: [], C.PI_PROTOCOLS: [],
C.PI_DEPENDENCIES: [ C.PI_DEPENDENCIES: [
"XEP-0033", "XEP-0077", "XEP-0106", "XEP-0277", "XEP-0498", "XEP-0499", "GRE", "GRE-MIME", "XEP-0033",
"GRE-OpenPGP", "PUBSUB_CACHE", "TEXT_SYNTAXES" "XEP-0077",
"XEP-0106",
"XEP-0277",
"XEP-0498",
"XEP-0499",
"GRE",
"GRE-MIME",
"GRE-OpenPGP",
"PUBSUB_CACHE",
"TEXT_SYNTAXES",
], ],
C.PI_RECOMMENDATIONS: [], C.PI_RECOMMENDATIONS: [],
C.PI_MAIN: "EmailGatewayComponent", C.PI_MAIN: "EmailGatewayComponent",
@ -276,26 +291,30 @@ class EmailGatewayComponent(GetDataHandler):
assert to_email is not None assert to_email is not None
subject = "This is an encrypted message." subject = "This is an encrypted message."
outer = MIMEMultipart('encrypted', protocol="application/pgp-encrypted") outer = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
outer["Subject"] = subject outer["Subject"] = subject
# FIXME: use credentials here. # FIXME: use credentials here.
outer["From"] = from_jid.userhost() outer["From"] = from_jid.userhost()
outer["To"] = to_email outer["To"] = to_email
outer["Content-Type"] = "multipart/encrypted; protocol=\"application/pgp-encrypted\"" outer["Content-Type"] = (
'multipart/encrypted; protocol="application/pgp-encrypted"'
)
version = MIMEApplication( version = MIMEApplication(
"Version: 1\n", "Version: 1\n",
_subtype='pgp-encrypted', _subtype="pgp-encrypted",
_encoder=encoders.encode_7or8bit _encoder=encoders.encode_7or8bit,
) )
version["Content-Description"] = "PGP/MIME version identification" version["Content-Description"] = "PGP/MIME version identification"
encrypted_part = MIMEApplication( encrypted_part = MIMEApplication(
encrypted_payload, encrypted_payload,
_subtype='octet-stream', _subtype="octet-stream",
_encoder=encoders.encode_7or8bit _encoder=encoders.encode_7or8bit,
) )
encrypted_part["Content-Description"] = "OpenPGP encrypted message" encrypted_part["Content-Description"] = "OpenPGP encrypted message"
encrypted_part["Content-Type"] = "application/octet-stream; name=\"encrypted.asc\"" encrypted_part["Content-Type"] = (
encrypted_part["Content-Disposition"] = "inline; filename=\"encrypted.asc\"" 'application/octet-stream; name="encrypted.asc"'
)
encrypted_part["Content-Disposition"] = 'inline; filename="encrypted.asc"'
outer.attach(version) outer.attach(version)
outer.attach(encrypted_part) outer.attach(encrypted_part)
body = outer.as_bytes() body = outer.as_bytes()
@ -497,9 +516,7 @@ class EmailGatewayComponent(GetDataHandler):
msg = MIMEText(body, "plain", "UTF-8") msg = MIMEText(body, "plain", "UTF-8")
if subject is not None: if subject is not None:
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = formataddr( msg["From"] = formataddr((credentials.user_name or None, credentials.user_email))
(credentials.user_name or None, credentials.user_email)
)
if extra.addresses: if extra.addresses:
assert extra.addresses.to assert extra.addresses.to
main_to_address = extra.addresses.to[0] main_to_address = extra.addresses.to[0]
@ -810,7 +827,9 @@ class EmailGatewayComponent(GetDataHandler):
user_email = user_data.credentials.user_email user_email = user_data.credentials.user_email
author_name, author_email = parseaddr(email["from"]) author_name, author_email = parseaddr(email["from"])
author_email = author_email.lower() author_email = author_email.lower()
from_jid = jid.JID(None, (self._e.escape(author_email), self.client.jid.host, None)) from_jid = jid.JID(
None, (self._e.escape(author_email), self.client.jid.host, None)
)
# Get the email body # Get the email body
body_mime = email.get_body(("plain",)) body_mime = email.get_body(("plain",))
@ -937,7 +956,7 @@ class EmailGatewayComponent(GetDataHandler):
client = self.client.get_virtual_client(from_jid) client = self.client.get_virtual_client(from_jid)
# Now that the message is parsed, we check if it's a mailing list. # Now that the message is parsed, we check if it's a mailing list.
list_ids = email.get_all("list-id") list_ids = email.get_all("list-id")
if list_ids: if list_ids:
try: try:
await self.handle_mailing_list( await self.handle_mailing_list(
@ -950,7 +969,7 @@ class EmailGatewayComponent(GetDataHandler):
user_jid, user_jid,
body, body,
subject, subject,
extra extra,
) )
except exceptions.DataError as e: except exceptions.DataError as e:
log.warning(f"Can't parse mailing list email: {e}.") log.warning(f"Can't parse mailing list email: {e}.")
@ -963,6 +982,7 @@ class EmailGatewayComponent(GetDataHandler):
{"": subject} if subject else None, {"": subject} if subject else None,
extra=extra, extra=extra,
) )
def parse_references(self, email: EmailMessage) -> list[str]: def parse_references(self, email: EmailMessage) -> list[str]:
"""Extract message IDs from the "References" header. """Extract message IDs from the "References" header.
@ -970,7 +990,7 @@ class EmailGatewayComponent(GetDataHandler):
@returns: Message IDs @returns: Message IDs
""" """
references = [] references = []
for header in email.get_all('references', []): for header in email.get_all("references", []):
for token in header.split(): for token in header.split():
_, address = parseaddr(token) _, address = parseaddr(token)
if address: if address:
@ -984,7 +1004,7 @@ class EmailGatewayComponent(GetDataHandler):
node: str, node: str,
item_id: str, item_id: str,
author_name: str, author_name: str,
author_email: str author_email: str,
) -> MbData: ) -> MbData:
"""Convert an email to blog data. """Convert an email to blog data.
@ -994,16 +1014,18 @@ class EmailGatewayComponent(GetDataHandler):
tags = set() tags = set()
# Title # Title
title = email.get('subject', '') title = email.get("subject", "")
def strip_tags(m): def strip_tags(m):
value = m.group()[1:-1].strip().lower() value = m.group()[1:-1].strip().lower()
if value: if value:
tags.add(value) tags.add(value)
return "" return ""
title = re.sub(r"\[.+?\]", strip_tags, title).strip() title = re.sub(r"\[.+?\]", strip_tags, title).strip()
# Dates # Dates
date_header = email.get('Date') date_header = email.get("Date")
if date_header: if date_header:
try: try:
published = date_utils.date_parse(date_header) published = date_utils.date_parse(date_header)
@ -1023,10 +1045,10 @@ class EmailGatewayComponent(GetDataHandler):
for part in email.walk(): for part in email.walk():
content_type = part.get_content_type() content_type = part.get_content_type()
content_disposition = part.get('Content-Disposition', '').lower() content_disposition = part.get("Content-Disposition", "").lower()
# Skip attachments # Skip attachments
if 'attachment' in content_disposition: if "attachment" in content_disposition:
continue continue
payload = part.get_payload(decode=True) payload = part.get_payload(decode=True)
@ -1034,15 +1056,15 @@ class EmailGatewayComponent(GetDataHandler):
continue continue
payload = cast(bytes, payload) payload = cast(bytes, payload)
charset = part.get_content_charset() or 'utf-8' charset = part.get_content_charset() or "utf-8"
try: try:
payload_text = payload.decode(charset, errors='replace') payload_text = payload.decode(charset, errors="replace")
except (LookupError, TypeError): except (LookupError, TypeError):
payload_text = payload.decode('utf-8', errors='replace') payload_text = payload.decode("utf-8", errors="replace")
if content_type == 'text/plain': if content_type == "text/plain":
plain_parts.append(payload_text) plain_parts.append(payload_text)
elif content_type == 'text/html': elif content_type == "text/html":
html_parts.append(payload_text) html_parts.append(payload_text)
# Prefer first valid parts for safety # Prefer first valid parts for safety
@ -1055,17 +1077,17 @@ class EmailGatewayComponent(GetDataHandler):
payload = email.get_payload(decode=True) payload = email.get_payload(decode=True)
if payload is not None: if payload is not None:
payload = cast(bytes, payload) payload = cast(bytes, payload)
charset = email.get_content_charset() or 'utf-8' charset = email.get_content_charset() or "utf-8"
try: try:
content_text = payload.decode(charset, errors='replace') content_text = payload.decode(charset, errors="replace")
except (LookupError, TypeError): except (LookupError, TypeError):
content_text = payload.decode('utf-8', errors='replace') content_text = payload.decode("utf-8", errors="replace")
content_type = email.get_content_type() content_type = email.get_content_type()
if content_type == 'text/plain': if content_type == "text/plain":
content = content_text content = content_text
elif content_type == 'text/html': elif content_type == "text/html":
content_xhtml = content_text content_xhtml = content_text
if content_xhtml is None: if content_xhtml is None:
@ -1086,13 +1108,11 @@ class EmailGatewayComponent(GetDataHandler):
content_xhtml=content_xhtml, content_xhtml=content_xhtml,
author=author_name, author=author_name,
author_email=author_email, author_email=author_email,
tags=list(tags) tags=list(tags),
) )
async def mb_data_to_email( async def mb_data_to_email(
self, self, credentials: Credentials, mb_data: MbData
credentials: Credentials,
mb_data: MbData
) -> EmailMessage: ) -> EmailMessage:
"""Convert blog data to an email message. """Convert blog data to an email message.
@ -1113,28 +1133,28 @@ class EmailGatewayComponent(GetDataHandler):
if mb_data.title: if mb_data.title:
title_parts.append(mb_data.title) title_parts.append(mb_data.title)
email['Subject'] = ' '.join(title_parts) email["Subject"] = " ".join(title_parts)
if tags: if tags:
email['Keywords'] = ', '.join(tags) email["Keywords"] = ", ".join(tags)
# From # From
# FIXME: Check email according to sender. # FIXME: Check email according to sender.
if mb_data.author and mb_data.author_email: if mb_data.author and mb_data.author_email:
email['From'] = f"{mb_data.author} <{mb_data.author_email}>" email["From"] = f"{mb_data.author} <{mb_data.author_email}>"
elif mb_data.author: elif mb_data.author:
email['From'] = f"{mb_data.author} <{credentials.user_email}>" email["From"] = f"{mb_data.author} <{credentials.user_email}>"
elif mb_data.author_email: elif mb_data.author_email:
email['From'] = mb_data.author_email email["From"] = mb_data.author_email
else: else:
email['From'] = credentials.user_email email["From"] = credentials.user_email
# Message ID # Message ID
if mb_data.id: if mb_data.id:
email['Message-ID'] = f"<{mb_data.id}>" email["Message-ID"] = f"<{mb_data.id}>"
else: else:
msg_id = make_msgid() msg_id = make_msgid()
email['Message-ID'] = msg_id email["Message-ID"] = msg_id
mb_data.id = msg_id[1:-1] mb_data.id = msg_id[1:-1]
# In-Reply-To # In-Reply-To
@ -1142,20 +1162,20 @@ class EmailGatewayComponent(GetDataHandler):
# Use the first reply-to reference # Use the first reply-to reference
in_reply_to = mb_data.in_reply_tos[0] in_reply_to = mb_data.in_reply_tos[0]
if in_reply_to.ref: if in_reply_to.ref:
email['In-Reply-To'] = f"<{in_reply_to.ref}>" email["In-Reply-To"] = f"<{in_reply_to.ref}>"
# Dates # Dates
date_set = False date_set = False
if mb_data.published is not None: if mb_data.published is not None:
try: try:
email['Date'] = formatdate(mb_data.published, localtime=True) email["Date"] = formatdate(mb_data.published, localtime=True)
date_set = True date_set = True
except Exception: except Exception:
pass pass
if not date_set and mb_data.updated is not None: if not date_set and mb_data.updated is not None:
try: try:
email['Date'] = formatdate(mb_data.updated, localtime=True) email["Date"] = formatdate(mb_data.updated, localtime=True)
except Exception: except Exception:
pass pass
@ -1172,7 +1192,7 @@ class EmailGatewayComponent(GetDataHandler):
) )
email.set_content(mb_data.content) email.set_content(mb_data.content)
email.add_alternative(xhtml_content, subtype='html') email.add_alternative(xhtml_content, subtype="html")
elif mb_data.content: elif mb_data.content:
email.set_content(mb_data.content) email.set_content(mb_data.content)
else: else:
@ -1190,8 +1210,8 @@ class EmailGatewayComponent(GetDataHandler):
email: EmailMessage, email: EmailMessage,
user_jid: jid.JID, user_jid: jid.JID,
body: str, body: str,
subject: str|None, subject: str | None,
extra: dict extra: dict,
) -> None: ) -> None:
"""Handle emails from mailing lists. """Handle emails from mailing lists.
@ -1224,16 +1244,15 @@ class EmailGatewayComponent(GetDataHandler):
create_kwargs={ create_kwargs={
"access_model": AccessModel.whitelist, "access_model": AccessModel.whitelist,
"publish_model": PublishModel.publishers, "publish_model": PublishModel.publishers,
"affiliations": [PubsubAffiliation( "affiliations": [
entity = user_jid, PubsubAffiliation(entity=user_jid, affiliation=Affiliation.owner)
affiliation = Affiliation.owner ],
)], "subscribed": True,
"subscribed": True
}, },
) )
assert root_node is not None assert root_node is not None
in_reply_to_value = ''.join(email.get_all("in-reply-to", [])) in_reply_to_value = "".join(email.get_all("in-reply-to", []))
__, in_reply_to = parseaddr(in_reply_to_value) __, in_reply_to = parseaddr(in_reply_to_value)
references = self.parse_references(email) references = self.parse_references(email)
if in_reply_to: if in_reply_to:
@ -1269,14 +1288,12 @@ class EmailGatewayComponent(GetDataHandler):
Comment( Comment(
uri=parent_node_uri, uri=parent_node_uri,
service=pubsub_service, service=pubsub_service,
node=parent_node_name node=parent_node_name,
) )
] ],
) )
await G.storage.cache_pubsub_items( await G.storage.cache_pubsub_items(
client, client, root_node, [await empty_item_data.to_element(client)]
root_node,
[await empty_item_data.to_element(client)]
) )
parent_node = await G.storage.get_pubsub_node( parent_node = await G.storage.get_pubsub_node(
client, client,
@ -1288,11 +1305,10 @@ class EmailGatewayComponent(GetDataHandler):
"access_model": AccessModel.whitelist, "access_model": AccessModel.whitelist,
"publish_model": PublishModel.publishers, "publish_model": PublishModel.publishers,
"subscribed": True, "subscribed": True,
"affiliations": [PubsubAffiliation( "affiliations": [
entity = user_jid, PubsubAffiliation(entity=user_jid, affiliation=Affiliation.owner)
affiliation = Affiliation.owner ],
)], "parent_node": root_node,
"parent_node": root_node
}, },
) )
else: else:
@ -1300,24 +1316,19 @@ class EmailGatewayComponent(GetDataHandler):
parent_node_name = cast(str, root_node.name) parent_node_name = cast(str, root_node.name)
mb_data = self.email_to_mb_data( mb_data = self.email_to_mb_data(
email, email, pubsub_service, parent_node_name, message_id, author_name, author_email
pubsub_service,
parent_node_name,
message_id,
author_name,
author_email
) )
mb_data.comments.append( mb_data.comments.append(
Comment( Comment(
service = pubsub_service, service=pubsub_service,
node = self._mb.get_comments_node(message_id), node=self._mb.get_comments_node(message_id),
) )
) )
await G.storage.cache_pubsub_items( await G.storage.cache_pubsub_items(
client, client,
parent_node, parent_node,
[await mb_data.to_element(client)], [await mb_data.to_element(client)],
[mb_data.model_dump(mode="json")] [mb_data.model_dump(mode="json")],
) )
async def handle_attachment(self, part: EmailMessage, recipient_jid: jid.JID) -> None: async def handle_attachment(self, part: EmailMessage, recipient_jid: jid.JID) -> None:
@ -1393,9 +1404,7 @@ class EmailGatewayComponent(GetDataHandler):
partial(self.on_new_email, user_data, from_jid.userhostJID()), partial(self.on_new_email, user_data, from_jid.userhostJID()),
connected, connected,
) )
reactor.connectTCP( reactor.connectTCP(credentials.imap_host, int(credentials.imap_port), factory)
credentials.imap_host, int(credentials.imap_port), factory
)
await connected await connected
async def _on_registration_submit( async def _on_registration_submit(
@ -1436,9 +1445,9 @@ class EmailGatewayComponent(GetDataHandler):
return True return True
self.validate_imap_smtp_form(submit_form) self.validate_imap_smtp_form(submit_form)
credentials = Credentials(**{ credentials = Credentials(
key: field.value for key, field in submit_form.fields.items() **{key: field.value for key, field in submit_form.fields.items()}
}) )
user_data = self.users_data.get(from_jid) user_data = self.users_data.get(from_jid)
if user_data is None: if user_data is None:
# The user is not in cache, we cache current credentials. # The user is not in cache, we cache current credentials.
@ -1460,19 +1469,14 @@ class EmailGatewayComponent(GetDataHandler):
await self.storage.aset(key, credentials) await self.storage.aset(key, credentials)
return True return True
async def on_relayed_encryption_data( async def on_relayed_encryption_data(
self, self, client: SatXMPPEntity, iq_elt: domish.Element, form: data_form.Form
client: SatXMPPEntity,
iq_elt: domish.Element,
form: data_form.Form
) -> None: ) -> None:
from_jid = jid.JID(iq_elt["from"]).userhostJID() from_jid = jid.JID(iq_elt["from"]).userhostJID()
credentials = await self.get_credentials(from_jid) credentials = await self.get_credentials(from_jid)
form.addField(data_form.Field(var="sender_id", value=credentials.user_email)) form.addField(data_form.Field(var="sender_id", value=credentials.user_email))
@implementer(iwokkel.IDisco) @implementer(iwokkel.IDisco)
class EmailGatewayHandler(XMPPHandler): class EmailGatewayHandler(XMPPHandler):
@ -1482,28 +1486,13 @@ class EmailGatewayHandler(XMPPHandler):
"result", "result",
formNamespace="urn:xmpp:data-policy:0", formNamespace="urn:xmpp:data-policy:0",
fields=( fields=(
data_form.Field( data_form.Field("list-single", "auth_data", "plain"),
"list-single", data_form.Field("list-single", "data-transmission", "encrypted"),
"auth_data", data_form.Field("text-single", "encryption_algorithm", "TLS"),
"plain"
),
data_form.Field(
"list-single",
"data-transmission",
"encrypted"
),
data_form.Field(
"text-single",
"encryption_algorithm",
"TLS"
),
data_form.Field( data_form.Field(
"text-single", "text-single",
"data_retention", "data_retention",
"0", "0",
), ),
), ),
), ),
@ -1515,7 +1504,6 @@ class EmailGatewayHandler(XMPPHandler):
"text-multi", "text-multi",
"extra_info", "extra_info",
"This gateway acts as a relay to external IMAP/SMTP servers. Data policies depend entirely on the external server chosen by the user. This gateway does not store or process user data.", "This gateway acts as a relay to external IMAP/SMTP servers. Data policies depend entirely on the external server chosen by the user. This gateway does not store or process user data.",
), ),
), ),
), ),

View file

@ -22,7 +22,7 @@ from twisted.mail import imap4
class Credentials(BaseModel): class Credentials(BaseModel):
model_config = ConfigDict(extra='forbid') model_config = ConfigDict(extra="forbid")
user_name: str user_name: str
user_email: str user_email: str
smtp_host: str smtp_host: str
@ -36,7 +36,6 @@ class Credentials(BaseModel):
imap_password: str imap_password: str
@dataclass @dataclass
class UserData: class UserData:
credentials: Credentials credentials: Credentials

View file

@ -31,8 +31,14 @@ from libervia.backend.core.log import getLogger
from libervia.backend.memory.sqla_mapping import AccessModel, Affiliation from libervia.backend.memory.sqla_mapping import AccessModel, Affiliation
from libervia.backend.plugins.plugin_pubsub_cache import PubsubCache from libervia.backend.plugins.plugin_pubsub_cache import PubsubCache
from libervia.backend.plugins.plugin_xep_0498 import NodeData from libervia.backend.plugins.plugin_xep_0498 import NodeData
from libervia.backend.plugins.plugin_xep_0499 import ExtDiscoOptions, ExtDiscoResult, DiscoPubsubItem, DiscoPubsubNode from libervia.backend.plugins.plugin_xep_0499 import (
ExtDiscoOptions,
ExtDiscoResult,
DiscoPubsubItem,
DiscoPubsubNode,
)
from libervia.backend.tools.utils import ensure_deferred from libervia.backend.tools.utils import ensure_deferred
if TYPE_CHECKING: if TYPE_CHECKING:
from . import EmailGatewayComponent from . import EmailGatewayComponent
@ -77,10 +83,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
return defer.ensureDeferred(self.get_nodes(requestor, service, nodeIdentifier)) return defer.ensureDeferred(self.get_nodes(requestor, service, nodeIdentifier))
async def get_nodes( async def get_nodes(
self, self, requestor: jid.JID, service: jid.JID, node_id: str
requestor: jid.JID,
service: jid.JID,
node_id: str
) -> list[str]: ) -> list[str]:
nodes = await G.storage.get_pubsub_nodes(self.client, self.client.jid) nodes = await G.storage.get_pubsub_nodes(self.client, self.client.jid)
return [self._pfs.namespace] + [cast(str, node.name) for node in nodes] return [self._pfs.namespace] + [cast(str, node.name) for node in nodes]
@ -103,10 +106,8 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
return node_data.to_elements(), None return node_data.to_elements(), None
async def items_from_mailing_list( async def items_from_mailing_list(
self, self, request: rsm.PubSubRequest, requestor_jid: jid.JID
request: rsm.PubSubRequest, ) -> tuple[list[domish.Element], rsm.RSMResponse | None]:
requestor_jid: jid.JID
) -> tuple[list[domish.Element], rsm.RSMResponse|None]:
"""Handle items coming from mailing lists. """Handle items coming from mailing lists.
@param request: Pubsub request. @param request: Pubsub request.
@ -118,10 +119,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
""" """
node = request.nodeIdentifier node = request.nodeIdentifier
node = await G.storage.get_pubsub_node( node = await G.storage.get_pubsub_node(
self.client, self.client, self.client.jid, node, with_affiliations=True
self.client.jid,
node,
with_affiliations=True
) )
if node is None: if node is None:
raise error.StanzaError("item-not-found") raise error.StanzaError("item-not-found")
@ -133,9 +131,8 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
for affiliation in node.affiliations: for affiliation in node.affiliations:
if ( if (
affiliation.entity == requestor_jid affiliation.entity == requestor_jid
and affiliation.affiliation in { and affiliation.affiliation
Affiliation.owner, Affiliation.publisher, Affiliation.member in {Affiliation.owner, Affiliation.publisher, Affiliation.member}
}
): ):
break break
else: else:
@ -151,7 +148,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
request.maxItems, request.maxItems,
request.itemIdentifiers, request.itemIdentifiers,
request.subscriptionIdentifier, request.subscriptionIdentifier,
request.rsm request.rsm,
) )
if rsm_data := metadata.get("rsm"): if rsm_data := metadata.get("rsm"):
rsm_response = rsm.RSMResponse(**rsm_data) rsm_response = rsm.RSMResponse(**rsm_data)
@ -159,7 +156,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
rsm_response = None rsm_response = None
return ( return (
[cast(domish.Element, ps_item.data) for ps_item in pubsub_items], [cast(domish.Element, ps_item.data) for ps_item in pubsub_items],
rsm_response rsm_response,
) )
@ensure_deferred @ensure_deferred
@ -241,7 +238,7 @@ class EmailGWPubsubService(rsm.PubSubService):
self, self,
client: SatXMPPComponent, client: SatXMPPComponent,
iq_elt: domish.Element, iq_elt: domish.Element,
ext_disco_options: ExtDiscoOptions ext_disco_options: ExtDiscoOptions,
) -> ExtDiscoResult: ) -> ExtDiscoResult:
"""Handle pubsub extended disco""" """Handle pubsub extended disco"""
query_elt = iq_elt.query query_elt = iq_elt.query
@ -253,25 +250,26 @@ class EmailGWPubsubService(rsm.PubSubService):
nodes = await G.storage.get_pubsub_nodes( nodes = await G.storage.get_pubsub_nodes(
client, client,
service=client.jid, service=client.jid,
node_name = node, node_name=node,
linked_node=ext_disco_options.linked_nodes, linked_node=ext_disco_options.linked_nodes,
depth=depth, depth=depth,
with_items="items" in ext_disco_options.type with_items="items" in ext_disco_options.type,
) )
if depth == 0: if depth == 0:
# If depth == 0, we don't want to use `from_sqlalchemy_full_hierarchy` to # If depth == 0, we don't want to use `from_sqlalchemy_full_hierarchy` to
# avoid returning children, and because children `parent` and `child_nodes` # avoid returning children, and because children `parent` and `child_nodes`
# fields are not loaded. # fields are not loaded.
return ExtDiscoResult([ return ExtDiscoResult(
DiscoPubsubNode.from_sqlalchemy(node, self.client.jid) [DiscoPubsubNode.from_sqlalchemy(node, self.client.jid) for node in nodes]
for node in nodes )
])
else: else:
return ExtDiscoResult([ return ExtDiscoResult(
DiscoPubsubNode.from_sqlalchemy_full_hierarchy(node, self.client.jid) [
for node in nodes DiscoPubsubNode.from_sqlalchemy_full_hierarchy(node, self.client.jid)
]) for node in nodes
]
)
@ensure_deferred @ensure_deferred
async def getDiscoInfo( async def getDiscoInfo(

View file

@ -121,7 +121,7 @@ class GroupBlog(object):
client: SatXMPPClient, client: SatXMPPClient,
mb_data: MbData, mb_data: MbData,
entry_elt: domish.Element, entry_elt: domish.Element,
item_elt: domish.Element item_elt: domish.Element,
) -> None: ) -> None:
"""Build fine access permission if needed """Build fine access permission if needed

View file

@ -46,7 +46,7 @@ PLUGIN_INFO = {
C.PI_DESCRIPTION: _("""Search for XMPP entities"""), C.PI_DESCRIPTION: _("""Search for XMPP entities"""),
} }
RATIO_CUTOFF = 0.6 RATIO_CUTOFF = 0.6
# Used when a search term matches a substring of a relevant data. # Used when a search term matches a substring of a relevant data.
PARTIAL_MATCH_RATIO = 0.8 PARTIAL_MATCH_RATIO = 0.8
MAX_CACHE_SIZE = 10 MAX_CACHE_SIZE = 10
@ -103,9 +103,7 @@ class Options(BaseModel):
entities: bool = Field( entities: bool = Field(
default=False, description="Search for entities for direct chat" default=False, description="Search for entities for direct chat"
) )
groupchat: bool = Field( groupchat: bool = Field(default=False, description="Search for group chats.")
default=False, description="Search for group chats."
)
allow_external: bool = Field( allow_external: bool = Field(
default=False, description="Authorise doing request to external services." default=False, description="Authorise doing request to external services."
) )
@ -221,7 +219,9 @@ class JidSearch:
matches.sort( matches.sort(
key=lambda item: ( key=lambda item: (
item.exact_match, item.relevance or 0, getattr(item, "in_roster", False) item.exact_match,
item.relevance or 0,
getattr(item, "in_roster", False),
), ),
reverse=True, reverse=True,
) )

View file

@ -197,7 +197,7 @@ SAFE_CLASSES = {
"write", "write",
# "noise" class is used by gateways (in particular email gateway) to mark annoying and # "noise" class is used by gateways (in particular email gateway) to mark annoying and
# not useful content. # not useful content.
"noise" "noise",
} }
STYLES_VALUES_REGEX = ( STYLES_VALUES_REGEX = (
"^(" "^("

View file

@ -350,7 +350,7 @@ class PubsubCache:
client: SatXMPPEntity, client: SatXMPPEntity,
service: jid.JID, service: jid.JID,
node: str, node: str,
pubsub_node: PubsubNode|None = None, pubsub_node: PubsubNode | None = None,
) -> dict: ) -> dict:
"""Use registered analysers on a node to determine what it is used for""" """Use registered analysers on a node to determine what it is used for"""
analyse = {"service": service, "node": node} analyse = {"service": service, "node": node}

View file

@ -232,7 +232,9 @@ class Autocrypt:
post_treat.addCallback(partial(self._check_headers, client)) post_treat.addCallback(partial(self._check_headers, client))
return True return True
async def add_autocrypt_header(self, client, mess_data: MessageDataLegacy) -> MessageDataLegacy: async def add_autocrypt_header(
self, client, mess_data: MessageDataLegacy
) -> MessageDataLegacy:
to_jid = mess_data["to"] to_jid = mess_data["to"]
if await self.host.memory.disco.has_identity( if await self.host.memory.disco.has_identity(
client, "gateway", "smtp", jid.JID(to_jid.host) client, "gateway", "smtp", jid.JID(to_jid.host)

View file

@ -83,9 +83,7 @@ class GREEncrypterOpenPGP(Encrypter):
raise exceptions.NotFound( raise exceptions.NotFound(
f"No public keys found for {recipient_id!r}, we can't encrypt." f"No public keys found for {recipient_id!r}, we can't encrypt."
) )
encrypted_data = gpg_provider.encrypt( encrypted_data = gpg_provider.encrypt(formatted_payload, public_keys)
formatted_payload, public_keys
)
return base64.b64encode(encrypted_data).decode("ASCII") return base64.b64encode(encrypted_data).decode("ASCII")

View file

@ -264,24 +264,21 @@ class XEP_0045:
search_term: str, search_term: str,
options: plugin_misc_jid_search.Options, options: plugin_misc_jid_search.Options,
sequence_matcher: difflib.SequenceMatcher, sequence_matcher: difflib.SequenceMatcher,
matches: plugin_misc_jid_search.SearchItems matches: plugin_misc_jid_search.SearchItems,
) -> bool: ) -> bool:
if options.groupchat: if options.groupchat:
if client.muc_service is not None: if client.muc_service is not None:
# FIXME: RSM is not handled, we need proper RSM handling as number of # FIXME: RSM is not handled, we need proper RSM handling as number of
# rooms may be large. # rooms may be large.
disco_items = await self.host.memory.disco.get_items( disco_items = await self.host.memory.disco.get_items(
client, client, client.muc_service
client.muc_service
) )
for disco_item in disco_items: for disco_item in disco_items:
search_item = plugin_misc_jid_search.RoomSearchItem( search_item = plugin_misc_jid_search.RoomSearchItem(
entity = disco_item.entity, entity=disco_item.entity, name=disco_item.name or "", local=True
name = disco_item.name or "",
local = True
) )
self._jid_search.process_matching( self._jid_search.process_matching(
search_term, sequence_matcher, matches,search_item search_term, sequence_matcher, matches, search_item
) )
return True return True

View file

@ -47,12 +47,13 @@ RSM_PREFIX = "rsm_"
class RSMRequest(BaseModel): class RSMRequest(BaseModel):
"""Pydantic model for RSM request parameters""" """Pydantic model for RSM request parameters"""
max: int = Field(default=10, gt=0) max: int = Field(default=10, gt=0)
after: str | None = None after: str | None = None
before: str | None = None before: str | None = None
index: int | None = None index: int | None = None
@field_validator('after') @field_validator("after")
def check_after_not_empty(cls, v: str | None) -> str | None: def check_after_not_empty(cls, v: str | None) -> str | None:
"""Validate that after value isn't empty string """Validate that after value isn't empty string
@ -62,7 +63,7 @@ class RSMRequest(BaseModel):
@raise ValueError: if value is an empty string @raise ValueError: if value is an empty string
""" """
if v == "": if v == "":
raise ValueError("RSM \"after\" can't be empty") raise ValueError('RSM "after" can\'t be empty')
return v return v
def to_wokkel_request(self) -> rsm.RSMRequest: def to_wokkel_request(self) -> rsm.RSMRequest:
@ -71,10 +72,7 @@ class RSMRequest(BaseModel):
@return: wokkel RSMRequest instance @return: wokkel RSMRequest instance
""" """
return rsm.RSMRequest( return rsm.RSMRequest(
max_=self.max, max_=self.max, after=self.after, before=self.before, index=self.index
after=self.after,
before=self.before,
index=self.index
) )
@classmethod @classmethod
@ -88,7 +86,7 @@ class RSMRequest(BaseModel):
max=request.max, max=request.max,
after=request.after, after=request.after,
before=request.before, before=request.before,
index=request.index index=request.index,
) )
def to_element(self) -> domish.Element: def to_element(self) -> domish.Element:
@ -117,6 +115,7 @@ class RSMRequest(BaseModel):
class RSMResponse(BaseModel): class RSMResponse(BaseModel):
"""Pydantic model for RSM response parameters""" """Pydantic model for RSM response parameters"""
first: str | None = None first: str | None = None
last: str | None = None last: str | None = None
index: int | None = None index: int | None = None
@ -128,10 +127,7 @@ class RSMResponse(BaseModel):
@return: wokkel RSMResponse instance @return: wokkel RSMResponse instance
""" """
return rsm.RSMResponse( return rsm.RSMResponse(
first=self.first, first=self.first, last=self.last, index=self.index, count=self.count
last=self.last,
index=self.index,
count=self.count
) )
@classmethod @classmethod
@ -145,7 +141,7 @@ class RSMResponse(BaseModel):
first=response.first, first=response.first,
last=response.last, last=response.last,
index=response.index, index=response.index,
count=response.count count=response.count,
) )
def to_element(self) -> domish.Element: def to_element(self) -> domish.Element:
@ -180,7 +176,7 @@ class XEP_0059:
""" """
log.info(f"Plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization.") log.info(f"Plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization.")
def get_handler(self, client) -> 'XEP_0059_handler': def get_handler(self, client) -> "XEP_0059_handler":
"""Get the XMPP handler for this plugin """Get the XMPP handler for this plugin
@param client: client instance @param client: client instance
@ -209,9 +205,7 @@ class XEP_0059:
return RSMRequest(**rsm_args).to_wokkel_request() if rsm_args else None return RSMRequest(**rsm_args).to_wokkel_request() if rsm_args else None
def response2dict( def response2dict(
self, self, rsm_response: rsm.RSMResponse, data: dict[str, str] | None = None
rsm_response: rsm.RSMResponse,
data: dict[str, str] | None = None
) -> dict[str, str]: ) -> dict[str, str]:
"""Return a dict with RSM response data """Return a dict with RSM response data
@ -281,19 +275,13 @@ class XEP_0059:
log.warning('Can\'t do pagination, no "last" received.') log.warning('Can\'t do pagination, no "last" received.')
return None return None
return RSMRequest( return RSMRequest(max=rsm_request.max, after=rsm_response.last)
max=rsm_request.max,
after=rsm_response.last
)
@implementer(iwokkel.IDisco) @implementer(iwokkel.IDisco)
class XEP_0059_handler(xmlstream.XMPPHandler): class XEP_0059_handler(xmlstream.XMPPHandler):
def getDiscoInfo( def getDiscoInfo(
self, self, requestor: str, target: str, nodeIdentifier: str = ""
requestor: str,
target: str,
nodeIdentifier: str = ""
) -> list[disco.DiscoFeature]: ) -> list[disco.DiscoFeature]:
"""Get disco info for RSM """Get disco info for RSM
@ -305,10 +293,7 @@ class XEP_0059_handler(xmlstream.XMPPHandler):
return [disco.DiscoFeature(rsm.NS_RSM)] return [disco.DiscoFeature(rsm.NS_RSM)]
def getDiscoItems( def getDiscoItems(
self, self, requestor: str, target: str, nodeIdentifier: str = ""
requestor: str,
target: str,
nodeIdentifier: str = ""
) -> list: ) -> list:
"""Get disco items for RSM """Get disco items for RSM

View file

@ -19,7 +19,18 @@
from collections import namedtuple from collections import namedtuple
from functools import reduce from functools import reduce
from typing import Any, Callable, ClassVar, Dict, Iterable, List, Optional, Self, Set, Union from typing import (
Any,
Callable,
ClassVar,
Dict,
Iterable,
List,
Optional,
Self,
Set,
Union,
)
import urllib.parse import urllib.parse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -79,9 +90,8 @@ DEFAULT_PUBSUB_MIN_FEAT = {
class NodeMetadata(BaseModel): class NodeMetadata(BaseModel):
"""Node metadata model.""" """Node metadata model."""
model_config = {
"extra": "allow" model_config = {"extra": "allow"}
}
type: str | None = None type: str | None = None
creator: JIDType | None = None creator: JIDType | None = None
@ -199,9 +209,7 @@ class NodeMetadata(BaseModel):
"pubsub#owner": self.owner, "pubsub#owner": self.owner,
"pubsub#publisher": self.publisher, "pubsub#publisher": self.publisher,
"pubsub#num_subscribers": ( "pubsub#num_subscribers": (
str(self.num_subscribers) str(self.num_subscribers) if self.num_subscribers is not None else None
if self.num_subscribers is not None
else None
), ),
} }
form.makeFields( form.makeFields(

View file

@ -90,20 +90,20 @@ class Comment(BaseModel):
service: JIDType | None = None service: JIDType | None = None
node: str = "" node: str = ""
@model_validator(mode='after') @model_validator(mode="after")
def set_uri_if_missing(self) -> Self: def set_uri_if_missing(self) -> Self:
if not self.uri and self.service is not None and self.node: if not self.uri and self.service is not None and self.node:
self.uri = xmpp_uri.build_xmpp_uri( self.uri = xmpp_uri.build_xmpp_uri(
"pubsub", "pubsub", path=self.service.full(), node=self.node
path=self.service.full(),
node=self.node
) )
return self return self
class InReplyTo(BaseModel): class InReplyTo(BaseModel):
ref: str ref: str
href: str|None href: str | None
type: str|None = None type: str | None = None
class Attachment(BaseModel): class Attachment(BaseModel):
sources: list[dict[str, str]] = Field(default_factory=list) sources: list[dict[str, str]] = Field(default_factory=list)
@ -157,7 +157,6 @@ class MbData(BaseModel):
updated: float | None = None updated: float | None = None
language: str | None = None language: str | None = None
title: str | None = None title: str | None = None
title_xhtml: str | None = None title_xhtml: str | None = None
title_rich: str | None = Field(default=None, exclude=True) title_rich: str | None = Field(default=None, exclude=True)
@ -172,7 +171,7 @@ class MbData(BaseModel):
allow_comments: bool | None = None allow_comments: bool | None = None
comments: list[Comment] = Field(default_factory=list) comments: list[Comment] = Field(default_factory=list)
in_reply_tos: list[InReplyTo]|None = None in_reply_tos: list[InReplyTo] | None = None
tags: list[str] = Field(default_factory=list) tags: list[str] = Field(default_factory=list)
extra: MbExtra = Field(default_factory=MbExtra) extra: MbExtra = Field(default_factory=MbExtra)
@ -365,7 +364,7 @@ class MbData(BaseModel):
in_reply_to = InReplyTo( in_reply_to = InReplyTo(
ref=in_reply_to_elt.getAttribute("ref", ""), ref=in_reply_to_elt.getAttribute("ref", ""),
href=in_reply_to_elt.getAttribute("href"), href=in_reply_to_elt.getAttribute("href"),
type=in_reply_to_elt.getAttribute("type") type=in_reply_to_elt.getAttribute("type"),
) )
if not in_reply_to_elt.ref: if not in_reply_to_elt.ref:
log.warning( log.warning(
@ -722,7 +721,7 @@ class MbData(BaseModel):
if href := in_reply_to.href is not None: if href := in_reply_to.href is not None:
in_reply_to_elt["href"] = href in_reply_to_elt["href"] = href
if type_ := in_reply_to.type is not None: if type_ := in_reply_to.type is not None:
in_reply_to_elt["type"] = type_ in_reply_to_elt["type"] = type_
## Reposts ## ## Reposts ##
if self.extra.repeated: if self.extra.repeated:

View file

@ -41,11 +41,9 @@ from libervia.backend.plugins.plugin_xep_0428 import XEP_0428
from libervia.backend.tools.common.date_utils import date_fmt from libervia.backend.tools.common.date_utils import date_fmt
log = getLogger(__name__) log = getLogger(__name__)
PLUGIN_INFO = { PLUGIN_INFO = {
C.PI_NAME: "Stanza Forwarding", C.PI_NAME: "Stanza Forwarding",
C.PI_IMPORT_NAME: "XEP-0297", C.PI_IMPORT_NAME: "XEP-0297",
@ -97,32 +95,27 @@ class XEP_0297:
XEP_0297.update_uri(child, uri) XEP_0297.update_uri(child, uri)
def _forward( def _forward(
self, self, message_id: str, recipient_jid_s: str, profile_key: str
message_id: str,
recipient_jid_s: str,
profile_key: str
) -> defer.Deferred[None]: ) -> defer.Deferred[None]:
client = self.host.get_client(profile_key) client = self.host.get_client(profile_key)
recipient_jid = jid.JID(recipient_jid_s) recipient_jid = jid.JID(recipient_jid_s)
return defer.ensureDeferred(self.forward_by_id(client, message_id, recipient_jid)) return defer.ensureDeferred(self.forward_by_id(client, message_id, recipient_jid))
async def forward_by_id( async def forward_by_id(
self, self, client: SatXMPPEntity, message_id: str, recipient_jid: jid.JID
client: SatXMPPEntity,
message_id: str,
recipient_jid: jid.JID
) -> None: ) -> None:
history = cast(History, await G.storage.get( history = cast(
client,
History, History,
History.uid, await G.storage.get(
message_id, client,
joined_loads=[History.messages, History.subjects, History.thread], History,
)) History.uid,
message_id,
joined_loads=[History.messages, History.subjects, History.thread],
),
)
if not history: if not history:
raise exceptions.NotFound( raise exceptions.NotFound(f"No history found with message {message_id!r}.")
f"No history found with message {message_id!r}."
)
# FIXME: Q&D way to get MessageData from History. History should have a proper # FIXME: Q&D way to get MessageData from History. History should have a proper
# way to do that. # way to do that.
serialised_history = history.serialise() serialised_history = history.serialise()
@ -138,7 +131,7 @@ class XEP_0297:
f"{client.jid.userhost()} is forwarding this message to you:", f"{client.jid.userhost()} is forwarding this message to you:",
f"[{date_fmt(timestamp_float)}]", f"[{date_fmt(timestamp_float)}]",
f"From: {history.source_jid.full()}", f"From: {history.source_jid.full()}",
f"To: {history.dest_jid.full()}" f"To: {history.dest_jid.full()}",
] ]
for subject in history.subjects: for subject in history.subjects:
@ -162,8 +155,8 @@ class XEP_0297:
client: SatXMPPEntity, client: SatXMPPEntity,
stanza: domish.Element, stanza: domish.Element,
to_jid: jid.JID, to_jid: jid.JID,
timestamp: datetime|None, timestamp: datetime | None,
fallback_msg: str | None = None fallback_msg: str | None = None,
): ):
"""Forward a message to the given JID. """Forward a message to the given JID.

View file

@ -120,8 +120,8 @@ def crc24(data: bytes) -> int:
def binary_to_ascii_armor( def binary_to_ascii_armor(
data: bytes, data: bytes,
armor_type: str = "MESSAGE", armor_type: str = "MESSAGE",
headers: dict|None = None, headers: dict | None = None,
line_length: int = 76 line_length: int = 76,
) -> str: ) -> str:
"""Convert binary data to OpenPGP ASCII Armor format. """Convert binary data to OpenPGP ASCII Armor format.
@ -131,14 +131,15 @@ def binary_to_ascii_armor(
@param line_length: Maximum length of each line. @param line_length: Maximum length of each line.
@return: The ASCII Armor encoded string. @return: The ASCII Armor encoded string.
""" """
encoded_data = base64.b64encode(data).decode('ascii') encoded_data = base64.b64encode(data).decode("ascii")
encoded_lines = [ encoded_lines = [
encoded_data[i:i+line_length] for i in range(0, len(encoded_data), line_length) encoded_data[i : i + line_length]
for i in range(0, len(encoded_data), line_length)
] ]
checksum = crc24(data) checksum = crc24(data)
checksum_str = base64.b64encode(checksum.to_bytes(3, 'big')).decode('ascii') checksum_str = base64.b64encode(checksum.to_bytes(3, "big")).decode("ascii")
header_lines = [] header_lines = []
if headers: if headers:
@ -153,7 +154,7 @@ def binary_to_ascii_armor(
armor.append(f"={checksum_str}") armor.append(f"={checksum_str}")
armor.append(f"-----END PGP {armor_type}-----") armor.append(f"-----END PGP {armor_type}-----")
return '\n'.join(armor) return "\n".join(armor)
class VerificationError(Exception): class VerificationError(Exception):

View file

@ -142,7 +142,7 @@ class XEP_0376:
self, self,
client: SatXMPPEntity, client: SatXMPPEntity,
service: Optional[jid.JID], service: Optional[jid.JID],
node: str|None, node: str | None,
) -> Tuple[bool, Optional[List[Dict[str, Any]]]]: ) -> Tuple[bool, Optional[List[Dict[str, Any]]]]:
if not await self.host.has_feature(client, NS_PAM): if not await self.host.has_feature(client, NS_PAM):
return True, None return True, None

View file

@ -50,10 +50,7 @@ class XEP_0428:
host.register_namespace("fallback", NS_FALLBACK) host.register_namespace("fallback", NS_FALLBACK)
def add_fallback_elt( def add_fallback_elt(
self, self, message_elt: domish.Element, for_: str, msg: str | None = None
message_elt: domish.Element,
for_: str,
msg: str|None = None
) -> None: ) -> None:
"""Add the fallback indication element """Add the fallback indication element

View file

@ -63,7 +63,7 @@ class ReplyTo(BaseModel):
internal_uid: bool = Field( internal_uid: bool = Field(
default=True, default=True,
description="True if the id is a message UID, i.e. Libervia internal ID of " description="True if the id is a message UID, i.e. Libervia internal ID of "
"message, otherwise it's an XMPP stanza ID or origin ID as specified in the XEP" "message, otherwise it's an XMPP stanza ID or origin ID as specified in the XEP",
) )
async def ensure_xmpp_id(self, client: SatXMPPEntity) -> None: async def ensure_xmpp_id(self, client: SatXMPPEntity) -> None:
@ -118,7 +118,8 @@ class ReplyTo(BaseModel):
if self.internal_uid: if self.internal_uid:
raise exceptions.DataError( raise exceptions.DataError(
'"id" must be converted to XMPP id before calling "to_element". Please ' '"id" must be converted to XMPP id before calling "to_element". Please '
'"use ensure_xmpp_id" first.') '"use ensure_xmpp_id" first.'
)
reply_elt = domish.Element((NS_REPLY, "reply"), attribs={"id": self.id}) reply_elt = domish.Element((NS_REPLY, "reply"), attribs={"id": self.id})
if self.to is not None: if self.to is not None:
reply_elt["to"] = self.to.full() reply_elt["to"] = self.to.full()
@ -188,7 +189,9 @@ class XEP_0461:
return True return True
@ensure_deferred @ensure_deferred
async def _add_reply_to_elt(self, mess_data: MessageData, client: SatXMPPEntity) -> MessageData: async def _add_reply_to_elt(
self, mess_data: MessageData, client: SatXMPPEntity
) -> MessageData:
try: try:
# FIXME: Once MessageData is fully moved to Pydantic, we should have directly # FIXME: Once MessageData is fully moved to Pydantic, we should have directly
# the ReplyTo instance here. # the ReplyTo instance here.

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Helpers class for plugin dependencies """ """Helpers class for plugin dependencies"""
from twisted.internet import defer from twisted.internet import defer

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Test the helper classes to see if they behave well""" """Test the helper classes to see if they behave well"""
from libervia.backend.test import helpers from libervia.backend.test import helpers
from libervia.backend.test import helpers_plugins from libervia.backend.test import helpers_plugins

View file

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Tests for the plugin radiocol """ """Tests for the plugin radiocol"""
from libervia.backend.test import helpers from libervia.backend.test import helpers
from libervia.backend.memory.crypto import BlockCipher, PasswordHasher from libervia.backend.memory.crypto import BlockCipher, PasswordHasher

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin groupblogs """ """Plugin groupblogs"""
from .constants import Const as C from .constants import Const as C
from libervia.backend.test import helpers, helpers_plugins from libervia.backend.test import helpers, helpers_plugins

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Tests for the plugin radiocol """ """Tests for the plugin radiocol"""
from libervia.backend.core import exceptions from libervia.backend.core import exceptions
from libervia.backend.test import helpers, helpers_plugins from libervia.backend.test import helpers, helpers_plugins

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Tests for the plugin room game (base class for MUC games) """ """Tests for the plugin room game (base class for MUC games)"""
from libervia.backend.core.i18n import _ from libervia.backend.core.i18n import _
from .constants import Const from .constants import Const

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin text syntaxes tests """ """Plugin text syntaxes tests"""
from libervia.backend.test import helpers from libervia.backend.test import helpers
from libervia.backend.plugins import plugin_misc_text_syntaxes from libervia.backend.plugins import plugin_misc_text_syntaxes

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin chat states notification tests """ """Plugin chat states notification tests"""
from .constants import Const from .constants import Const
from libervia.backend.test import helpers from libervia.backend.test import helpers

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin XEP-0203 """ """Plugin XEP-0203"""
from libervia.backend.test import helpers from libervia.backend.test import helpers
from libervia.backend.plugins.plugin_xep_0203 import XEP_0203 from libervia.backend.plugins.plugin_xep_0203 import XEP_0203

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin XEP-0277 tests """ """Plugin XEP-0277 tests"""
from libervia.backend.test import helpers from libervia.backend.test import helpers
from libervia.backend.plugins import plugin_xep_0277 from libervia.backend.plugins import plugin_xep_0277

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin XEP-0297 """ """Plugin XEP-0297"""
from .constants import Const as C from .constants import Const as C
from libervia.backend.test import helpers from libervia.backend.test import helpers

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin XEP-0313 """ """Plugin XEP-0313"""
from .constants import Const as C from .constants import Const as C
from libervia.backend.test import helpers from libervia.backend.test import helpers

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Plugin XEP-0334 """ """Plugin XEP-0334"""
from .constants import Const as C from .constants import Const as C
from libervia.backend.test import helpers from libervia.backend.test import helpers

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" tools common to backend and frontends """ """tools common to backend and frontends"""
#  FIXME: json may be more appropriate than manual serialising like done here #  FIXME: json may be more appropriate than manual serialising like done here
from typing import Any from typing import Any

View file

@ -61,7 +61,7 @@ TZ_UTC = tz.tzutc()
TZ_LOCAL = tz.gettz() TZ_LOCAL = tz.gettz()
def date_parse(value: str, default_tz: datetime.tzinfo=TZ_UTC) -> int: def date_parse(value: str, default_tz: datetime.tzinfo = TZ_UTC) -> int:
"""Parse a date and return corresponding unix timestamp """Parse a date and return corresponding unix timestamp
@param value: date to parse, in any format supported by parser @param value: date to parse, in any format supported by parser

View file

@ -16,7 +16,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" tools dynamic import """ """tools dynamic import"""
from importlib import import_module from importlib import import_module
from libervia.backend.core.log import getLogger from libervia.backend.core.log import getLogger

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" regex tools common to backend and frontends """ """regex tools common to backend and frontends"""
import re import re
import unicodedata import unicodedata

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" template XMLUI parsing """template XMLUI parsing
XMLUI classes from this modules can then be iterated to create the template XMLUI classes from this modules can then be iterated to create the template
""" """

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" XMPP uri parsing tools """ """XMPP uri parsing tools"""
from typing import Optional from typing import Optional
import sys import sys

View file

@ -18,7 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Configuration related useful methods """ """Configuration related useful methods"""
import os import os
import csv import csv

View file

@ -16,7 +16,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" interfaces """ """interfaces"""
from argparse import OPTIONAL from argparse import OPTIONAL
from pathlib import Path from pathlib import Path

View file

@ -16,7 +16,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" various useful methods """ """various useful methods"""
from typing import Optional, Union from typing import Optional, Union
import unicodedata import unicodedata

View file

@ -54,7 +54,6 @@ log.basicConfig(level=log.WARNING, format="[%(name)s] %(message)s")
### ###
## bridge handling ## bridge handling
# we get bridge name from conf and initialise the right class accordingly # we get bridge name from conf and initialise the right class accordingly
main_config = config.parse_main_conf() main_config = config.parse_main_conf()
@ -91,6 +90,7 @@ PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s
def date_decoder(arg): def date_decoder(arg):
return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
def optional_bool_decoder(arg: str) -> bool: def optional_bool_decoder(arg: str) -> bool:
"""Decode an optional string to a boolean value. """Decode an optional string to a boolean value.
@ -99,9 +99,9 @@ def optional_bool_decoder(arg: str) -> bool:
@raise ValueError: If the input string is not a valid boolean representation. @raise ValueError: If the input string is not a valid boolean representation.
""" """
lower_arg = arg.lower() lower_arg = arg.lower()
if lower_arg in ['true', '1', 't', 'y', 'yes']: if lower_arg in ["true", "1", "t", "y", "yes"]:
return True return True
elif lower_arg in ['false', '0', 'f', 'n', 'no']: elif lower_arg in ["false", "0", "f", "n", "no"]:
return False return False
else: else:
raise ValueError(f"Invalid boolean value: {arg}") raise ValueError(f"Invalid boolean value: {arg}")

View file

@ -33,9 +33,11 @@ class BookmarksList(base.CommandBase):
def __init__(self, host): def __init__(self, host):
extra_outputs = {"default": self.default_output} extra_outputs = {"default": self.default_output}
super().__init__( super().__init__(
host, "list", help=_("list bookmarks"), host,
"list",
help=_("list bookmarks"),
use_output=C.OUTPUT_COMPLEX, use_output=C.OUTPUT_COMPLEX,
extra_outputs=extra_outputs extra_outputs=extra_outputs,
) )
def add_parser_options(self): def add_parser_options(self):
@ -55,16 +57,16 @@ class BookmarksList(base.CommandBase):
conference_data.get("name", ""), conference_data.get("name", ""),
conference_data.get("nick", ""), conference_data.get("nick", ""),
conference_data.get("password", ""), conference_data.get("password", ""),
"✅" if conference_data.get("autojoin", False) else "❌" "✅" if conference_data.get("autojoin", False) else "❌",
) )
self.console.print(table) self.console.print(table)
async def start(self): async def start(self):
try: try:
data = data_format.deserialise(await self.host.bridge.bookmarks_list( data = data_format.deserialise(
"", self.host.profile await self.host.bridge.bookmarks_list("", self.host.profile)
)) )
except Exception as e: except Exception as e:
self.disp(f"can't get bookmarks list: {e}", error=True) self.disp(f"can't get bookmarks list: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK) self.host.quit(C.EXIT_BRIDGE_ERRBACK)
@ -79,9 +81,7 @@ class BookmarksRemove(base.CommandBase):
super().__init__(host, "remove", help=_("remove a bookmark")) super().__init__(host, "remove", help=_("remove a bookmark"))
def add_parser_options(self): def add_parser_options(self):
self.parser.add_argument( self.parser.add_argument("bookmark", help=_("jid of the bookmark to remove"))
"bookmark", help=_("jid of the bookmark to remove")
)
self.parser.add_argument( self.parser.add_argument(
"-f", "-f",
"--force", "--force",
@ -92,14 +92,13 @@ class BookmarksRemove(base.CommandBase):
async def start(self): async def start(self):
if not self.args.force: if not self.args.force:
await self.host.confirm_or_quit( await self.host.confirm_or_quit(
_("Are you sure to delete the bookmark {bookmark_id!r}?") _("Are you sure to delete the bookmark {bookmark_id!r}?").format(
.format(bookmark_id=self.args.bookmark) bookmark_id=self.args.bookmark
)
) )
try: try:
await self.host.bridge.bookmark_remove( await self.host.bridge.bookmark_remove(self.args.bookmark, self.host.profile)
self.args.bookmark, self.host.profile
)
except Exception as e: except Exception as e:
self.disp(_("can't delete bookmark: {e}").format(e=e), error=True) self.disp(_("can't delete bookmark: {e}").format(e=e), error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK) self.host.quit(C.EXIT_BRIDGE_ERRBACK)
@ -110,9 +109,7 @@ class BookmarksRemove(base.CommandBase):
class BookmarksSet(base.CommandBase): class BookmarksSet(base.CommandBase):
def __init__(self, host): def __init__(self, host):
super().__init__( super().__init__(host, "set", help=_("add or update a bookmark"))
host, "set", help=_("add or update a bookmark")
)
def add_parser_options(self): def add_parser_options(self):
self.parser.add_argument("bookmark", help=_("jid of the chat room")) self.parser.add_argument("bookmark", help=_("jid of the chat room"))
@ -133,18 +130,16 @@ class BookmarksSet(base.CommandBase):
help=_("join the conference room"), help=_("join the conference room"),
) )
self.parser.add_argument( self.parser.add_argument(
"-N", "-N", "--nick", help=_("preferred roomnick for the chatroom")
"--nick", help=_("preferred roomnick for the chatroom")
) )
self.parser.add_argument( self.parser.add_argument(
"-P", "-P", "--password", help=_("password used to access the chatroom")
"--password", help=_("password used to access the chatroom")
) )
self.parser.add_argument( self.parser.add_argument(
"-u", "-u",
"--update", "--update",
action="store_true", action="store_true",
help=_("update bookmark data instead of replacing") help=_("update bookmark data instead of replacing"),
) )
async def start(self): async def start(self):
@ -167,7 +162,7 @@ class BookmarksSet(base.CommandBase):
self.disp( self.disp(
f"Can't find existing bookmark {self.args.bookmark!r}: {e}. We " f"Can't find existing bookmark {self.args.bookmark!r}: {e}. We "
"create it.", "create it.",
error=True error=True,
) )
else: else:
old_conference_data.update(conference_data) old_conference_data.update(conference_data)
@ -190,6 +185,4 @@ class Bookmarks(base.CommandBase):
subcommands = (BookmarksList, BookmarksSet, BookmarksRemove, BookmarksLegacy) subcommands = (BookmarksList, BookmarksSet, BookmarksRemove, BookmarksLegacy)
def __init__(self, host): def __init__(self, host):
super().__init__( super().__init__(host, "bookmarks", use_profile=False, help=_("manage bookmarks"))
host, "bookmarks", use_profile=False, help=_("manage bookmarks")
)

View file

@ -1592,9 +1592,7 @@ class List(base.CommandBase):
) )
async def start(self): async def start(self):
options = { options = {"depth": self.args.depth}
"depth": self.args.depth
}
try: try:
result_s = await self.host.bridge.ps_disco_get( result_s = await self.host.bridge.ps_disco_get(
self.args.service, self.args.service,
@ -1617,7 +1615,6 @@ class List(base.CommandBase):
node_tree.add(self.recursive_tree(child)) node_tree.add(self.recursive_tree(child))
return node_tree return node_tree
def default_output(self, data: list) -> None: def default_output(self, data: list) -> None:
"""Output the node as a tree view.""" """Output the node as a tree view."""
for item in data: for item in data:

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Contact List handling multi profiles at once, """Contact List handling multi profiles at once,
should replace quick_contact_list module in the future""" should replace quick_contact_list module in the future"""
from libervia.backend.core.i18n import _ from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger from libervia.backend.core.log import getLogger

View file

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""This library help manage XML used in SàT frontends """ """This library help manage XML used in SàT frontends"""
# we don't import minidom as a different class can be used in frontends # we don't import minidom as a different class can be used in frontends
# (e.g. NativeDOM in Libervia) # (e.g. NativeDOM in Libervia)

View file

@ -2,7 +2,6 @@ from typing import Optional
from twisted.words.xish import domish from twisted.words.xish import domish
class BaseError(Exception): class BaseError(Exception):
namespace: str namespace: str
condition: str condition: str
@ -10,5 +9,4 @@ class BaseError(Exception):
textLang: Optional[str] textLang: Optional[str]
appCondition: Optional[domish.Element] appCondition: Optional[domish.Element]
class StanzaError(BaseError): class StanzaError(BaseError): ...
...

View file

@ -1,11 +1,9 @@
from typing import Tuple, overload from typing import Tuple, overload
User = str User = str
Host = str Host = str
Resource = str Resource = str
# Note: these typings are incomplete and evolve as needed. # Note: these typings are incomplete and evolve as needed.
class JID: class JID:
user: User user: User
@ -13,18 +11,9 @@ class JID:
resource: Resource resource: Resource
@overload @overload
def __init__(self, str: str, tuple: None = None) -> None: def __init__(self, str: str, tuple: None = None) -> None: ...
...
@overload @overload
def __init__(self, str: None, tuple: Tuple[User, Host, Resource]) -> None: def __init__(self, str: None, tuple: Tuple[User, Host, Resource]) -> None: ...
... def userhost(self) -> str: ...
def userhostJID(self) -> "JID": ...
def userhost(self) -> str: def full(self) -> str: ...
...
def userhostJID(self) -> "JID":
...
def full(self) -> str:
...

View file

@ -1,8 +1,15 @@
from typing import ( from typing import (
Dict, Iterator, List, Literal, Optional, Tuple, TypeVar, Union, overload Dict,
Iterator,
List,
Literal,
Optional,
Tuple,
TypeVar,
Union,
overload,
) )
URI = str URI = str
Name = str Name = str
QName = Tuple[Optional[URI], Name] QName = Tuple[Optional[URI], Name]
@ -10,10 +17,8 @@ AttributeKey = Union[QName, Name]
Attributes = Dict[AttributeKey, str] Attributes = Dict[AttributeKey, str]
Prefix = str Prefix = str
D = TypeVar("D") D = TypeVar("D")
# Note: these typings are incomplete and evolve as needed. # Note: these typings are incomplete and evolve as needed.
class Element: class Element:
uri: Optional[URI] uri: Optional[URI]
@ -27,54 +32,31 @@ class Element:
qname: QName, qname: QName,
defaultUri: Optional[URI] = None, defaultUri: Optional[URI] = None,
attribs: Optional[Attributes] = None, attribs: Optional[Attributes] = None,
localPrefixes: Optional[Dict[URI, Prefix]] = None localPrefixes: Optional[Dict[URI, Prefix]] = None,
) -> None: ) -> None: ...
... def __getitem__(self, key: AttributeKey) -> str: ...
def __setitem__(self, key: AttributeKey, value: str) -> None: ...
def __getitem__(self, key: AttributeKey) -> str:
...
def __setitem__(self, key: AttributeKey, value: str) -> None:
...
@overload @overload
def getAttribute( def getAttribute(
self, self, attribname: AttributeKey, default: None = None
attribname: AttributeKey, ) -> Union[str, None]: ...
default: None = None
) -> Union[str, None]:
...
@overload @overload
def getAttribute(self, attribname: AttributeKey, default: D) -> Union[str, D]: def getAttribute(self, attribname: AttributeKey, default: D) -> Union[str, D]: ...
... def addChild(self, node: "Element") -> "Element": ...
def addContent(self, text: str) -> str: ...
def addChild(self, node: "Element") -> "Element":
...
def addContent(self, text: str) -> str:
...
def addElement( def addElement(
self, self,
name: Union[QName, Name], name: Union[QName, Name],
defaultUri: Optional[URI] = None, defaultUri: Optional[URI] = None,
content: Optional[str] = None content: Optional[str] = None,
) -> "Element": ) -> "Element": ...
...
def elements( def elements(
self, self, uri: Optional[URI] = None, name: Optional[Name] = None
uri: Optional[URI] = None, ) -> Iterator["Element"]: ...
name: Optional[Name] = None
) -> Iterator["Element"]:
...
def toXml( def toXml(
self, self,
prefixes: Optional[Dict[URI, Prefix]] = None, prefixes: Optional[Dict[URI, Prefix]] = None,
closeElement: Literal[0, 1] = 1, closeElement: Literal[0, 1] = 1,
defaultUri: str = "", defaultUri: str = "",
prefixesInScope: Optional[List[Prefix]] = None prefixesInScope: Optional[List[Prefix]] = None,
) -> str: ) -> str: ...
...

View file

@ -42,6 +42,7 @@ from libervia.backend.plugins.plugin_comp_email_gateway.pubsub_service import (
) )
from libervia.backend.plugins.plugin_xep_0131 import HeadersData, Urgency from libervia.backend.plugins.plugin_xep_0131 import HeadersData, Urgency
@pytest.fixture @pytest.fixture
def email_gw(host): def email_gw(host):
email_gw = EmailGatewayComponent(host) email_gw = EmailGatewayComponent(host)
@ -49,6 +50,7 @@ def email_gw(host):
email_gw.users_data = {} email_gw.users_data = {}
return email_gw return email_gw
class TestEmailGatewayComponent: class TestEmailGatewayComponent:
def test_jid_to_email_gateway_jid(self, email_gw): def test_jid_to_email_gateway_jid(self, email_gw):
@ -284,12 +286,26 @@ class TestPubsubService:
request.sender = client.jid request.sender = client.jid
request.nodeIdentifier = pubsub_resource._pfs.namespace request.nodeIdentifier = pubsub_resource._pfs.namespace
files = [ files = [
{"id": "1", "name": "file1", "media_type": "application", "media_subtype": {
"octet-stream", "size": 123, "hash_algo": "sha-256", "file_hash": "0123456789abcdef", "id": "1",
"created": 123}, "name": "file1",
{"id": "2", "name": "file2", "media_type": "application", "media_subtype": "media_type": "application",
"octet-stream", "size": 456, "hash_algo": "sha-256", "file_hash": "0123456789abcdef", "media_subtype": "octet-stream",
"created": 123}, "size": 123,
"hash_algo": "sha-256",
"file_hash": "0123456789abcdef",
"created": 123,
},
{
"id": "2",
"name": "file2",
"media_type": "application",
"media_subtype": "octet-stream",
"size": 456,
"hash_algo": "sha-256",
"file_hash": "0123456789abcdef",
"created": 123,
},
] ]
host.memory.get_files = AsyncMock(return_value=files) host.memory.get_files = AsyncMock(return_value=files)
result, _ = await pubsub_resource.items(request) result, _ = await pubsub_resource.items(request)

View file

@ -51,14 +51,13 @@ class TestSQLA:
from_jid=None, from_jid=None,
to_jid=None, to_jid=None,
filters={"keyword": "test"}, filters={"keyword": "test"},
profile="test_profile" profile="test_profile",
) )
# We check that the SQL query is as expected. # We check that the SQL query is as expected.
executed_stmt = mock_session_instance.execute.call_args[0][0] executed_stmt = mock_session_instance.execute.call_args[0][0]
compiled = executed_stmt.compile( compiled = executed_stmt.compile(
dialect=sqlite.dialect(), dialect=sqlite.dialect(), compile_kwargs={"literal_binds": True}
compile_kwargs={"literal_binds": True}
) )
sql = str(compiled) sql = str(compiled)
assert "json_each" in sql assert "json_each" in sql

View file

@ -31,7 +31,6 @@ class TestXEP0215:
host.memory.disco.has_feature = AsyncMock() host.memory.disco.has_feature = AsyncMock()
host.memory.disco.has_feature.return_value = True host.memory.disco.has_feature.return_value = True
services_elt = xml_tools.parse( services_elt = xml_tools.parse(
""" """
<services xmlns="urn:xmpp:extdisco:2"> <services xmlns="urn:xmpp:extdisco:2">

View file

@ -35,7 +35,9 @@ class TestXEP0461:
async def test_reply_to_from_element(self): async def test_reply_to_from_element(self):
"""<reply> element parsing works.""" """<reply> element parsing works."""
# Test correct case # Test correct case
element = domish.Element((NS_REPLY, "reply"), attribs={"id": "123", "to": "user@example.com"}) element = domish.Element(
(NS_REPLY, "reply"), attribs={"id": "123", "to": "user@example.com"}
)
reply = ReplyTo.from_element(element) reply = ReplyTo.from_element(element)
assert reply.to == jid.JID("user@example.com") assert reply.to == jid.JID("user@example.com")
assert reply.id == "123" assert reply.id == "123"
@ -92,5 +94,7 @@ class TestXEP0461:
mess_data = MessageData(extra={"reply": {"id": "123", "internal_uid": False}}) mess_data = MessageData(extra={"reply": {"id": "123", "internal_uid": False}})
pre_xml_treatments = MagicMock() pre_xml_treatments = MagicMock()
post_xml_treatments = MagicMock() post_xml_treatments = MagicMock()
xep._sendMessage_trigger(client, mess_data, pre_xml_treatments, post_xml_treatments) xep._sendMessage_trigger(
client, mess_data, pre_xml_treatments, post_xml_treatments
)
assert post_xml_treatments.addCallback.called assert post_xml_treatments.addCallback.called

View file

@ -36,6 +36,7 @@ def initialise(options):
"""Method to initialise global modules""" """Method to initialise global modules"""
# XXX: We need to configure logs before any log method is used, so here is the best place. # XXX: We need to configure logs before any log method is used, so here is the best place.
from libervia.backend.core import log_config from libervia.backend.core import log_config
log_config.libervia_configure(C.LOG_BACKEND_TWISTED, C, backend_data=options) log_config.libervia_configure(C.LOG_BACKEND_TWISTED, C, backend_data=options)
@ -52,11 +53,13 @@ class LiberviaMaker:
def set_debugger(self): def set_debugger(self):
from twisted.internet import defer from twisted.internet import defer
if defer.Deferred.debug: if defer.Deferred.debug:
# if we are in debug mode, we want to use ipdb instead of pdb # if we are in debug mode, we want to use ipdb instead of pdb
try: try:
import ipdb import ipdb
import pdb import pdb
pdb.set_trace = ipdb.set_trace pdb.set_trace = ipdb.set_trace
pdb.post_mortem = ipdb.post_mortem pdb.post_mortem = ipdb.post_mortem
except ImportError: except ImportError:
@ -64,12 +67,14 @@ class LiberviaMaker:
def makeService(self, options): def makeService(self, options):
from twisted.internet import asyncioreactor from twisted.internet import asyncioreactor
asyncioreactor.install() asyncioreactor.install()
self.set_debugger() self.set_debugger()
# XXX: Libervia must be imported after log configuration, # XXX: Libervia must be imported after log configuration,
# because it write stuff to logs # because it write stuff to logs
initialise(options.parent) initialise(options.parent)
from libervia.backend.core.main import LiberviaBackend from libervia.backend.core.main import LiberviaBackend
return LiberviaBackend() return LiberviaBackend()