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):
lines[:] = [
l.replace("@param", ":param").replace("@raise", ":raises")
for l in lines
]
lines[:] = [l.replace("@param", ":param").replace("@raise", ":raises") for l in lines]
def setup(app):
app.connect("autodoc-process-docstring", process_docstring)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View file

@ -1,4 +1,3 @@
#
# Configuration file for the Sphinx documentation builder.
#
@ -24,9 +23,9 @@ sys.path.insert(0, os.path.abspath("./_ext"))
# -- Project information -----------------------------------------------------
project = 'Libervia Backend'
copyright = '2019-2023 Libervia'
author = 'Libervia team'
project = "Libervia Backend"
copyright = "2019-2023 Libervia"
author = "Libervia team"
version = __version__
@ -60,22 +59,19 @@ rst_prolog = """
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"docstring"
]
extensions = ["sphinx.ext.autodoc", "docstring"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['.templates']
templates_path = [".templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -87,7 +83,7 @@ language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# 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.
pygments_style = None
@ -98,7 +94,7 @@ pygments_style = None
# The theme to use for HTML and HTML Help pages. See the documentation for
# 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
# 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,
# relative to this directory. They are copied after the builtin static files,
# 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
# to template names.
@ -125,7 +121,7 @@ html_static_path = ['.static']
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'SalutToidoc'
htmlhelp_basename = "SalutToidoc"
# -- Options for LaTeX output ------------------------------------------------
@ -134,15 +130,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -152,8 +145,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'SalutToi.tex', u'Libervia Documentation',
u'Libervia team', 'manual'),
(master_doc, "SalutToi.tex", "Libervia Documentation", "Libervia team", "manual"),
]
@ -161,10 +153,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'saluttoi', u'Libervia Documentation',
[author], 1)
]
man_pages = [(master_doc, "saluttoi", "Libervia Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
@ -173,9 +162,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'SalutToi', u'Libervia Documentation',
author, 'SalutToi', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
"SalutToi",
"Libervia Documentation",
author,
"SalutToi",
"One line description of project.",
"Miscellaneous",
),
]
@ -194,7 +189,7 @@ epub_title = project
# epub_uid = ''
# 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 -------------------------------------------------

View file

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

View file

@ -46,7 +46,9 @@ class EncryptionSession(TypedDict):
# Incomplete types built through observation rather than code inspection.
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(
self,
data: MessageData,
post_xml_treatments: defer.Deferred|None = None,
post_xml_treatments: defer.Deferred | None = None,
) -> MessageData:
"""Generate <message/> stanza from message data

View file

@ -515,7 +515,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
xs.add_hook(C.STREAM_HOOK_SEND, hook)
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:
self.host_app.bridge.disconnected(
self.profile
@ -619,8 +619,8 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
self,
iq_elt: domish.Element,
condition: str,
text: str|None = None,
appCondition: str|None = None
text: str | None = None,
appCondition: str | None = None,
) -> None:
"""Send error stanza build from iq_elt
@ -1355,7 +1355,8 @@ class SatMessageProtocol(xmppim.MessageProtocol):
message = {}
subject = {}
extra = {}
data = MessageData({
data = MessageData(
{
"from": from_jid,
"to": jid.JID(message_elt["to"]),
"uid": message_elt.getAttribute(
@ -1365,7 +1366,8 @@ class SatMessageProtocol(xmppim.MessageProtocol):
"subject": subject,
"type": message_elt.getAttribute("type", "normal"),
"extra": extra,
})
}
)
try:
message_id = extra["message_id"] = message_elt["id"]

View file

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

View file

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

View file

@ -1672,7 +1672,7 @@ class Memory:
access: dict | None = None,
extra: dict | 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:
"""Set a file metadata.

View file

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

View file

@ -5,29 +5,34 @@ Revises: 29f85e56b6d4
Create Date: 2025-05-02 11:21:54.023587
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import false
# revision identifiers, used by Alembic.
revision = '6af2d8f6be76'
down_revision = '29f85e56b6d4'
revision = "6af2d8f6be76"
down_revision = "29f85e56b6d4"
branch_labels = None
depends_on = None
def upgrade():
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.alter_column('thread_id',
existing_type=sa.TEXT(),
nullable=False)
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.alter_column("thread_id", existing_type=sa.TEXT(), nullable=False)
def downgrade():
with op.batch_alter_table('thread', schema=None) as batch_op:
batch_op.alter_column('thread_id',
existing_type=sa.TEXT(),
nullable=True)
batch_op.drop_column('is_retroactive')
with op.batch_alter_table("thread", schema=None) as batch_op:
batch_op.alter_column("thread_id", existing_type=sa.TEXT(), 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
"""
from alembic import op
import sqlalchemy as sa
from libervia.backend.memory.sqla_mapping import JID
# revision identifiers, used by Alembic.
revision = '8db042adb973'
down_revision = '6af2d8f6be76'
revision = "8db042adb973"
down_revision = "6af2d8f6be76"
branch_labels = None
depends_on = None
def upgrade():
op.create_table('pubsub_affiliations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('node_id', sa.Integer(), nullable=False),
sa.Column('entity', JID(), nullable=False),
sa.Column('affiliation', 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'))
op.create_table(
"pubsub_affiliations",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("node_id", sa.Integer(), nullable=False),
sa.Column("entity", JID(), nullable=False),
sa.Column(
"affiliation",
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:
batch_op.add_column(
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,
)
)
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(sa.Column('publish_model', sa.Enum('publishers', 'subscribers', 'open', name='publishmodel', create_constraint=True), nullable=True))
def downgrade():
with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op:
batch_op.drop_column('publish_model')
batch_op.drop_column('access_model')
with op.batch_alter_table("pubsub_nodes", schema=None) as batch_op:
batch_op.drop_column("publish_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
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ece2a6d7e4bb'
down_revision = '8db042adb973'
revision = "ece2a6d7e4bb"
down_revision = "8db042adb973"
branch_labels = None
depends_on = None
def upgrade():
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('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.f('fk_pubsub_nodes_linked_node_id_pubsub_nodes'), 'pubsub_nodes', ['linked_node_id'], ['id'], ondelete='CASCADE')
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("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.f("fk_pubsub_nodes_linked_node_id_pubsub_nodes"),
"pubsub_nodes",
["linked_node_id"],
["id"],
ondelete="CASCADE",
)
def downgrade():
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.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')
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.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
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:
"""Retrieves history row that match a specific message ID.
@ -524,8 +524,7 @@ class Storage:
)
query = (
query
.outerjoin(History.messages)
query.outerjoin(History.messages)
.outerjoin(History.subjects)
.outerjoin(History.thread)
.options(
@ -540,7 +539,6 @@ class Storage:
return history
@aio
async def history_get(
self,
@ -676,9 +674,7 @@ class Storage:
# FIXME: "json_each" works with SQLite, but when PostGreSQL support will
# be added, "json_array_elements" may have to be used with it.
subquery = func.json_each(History.extra["keywords"]).table_valued("value")
stmt = stmt.where(
exists().where(subquery.c.value == keyword)
)
stmt = stmt.where(exists().where(subquery.c.value == keyword))
if "origin" in filters:
stmt = stmt.where(
History.extra["origin"].as_string() == filters["origin"]
@ -752,7 +748,7 @@ class Storage:
self,
message_uid: str,
thread_id: str,
parent_id: str|None,
parent_id: str | None,
is_retroactive: bool,
) -> None:
"""Add a thread to a message.
@ -1180,8 +1176,8 @@ class Storage:
with_subscriptions: bool = False,
with_affiliations: bool = False,
create: bool = False,
create_kwargs: dict|None = None,
) -> PubsubNode|None:
create_kwargs: dict | None = None,
) -> PubsubNode | None:
"""Retrieve a PubsubNode from DB
@param service: service hosting the node
@ -1238,9 +1234,9 @@ class Storage:
@aio
async def get_pubsub_nodes(
self,
client: SatXMPPEntity|None,
service: jid.JID|None,
node_name: str|None = None,
client: SatXMPPEntity | None,
service: jid.JID | None,
node_name: str | None = None,
linked_node: bool = False,
depth: int = 0,
with_items: bool = False,
@ -1279,7 +1275,7 @@ class Storage:
if depth > 0:
options = [
selectinload(PubsubNode.child_nodes),
joinedload(PubsubNode.parent_node)
joinedload(PubsubNode.parent_node),
]
else:
options = []
@ -1323,24 +1319,21 @@ class Storage:
base_node_ids = [node.id for node in base_nodes]
# Create recursive CTE to find child base nodes.
hierarchy = select(
PubsubNode.id,
literal(0).label('depth')
).filter(
hierarchy = (
select(PubsubNode.id, literal(0).label("depth"))
.filter(
PubsubNode.id.in_(base_node_ids),
PubsubNode.linked_node_id.is_(None)
).cte(name="hierarchy", recursive=True)
PubsubNode.linked_node_id.is_(None),
)
.cte(name="hierarchy", recursive=True)
)
# Recursive part: get direct children that are also base nodes.
children = (
select(
PubsubNode.id,
hierarchy.c.depth + 1
)
select(PubsubNode.id, hierarchy.c.depth + 1)
.join(hierarchy, PubsubNode.parent_node_id == hierarchy.c.id)
.filter(
hierarchy.c.depth < depth,
PubsubNode.linked_node_id.is_(None)
hierarchy.c.depth < depth, PubsubNode.linked_node_id.is_(None)
)
)
@ -1348,11 +1341,8 @@ class Storage:
full_hierarchy = hierarchy.union_all(children)
# Final query for all base nodes in the hierarchy.
hierarchy_query = (
select(PubsubNode)
.filter(PubsubNode.id.in_(
select(full_hierarchy.c.id)
))
hierarchy_query = select(PubsubNode).filter(
PubsubNode.id.in_(select(full_hierarchy.c.id))
)
# Apply the same eager loading options.
@ -1377,18 +1367,18 @@ class Storage:
client: SatXMPPEntity,
service: jid.JID,
name: str,
access_model: AccessModel|None = None,
publish_model: PublishModel|None = None,
access_model: AccessModel | None = None,
publish_model: PublishModel | None = None,
analyser: Optional[str] = None,
type_: Optional[str] = None,
subtype: Optional[str] = None,
subscribed: bool = False,
extra: dict|None = None,
items: list[PubsubItem]|None = None,
affiliations: list[PubsubAffiliation]|None = None,
subscriptions: list[PubsubSub]|None = None,
parent_node: PubsubNode|None = None,
linked_node: PubsubNode|None = None
extra: dict | None = None,
items: list[PubsubItem] | None = None,
affiliations: list[PubsubAffiliation] | None = None,
subscriptions: list[PubsubSub] | None = None,
parent_node: PubsubNode | None = None,
linked_node: PubsubNode | None = None,
) -> PubsubNode:
node = PubsubNode(
profile_id=self.profiles[client.profile],
@ -1400,12 +1390,12 @@ class Storage:
analyser=analyser,
type_=type_,
subtype=subtype,
extra = extra,
items = items or [],
affiliations = affiliations or [],
subscriptions = subscriptions or [],
parent_node = parent_node,
linked_node = linked_node
extra=extra,
items=items or [],
affiliations=affiliations or [],
subscriptions=subscriptions or [],
parent_node=parent_node,
linked_node=linked_node,
)
async with self.session() as session:
async with session.begin():

View file

@ -462,7 +462,7 @@ class Thread(Base):
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)."
)
),
)
history = relationship("History", uselist=False, back_populates="thread")
@ -653,18 +653,24 @@ class PubsubNode(Base):
subtype = Column(Text, nullable=True)
extra = Column(JSON)
linked_node_id = Column(Integer, ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=True)
parent_node_id = Column(Integer, ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=True)
linked_node_id = Column(
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)
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)
linked_node = relationship(
"PubsubNode",
remote_side=[id],
foreign_keys=[linked_node_id],
back_populates="linking_nodes"
back_populates="linking_nodes",
)
linking_nodes = relationship(
"PubsubNode",
@ -676,7 +682,7 @@ class PubsubNode(Base):
"PubsubNode",
remote_side=[id],
foreign_keys=[parent_node_id],
back_populates="child_nodes"
back_populates="child_nodes",
)
child_nodes = relationship(
"PubsubNode",
@ -693,10 +699,10 @@ class PubsubAffiliation(Base):
User by components managing a pubsub service.
"""
__tablename__ = "pubsub_affiliations"
__table_args__ = (UniqueConstraint("node_id", "entity"),)
id = Column(Integer, primary_key=True)
node_id = Column(ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=False)
entity = Column(JID, nullable=False)

View file

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

View file

@ -22,7 +22,7 @@ from twisted.mail import imap4
class Credentials(BaseModel):
model_config = ConfigDict(extra='forbid')
model_config = ConfigDict(extra="forbid")
user_name: str
user_email: str
smtp_host: str
@ -36,7 +36,6 @@ class Credentials(BaseModel):
imap_password: str
@dataclass
class UserData:
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.plugins.plugin_pubsub_cache import PubsubCache
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
if TYPE_CHECKING:
from . import EmailGatewayComponent
@ -77,10 +83,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
return defer.ensureDeferred(self.get_nodes(requestor, service, nodeIdentifier))
async def get_nodes(
self,
requestor: jid.JID,
service: jid.JID,
node_id: str
self, requestor: jid.JID, service: jid.JID, node_id: str
) -> list[str]:
nodes = await G.storage.get_pubsub_nodes(self.client, self.client.jid)
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
async def items_from_mailing_list(
self,
request: rsm.PubSubRequest,
requestor_jid: jid.JID
) -> tuple[list[domish.Element], rsm.RSMResponse|None]:
self, request: rsm.PubSubRequest, requestor_jid: jid.JID
) -> tuple[list[domish.Element], rsm.RSMResponse | None]:
"""Handle items coming from mailing lists.
@param request: Pubsub request.
@ -118,10 +119,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
"""
node = request.nodeIdentifier
node = await G.storage.get_pubsub_node(
self.client,
self.client.jid,
node,
with_affiliations=True
self.client, self.client.jid, node, with_affiliations=True
)
if node is None:
raise error.StanzaError("item-not-found")
@ -133,9 +131,8 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
for affiliation in node.affiliations:
if (
affiliation.entity == requestor_jid
and affiliation.affiliation in {
Affiliation.owner, Affiliation.publisher, Affiliation.member
}
and affiliation.affiliation
in {Affiliation.owner, Affiliation.publisher, Affiliation.member}
):
break
else:
@ -151,7 +148,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
request.maxItems,
request.itemIdentifiers,
request.subscriptionIdentifier,
request.rsm
request.rsm,
)
if rsm_data := metadata.get("rsm"):
rsm_response = rsm.RSMResponse(**rsm_data)
@ -159,7 +156,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource):
rsm_response = None
return (
[cast(domish.Element, ps_item.data) for ps_item in pubsub_items],
rsm_response
rsm_response,
)
@ensure_deferred
@ -241,7 +238,7 @@ class EmailGWPubsubService(rsm.PubSubService):
self,
client: SatXMPPComponent,
iq_elt: domish.Element,
ext_disco_options: ExtDiscoOptions
ext_disco_options: ExtDiscoOptions,
) -> ExtDiscoResult:
"""Handle pubsub extended disco"""
query_elt = iq_elt.query
@ -253,25 +250,26 @@ class EmailGWPubsubService(rsm.PubSubService):
nodes = await G.storage.get_pubsub_nodes(
client,
service=client.jid,
node_name = node,
node_name=node,
linked_node=ext_disco_options.linked_nodes,
depth=depth,
with_items="items" in ext_disco_options.type
with_items="items" in ext_disco_options.type,
)
if depth == 0:
# If depth == 0, we don't want to use `from_sqlalchemy_full_hierarchy` to
# avoid returning children, and because children `parent` and `child_nodes`
# fields are not loaded.
return ExtDiscoResult([
DiscoPubsubNode.from_sqlalchemy(node, self.client.jid)
for node in nodes
])
return ExtDiscoResult(
[DiscoPubsubNode.from_sqlalchemy(node, self.client.jid) for node in nodes]
)
else:
return ExtDiscoResult([
return ExtDiscoResult(
[
DiscoPubsubNode.from_sqlalchemy_full_hierarchy(node, self.client.jid)
for node in nodes
])
]
)
@ensure_deferred
async def getDiscoInfo(

View file

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

View file

@ -46,7 +46,7 @@ PLUGIN_INFO = {
C.PI_DESCRIPTION: _("""Search for XMPP entities"""),
}
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
MAX_CACHE_SIZE = 10
@ -103,9 +103,7 @@ class Options(BaseModel):
entities: bool = Field(
default=False, description="Search for entities for direct chat"
)
groupchat: bool = Field(
default=False, description="Search for group chats."
)
groupchat: bool = Field(default=False, description="Search for group chats.")
allow_external: bool = Field(
default=False, description="Authorise doing request to external services."
)
@ -221,7 +219,9 @@ class JidSearch:
matches.sort(
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,
)

View file

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

View file

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

View file

@ -232,7 +232,9 @@ class Autocrypt:
post_treat.addCallback(partial(self._check_headers, client))
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"]
if await self.host.memory.disco.has_identity(
client, "gateway", "smtp", jid.JID(to_jid.host)

View file

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

View file

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

View file

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

View file

@ -19,7 +19,18 @@
from collections import namedtuple
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
from pydantic import BaseModel, Field
@ -79,9 +90,8 @@ DEFAULT_PUBSUB_MIN_FEAT = {
class NodeMetadata(BaseModel):
"""Node metadata model."""
model_config = {
"extra": "allow"
}
model_config = {"extra": "allow"}
type: str | None = None
creator: JIDType | None = None
@ -199,9 +209,7 @@ class NodeMetadata(BaseModel):
"pubsub#owner": self.owner,
"pubsub#publisher": self.publisher,
"pubsub#num_subscribers": (
str(self.num_subscribers)
if self.num_subscribers is not None
else None
str(self.num_subscribers) if self.num_subscribers is not None else None
),
}
form.makeFields(

View file

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

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

View file

@ -120,8 +120,8 @@ def crc24(data: bytes) -> int:
def binary_to_ascii_armor(
data: bytes,
armor_type: str = "MESSAGE",
headers: dict|None = None,
line_length: int = 76
headers: dict | None = None,
line_length: int = 76,
) -> str:
"""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.
@return: The ASCII Armor encoded string.
"""
encoded_data = base64.b64encode(data).decode('ascii')
encoded_data = base64.b64encode(data).decode("ascii")
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_str = base64.b64encode(checksum.to_bytes(3, 'big')).decode('ascii')
checksum_str = base64.b64encode(checksum.to_bytes(3, "big")).decode("ascii")
header_lines = []
if headers:
@ -153,7 +154,7 @@ def binary_to_ascii_armor(
armor.append(f"={checksum_str}")
armor.append(f"-----END PGP {armor_type}-----")
return '\n'.join(armor)
return "\n".join(armor)
class VerificationError(Exception):

View file

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

View file

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

View file

@ -63,7 +63,7 @@ class ReplyTo(BaseModel):
internal_uid: bool = Field(
default=True,
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:
@ -118,7 +118,8 @@ class ReplyTo(BaseModel):
if self.internal_uid:
raise exceptions.DataError(
'"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})
if self.to is not None:
reply_elt["to"] = self.to.full()
@ -188,7 +189,9 @@ class XEP_0461:
return True
@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:
# FIXME: Once MessageData is fully moved to Pydantic, we should have directly
# the ReplyTo instance here.

View file

@ -18,7 +18,7 @@
# 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/>.
""" Helpers class for plugin dependencies """
"""Helpers class for plugin dependencies"""
from twisted.internet import defer

View file

@ -18,7 +18,7 @@
# 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/>.
""" 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_plugins

View file

@ -19,7 +19,7 @@
# 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.memory.crypto import BlockCipher, PasswordHasher

View file

@ -18,7 +18,7 @@
# 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/>.
""" Plugin groupblogs """
"""Plugin groupblogs"""
from .constants import Const as C
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
# 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.test import helpers, helpers_plugins

View file

@ -18,7 +18,7 @@
# 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/>.
""" 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 .constants import Const

View file

@ -17,7 +17,7 @@
# 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/>.
""" Plugin text syntaxes tests """
"""Plugin text syntaxes tests"""
from libervia.backend.test import helpers
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
# 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 libervia.backend.test import helpers

View file

@ -18,7 +18,7 @@
# 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/>.
""" Plugin XEP-0203 """
"""Plugin XEP-0203"""
from libervia.backend.test import helpers
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
# 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.plugins import plugin_xep_0277

View file

@ -18,7 +18,7 @@
# 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/>.
""" Plugin XEP-0297 """
"""Plugin XEP-0297"""
from .constants import Const as C
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
# 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 libervia.backend.test import helpers

View file

@ -18,7 +18,7 @@
# 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/>.
""" Plugin XEP-0334 """
"""Plugin XEP-0334"""
from .constants import Const as C
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
# 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
from typing import Any

View file

@ -61,7 +61,7 @@ TZ_UTC = tz.tzutc()
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
@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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" tools dynamic import """
"""tools dynamic import"""
from importlib import import_module
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
# 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 unicodedata

View file

@ -17,7 +17,7 @@
# 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/>.
""" template XMLUI parsing
"""template XMLUI parsing
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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" XMPP uri parsing tools """
"""XMPP uri parsing tools"""
from typing import Optional
import sys

View file

@ -18,7 +18,7 @@
# 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/>.
""" Configuration related useful methods """
"""Configuration related useful methods"""
import os
import csv

View file

@ -16,7 +16,7 @@
# 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/>.
""" interfaces """
"""interfaces"""
from argparse import OPTIONAL
from pathlib import Path

View file

@ -16,7 +16,7 @@
# 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/>.
""" various useful methods """
"""various useful methods"""
from typing import Optional, Union
import unicodedata

View file

@ -54,7 +54,6 @@ log.basicConfig(level=log.WARNING, format="[%(name)s] %(message)s")
###
## bridge handling
# we get bridge name from conf and initialise the right class accordingly
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):
return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL)
def optional_bool_decoder(arg: str) -> bool:
"""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.
"""
lower_arg = arg.lower()
if lower_arg in ['true', '1', 't', 'y', 'yes']:
if lower_arg in ["true", "1", "t", "y", "yes"]:
return True
elif lower_arg in ['false', '0', 'f', 'n', 'no']:
elif lower_arg in ["false", "0", "f", "n", "no"]:
return False
else:
raise ValueError(f"Invalid boolean value: {arg}")

View file

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

View file

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

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""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.log import getLogger

View file

@ -17,7 +17,7 @@
# 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/>.
"""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
# (e.g. NativeDOM in Libervia)

View file

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

View file

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

View file

@ -1,8 +1,15 @@
from typing import (
Dict, Iterator, List, Literal, Optional, Tuple, TypeVar, Union, overload
Dict,
Iterator,
List,
Literal,
Optional,
Tuple,
TypeVar,
Union,
overload,
)
URI = str
Name = str
QName = Tuple[Optional[URI], Name]
@ -10,10 +17,8 @@ AttributeKey = Union[QName, Name]
Attributes = Dict[AttributeKey, str]
Prefix = str
D = TypeVar("D")
# Note: these typings are incomplete and evolve as needed.
class Element:
uri: Optional[URI]
@ -27,54 +32,31 @@ class Element:
qname: QName,
defaultUri: Optional[URI] = None,
attribs: Optional[Attributes] = None,
localPrefixes: Optional[Dict[URI, Prefix]] = None
) -> None:
...
def __getitem__(self, key: AttributeKey) -> str:
...
def __setitem__(self, key: AttributeKey, value: str) -> None:
...
localPrefixes: Optional[Dict[URI, Prefix]] = None,
) -> None: ...
def __getitem__(self, key: AttributeKey) -> str: ...
def __setitem__(self, key: AttributeKey, value: str) -> None: ...
@overload
def getAttribute(
self,
attribname: AttributeKey,
default: None = None
) -> Union[str, None]:
...
self, attribname: AttributeKey, default: None = None
) -> Union[str, None]: ...
@overload
def getAttribute(self, attribname: AttributeKey, default: D) -> Union[str, D]:
...
def addChild(self, node: "Element") -> "Element":
...
def addContent(self, text: str) -> str:
...
def getAttribute(self, attribname: AttributeKey, default: D) -> Union[str, D]: ...
def addChild(self, node: "Element") -> "Element": ...
def addContent(self, text: str) -> str: ...
def addElement(
self,
name: Union[QName, Name],
defaultUri: Optional[URI] = None,
content: Optional[str] = None
) -> "Element":
...
content: Optional[str] = None,
) -> "Element": ...
def elements(
self,
uri: Optional[URI] = None,
name: Optional[Name] = None
) -> Iterator["Element"]:
...
self, uri: Optional[URI] = None, name: Optional[Name] = None
) -> Iterator["Element"]: ...
def toXml(
self,
prefixes: Optional[Dict[URI, Prefix]] = None,
closeElement: Literal[0, 1] = 1,
defaultUri: str = "",
prefixesInScope: Optional[List[Prefix]] = None
) -> str:
...
prefixesInScope: Optional[List[Prefix]] = None,
) -> 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
@pytest.fixture
def email_gw(host):
email_gw = EmailGatewayComponent(host)
@ -49,6 +50,7 @@ def email_gw(host):
email_gw.users_data = {}
return email_gw
class TestEmailGatewayComponent:
def test_jid_to_email_gateway_jid(self, email_gw):
@ -284,12 +286,26 @@ class TestPubsubService:
request.sender = client.jid
request.nodeIdentifier = pubsub_resource._pfs.namespace
files = [
{"id": "1", "name": "file1", "media_type": "application", "media_subtype":
"octet-stream", "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},
{
"id": "1",
"name": "file1",
"media_type": "application",
"media_subtype": "octet-stream",
"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)
result, _ = await pubsub_resource.items(request)

View file

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

View file

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

View file

@ -35,7 +35,9 @@ class TestXEP0461:
async def test_reply_to_from_element(self):
"""<reply> element parsing works."""
# 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)
assert reply.to == jid.JID("user@example.com")
assert reply.id == "123"
@ -92,5 +94,7 @@ class TestXEP0461:
mess_data = MessageData(extra={"reply": {"id": "123", "internal_uid": False}})
pre_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

View file

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