Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
gajim
python-nbxmpp
Commits
8293ab4c
Commit
8293ab4c
authored
Dec 09, 2022
by
Nicoco
Committed by
Philipp Hörist
Dec 09, 2022
Browse files
feat: Add message reactions (XEP-0444) support
parent
6f310728
Changes
7
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
nbxmpp/dispatcher.py
View file @
8293ab4c
...
...
@@ -85,6 +85,7 @@ from nbxmpp.modules.delimiter import Delimiter
from
nbxmpp.modules.roster
import
Roster
from
nbxmpp.modules.last_activity
import
LastActivity
from
nbxmpp.modules.entity_time
import
EntityTime
from
nbxmpp.modules.reactions
import
Reactions
from
nbxmpp.modules.misc
import
unwrap_carbon
from
nbxmpp.modules.misc
import
unwrap_mam
from
nbxmpp.util
import
get_properties_struct
...
...
@@ -199,6 +200,7 @@ class StanzaDispatcher(Observable):
self
.
_modules
[
'Roster'
]
=
Roster
(
self
.
_client
)
self
.
_modules
[
'LastActivity'
]
=
LastActivity
(
self
.
_client
)
self
.
_modules
[
'EntityTime'
]
=
EntityTime
(
self
.
_client
)
self
.
_modules
[
'Reactions'
]
=
Reactions
(
self
.
_client
)
for
instance
in
self
.
_modules
.
values
():
for
handler
in
instance
.
handlers
:
...
...
nbxmpp/modules/reactions.py
0 → 100644
View file @
8293ab4c
# Copyright (C) 2022 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of nbxmpp.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
import
typing
from
nbxmpp.namespaces
import
Namespace
from
nbxmpp.structs
import
StanzaHandler
,
MessageProperties
from
nbxmpp.structs
import
Reactions
as
ReactionStruct
from
nbxmpp.modules.base
import
BaseModule
if
typing
.
TYPE_CHECKING
:
from
nbxmpp.client
import
Client
from
nbxmpp.protocol
import
Message
class
Reactions
(
BaseModule
):
def
__init__
(
self
,
client
:
'Client'
):
BaseModule
.
__init__
(
self
,
client
)
self
.
_client
=
client
self
.
handlers
=
[
StanzaHandler
(
name
=
'message'
,
callback
=
self
.
_process_message_reaction
,
ns
=
Namespace
.
REACTIONS
,
priority
=
15
,
),
]
def
_process_message_reaction
(
self
,
_client
:
'Client'
,
stanza
:
'Message'
,
properties
:
MessageProperties
)
->
None
:
reactions
=
stanza
.
getTag
(
'reactions'
,
namespace
=
Namespace
.
REACTIONS
)
if
reactions
is
None
:
return
id_
=
reactions
.
getAttr
(
'id'
)
if
not
id_
:
self
.
_log
.
warning
(
'Reactions without ID'
)
return
emojis
=
set
()
for
reaction
in
reactions
.
getTags
(
'reaction'
):
# we strip for clients that might add white spaces and/or
# new lines in the reaction content.
emoji
=
reaction
.
getData
().
strip
()
if
emoji
:
emojis
.
add
(
emoji
)
else
:
self
.
_log
.
warning
(
'Empty reaction'
)
properties
.
reactions
=
ReactionStruct
(
id_
,
emojis
)
nbxmpp/namespaces.py
View file @
8293ab4c
...
...
@@ -134,6 +134,7 @@ class _Namespaces:
PUBSUB_OWNER
:
str
=
'http://jabber.org/protocol/pubsub#owner'
PUBSUB_PUBLISH_OPTIONS
:
str
=
'http://jabber.org/protocol/pubsub#publish-options'
PUBSUB_NODE_MAX
:
str
=
'http://jabber.org/protocol/pubsub#config-node-max'
REACTIONS
:
str
=
'urn:xmpp:reactions:0'
RECEIPTS
:
str
=
'urn:xmpp:receipts'
REGISTER
:
str
=
'jabber:iq:register'
REGISTER_FEATURE
:
str
=
'http://jabber.org/features/iq-register'
...
...
nbxmpp/protocol.py
View file @
8293ab4c
...
...
@@ -21,6 +21,7 @@ sub- stanzas) handling routines
from
__future__
import
annotations
from
typing
import
Any
from
typing
import
Iterable
from
typing
import
Union
from
typing
import
Optional
from
typing
import
cast
...
...
@@ -1272,6 +1273,27 @@ class Message(Protocol):
def
setHint
(
self
,
hint
):
self
.
setTag
(
hint
,
namespace
=
Namespace
.
HINTS
)
def
setReactions
(
self
,
target_id
:
str
,
emojis
:
Iterable
[
str
]):
reactions
=
self
.
addChild
(
'reactions'
,
namespace
=
Namespace
.
REACTIONS
,
attrs
=
{
"id"
:
target_id
})
for
e
in
emojis
:
reactions
.
addChild
(
'reaction'
,
namespace
=
Namespace
.
REACTIONS
,
payload
=
[
e
])
def
getReactions
(
self
)
->
Optional
[
tuple
[
str
,
set
[
str
]]]:
reactions
=
self
.
getTag
(
'reactions'
,
namespace
=
Namespace
.
REACTIONS
)
if
not
reactions
:
return
None
react_to
=
reactions
.
getAttr
(
'id'
)
if
not
react_to
:
return
None
tags
=
reactions
.
getTags
(
'reaction'
,
namespace
=
Namespace
.
REACTIONS
)
# strip() in case clients surround emojis with whitespace
return
react_to
,
{
t
.
getData
().
strip
()
for
t
in
tags
}
class
Presence
(
Protocol
):
...
...
nbxmpp/structs.py
View file @
8293ab4c
...
...
@@ -746,6 +746,11 @@ class ChatMarker(NamedTuple):
return
self
.
type
==
'acknowledged'
class
Reactions
(
NamedTuple
):
id
:
str
emojis
:
set
[
str
]
class
CommonError
:
def
__init__
(
self
,
stanza
):
self
.
_stanza_name
=
stanza
.
getName
()
...
...
@@ -993,6 +998,7 @@ class MessageProperties:
xhtml
:
Optional
[
str
]
=
None
security_label
=
None
chatstate
=
None
reactions
:
Optional
[
Reactions
]
=
None
def
is_from_us
(
self
,
bare_match
:
bool
=
True
):
if
self
.
from_
is
None
:
...
...
python-nbxmpp.doap
View file @
8293ab4c
...
...
@@ -415,5 +415,12 @@
<xmpp:version>
0.2.1
</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep
rdf:resource=
"https://xmpp.org/extensions/xep-0444.html"
/>
<xmpp:status>
complete
</xmpp:status>
<xmpp:version>
0.1.0
</xmpp:version>
</xmpp:SupportedXep>
</implements>
</Project>
</rdf:RDF>
test/unit/test_reactions.py
0 → 100644
View file @
8293ab4c
from
test.lib.util
import
StanzaHandlerTest
from
nbxmpp.structs
import
Reactions
as
ReactionStruct
from
nbxmpp.modules.reactions
import
Reactions
from
nbxmpp.protocol
import
Message
class
MockLog
:
@
staticmethod
def
warning
(
_
):
pass
class
MockModule
:
_log
=
MockLog
@
staticmethod
def
is_emoji
(
s
):
return
Reactions
.
is_emoji
(
s
)
class
ReactionsTest
(
StanzaHandlerTest
):
def
test_reaction_parsing
(
self
):
class
P
:
reactions
:
ReactionStruct
xml
=
'''
<message to='romeo@capulet.net/orchard' id='96d73204-a57a-11e9-88b8-4889e7820c76' type='chat'>
<reactions id='744f6e18-a57a-11e9-a656-4889e7820c76' xmlns='urn:xmpp:reactions:0'>
<reaction>👋
</reaction>
<reaction>🐢</reaction>
</reactions>
<store xmlns='urn:xmpp:hints'/>
</message>
'''
msg
=
Message
(
node
=
xml
)
Reactions
.
_process_message_reaction
(
MockModule
,
self
,
msg
,
P
)
self
.
assertEqual
(
P
.
reactions
.
id
,
'744f6e18-a57a-11e9-a656-4889e7820c76'
)
self
.
assertEqual
(
P
.
reactions
.
emojis
,
{
'👋'
,
'🐢'
})
def
test_no_reactions
(
self
):
class
P
:
reactions
:
ReactionStruct
=
None
xml
=
'''
<message to='romeo@capulet.net/orchard' id='96d73204-a57a-11e9-88b8-4889e7820c76' type='chat'>
<store xmlns='urn:xmpp:hints'/>
</message>
'''
msg
=
Message
(
node
=
xml
)
Reactions
.
_process_message_reaction
(
MockModule
,
self
,
msg
,
P
)
self
.
assertIsNone
(
P
.
reactions
)
def
test_empty_reactions
(
self
):
class
P
:
reactions
:
ReactionStruct
xml
=
'''
<message to='romeo@capulet.net/orchard' id='96d73204-a57a-11e9-88b8-4889e7820c76' type='chat'>
<reactions id='744f6e18-a57a-11e9-a656-4889e7820c76' xmlns='urn:xmpp:reactions:0' />
<store xmlns='urn:xmpp:hints'/>
</message>
'''
msg
=
Message
(
node
=
xml
)
Reactions
.
_process_message_reaction
(
MockModule
,
self
,
msg
,
P
)
self
.
assertEqual
(
len
(
P
.
reactions
.
emojis
),
0
)
def
test_invalid_reactions_no_id
(
self
):
class
P
:
reactions
:
ReactionStruct
xml
=
'''
<message to='romeo@capulet.net/orchard' id='96d73204-a57a-11e9-88b8-4889e7820c76' type='chat'>
<reactions xmlns='urn:xmpp:reactions:0'>
<reaction>👋</reaction>
<reaction>🐢</reaction>
</reactions>
<store xmlns='urn:xmpp:hints'/>
</message>
'''
msg
=
Message
(
node
=
xml
)
Reactions
.
_process_message_reaction
(
MockModule
,
self
,
msg
,
P
)
self
.
assertFalse
(
hasattr
(
P
,
'reactions'
))
def
test_invalid_reactions_empty_id
(
self
):
class
P
:
reactions
:
ReactionStruct
xml
=
'''
<message to='romeo@capulet.net/orchard' id='96d73204-a57a-11e9-88b8-4889e7820c76' type='chat'>
<reactions id='' xmlns='urn:xmpp:reactions:0'>
<reaction>👋</reaction>
<reaction>🐢</reaction>
</reactions>
<store xmlns='urn:xmpp:hints'/>
</message>
'''
msg
=
Message
(
node
=
xml
)
Reactions
.
_process_message_reaction
(
MockModule
,
self
,
msg
,
P
)
self
.
assertFalse
(
hasattr
(
P
,
'reactions'
))
def
test_invalid_reactions_empty_emoji
(
self
):
class
P
:
reactions
:
ReactionStruct
xml
=
'''
<message to='romeo@capulet.net/orchard' id='96d73204-a57a-11e9-88b8-4889e7820c76' type='chat'>
<reactions id='sadfsadf' xmlns='urn:xmpp:reactions:0'>
<reaction></reaction>
<reaction>🐢</reaction>
</reactions>
<store xmlns='urn:xmpp:hints'/>
</message>
'''
msg
=
Message
(
node
=
xml
)
Reactions
.
_process_message_reaction
(
MockModule
,
self
,
msg
,
P
)
self
.
assertEqual
(
P
.
reactions
.
emojis
,
{
'🐢'
})
def
test_set_reactions
(
self
):
x
=
Message
()
x
.
setReactions
(
'id'
,
'🐢'
)
self
.
assertEqual
(
x
.
getReactions
(),
(
'id'
,
{
'🐢'
}))
x
=
Message
()
x
.
setReactions
(
'id'
,
'🐢👋'
)
self
.
assertEqual
(
x
.
getReactions
(),
(
'id'
,
{
'🐢'
,
'👋'
}))
x
=
Message
()
x
.
setReactions
(
'id'
,
''
)
self
.
assertEqual
(
x
.
getReactions
(),
(
'id'
,
set
()))
x
=
Message
()
self
.
assertIsNone
(
x
.
getReactions
())
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment