Global reformatting with black.
This commit is contained in:
parent
5ec4fdd787
commit
96bf3af324
68 changed files with 557 additions and 561 deletions
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
doc/conf.py
51
doc/conf.py
|
|
@ -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 -------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = (
|
||||||
"^("
|
"^("
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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): ...
|
||||||
...
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
|
||||||
...
|
|
||||||
|
|
|
||||||
|
|
@ -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: ...
|
||||||
...
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue