Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
converse.js
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
converse.js
Commits
e80afbfe
Commit
e80afbfe
authored
Dec 14, 2020
by
JC Brand
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move MUC and stanza utils into shared and plugin-specific files
parent
e8eea632
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
1125 additions
and
1068 deletions
+1125
-1068
karma.conf.js
karma.conf.js
+1
-0
spec/chatbox.js
spec/chatbox.js
+27
-36
spec/markers.js
spec/markers.js
+186
-0
spec/messages.js
spec/messages.js
+0
-119
spec/muc_messages.js
spec/muc_messages.js
+14
-71
src/headless/core.js
src/headless/core.js
+0
-2
src/headless/plugins/adhoc.js
src/headless/plugins/adhoc.js
+2
-2
src/headless/plugins/chat/index.js
src/headless/plugins/chat/index.js
+4
-3
src/headless/plugins/chat/model.js
src/headless/plugins/chat/model.js
+10
-26
src/headless/plugins/chat/parsers.js
src/headless/plugins/chat/parsers.js
+219
-0
src/headless/plugins/headlines.js
src/headless/plugins/headlines.js
+4
-3
src/headless/plugins/mam.js
src/headless/plugins/mam.js
+5
-4
src/headless/plugins/muc/index.js
src/headless/plugins/muc/index.js
+1
-1
src/headless/plugins/muc/muc.js
src/headless/plugins/muc/muc.js
+14
-13
src/headless/plugins/muc/parsers.js
src/headless/plugins/muc/parsers.js
+307
-0
src/headless/plugins/muc/utils.js
src/headless/plugins/muc/utils.js
+1
-47
src/headless/shared/actions.js
src/headless/shared/actions.js
+41
-0
src/headless/shared/parsers.js
src/headless/shared/parsers.js
+287
-0
src/headless/utils/stanza.js
src/headless/utils/stanza.js
+0
-738
src/modals/muc-list.js
src/modals/muc-list.js
+2
-2
src/plugins/muc-views/index.js
src/plugins/muc-views/index.js
+0
-1
No files found.
karma.conf.js
View file @
e80afbfe
...
...
@@ -49,6 +49,7 @@ module.exports = function(config) {
{
pattern
:
"
spec/corrections.js
"
,
type
:
'
module
'
},
{
pattern
:
"
spec/styling.js
"
,
type
:
'
module
'
},
{
pattern
:
"
spec/receipts.js
"
,
type
:
'
module
'
},
{
pattern
:
"
spec/markers.js
"
,
type
:
'
module
'
},
{
pattern
:
"
spec/muc_messages.js
"
,
type
:
'
module
'
},
{
pattern
:
"
spec/me-messages.js
"
,
type
:
'
module
'
},
{
pattern
:
"
spec/mentions.js
"
,
type
:
'
module
'
},
...
...
spec/chatbox.js
View file @
e80afbfe
...
...
@@ -1007,19 +1007,17 @@ describe("Chatboxes", function () {
const
sender_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
,
msg
=
mock
.
createChatMessage
(
_converse
,
sender_jid
,
'
This message will be unread
'
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
const
view
=
await
mock
.
openChatBoxFor
(
_converse
,
sender_jid
)
spyOn
(
view
.
model
,
'
sendMarker
'
).
and
.
callThrough
();
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
view
.
model
.
save
(
'
scrolled
'
,
true
);
await
_converse
.
handleMessageStanza
(
msg
);
await
u
.
waitUntil
(()
=>
view
.
model
.
messages
.
length
);
expect
(
view
.
model
.
get
(
'
num_unread
'
)).
toBe
(
1
);
const
msgid
=
view
.
model
.
messages
.
last
().
get
(
'
id
'
);
expect
(
view
.
model
.
get
(
'
first_unread_id
'
)).
toBe
(
msgid
);
await
u
.
waitUntil
(()
=>
view
.
model
.
sendMarker
.
calls
.
count
()
===
1
);
expect
(
sent_stanzas
[
0
].
nodeTree
.
querySelector
(
'
received
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
length
);
expect
(
sent_stanzas
[
0
].
querySelector
(
'
received
'
)).
toBeDefined
();
done
();
}));
...
...
@@ -1031,15 +1029,14 @@ describe("Chatboxes", function () {
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
const
sender_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
const
msg
=
mock
.
createChatMessage
(
_converse
,
sender_jid
,
'
This message will be read
'
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
mock
.
openChatBoxFor
(
_converse
,
sender_jid
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
const
chatbox
=
_converse
.
chatboxes
.
get
(
sender_jid
);
spyOn
(
chatbox
,
'
sendMarker
'
).
and
.
callThrough
();
await
_converse
.
handleMessageStanza
(
msg
);
expect
(
chatbox
.
get
(
'
num_unread
'
)).
toBe
(
0
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
2
);
expect
(
sent_stanzas
[
1
].
nodeTree
.
querySelector
(
'
displayed
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
filter
(
s
=>
s
.
nodeName
===
'
message
'
).
length
===
2
);
expect
(
sent_stanzas
[
1
].
querySelector
(
'
displayed
'
)).
toBeDefined
();
done
();
}));
...
...
@@ -1053,12 +1050,10 @@ describe("Chatboxes", function () {
const
msgFactory
=
function
()
{
return
mock
.
createChatMessage
(
_converse
,
sender_jid
,
'
This message will be unread
'
);
};
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
mock
.
openChatBoxFor
(
_converse
,
sender_jid
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
const
chatbox
=
_converse
.
chatboxes
.
get
(
sender_jid
);
spyOn
(
chatbox
,
'
sendMarker
'
).
and
.
callThrough
();
_converse
.
windowState
=
'
hidden
'
;
const
msg
=
msgFactory
();
_converse
.
handleMessageStanza
(
msg
);
...
...
@@ -1066,8 +1061,8 @@ describe("Chatboxes", function () {
expect
(
chatbox
.
get
(
'
num_unread
'
)).
toBe
(
1
);
const
msgid
=
chatbox
.
messages
.
last
().
get
(
'
id
'
);
expect
(
chatbox
.
get
(
'
first_unread_id
'
)).
toBe
(
msgid
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
1
);
expect
(
sent_stanzas
[
0
].
nodeTree
.
querySelector
(
'
received
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
filter
(
s
=>
s
.
nodeName
===
'
message
'
).
length
);
expect
(
sent_stanzas
[
0
].
querySelector
(
'
received
'
)).
toBeDefined
();
done
();
}));
...
...
@@ -1079,11 +1074,10 @@ describe("Chatboxes", function () {
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
const
sender_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
const
msgFactory
=
()
=>
mock
.
createChatMessage
(
_converse
,
sender_jid
,
'
This message will be unread
'
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
mock
.
openChatBoxFor
(
_converse
,
sender_jid
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
const
chatbox
=
_converse
.
chatboxes
.
get
(
sender_jid
);
spyOn
(
chatbox
,
'
sendMarker
'
).
and
.
callThrough
();
chatbox
.
save
(
'
scrolled
'
,
true
);
_converse
.
windowState
=
'
hidden
'
;
const
msg
=
msgFactory
();
...
...
@@ -1092,8 +1086,8 @@ describe("Chatboxes", function () {
expect
(
chatbox
.
get
(
'
num_unread
'
)).
toBe
(
1
);
const
msgid
=
chatbox
.
messages
.
last
().
get
(
'
id
'
);
expect
(
chatbox
.
get
(
'
first_unread_id
'
)).
toBe
(
msgid
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
1
);
expect
(
sent_stanzas
[
0
].
nodeTree
.
querySelector
(
'
received
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
filter
(
s
=>
s
.
nodeName
===
'
message
'
).
length
===
1
);
expect
(
sent_stanzas
[
0
].
querySelector
(
'
received
'
)).
toBeDefined
();
done
();
}));
...
...
@@ -1105,11 +1099,10 @@ describe("Chatboxes", function () {
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
const
sender_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
const
msgFactory
=
()
=>
mock
.
createChatMessage
(
_converse
,
sender_jid
,
'
This message will be unread
'
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
mock
.
openChatBoxFor
(
_converse
,
sender_jid
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
const
chatbox
=
_converse
.
chatboxes
.
get
(
sender_jid
);
spyOn
(
chatbox
,
'
sendMarker
'
).
and
.
callThrough
();
_converse
.
windowState
=
'
hidden
'
;
const
msg
=
msgFactory
();
_converse
.
handleMessageStanza
(
msg
);
...
...
@@ -1117,12 +1110,12 @@ describe("Chatboxes", function () {
expect
(
chatbox
.
get
(
'
num_unread
'
)).
toBe
(
1
);
const
msgid
=
chatbox
.
messages
.
last
().
get
(
'
id
'
);
expect
(
chatbox
.
get
(
'
first_unread_id
'
)).
toBe
(
msgid
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
1
);
expect
(
sent_stanzas
[
0
].
nodeTree
.
querySelector
(
'
received
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
filter
(
s
=>
s
.
nodeName
===
'
message
'
).
length
===
1
);
expect
(
sent_stanzas
[
0
].
querySelector
(
'
received
'
)).
toBeDefined
();
_converse
.
saveWindowState
({
'
type
'
:
'
focus
'
});
expect
(
chatbox
.
get
(
'
num_unread
'
)).
toBe
(
0
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
2
);
expect
(
sent_stanzas
[
1
].
nodeTree
.
querySelector
(
'
displayed
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
filter
(
s
=>
s
.
nodeName
===
'
message
'
).
length
===
2
);
expect
(
sent_stanzas
[
1
].
querySelector
(
'
displayed
'
)).
toBeDefined
();
done
();
}));
...
...
@@ -1134,11 +1127,10 @@ describe("Chatboxes", function () {
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
const
sender_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
const
msgFactory
=
()
=>
mock
.
createChatMessage
(
_converse
,
sender_jid
,
'
This message will be unread
'
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
mock
.
openChatBoxFor
(
_converse
,
sender_jid
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
const
chatbox
=
_converse
.
chatboxes
.
get
(
sender_jid
);
spyOn
(
chatbox
,
'
sendMarker
'
).
and
.
callThrough
();
chatbox
.
save
(
'
scrolled
'
,
true
);
_converse
.
windowState
=
'
hidden
'
;
const
msg
=
msgFactory
();
...
...
@@ -1147,13 +1139,12 @@ describe("Chatboxes", function () {
expect
(
chatbox
.
get
(
'
num_unread
'
)).
toBe
(
1
);
const
msgid
=
chatbox
.
messages
.
last
().
get
(
'
id
'
);
expect
(
chatbox
.
get
(
'
first_unread_id
'
)).
toBe
(
msgid
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
1
);
expect
(
sent_stanzas
[
0
].
nodeTree
.
querySelector
(
'
received
'
)).
toBeDefined
();
await
u
.
waitUntil
(()
=>
sent_stanzas
.
filter
(
s
=>
s
.
nodeName
===
'
message
'
).
length
===
1
);
expect
(
sent_stanzas
[
0
].
querySelector
(
'
received
'
)).
toBeDefined
();
_converse
.
saveWindowState
({
'
type
'
:
'
focus
'
});
await
u
.
waitUntil
(()
=>
chatbox
.
get
(
'
num_unread
'
)
===
1
);
expect
(
chatbox
.
get
(
'
first_unread_id
'
)).
toBe
(
msgid
);
await
u
.
waitUntil
(()
=>
chatbox
.
sendMarker
.
calls
.
count
()
===
1
);
expect
(
sent_stanzas
[
0
].
nodeTree
.
querySelector
(
'
received
'
)).
toBeDefined
();
expect
(
sent_stanzas
[
0
].
querySelector
(
'
received
'
)).
toBeDefined
();
done
();
}));
});
...
...
spec/markers.js
0 → 100644
View file @
e80afbfe
/*global mock, converse */
const
Strophe
=
converse
.
env
.
Strophe
;
const
u
=
converse
.
env
.
utils
;
// See: https://xmpp.org/rfcs/rfc3921.html
describe
(
"
A XEP-0333 Chat Marker
"
,
function
()
{
it
(
"
is sent when a markable message is received from a roster contact
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
const
contact_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
await
mock
.
openChatBoxFor
(
_converse
,
contact_jid
);
const
msgid
=
u
.
getUniqueId
();
const
stanza
=
u
.
toStanza
(
`
<message from='
${
contact_jid
}
'
id='
${
msgid
}
'
type="chat"
to='
${
_converse
.
jid
}
'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
?.
nodeTree
??
s
));
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
sent_stanzas
.
length
===
2
);
expect
(
Strophe
.
serialize
(
sent_stanzas
[
0
])).
toBe
(
`<message from="romeo@montague.lit/orchard" `
+
`id="
${
sent_stanzas
[
0
].
getAttribute
(
'
id
'
)}
" `
+
`to="
${
contact_jid
}
" type="chat" xmlns="jabber:client">`
+
`<received id="
${
msgid
}
" xmlns="urn:xmpp:chat-markers:0"/>`
+
`</message>`
);
done
();
}));
it
(
"
is not sent when a markable message is received from someone not on the roster
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{
'
allow_non_roster_messaging
'
:
true
},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
0
);
const
contact_jid
=
'
someone@montague.lit
'
;
const
msgid
=
u
.
getUniqueId
();
const
stanza
=
u
.
toStanza
(
`
<message from='
${
contact_jid
}
'
id='
${
msgid
}
'
type="chat"
to='
${
_converse
.
jid
}
'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
_converse
.
handleMessageStanza
(
stanza
);
const
sent_messages
=
sent_stanzas
.
map
(
s
=>
s
?.
nodeTree
??
s
)
.
filter
(
e
=>
e
.
nodeName
===
'
message
'
);
await
u
.
waitUntil
(()
=>
sent_messages
.
length
===
2
);
expect
(
Strophe
.
serialize
(
sent_messages
[
0
])).
toBe
(
`<message id="
${
sent_messages
[
0
].
getAttribute
(
'
id
'
)}
" to="
${
contact_jid
}
" type="chat" xmlns="jabber:client">`
+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`
+
`<no-store xmlns="urn:xmpp:hints"/>`
+
`<no-permanent-store xmlns="urn:xmpp:hints"/>`
+
`</message>`
);
done
();
}));
it
(
"
is ignored if it's a carbon copy of one that I sent from a different client
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
await
mock
.
waitUntilDiscoConfirmed
(
_converse
,
_converse
.
bare_jid
,
[],
[
Strophe
.
NS
.
SID
]);
const
contact_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
await
mock
.
openChatBoxFor
(
_converse
,
contact_jid
);
const
view
=
_converse
.
api
.
chatviews
.
get
(
contact_jid
);
let
stanza
=
u
.
toStanza
(
`
<message xmlns="jabber:client"
to="
${
_converse
.
bare_jid
}
"
type="chat"
id="2e972ea0-0050-44b7-a830-f6638a2595b3"
from="
${
contact_jid
}
">
<body>😊</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
<origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="
${
_converse
.
bare_jid
}
"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
new
Promise
(
resolve
=>
view
.
model
.
messages
.
once
(
'
rendered
'
,
resolve
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
model
.
messages
.
length
).
toBe
(
1
);
stanza
=
u
.
toStanza
(
`<message xmlns="jabber:client" to="
${
_converse
.
bare_jid
}
" type="chat" from="
${
contact_jid
}
">
<sent xmlns="urn:xmpp:carbons:2">
<forwarded xmlns="urn:xmpp:forward:0">
<message xmlns="jabber:client" to="
${
contact_jid
}
" type="chat" from="
${
_converse
.
bare_jid
}
/other-resource">
<received xmlns="urn:xmpp:chat-markers:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<store xmlns="urn:xmpp:hints"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="F4TC6CvHwzqRbeHb" by="
${
_converse
.
bare_jid
}
"/>
</message>
</forwarded>
</sent>
</message>`
);
spyOn
(
_converse
.
api
,
"
trigger
"
).
and
.
callThrough
();
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
_converse
.
api
.
trigger
.
calls
.
count
(),
500
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
model
.
messages
.
length
).
toBe
(
1
);
done
();
}));
it
(
"
may be returned for a MUC message
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
);
const
muc_jid
=
'
lounge@montague.lit
'
;
await
mock
.
openAndEnterChatRoom
(
_converse
,
muc_jid
,
'
romeo
'
);
const
view
=
_converse
.
api
.
chatviews
.
get
(
muc_jid
);
const
textarea
=
view
.
el
.
querySelector
(
'
textarea.chat-textarea
'
);
textarea
.
value
=
'
But soft, what light through yonder airlock breaks?
'
;
view
.
onKeyDown
({
target
:
textarea
,
preventDefault
:
function
preventDefault
()
{},
keyCode
:
13
// Enter
});
await
new
Promise
(
resolve
=>
view
.
model
.
messages
.
once
(
'
rendered
'
,
resolve
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelector
(
'
.chat-msg .chat-msg__body
'
).
textContent
.
trim
())
.
toBe
(
"
But soft, what light through yonder airlock breaks?
"
);
const
msg_obj
=
view
.
model
.
messages
.
at
(
0
);
let
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<received xmlns="urn:xmpp:chat-markers:0" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
===
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<displayed xmlns="urn:xmpp:chat-markers:0" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<acknowledged xmlns="urn:xmpp:chat-markers:0" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<body>'tis I!</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
===
2
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
done
();
}));
});
spec/messages.js
View file @
e80afbfe
...
...
@@ -1548,122 +1548,3 @@ describe("A Chat Message", function () {
}));
});
});
describe
(
"
A XEP-0333 Chat Marker
"
,
function
()
{
it
(
"
is sent when a markable message is received from a roster contact
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
const
contact_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
await
mock
.
openChatBoxFor
(
_converse
,
contact_jid
);
const
view
=
_converse
.
api
.
chatviews
.
get
(
contact_jid
);
const
msgid
=
u
.
getUniqueId
();
const
stanza
=
u
.
toStanza
(
`
<message from='
${
contact_jid
}
'
id='
${
msgid
}
'
type="chat"
to='
${
_converse
.
jid
}
'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
spyOn
(
view
.
model
,
'
sendMarker
'
).
and
.
callThrough
();
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
view
.
model
.
sendMarker
.
calls
.
count
()
===
2
);
expect
(
Strophe
.
serialize
(
sent_stanzas
[
0
])).
toBe
(
`<message from="romeo@montague.lit/orchard" `
+
`id="
${
sent_stanzas
[
0
].
nodeTree
.
getAttribute
(
'
id
'
)}
" `
+
`to="
${
contact_jid
}
" type="chat" xmlns="jabber:client">`
+
`<received id="
${
msgid
}
" xmlns="urn:xmpp:chat-markers:0"/>`
+
`</message>`
);
done
();
}));
it
(
"
is not sent when a markable message is received from someone not on the roster
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{
'
allow_non_roster_messaging
'
:
true
},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
0
);
const
contact_jid
=
'
someone@montague.lit
'
;
const
msgid
=
u
.
getUniqueId
();
const
stanza
=
u
.
toStanza
(
`
<message from='
${
contact_jid
}
'
id='
${
msgid
}
'
type="chat"
to='
${
_converse
.
jid
}
'>
<body>My lord, dispatch; read o'er these articles.</body>
<markable xmlns='urn:xmpp:chat-markers:0'/>
</message>`
);
const
sent_stanzas
=
[];
spyOn
(
_converse
.
connection
,
'
send
'
).
and
.
callFake
(
s
=>
sent_stanzas
.
push
(
s
));
await
_converse
.
handleMessageStanza
(
stanza
);
const
sent_messages
=
sent_stanzas
.
map
(
s
=>
_
.
isElement
(
s
)
?
s
:
s
.
nodeTree
)
.
filter
(
e
=>
e
.
nodeName
===
'
message
'
);
await
u
.
waitUntil
(()
=>
sent_messages
.
length
===
2
);
expect
(
Strophe
.
serialize
(
sent_messages
[
0
])).
toBe
(
`<message id="
${
sent_messages
[
0
].
getAttribute
(
'
id
'
)}
" to="
${
contact_jid
}
" type="chat" xmlns="jabber:client">`
+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`
+
`<no-store xmlns="urn:xmpp:hints"/>`
+
`<no-permanent-store xmlns="urn:xmpp:hints"/>`
+
`</message>`
);
done
();
}));
it
(
"
is ignored if it's a carbon copy of one that I sent from a different client
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
,
1
);
await
mock
.
waitUntilDiscoConfirmed
(
_converse
,
_converse
.
bare_jid
,
[],
[
Strophe
.
NS
.
SID
]);
const
contact_jid
=
mock
.
cur_names
[
0
].
replace
(
/ /g
,
'
.
'
).
toLowerCase
()
+
'
@montague.lit
'
;
await
mock
.
openChatBoxFor
(
_converse
,
contact_jid
);
const
view
=
_converse
.
api
.
chatviews
.
get
(
contact_jid
);
let
stanza
=
u
.
toStanza
(
`
<message xmlns="jabber:client"
to="
${
_converse
.
bare_jid
}
"
type="chat"
id="2e972ea0-0050-44b7-a830-f6638a2595b3"
from="
${
contact_jid
}
">
<body>😊</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
<origin-id xmlns="urn:xmpp:sid:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="IxVDLJ0RYbWcWvqC" by="
${
_converse
.
bare_jid
}
"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
new
Promise
(
resolve
=>
view
.
model
.
messages
.
once
(
'
rendered
'
,
resolve
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
model
.
messages
.
length
).
toBe
(
1
);
stanza
=
u
.
toStanza
(
`<message xmlns="jabber:client" to="
${
_converse
.
bare_jid
}
" type="chat" from="
${
contact_jid
}
">
<sent xmlns="urn:xmpp:carbons:2">
<forwarded xmlns="urn:xmpp:forward:0">
<message xmlns="jabber:client" to="
${
contact_jid
}
" type="chat" from="
${
_converse
.
bare_jid
}
/other-resource">
<received xmlns="urn:xmpp:chat-markers:0" id="2e972ea0-0050-44b7-a830-f6638a2595b3"/>
<store xmlns="urn:xmpp:hints"/>
<stanza-id xmlns="urn:xmpp:sid:0" id="F4TC6CvHwzqRbeHb" by="
${
_converse
.
bare_jid
}
"/>
</message>
</forwarded>
</sent>
</message>`
);
spyOn
(
_converse
.
api
,
"
trigger
"
).
and
.
callThrough
();
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
_converse
.
api
.
trigger
.
calls
.
count
(),
500
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
model
.
messages
.
length
).
toBe
(
1
);
done
();
}));
});
spec/muc_messages.js
View file @
e80afbfe
/*global mock, converse */
const
{
Promise
,
Strophe
,
$msg
,
$pres
,
sizzle
,
stanza_utils
}
=
converse
.
env
;
const
{
Promise
,
Strophe
,
$msg
,
$pres
,
sizzle
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
const
original_timeout
=
jasmine
.
DEFAULT_TIMEOUT_INTERVAL
;
...
...
@@ -620,86 +620,29 @@ describe("A Groupchat Message", function () {
await
new
Promise
(
resolve
=>
view
.
model
.
messages
.
once
(
'
rendered
'
,
resolve
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
const
msg_obj
=
view
.
model
.
messages
.
at
(
0
);
const
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<received xmlns="urn:xmpp:receipts" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
<origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
</message>`
);
spyOn
(
stanza_utils
,
"
parseMUCMessage
"
).
and
.
callThrough
();
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
stanza_utils
.
parseMUCMessage
.
calls
.
count
()
===
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
done
();
}));
it
(
"
can cause a chat marker to be returned
"
,
mock
.
initConverse
(
[
'
rosterGroupsFetched
'
],
{},
async
function
(
done
,
_converse
)
{
await
mock
.
waitForRoster
(
_converse
,
'
current
'
);
const
muc_jid
=
'
lounge@montague.lit
'
;
await
mock
.
openAndEnterChatRoom
(
_converse
,
muc_jid
,
'
romeo
'
);
const
view
=
_converse
.
api
.
chatviews
.
get
(
muc_jid
);
const
textarea
=
view
.
el
.
querySelector
(
'
textarea.chat-textarea
'
);
textarea
.
value
=
'
But soft, what light through yonder airlock breaks?
'
;
view
.
onKeyDown
({
target
:
textarea
,
preventDefault
:
function
preventDefault
()
{},
keyCode
:
13
// Enter
});
await
new
Promise
(
resolve
=>
view
.
model
.
messages
.
once
(
'
rendered
'
,
resolve
));
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelector
(
'
.chat-msg .chat-msg__body
'
).
textContent
.
trim
())
.
toBe
(
"
But soft, what light through yonder airlock breaks?
"
);
const
msg_obj
=
view
.
model
.
messages
.
at
(
0
);
let
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<received xmlns="urn:xmpp:chat-markers:0" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
</message>`
);
const
stanza_utils
=
converse
.
env
.
stanza_utils
;
spyOn
(
stanza_utils
,
"
getChatMarker
"
).
and
.
callThrough
();
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
stanza_utils
.
getChatMarker
.
calls
.
count
()
===
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<displayed xmlns="urn:xmpp:chat-markers:0" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
<message xmlns="jabber:client"
from="
${
msg_obj
.
get
(
'
from
'
)}
"
to="
${
_converse
.
connection
.
jid
}
"
type="groupchat">
<body>
${
msg_obj
.
get
(
'
message
'
)}
</body>
<stanza-id xmlns="urn:xmpp:sid:0"
id="5f3dbc5e-e1d3-4077-a492-693f3769c7ad"
by="lounge@montague.lit"/>
<origin-id xmlns="urn:xmpp:sid:0" id="
${
msg_obj
.
get
(
'
origin_id
'
)}
"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
stanza_utils
.
getChatMarker
.
calls
.
count
()
===
2
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
await
view
.
model
.
handleMessageStanza
(
stanza
);
await
u
.
waitUntil
(()
=>
view
.
model
.
messages
.
last
().
get
(
'
received
'
));
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<acknowledged xmlns="urn:xmpp:chat-markers:0" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
<received xmlns="urn:xmpp:receipts" id="
${
msg_obj
.
get
(
'
msgid
'
)}
"/>
<origin-id xmlns="urn:xmpp:sid:0" id="CE08D448-5ED8-4B6A-BB5B-07ED9DFE4FF0"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
stanza_utils
.
getChatMarker
.
calls
.
count
()
===
3
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
).
toBe
(
1
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
stanza
=
u
.
toStanza
(
`
<message xml:lang="en" to="romeo@montague.lit/orchard"
from="lounge@montague.lit/some1" type="groupchat" xmlns="jabber:client">
<body>'tis I!</body>
<markable xmlns="urn:xmpp:chat-markers:0"/>
</message>`
);
_converse
.
connection
.
_dataRecv
(
mock
.
createRequest
(
stanza
));
await
u
.
waitUntil
(()
=>
stanza_utils
.
getChatMarker
.
calls
.
count
()
===
4
);
await
u
.
waitUntil
(()
=>
view
.
el
.
querySelectorAll
(
'
.chat-msg
'
).
length
===
2
);
expect
(
view
.
el
.
querySelectorAll
(
'
.chat-msg__receipt
'
).
length
).
toBe
(
0
);
done
();
}));
});
src/headless/core.js
View file @
e80afbfe
...
...
@@ -12,7 +12,6 @@ import pluggable from 'pluggable.js/src/pluggable';
import
syncDriver
from
'
localforage-webextensionstorage-driver/sync
'
;
import
localDriver
from
'
localforage-webextensionstorage-driver/local
'
;
import
sizzle
from
'
sizzle
'
;
import
stanza_utils
from
"
@converse/headless/utils/stanza
"
;
import
u
from
'
@converse/headless/utils/core
'
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
Connection
,
MockConnection
}
from
'
@converse/headless/shared/connection.js
'
;
...
...
@@ -1654,7 +1653,6 @@ Object.assign(converse, {
log
,
sizzle
,
sprintf
,
stanza_utils
,
u
,
}
});
...
...
src/headless/plugins/adhoc.js
View file @
e80afbfe
import
{
converse
}
from
"
../core.js
"
;
import
log
from
"
@converse/headless/log
"
;
import
sizzle
from
'
sizzle
'
;
import
st
from
"
../utils/stanza
"
;
import
{
getAttributes
}
from
'
@converse/headless/shared/parsers
'
;
const
{
Strophe
}
=
converse
.
env
;
let
_converse
,
api
;
...
...
@@ -11,7 +11,7 @@ Strophe.addNamespace('ADHOC', 'http://jabber.org/protocol/commands');
function
parseForCommands
(
stanza
)
{
const
items
=
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
DISCO_ITEMS
}
"][node="
${
Strophe
.
NS
.
ADHOC
}
"] item`
,
stanza
);
return
items
.
map
(
st
.
getAttributes
)
return
items
.
map
(
getAttributes
)
}
...
...
src/headless/plugins/chat/index.js
View file @
e80afbfe
...
...
@@ -8,9 +8,10 @@ import MessageMixin from './message.js';
import
ModelWithContact
from
'
./model-with-contact.js
'
;
import
chat_api
from
'
./api.js
'
;
import
log
from
'
../../log.js
'
;
import
st
from
'
../../utils/stanza
'
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
_converse
,
api
,
converse
}
from
'
../../core.js
'
;
import
{
isServerMessage
,
}
from
'
@converse/headless/shared/parsers
'
;
import
{
parseMessage
}
from
'
./parsers.js
'
;
const
{
Strophe
,
sizzle
,
utils
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
...
...
@@ -74,12 +75,12 @@ converse.plugins.add('converse-chat', {
* @param { MessageAttributes } attrs - The message attributes
*/
_converse
.
handleMessageStanza
=
async
function
(
stanza
)
{
if
(
st
.
isServerMessage
(
stanza
))
{
if
(
isServerMessage
(
stanza
))
{
// Prosody sends headline messages with type `chat`, so we need to filter them out here.
const
from
=
stanza
.
getAttribute
(
'
from
'
);
return
log
.
info
(
`handleMessageStanza: Ignoring incoming server message from JID:
${
from
}
`
);
}
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
const
attrs
=
await
parseMessage
(
stanza
,
_converse
);
if
(
u
.
isErrorObject
(
attrs
))
{
attrs
.
stanza
&&
log
.
error
(
attrs
.
stanza
);
return
log
.
error
(
attrs
.
message
);
...
...
src/headless/plugins/chat/model.js
View file @
e80afbfe
import
ModelWithContact
from
'
./model-with-contact.js
'
;
import
filesize
from
"
filesize
"
;
import
log
from
"
../../log.js
"
;
import
st
from
"
../../utils/stanza
"
;
import
log
from
'
@converse/headless/log
'
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
_converse
,
api
,
converse
}
from
"
../../core.js
"
;
import
{
find
,
isMatch
,
isObject
,
pick
}
from
"
lodash-es
"
;
import
{
parseMessage
}
from
'
./parsers.js
'
;
import
{
sendMarker
}
from
'
@converse/headless/shared/actions
'
;
const
{
Strophe
,
$msg
}
=
converse
.
env
;
...
...
@@ -130,7 +131,7 @@ const ChatBox = ModelWithContact.extend({
async
handleErrorMessageStanza
(
stanza
)
{
const
{
__
}
=
_converse
;
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
const
attrs
=
await
parseMessage
(
stanza
,
_converse
);
if
(
!
await
this
.
shouldShowErrorMessage
(
attrs
))
{
return
;
}
...
...
@@ -392,7 +393,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#findDanglingRetraction
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMessage}
* message, as returned by {@link parseMessage}
* @returns { _converse.Message }
*/
findDanglingRetraction
(
attrs
)
{
...
...
@@ -419,7 +420,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#handleRetraction
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMessage}
* message, as returned by {@link parseMessage}
* @returns { Boolean } Returns `true` or `false` depending on
* whether a message was retracted or not.
*/
...
...
@@ -459,7 +460,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#handleCorrection
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMessage}
* message, as returned by {@link parseMessage}
* @returns { _converse.Message|undefined } Returns the corrected
* message or `undefined` if not applicable.
*/
...
...
@@ -497,7 +498,7 @@ const ChatBox = ModelWithContact.extend({
* @private
* @method _converse.ChatBox#getDuplicateMessage
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMessage}
* message, as returned by {@link parseMessage}
* @returns {Promise<_converse.Message>}
*/
getDuplicateMessage
(
attrs
)
{
...
...
@@ -604,27 +605,10 @@ const ChatBox = ModelWithContact.extend({
if
(
!
msg
)
return
;
if
(
msg
?.
get
(
'
is_markable
'
)
||
force
)
{
const
from_jid
=
Strophe
.
getBareJidFromJid
(
msg
.
get
(
'
from
'
));
this
.
sendMarker
(
from_jid
,
msg
.
get
(
'
msgid
'
),
type
,
msg
.
get
(
'
type
'
));
sendMarker
(
from_jid
,
msg
.
get
(
'
msgid
'
),
type
,
msg
.
get
(
'
type
'
));
}
},
/**
* Send out a XEP-0333 chat marker
* @param { String } to_jid
* @param { String } id - The id of the message being marked
* @param { String } type - The marker type
* @param { String } msg_type
*/
sendMarker
(
to_jid
,
id
,
type
,
msg_type
)
{
const
stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
to_jid
,
'
type
'
:
msg_type
?
msg_type
:
'
chat
'
}).
c
(
type
,
{
'
xmlns
'
:
Strophe
.
NS
.
MARKERS
,
'
id
'
:
id
});
api
.
send
(
stanza
);
},
handleChatMarker
(
attrs
)
{
const
to_bare_jid
=
Strophe
.
getBareJidFromJid
(
attrs
.
to
);
if
(
to_bare_jid
!==
_converse
.
bare_jid
)
{
...
...
@@ -632,7 +616,7 @@ const ChatBox = ModelWithContact.extend({
}
if
(
attrs
.
is_markable
)
{
if
(
this
.
contact
&&
!
attrs
.
is_archived
&&
!
attrs
.
is_carbon
)
{
this
.
sendMarker
(
attrs
.
from
,
attrs
.
msgid
,
'
received
'
);
sendMarker
(
attrs
.
from
,
attrs
.
msgid
,
'
received
'
);
}
return
false
;
}
else
if
(
attrs
.
marker_id
)
{
...
...
src/headless/plugins/chat/parsers.js
0 → 100644
View file @
e80afbfe
import
dayjs
from
'
dayjs
'
;
import
log
from
'
@converse/headless/log
'
;
import
u
from
'
@converse/headless/utils/core
'
;
import
{
api
,
converse
}
from
'
@converse/headless/core
'
;
import
{
rejectMessage
}
from
'
@converse/headless/shared/actions
'
;
import
{
StanzaParseError
,
getChatMarker
,
getChatState
,
getCorrectionAttributes
,
getEncryptionAttributes
,
getErrorAttributes
,
getOutOfBandAttributes
,
getReceiptId
,
getReferences
,
getRetractionAttributes
,
getSpoilerAttributes
,
getStanzaIDs
,
isArchived
,
isCarbon
,
isHeadline
,
isServerMessage
,
isValidReceiptRequest
,
rejectUnencapsulatedForward
,
}
from
'
@converse/headless/shared/parsers
'
;
const
{
Strophe
,
sizzle
}
=
converse
.
env
;
/**
* Parses a passed in message stanza and returns an object of attributes.
* @method st#parseMessage
* @param { XMLElement } stanza - The message stanza
* @param { _converse } _converse
* @returns { (MessageAttributes|Error) }
*/
export
async
function
parseMessage
(
stanza
,
_converse
)
{
const
err
=
rejectUnencapsulatedForward
(
stanza
);
if
(
err
)
{
return
err
;
}
let
to_jid
=
stanza
.
getAttribute
(
'
to
'
);
const
to_resource
=
Strophe
.
getResourceFromJid
(
to_jid
);
if
(
api
.
settings
.
get
(
'
filter_by_resource
'
)
&&
to_resource
&&
to_resource
!==
_converse
.
resource
)
{
return
new
StanzaParseError
(
`Ignoring incoming message intended for a different resource:
${
to_jid
}
`
,
stanza
);
}
const
original_stanza
=
stanza
;
let
from_jid
=
stanza
.
getAttribute
(
'
from
'
)
||
_converse
.
bare_jid
;
if
(
isCarbon
(
stanza
))
{
if
(
from_jid
===
_converse
.
bare_jid
)
{
const
selector
=
`[xmlns="
${
Strophe
.
NS
.
CARBONS
}
"] > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"] > message`
;
stanza
=
sizzle
(
selector
,
stanza
).
pop
();
to_jid
=
stanza
.
getAttribute
(
'
to
'
);
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
}
else
{
// Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
rejectMessage
(
stanza
,
'
Rejecting carbon from invalid JID
'
);
return
new
StanzaParseError
(
`Rejecting carbon from invalid JID
${
to_jid
}
`
,
stanza
);
}
}
const
is_archived
=
isArchived
(
stanza
);
if
(
is_archived
)
{
if
(
from_jid
===
_converse
.
bare_jid
)
{
const
selector
=
`[xmlns="
${
Strophe
.
NS
.
MAM
}
"] > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"] > message`
;
stanza
=
sizzle
(
selector
,
stanza
).
pop
();
to_jid
=
stanza
.
getAttribute
(
'
to
'
);
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
}
else
{
return
new
StanzaParseError
(
`Invalid Stanza: alleged MAM message from
${
stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
}
const
from_bare_jid
=
Strophe
.
getBareJidFromJid
(
from_jid
);
const
is_me
=
from_bare_jid
===
_converse
.
bare_jid
;
if
(
is_me
&&
to_jid
===
null
)
{
return
new
StanzaParseError
(
`Don't know how to handle message stanza without 'to' attribute.
${
stanza
.
outerHTML
}
`
,
stanza
);
}
const
is_headline
=
isHeadline
(
stanza
);
const
is_server_message
=
isServerMessage
(
stanza
);
let
contact
,
contact_jid
;
if
(
!
is_headline
&&
!
is_server_message
)
{
contact_jid
=
is_me
?
Strophe
.
getBareJidFromJid
(
to_jid
)
:
from_bare_jid
;
contact
=
await
api
.
contacts
.
get
(
contact_jid
);
if
(
contact
===
undefined
&&
!
api
.
settings
.
get
(
'
allow_non_roster_messaging
'
))
{
log
.
error
(
stanza
);
return
new
StanzaParseError
(
`Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`
,
stanza
);
}
}
/**
* @typedef { Object } MessageAttributes
* The object which {@link parseMessage} returns
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
* @property { Boolean } editable - Is this message editable via XEP-0308?
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
* @property { Boolean } is_error - Whether an error was received for this message
* @property { Boolean } is_headline - Is this a "headline" message?
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Object } encrypted - XEP-0384 encryption payload attributes
* @property { String } body - The contents of the <body> tag of the message stanza
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
* @property { String } contact_jid - The JID of the other person or entity
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
* @property { String } error_condition - The defined error condition
* @property { String } error_text - The error text received from the server
* @property { String } error_type - The type of error received from the server
* @property { String } from - The sender JID
* @property { String } fullname - The full name of the sender
* @property { String } marker - The XEP-0333 Chat Marker value
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
* @property { String } msgid - The root `id` attribute of the stanza
* @property { String } nick - The roster nickname of the sender
* @property { String } oob_desc - The description of the XEP-0066 out of band data
* @property { String } oob_url - The URL of the XEP-0066 out of band data
* @property { String } origin_id - The XEP-0359 Origin ID
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
* @property { String } received - An ISO8601 string recording the time that the message was received
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
* @property { String } spoiler_hint The XEP-0382 spoiler hint
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
* @property { String } subject - The <subject> element value
* @property { String } thread - The <thread> element value
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
* @property { String } to - The recipient JID
* @property { String } type - The type of message
*/
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
marker
=
getChatMarker
(
stanza
);
const
now
=
new
Date
().
toISOString
();
let
attrs
=
Object
.
assign
(
{
contact_jid
,
is_archived
,
is_headline
,
is_server_message
,
'
body
'
:
stanza
.
querySelector
(
'
body
'
)?.
textContent
?.
trim
(),
'
chat_state
'
:
getChatState
(
stanza
),
'
from
'
:
Strophe
.
getBareJidFromJid
(
stanza
.
getAttribute
(
'
from
'
)),
'
is_carbon
'
:
isCarbon
(
original_stanza
),
'
is_delayed
'
:
!!
delay
,
'
is_markable
'
:
!!
sizzle
(
`markable[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"]`
,
stanza
).
length
,
'
is_marker
'
:
!!
marker
,
'
is_unstyled
'
:
!!
sizzle
(
`unstyled[xmlns="
${
Strophe
.
NS
.
STYLING
}
"]`
,
stanza
).
length
,
'
marker_id
'
:
marker
&&
marker
.
getAttribute
(
'
id
'
),
'
msgid
'
:
stanza
.
getAttribute
(
'
id
'
)
||
original_stanza
.
getAttribute
(
'
id
'
),
'
nick
'
:
contact
?.
attributes
?.
nickname
,
'
receipt_id
'
:
getReceiptId
(
stanza
),
'
received
'
:
new
Date
().
toISOString
(),
'
references
'
:
getReferences
(
stanza
),
'
sender
'
:
is_me
?
'
me
'
:
'
them
'
,
'
subject
'
:
stanza
.
querySelector
(
'
subject
'
)?.
textContent
,
'
thread
'
:
stanza
.
querySelector
(
'
thread
'
)?.
textContent
,
'
time
'
:
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
now
,
'
to
'
:
stanza
.
getAttribute
(
'
to
'
),
'
type
'
:
stanza
.
getAttribute
(
'
type
'
)
},
getErrorAttributes
(
stanza
),
getOutOfBandAttributes
(
stanza
),
getSpoilerAttributes
(
stanza
),
getCorrectionAttributes
(
stanza
,
original_stanza
),
getStanzaIDs
(
stanza
,
original_stanza
),
getRetractionAttributes
(
stanza
,
original_stanza
),
getEncryptionAttributes
(
stanza
,
_converse
)
);
if
(
attrs
.
is_archived
)
{
const
from
=
original_stanza
.
getAttribute
(
'
from
'
);
if
(
from
&&
from
!==
_converse
.
bare_jid
)
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM message from
${
from
}
`
,
stanza
);
}
}
await
api
.
emojis
.
initialize
();
attrs
=
Object
.
assign
(
{
'
message
'
:
attrs
.
body
||
attrs
.
error
,
// TODO: Remove and use body and error attributes instead
'
is_only_emojis
'
:
attrs
.
body
?
u
.
isOnlyEmojis
(
attrs
.
body
)
:
false
,
'
is_valid_receipt_request
'
:
isValidReceiptRequest
(
stanza
,
attrs
)
},
attrs
);
// We prefer to use one of the XEP-0359 unique and stable stanza IDs
// as the Model id, to avoid duplicates.
attrs
[
'
id
'
]
=
attrs
[
'
origin_id
'
]
||
attrs
[
`stanza_id
${
attrs
.
from
}
`
]
||
u
.
getUniqueId
();
/**
* *Hook* which allows plugins to add additional parsing
* @event _converse#parseMessage
*/
return
api
.
hook
(
'
parseMessage
'
,
stanza
,
attrs
);
}
src/headless/plugins/headlines.js
View file @
e80afbfe
...
...
@@ -4,7 +4,8 @@
* @description XEP-0045 Multi-User Chat Views
*/
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
st
from
"
../utils/stanza
"
;
import
{
isHeadline
,
isServerMessage
}
from
'
@converse/headless/shared/parsers
'
;
import
{
parseMessage
}
from
'
@converse/headless/plugins/chat/parsers
'
;
converse
.
plugins
.
add
(
'
converse-headlines
'
,
{
...
...
@@ -79,7 +80,7 @@ converse.plugins.add('converse-headlines', {
async
function
onHeadlineMessage
(
stanza
)
{
// Handler method for all incoming messages of type "headline".
if
(
st
.
isHeadline
(
stanza
)
||
st
.
isServerMessage
(
stanza
))
{
if
(
isHeadline
(
stanza
)
||
isServerMessage
(
stanza
))
{
const
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
if
(
from_jid
.
includes
(
'
@
'
)
&&
!
_converse
.
roster
.
get
(
from_jid
)
&&
...
...
@@ -96,7 +97,7 @@ converse.plugins.add('converse-headlines', {
'
type
'
:
_converse
.
HEADLINES_TYPE
,
'
from
'
:
from_jid
});
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
const
attrs
=
await
parseMessage
(
stanza
,
_converse
);
await
chatbox
.
createMessage
(
attrs
);
api
.
trigger
(
'
message
'
,
{
chatbox
,
stanza
,
attrs
});
}
...
...
src/headless/plugins/mam.js
View file @
e80afbfe
...
...
@@ -5,11 +5,12 @@
* @license Mozilla Public License (MPLv2)
*/
import
"
./disco
"
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
log
from
"
../log.js
"
;
import
log
from
'
@converse/headless/log
'
;
import
sizzle
from
"
sizzle
"
;
import
st
from
"
../utils/stanza
"
;
import
{
parseMessage
}
from
'
@converse/headless/plugins/chat/parsers
'
;
import
{
parseMUCMessage
}
from
'
@converse/headless/plugins/muc/parsers
'
;
import
{
RSM
}
from
'
@converse/headless/shared/rsm
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
const
{
Strophe
,
$iq
,
dayjs
}
=
converse
.
env
;
const
{
NS
}
=
Strophe
;
...
...
@@ -49,7 +50,7 @@ const MAMEnabledChat = {
await
api
.
emojis
.
initialize
();
const
is_muc
=
this
.
get
(
'
type
'
)
===
_converse
.
CHATROOMS_TYPE
;
result
.
messages
=
result
.
messages
.
map
(
s
=>
(
is_muc
?
st
.
parseMUCMessage
(
s
,
this
,
_converse
)
:
st
.
parseMessage
(
s
,
_converse
))
s
=>
(
is_muc
?
parseMUCMessage
(
s
,
this
,
_converse
)
:
parseMessage
(
s
,
_converse
))
);
/**
...
...
src/headless/plugins/muc/index.js
View file @
e80afbfe
...
...
@@ -13,7 +13,7 @@ import ChatRoomOccupant from './occupant.js';
import
ChatRoomOccupants
from
'
./occupants.js
'
;
import
log
from
'
../../log
'
;
import
muc_api
from
'
./api.js
'
;
import
muc_utils
from
'
.
./../utils/muc
'
;
import
muc_utils
from
'
.
/utils.js
'
;
import
u
from
'
../../utils/form
'
;
import
{
Collection
}
from
'
@converse/skeletor/src/collection
'
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
...
...
src/headless/plugins/muc/muc.js
View file @
e80afbfe
import
log
from
'
../../log
'
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
muc_utils
from
'
../../utils/muc
'
;
import
muc_utils
from
'
./utils.js
'
;
import
p
from
'
../../utils/parse-helpers
'
;
import
sizzle
from
'
sizzle
'
;
import
st
from
'
../../utils/stanza
'
;
import
u
from
'
../../utils/form
'
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
Strophe
,
$build
,
$iq
,
$msg
,
$pres
}
from
'
strophe.js/src/strophe
'
;
import
{
_converse
,
api
,
converse
}
from
'
../../core.js
'
;
import
{
debounce
,
intersection
,
invoke
,
isElement
,
pick
,
zipObject
}
from
'
lodash-es
'
;
import
{
isArchived
}
from
'
@converse/headless/shared/parsers
'
;
import
{
parseMemberListIQ
,
parseMUCMessage
,
parseMUCPresence
}
from
'
./parsers.js
'
;
import
{
sendMarker
}
from
'
@converse/headless/shared/actions
'
;
const
ACTION_INFO_CODES
=
[
'
301
'
,
'
303
'
,
'
333
'
,
'
307
'
,
'
321
'
,
'
322
'
];
...
...
@@ -194,7 +196,7 @@ const ChatRoomMixin = {
return
;
}
const
from_jid
=
Strophe
.
getBareJidFromJid
(
msg
.
get
(
'
from
'
));
this
.
sendMarker
(
from_jid
,
id
,
type
,
msg
.
get
(
'
type
'
));
sendMarker
(
from_jid
,
id
,
type
,
msg
.
get
(
'
type
'
));
}
},
...
...
@@ -365,7 +367,7 @@ const ChatRoomMixin = {
async
handleErrorMessageStanza
(
stanza
)
{
const
{
__
}
=
_converse
;
const
attrs
=
await
st
.
parseMUCMessage
(
stanza
,
this
,
_converse
);
const
attrs
=
await
parseMUCMessage
(
stanza
,
this
,
_converse
);
if
(
!
(
await
this
.
shouldShowErrorMessage
(
attrs
)))
{
return
;
}
...
...
@@ -414,7 +416,7 @@ const ChatRoomMixin = {
* @param { XMLElement } stanza
*/
async
handleMessageStanza
(
stanza
)
{
if
(
st
.
isArchived
(
stanza
))
{
if
(
isArchived
(
stanza
))
{
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
...
...
@@ -431,7 +433,7 @@ const ChatRoomMixin = {
* @property { MUCMessageAttributes } attrs
* @property { ChatRoom } chatbox
*/
const
attrs
=
await
st
.
parseMUCMessage
(
stanza
,
this
,
_converse
);
const
attrs
=
await
parseMUCMessage
(
stanza
,
this
,
_converse
);
const
data
=
{
stanza
,
attrs
,
'
chatbox
'
:
this
};
/**
* Triggered when a groupchat message stanza has been received and parsed.
...
...
@@ -1305,8 +1307,7 @@ const ChatRoomMixin = {
log
.
warn
(
result
);
return
err
;
}
return
muc_utils
.
parseMemberListIQ
(
result
)
return
parseMemberListIQ
(
result
)
.
filter
(
p
=>
p
)
.
sort
((
a
,
b
)
=>
(
a
.
nick
<
b
.
nick
?
-
1
:
a
.
nick
>
b
.
nick
?
1
:
0
));
},
...
...
@@ -1438,7 +1439,7 @@ const ChatRoomMixin = {
* @param { XMLElement } pres - The presence stanza
*/
updateOccupantsOnPresence
(
pres
)
{
const
data
=
st
.
parseMUCPresence
(
pres
);
const
data
=
parseMUCPresence
(
pres
);
if
(
data
.
type
===
'
error
'
||
(
!
data
.
jid
&&
!
data
.
nick
))
{
return
true
;
}
...
...
@@ -1538,7 +1539,7 @@ const ChatRoomMixin = {
* @private
* @method _converse.ChatRoom#handleSubjectChange
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMUCMessage}
* message, as returned by {@link parseMUCMessage}
*/
async
handleSubjectChange
(
attrs
)
{
const
__
=
_converse
.
__
;
...
...
@@ -1692,7 +1693,7 @@ const ChatRoomMixin = {
* @private
* @method _converse.ChatRoom#findDanglingModeration
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMUCMessage}
* message, as returned by {@link parseMUCMessage}
* @returns { _converse.ChatRoomMessage }
*/
findDanglingModeration
(
attrs
)
{
...
...
@@ -1723,7 +1724,7 @@ const ChatRoomMixin = {
* @private
* @method _converse.ChatRoom#handleModeration
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link
st.
parseMUCMessage}
* message, as returned by {@link parseMUCMessage}
* @returns { Boolean } Returns `true` or `false` depending on
* whether a message was moderated or not.
*/
...
...
src/headless/plugins/muc/parsers.js
0 → 100644
View file @
e80afbfe
import
dayjs
from
'
dayjs
'
;
import
{
StanzaParseError
,
getChatMarker
,
getChatState
,
getCorrectionAttributes
,
getEncryptionAttributes
,
getErrorAttributes
,
getOutOfBandAttributes
,
getReceiptId
,
getReferences
,
getRetractionAttributes
,
getSpoilerAttributes
,
getStanzaIDs
,
isArchived
,
isCarbon
,
isHeadline
,
isValidReceiptRequest
,
rejectUnencapsulatedForward
,
}
from
'
@converse/headless/shared/parsers
'
;
import
{
api
,
converse
}
from
'
@converse/headless/core
'
;
const
{
Strophe
,
sizzle
,
u
}
=
converse
.
env
;
const
{
NS
}
=
Strophe
;
/**
* @private
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @returns { Object }
*/
function
getModerationAttributes
(
stanza
)
{
const
fastening
=
sizzle
(
`apply-to[xmlns="
${
Strophe
.
NS
.
FASTEN
}
"]`
,
stanza
).
pop
();
if
(
fastening
)
{
const
applies_to_id
=
fastening
.
getAttribute
(
'
id
'
);
const
moderated
=
sizzle
(
`moderated[xmlns="
${
Strophe
.
NS
.
MODERATE
}
"]`
,
fastening
).
pop
();
if
(
moderated
)
{
const
retracted
=
sizzle
(
`retract[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
moderated
).
pop
();
if
(
retracted
)
{
return
{
'
editable
'
:
false
,
'
moderated
'
:
'
retracted
'
,
'
moderated_by
'
:
moderated
.
getAttribute
(
'
by
'
),
'
moderated_id
'
:
applies_to_id
,
'
moderation_reason
'
:
moderated
.
querySelector
(
'
reason
'
)?.
textContent
};
}
}
}
else
{
const
tombstone
=
sizzle
(
`> moderated[xmlns="
${
Strophe
.
NS
.
MODERATE
}
"]`
,
stanza
).
pop
();
if
(
tombstone
)
{
const
retracted
=
sizzle
(
`retracted[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
tombstone
).
pop
();
if
(
retracted
)
{
return
{
'
editable
'
:
false
,
'
is_tombstone
'
:
true
,
'
moderated_by
'
:
tombstone
.
getAttribute
(
'
by
'
),
'
retracted
'
:
tombstone
.
getAttribute
(
'
stamp
'
),
'
moderation_reason
'
:
tombstone
.
querySelector
(
'
reason
'
)?.
textContent
};
}
}
}
return
{};
}
/**
* Parses a passed in message stanza and returns an object of attributes.
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @param { _converse.ChatRoom } chatbox
* @param { _converse } _converse
* @returns { Promise<MUCMessageAttributes|Error> }
*/
export
async
function
parseMUCMessage
(
stanza
,
chatbox
,
_converse
)
{
const
err
=
rejectUnencapsulatedForward
(
stanza
);
if
(
err
)
{
return
err
;
}
const
selector
=
`[xmlns="
${
NS
.
MAM
}
"] > forwarded[xmlns="
${
NS
.
FORWARD
}
"] > message`
;
const
original_stanza
=
stanza
;
stanza
=
sizzle
(
selector
,
stanza
).
pop
()
||
stanza
;
if
(
sizzle
(
`message > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"]`
,
stanza
).
length
)
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM groupchat message from
${
stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
from
=
stanza
.
getAttribute
(
'
from
'
);
const
nick
=
Strophe
.
unescapeNode
(
Strophe
.
getResourceFromJid
(
from
));
const
marker
=
getChatMarker
(
stanza
);
const
now
=
new
Date
().
toISOString
();
/**
* @typedef { Object } MUCMessageAttributes
* The object which {@link parseMUCMessage} returns
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
* @property { Boolean } editable - Is this message editable via XEP-0308?
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
* @property { Boolean } is_error - Whether an error was received for this message
* @property { Boolean } is_headline - Is this a "headline" message?
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Object } encrypted - XEP-0384 encryption payload attributes
* @property { String } body - The contents of the <body> tag of the message stanza
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
* @property { String } error_condition - The defined error condition
* @property { String } error_text - The error text received from the server
* @property { String } error_type - The type of error received from the server
* @property { String } from - The sender JID (${muc_jid}/${nick})
* @property { String } from_muc - The JID of the MUC from which this message was sent
* @property { String } from_real_jid - The real JID of the sender, if available
* @property { String } fullname - The full name of the sender
* @property { String } marker - The XEP-0333 Chat Marker value
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
* @property { String } moderated - The type of XEP-0425 moderation (if any) that was applied
* @property { String } moderated_by - The JID of the user that moderated this message
* @property { String } moderated_id - The XEP-0359 Stanza ID of the message that this one moderates
* @property { String } moderation_reason - The reason provided why this message moderates another
* @property { String } msgid - The root `id` attribute of the stanza
* @property { String } nick - The MUC nickname of the sender
* @property { String } oob_desc - The description of the XEP-0066 out of band data
* @property { String } oob_url - The URL of the XEP-0066 out of band data
* @property { String } origin_id - The XEP-0359 Origin ID
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
* @property { String } received - An ISO8601 string recording the time that the message was received
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
* @property { String } spoiler_hint The XEP-0382 spoiler hint
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
* @property { String } subject - The <subject> element value
* @property { String } thread - The <thread> element value
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
* @property { String } to - The recipient JID
* @property { String } type - The type of message
*/
let
attrs
=
Object
.
assign
(
{
from
,
nick
,
'
body
'
:
stanza
.
querySelector
(
'
body
'
)?.
textContent
?.
trim
(),
'
chat_state
'
:
getChatState
(
stanza
),
'
from_muc
'
:
Strophe
.
getBareJidFromJid
(
from
),
'
from_real_jid
'
:
chatbox
.
occupants
.
findOccupant
({
nick
})?.
get
(
'
jid
'
),
'
is_archived
'
:
isArchived
(
original_stanza
),
'
is_carbon
'
:
isCarbon
(
original_stanza
),
'
is_delayed
'
:
!!
delay
,
'
is_headline
'
:
isHeadline
(
stanza
),
'
is_markable
'
:
!!
sizzle
(
`markable[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"]`
,
stanza
).
length
,
'
is_marker
'
:
!!
marker
,
'
is_unstyled
'
:
!!
sizzle
(
`unstyled[xmlns="
${
Strophe
.
NS
.
STYLING
}
"]`
,
stanza
).
length
,
'
marker_id
'
:
marker
&&
marker
.
getAttribute
(
'
id
'
),
'
msgid
'
:
stanza
.
getAttribute
(
'
id
'
)
||
original_stanza
.
getAttribute
(
'
id
'
),
'
receipt_id
'
:
getReceiptId
(
stanza
),
'
received
'
:
new
Date
().
toISOString
(),
'
references
'
:
getReferences
(
stanza
),
'
subject
'
:
stanza
.
querySelector
(
'
subject
'
)?.
textContent
,
'
thread
'
:
stanza
.
querySelector
(
'
thread
'
)?.
textContent
,
'
time
'
:
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
now
,
'
to
'
:
stanza
.
getAttribute
(
'
to
'
),
'
type
'
:
stanza
.
getAttribute
(
'
type
'
)
},
getErrorAttributes
(
stanza
),
getOutOfBandAttributes
(
stanza
),
getSpoilerAttributes
(
stanza
),
getCorrectionAttributes
(
stanza
,
original_stanza
),
getStanzaIDs
(
stanza
,
original_stanza
),
getRetractionAttributes
(
stanza
,
original_stanza
),
getModerationAttributes
(
stanza
),
getEncryptionAttributes
(
stanza
,
_converse
)
);
await
api
.
emojis
.
initialize
();
attrs
=
Object
.
assign
(
{
'
is_only_emojis
'
:
attrs
.
body
?
u
.
isOnlyEmojis
(
attrs
.
body
)
:
false
,
'
is_valid_receipt_request
'
:
isValidReceiptRequest
(
stanza
,
attrs
),
'
message
'
:
attrs
.
body
||
attrs
.
error
,
// TODO: Remove and use body and error attributes instead
'
sender
'
:
attrs
.
nick
===
chatbox
.
get
(
'
nick
'
)
?
'
me
'
:
'
them
'
},
attrs
);
if
(
attrs
.
is_archived
&&
original_stanza
.
getAttribute
(
'
from
'
)
!==
attrs
.
from_muc
)
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM message from
${
original_stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
else
if
(
attrs
.
is_archived
&&
original_stanza
.
getAttribute
(
'
from
'
)
!==
chatbox
.
get
(
'
jid
'
))
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM groupchat message from
${
stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
else
if
(
attrs
.
is_carbon
)
{
return
new
StanzaParseError
(
'
Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied
'
,
stanza
);
}
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs
[
'
id
'
]
=
attrs
[
'
origin_id
'
]
||
attrs
[
`stanza_id
${
attrs
.
from_muc
||
attrs
.
from
}
`
]
||
u
.
getUniqueId
();
/**
* *Hook* which allows plugins to add additional parsing
* @event _converse#parseMUCMessage
*/
return
api
.
hook
(
'
parseMUCMessage
'
,
stanza
,
attrs
);
}
/**
* Given an IQ stanza with a member list, create an array of objects containing
* known member data (e.g. jid, nick, role, affiliation).
* @private
* @method muc_utils#parseMemberListIQ
* @returns { MemberListItem[] }
*/
export
function
parseMemberListIQ
(
iq
)
{
return
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
MUC_ADMIN
}
"] item`
,
iq
).
map
(
item
=>
{
/**
* @typedef {Object} MemberListItem
* Either the JID or the nickname (or both) will be available.
* @property {string} affiliation
* @property {string} [role]
* @property {string} [jid]
* @property {string} [nick]
*/
const
data
=
{
'
affiliation
'
:
item
.
getAttribute
(
'
affiliation
'
)
};
const
jid
=
item
.
getAttribute
(
'
jid
'
);
if
(
u
.
isValidJID
(
jid
))
{
data
[
'
jid
'
]
=
jid
;
}
else
{
// XXX: Prosody sends nick for the jid attribute value
// Perhaps for anonymous room?
data
[
'
nick
'
]
=
jid
;
}
const
nick
=
item
.
getAttribute
(
'
nick
'
);
if
(
nick
)
{
data
[
'
nick
'
]
=
nick
;
}
const
role
=
item
.
getAttribute
(
'
role
'
);
if
(
role
)
{
data
[
'
role
'
]
=
nick
;
}
return
data
;
});
}
/**
* Parses a passed in MUC presence stanza and returns an object of attributes.
* @method parseMUCPresence
* @param { XMLElement } stanza - The presence stanza
* @returns { Object }
*/
export
function
parseMUCPresence
(
stanza
)
{
const
from
=
stanza
.
getAttribute
(
'
from
'
);
const
type
=
stanza
.
getAttribute
(
'
type
'
);
const
data
=
{
'
from
'
:
from
,
'
nick
'
:
Strophe
.
getResourceFromJid
(
from
),
'
type
'
:
type
,
'
states
'
:
[],
'
hats
'
:
[],
'
show
'
:
type
!==
'
unavailable
'
?
'
online
'
:
'
offline
'
};
Array
.
from
(
stanza
.
children
).
forEach
(
child
=>
{
if
(
child
.
matches
(
'
status
'
))
{
data
.
status
=
child
.
textContent
||
null
;
}
else
if
(
child
.
matches
(
'
show
'
))
{
data
.
show
=
child
.
textContent
||
'
online
'
;
}
else
if
(
child
.
matches
(
'
x
'
)
&&
child
.
getAttribute
(
'
xmlns
'
)
===
Strophe
.
NS
.
MUC_USER
)
{
Array
.
from
(
child
.
children
).
forEach
(
item
=>
{
if
(
item
.
nodeName
===
'
item
'
)
{
data
.
affiliation
=
item
.
getAttribute
(
'
affiliation
'
);
data
.
role
=
item
.
getAttribute
(
'
role
'
);
data
.
jid
=
item
.
getAttribute
(
'
jid
'
);
data
.
nick
=
item
.
getAttribute
(
'
nick
'
)
||
data
.
nick
;
}
else
if
(
item
.
nodeName
==
'
status
'
&&
item
.
getAttribute
(
'
code
'
))
{
data
.
states
.
push
(
item
.
getAttribute
(
'
code
'
));
}
});
}
else
if
(
child
.
matches
(
'
x
'
)
&&
child
.
getAttribute
(
'
xmlns
'
)
===
Strophe
.
NS
.
VCARDUPDATE
)
{
data
.
image_hash
=
child
.
querySelector
(
'
photo
'
)?.
textContent
;
}
else
if
(
child
.
matches
(
'
hats
'
)
&&
child
.
getAttribute
(
'
xmlns
'
)
===
Strophe
.
NS
.
MUC_HATS
)
{
data
[
'
hats
'
]
=
Array
.
from
(
child
.
children
).
map
(
c
=>
c
.
matches
(
'
hat
'
)
&&
{
'
title
'
:
c
.
getAttribute
(
'
title
'
),
'
uri
'
:
c
.
getAttribute
(
'
uri
'
)
}
);
}
});
return
data
;
}
src/headless/
utils/muc
.js
→
src/headless/
plugins/muc/utils
.js
View file @
e80afbfe
...
...
@@ -4,10 +4,6 @@
* @description This is the MUC utilities module.
*/
import
{
difference
,
indexOf
}
from
"
lodash-es
"
;
import
{
converse
}
from
"
@converse/headless/core
"
;
import
u
from
"
./core
"
;
const
{
Strophe
,
sizzle
}
=
converse
.
env
;
/**
* The MUC utils object. Contains utility functions related to multi-user chat.
...
...
@@ -58,49 +54,7 @@ const muc_utils = {
delta
=
delta
.
concat
(
difference
(
old_jids
,
new_jids
).
map
(
jid
=>
({
'
jid
'
:
jid
,
'
affiliation
'
:
'
none
'
})));
}
return
delta
;
},
/**
* Given an IQ stanza with a member list, create an array of objects containing
* known member data (e.g. jid, nick, role, affiliation).
* @private
* @method muc_utils#parseMemberListIQ
* @returns { MemberListItem[] }
*/
parseMemberListIQ
(
iq
)
{
return
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
MUC_ADMIN
}
"] item`
,
iq
).
map
(
(
item
)
=>
{
/**
* @typedef {Object} MemberListItem
* Either the JID or the nickname (or both) will be available.
* @property {string} affiliation
* @property {string} [role]
* @property {string} [jid]
* @property {string} [nick]
*/
const
data
=
{
'
affiliation
'
:
item
.
getAttribute
(
'
affiliation
'
),
}
const
jid
=
item
.
getAttribute
(
'
jid
'
);
if
(
u
.
isValidJID
(
jid
))
{
data
[
'
jid
'
]
=
jid
;
}
else
{
// XXX: Prosody sends nick for the jid attribute value
// Perhaps for anonymous room?
data
[
'
nick
'
]
=
jid
;
}
const
nick
=
item
.
getAttribute
(
'
nick
'
);
if
(
nick
)
{
data
[
'
nick
'
]
=
nick
;
}
const
role
=
item
.
getAttribute
(
'
role
'
);
if
(
role
)
{
data
[
'
role
'
]
=
nick
;
}
return
data
;
}
);
},
}
}
export
default
muc_utils
;
src/headless/shared/actions.js
0 → 100644
View file @
e80afbfe
import
log
from
'
../log
'
;
import
{
Strophe
,
$msg
}
from
'
strophe.js/src/strophe
'
;
import
{
_converse
,
api
,
converse
}
from
'
@converse/headless/core
'
;
const
u
=
converse
.
env
.
utils
;
export
function
rejectMessage
(
stanza
,
text
)
{
// Reject an incoming message by replying with an error message of type "cancel".
api
.
send
(
$msg
({
'
to
'
:
stanza
.
getAttribute
(
'
from
'
),
'
type
'
:
'
error
'
,
'
id
'
:
stanza
.
getAttribute
(
'
id
'
)
})
.
c
(
'
error
'
,
{
'
type
'
:
'
cancel
'
})
.
c
(
'
not-allowed
'
,
{
xmlns
:
'
urn:ietf:params:xml:ns:xmpp-stanzas
'
})
.
up
()
.
c
(
'
text
'
,
{
xmlns
:
'
urn:ietf:params:xml:ns:xmpp-stanzas
'
})
.
t
(
text
)
);
log
.
warn
(
`Rejecting message stanza with the following reason:
${
text
}
`
);
log
.
warn
(
stanza
);
}
/**
* Send out a XEP-0333 chat marker
* @param { String } to_jid
* @param { String } id - The id of the message being marked
* @param { String } type - The marker type
* @param { String } msg_type
*/
export
function
sendMarker
(
to_jid
,
id
,
type
,
msg_type
)
{
const
stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
to_jid
,
'
type
'
:
msg_type
?
msg_type
:
'
chat
'
}).
c
(
type
,
{
'
xmlns
'
:
Strophe
.
NS
.
MARKERS
,
'
id
'
:
id
});
api
.
send
(
stanza
);
}
src/headless/shared/parsers.js
0 → 100644
View file @
e80afbfe
import
dayjs
from
'
dayjs
'
;
import
sizzle
from
'
sizzle
'
;
import
{
Strophe
}
from
'
strophe.js/src/strophe
'
;
import
{
_converse
,
api
}
from
'
@converse/headless/core
'
;
import
{
rejectMessage
}
from
'
@converse/headless/shared/actions
'
;
const
{
NS
}
=
Strophe
;
export
class
StanzaParseError
extends
Error
{
constructor
(
message
,
stanza
)
{
super
(
message
,
stanza
);
this
.
name
=
'
StanzaParseError
'
;
this
.
stanza
=
stanza
;
}
}
/**
* Extract the XEP-0359 stanza IDs from the passed in stanza
* and return a map containing them.
* @private
* @param { XMLElement } stanza - The message stanza
* @returns { Object }
*/
export
function
getStanzaIDs
(
stanza
,
original_stanza
)
{
const
attrs
=
{};
// Store generic stanza ids
const
sids
=
sizzle
(
`stanza-id[xmlns="
${
Strophe
.
NS
.
SID
}
"]`
,
stanza
);
const
sid_attrs
=
sids
.
reduce
((
acc
,
s
)
=>
{
acc
[
`stanza_id
${
s
.
getAttribute
(
'
by
'
)}
`
]
=
s
.
getAttribute
(
'
id
'
);
return
acc
;
},
{});
Object
.
assign
(
attrs
,
sid_attrs
);
// Store the archive id
const
result
=
sizzle
(
`message > result[xmlns="
${
Strophe
.
NS
.
MAM
}
"]`
,
original_stanza
).
pop
();
if
(
result
)
{
const
by_jid
=
original_stanza
.
getAttribute
(
'
from
'
)
||
_converse
.
bare_jid
;
attrs
[
`stanza_id
${
by_jid
}
`
]
=
result
.
getAttribute
(
'
id
'
);
}
// Store the origin id
const
origin_id
=
sizzle
(
`origin-id[xmlns="
${
Strophe
.
NS
.
SID
}
"]`
,
stanza
).
pop
();
if
(
origin_id
)
{
attrs
[
'
origin_id
'
]
=
origin_id
.
getAttribute
(
'
id
'
);
}
return
attrs
;
}
export
function
getEncryptionAttributes
(
stanza
,
_converse
)
{
const
encrypted
=
sizzle
(
`encrypted[xmlns="
${
Strophe
.
NS
.
OMEMO
}
"]`
,
stanza
).
pop
();
const
attrs
=
{
'
is_encrypted
'
:
!!
encrypted
};
if
(
!
encrypted
||
api
.
settings
.
get
(
'
clear_cache_on_logout
'
))
{
return
attrs
;
}
const
header
=
encrypted
.
querySelector
(
'
header
'
);
attrs
[
'
encrypted
'
]
=
{
'
device_id
'
:
header
.
getAttribute
(
'
sid
'
)
};
const
device_id
=
_converse
.
omemo_store
?.
get
(
'
device_id
'
);
const
key
=
device_id
&&
sizzle
(
`key[rid="
${
device_id
}
"]`
,
encrypted
).
pop
();
if
(
key
)
{
Object
.
assign
(
attrs
.
encrypted
,
{
'
iv
'
:
header
.
querySelector
(
'
iv
'
).
textContent
,
'
key
'
:
key
.
textContent
,
'
payload
'
:
encrypted
.
querySelector
(
'
payload
'
)?.
textContent
||
null
,
'
prekey
'
:
[
'
true
'
,
'
1
'
].
includes
(
key
.
getAttribute
(
'
prekey
'
))
});
}
return
attrs
;
}
/**
* @private
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @returns { Object }
*/
export
function
getRetractionAttributes
(
stanza
,
original_stanza
)
{
const
fastening
=
sizzle
(
`> apply-to[xmlns="
${
Strophe
.
NS
.
FASTEN
}
"]`
,
stanza
).
pop
();
if
(
fastening
)
{
const
applies_to_id
=
fastening
.
getAttribute
(
'
id
'
);
const
retracted
=
sizzle
(
`> retract[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
fastening
).
pop
();
if
(
retracted
)
{
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
time
=
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
new
Date
().
toISOString
();
return
{
'
editable
'
:
false
,
'
retracted
'
:
time
,
'
retracted_id
'
:
applies_to_id
};
}
}
else
{
const
tombstone
=
sizzle
(
`> retracted[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
stanza
).
pop
();
if
(
tombstone
)
{
return
{
'
editable
'
:
false
,
'
is_tombstone
'
:
true
,
'
retracted
'
:
tombstone
.
getAttribute
(
'
stamp
'
)
};
}
}
return
{};
}
export
function
getCorrectionAttributes
(
stanza
,
original_stanza
)
{
const
el
=
sizzle
(
`replace[xmlns="
${
Strophe
.
NS
.
MESSAGE_CORRECT
}
"]`
,
stanza
).
pop
();
if
(
el
)
{
const
replace_id
=
el
.
getAttribute
(
'
id
'
);
const
msgid
=
replace_id
;
if
(
replace_id
)
{
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
time
=
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
new
Date
().
toISOString
();
return
{
msgid
,
replace_id
,
'
edited
'
:
time
};
}
}
return
{};
}
export
function
getSpoilerAttributes
(
stanza
)
{
const
spoiler
=
sizzle
(
`spoiler[xmlns="
${
Strophe
.
NS
.
SPOILER
}
"]`
,
stanza
).
pop
();
return
{
'
is_spoiler
'
:
!!
spoiler
,
'
spoiler_hint
'
:
spoiler
?.
textContent
};
}
export
function
getOutOfBandAttributes
(
stanza
)
{
const
xform
=
sizzle
(
`x[xmlns="
${
Strophe
.
NS
.
OUTOFBAND
}
"]`
,
stanza
).
pop
();
if
(
xform
)
{
return
{
'
oob_url
'
:
xform
.
querySelector
(
'
url
'
)?.
textContent
,
'
oob_desc
'
:
xform
.
querySelector
(
'
desc
'
)?.
textContent
};
}
return
{};
}
/**
* Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
* @private
* @param { XMLElement } stanza - The message stanza
*/
export
function
getErrorAttributes
(
stanza
)
{
if
(
stanza
.
getAttribute
(
'
type
'
)
===
'
error
'
)
{
const
error
=
stanza
.
querySelector
(
'
error
'
);
const
text
=
sizzle
(
`text[xmlns="
${
Strophe
.
NS
.
STANZAS
}
"]`
,
error
).
pop
();
return
{
'
is_error
'
:
true
,
'
error_text
'
:
text
?.
textContent
,
'
error_type
'
:
error
.
getAttribute
(
'
type
'
),
'
error_condition
'
:
error
.
firstElementChild
.
nodeName
};
}
return
{};
}
export
function
getReferences
(
stanza
)
{
const
text
=
stanza
.
querySelector
(
'
body
'
)?.
textContent
;
return
sizzle
(
`reference[xmlns="
${
Strophe
.
NS
.
REFERENCE
}
"]`
,
stanza
).
map
(
ref
=>
{
const
begin
=
ref
.
getAttribute
(
'
begin
'
);
const
end
=
ref
.
getAttribute
(
'
end
'
);
return
{
'
begin
'
:
begin
,
'
end
'
:
end
,
'
type
'
:
ref
.
getAttribute
(
'
type
'
),
'
value
'
:
text
.
slice
(
begin
,
end
),
'
uri
'
:
ref
.
getAttribute
(
'
uri
'
)
};
});
}
export
function
getReceiptId
(
stanza
)
{
const
receipt
=
sizzle
(
`received[xmlns="
${
Strophe
.
NS
.
RECEIPTS
}
"]`
,
stanza
).
pop
();
return
receipt
?.
getAttribute
(
'
id
'
);
}
/**
* Determines whether the passed in stanza is a XEP-0280 Carbon
* @private
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
export
function
isCarbon
(
stanza
)
{
const
xmlns
=
Strophe
.
NS
.
CARBONS
;
return
(
sizzle
(
`message > received[xmlns="
${
xmlns
}
"]`
,
stanza
).
length
>
0
||
sizzle
(
`message > sent[xmlns="
${
xmlns
}
"]`
,
stanza
).
length
>
0
);
}
/**
* Returns the XEP-0085 chat state contained in a message stanza
* @private
* @param { XMLElement } stanza - The message stanza
*/
export
function
getChatState
(
stanza
)
{
return
sizzle
(
`
composing[xmlns="
${
NS
.
CHATSTATES
}
"],
paused[xmlns="
${
NS
.
CHATSTATES
}
"],
inactive[xmlns="
${
NS
.
CHATSTATES
}
"],
active[xmlns="
${
NS
.
CHATSTATES
}
"],
gone[xmlns="
${
NS
.
CHATSTATES
}
"]`
,
stanza
).
pop
()?.
nodeName
;
}
export
function
isValidReceiptRequest
(
stanza
,
attrs
)
{
return
(
attrs
.
sender
!==
'
me
'
&&
!
attrs
.
is_carbon
&&
!
attrs
.
is_archived
&&
sizzle
(
`request[xmlns="
${
Strophe
.
NS
.
RECEIPTS
}
"]`
,
stanza
).
length
);
}
export
function
rejectUnencapsulatedForward
(
stanza
)
{
const
bare_forward
=
sizzle
(
`message > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"]`
,
stanza
).
length
;
if
(
bare_forward
)
{
rejectMessage
(
stanza
,
'
Forwarded messages not part of an encapsulating protocol are not supported
'
);
const
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
return
new
StanzaParseError
(
`Ignoring unencapsulated forwarded message from
${
from_jid
}
`
,
stanza
);
}
}
/**
* Determines whether the passed in stanza is a XEP-0333 Chat Marker
* @private
* @method getChatMarker
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
export
function
getChatMarker
(
stanza
)
{
// If we receive more than one marker (which shouldn't happen), we take
// the highest level of acknowledgement.
return
sizzle
(
`
acknowledged[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"],
displayed[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"],
received[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"]`
,
stanza
).
pop
();
}
export
function
isHeadline
(
stanza
)
{
return
stanza
.
getAttribute
(
'
type
'
)
===
'
headline
'
;
}
export
function
isServerMessage
(
stanza
)
{
const
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
if
(
stanza
.
getAttribute
(
'
type
'
)
!==
'
error
'
&&
from_jid
&&
!
from_jid
.
includes
(
'
@
'
))
{
// Some servers (e.g. Prosody) don't set the stanza
// type to "headline" when sending server messages.
// For now we check if an @ signal is included, and if not,
// we assume it's a headline stanza.
return
true
;
}
return
false
;
}
/**
* Determines whether the passed in stanza is a XEP-0313 MAM stanza
* @private
* @method isArchived
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
export
function
isArchived
(
original_stanza
)
{
return
!!
sizzle
(
`message > result[xmlns="
${
Strophe
.
NS
.
MAM
}
"]`
,
original_stanza
).
pop
();
}
/**
* Returns an object containing all attribute names and values for a particular element.
* @method getAttributes
* @param { XMLElement } stanza
* @returns { Object }
*/
export
function
getAttributes
(
stanza
)
{
return
stanza
.
getAttributeNames
().
reduce
((
acc
,
name
)
=>
{
acc
[
name
]
=
Strophe
.
xmlunescape
(
stanza
.
getAttribute
(
name
));
return
acc
;
},
{});
}
src/headless/utils/stanza.js
deleted
100644 → 0
View file @
e8eea632
import
{
Strophe
,
$msg
}
from
'
strophe.js/src/strophe
'
;
import
dayjs
from
'
dayjs
'
;
import
sizzle
from
'
sizzle
'
;
import
u
from
'
@converse/headless/utils/core
'
;
import
log
from
"
../log
"
;
import
{
_converse
,
api
}
from
"
@converse/headless/core
"
;
const
{
NS
}
=
Strophe
;
function
getSpoilerAttributes
(
stanza
)
{
const
spoiler
=
sizzle
(
`spoiler[xmlns="
${
Strophe
.
NS
.
SPOILER
}
"]`
,
stanza
).
pop
();
return
{
'
is_spoiler
'
:
!!
spoiler
,
'
spoiler_hint
'
:
spoiler
?.
textContent
}
}
function
getOutOfBandAttributes
(
stanza
)
{
const
xform
=
sizzle
(
`x[xmlns="
${
Strophe
.
NS
.
OUTOFBAND
}
"]`
,
stanza
).
pop
();
if
(
xform
)
{
return
{
'
oob_url
'
:
xform
.
querySelector
(
'
url
'
)?.
textContent
,
'
oob_desc
'
:
xform
.
querySelector
(
'
desc
'
)?.
textContent
}
}
return
{};
}
function
getCorrectionAttributes
(
stanza
,
original_stanza
)
{
const
el
=
sizzle
(
`replace[xmlns="
${
Strophe
.
NS
.
MESSAGE_CORRECT
}
"]`
,
stanza
).
pop
();
if
(
el
)
{
const
replace_id
=
el
.
getAttribute
(
'
id
'
);
const
msgid
=
replace_id
;
if
(
replace_id
)
{
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
time
=
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
(
new
Date
()).
toISOString
();
return
{
msgid
,
replace_id
,
'
edited
'
:
time
}
}
}
return
{};
}
function
getEncryptionAttributes
(
stanza
,
_converse
)
{
const
encrypted
=
sizzle
(
`encrypted[xmlns="
${
Strophe
.
NS
.
OMEMO
}
"]`
,
stanza
).
pop
();
const
attrs
=
{
'
is_encrypted
'
:
!!
encrypted
};
if
(
!
encrypted
||
api
.
settings
.
get
(
'
clear_cache_on_logout
'
))
{
return
attrs
;
}
const
header
=
encrypted
.
querySelector
(
'
header
'
);
attrs
[
'
encrypted
'
]
=
{
'
device_id
'
:
header
.
getAttribute
(
'
sid
'
)};
const
device_id
=
_converse
.
omemo_store
?.
get
(
'
device_id
'
);
const
key
=
device_id
&&
sizzle
(
`key[rid="
${
device_id
}
"]`
,
encrypted
).
pop
();
if
(
key
)
{
Object
.
assign
(
attrs
.
encrypted
,
{
'
iv
'
:
header
.
querySelector
(
'
iv
'
).
textContent
,
'
key
'
:
key
.
textContent
,
'
payload
'
:
encrypted
.
querySelector
(
'
payload
'
)?.
textContent
||
null
,
'
prekey
'
:
[
'
true
'
,
'
1
'
].
includes
(
key
.
getAttribute
(
'
prekey
'
))
});
}
return
attrs
;
}
function
isValidReceiptRequest
(
stanza
,
attrs
)
{
return
(
attrs
.
sender
!==
'
me
'
&&
!
attrs
.
is_carbon
&&
!
attrs
.
is_archived
&&
sizzle
(
`request[xmlns="
${
Strophe
.
NS
.
RECEIPTS
}
"]`
,
stanza
).
length
);
}
function
getReceiptId
(
stanza
)
{
const
receipt
=
sizzle
(
`received[xmlns="
${
Strophe
.
NS
.
RECEIPTS
}
"]`
,
stanza
).
pop
();
return
receipt
?.
getAttribute
(
'
id
'
);
}
/**
* Returns the XEP-0085 chat state contained in a message stanza
* @private
* @param { XMLElement } stanza - The message stanza
*/
function
getChatState
(
stanza
)
{
return
sizzle
(
`
composing[xmlns="
${
NS
.
CHATSTATES
}
"],
paused[xmlns="
${
NS
.
CHATSTATES
}
"],
inactive[xmlns="
${
NS
.
CHATSTATES
}
"],
active[xmlns="
${
NS
.
CHATSTATES
}
"],
gone[xmlns="
${
NS
.
CHATSTATES
}
"]`
,
stanza
).
pop
()?.
nodeName
;
}
/**
* Determines whether the passed in stanza is a XEP-0280 Carbon
* @private
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
function
isCarbon
(
stanza
)
{
const
xmlns
=
Strophe
.
NS
.
CARBONS
;
return
sizzle
(
`message > received[xmlns="
${
xmlns
}
"]`
,
stanza
).
length
>
0
||
sizzle
(
`message > sent[xmlns="
${
xmlns
}
"]`
,
stanza
).
length
>
0
;
}
/**
* Extract the XEP-0359 stanza IDs from the passed in stanza
* and return a map containing them.
* @private
* @param { XMLElement } stanza - The message stanza
* @returns { Object }
*/
function
getStanzaIDs
(
stanza
,
original_stanza
)
{
const
attrs
=
{};
// Store generic stanza ids
const
sids
=
sizzle
(
`stanza-id[xmlns="
${
Strophe
.
NS
.
SID
}
"]`
,
stanza
);
const
sid_attrs
=
sids
.
reduce
((
acc
,
s
)
=>
{
acc
[
`stanza_id
${
s
.
getAttribute
(
'
by
'
)}
`
]
=
s
.
getAttribute
(
'
id
'
);
return
acc
;
},
{});
Object
.
assign
(
attrs
,
sid_attrs
);
// Store the archive id
const
result
=
sizzle
(
`message > result[xmlns="
${
Strophe
.
NS
.
MAM
}
"]`
,
original_stanza
).
pop
();
if
(
result
)
{
const
by_jid
=
original_stanza
.
getAttribute
(
'
from
'
)
||
_converse
.
bare_jid
;
attrs
[
`stanza_id
${
by_jid
}
`
]
=
result
.
getAttribute
(
'
id
'
);
}
// Store the origin id
const
origin_id
=
sizzle
(
`origin-id[xmlns="
${
Strophe
.
NS
.
SID
}
"]`
,
stanza
).
pop
();
if
(
origin_id
)
{
attrs
[
'
origin_id
'
]
=
origin_id
.
getAttribute
(
'
id
'
);
}
return
attrs
;
}
/**
* @private
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @returns { Object }
*/
function
getModerationAttributes
(
stanza
)
{
const
fastening
=
sizzle
(
`apply-to[xmlns="
${
Strophe
.
NS
.
FASTEN
}
"]`
,
stanza
).
pop
();
if
(
fastening
)
{
const
applies_to_id
=
fastening
.
getAttribute
(
'
id
'
);
const
moderated
=
sizzle
(
`moderated[xmlns="
${
Strophe
.
NS
.
MODERATE
}
"]`
,
fastening
).
pop
();
if
(
moderated
)
{
const
retracted
=
sizzle
(
`retract[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
moderated
).
pop
();
if
(
retracted
)
{
return
{
'
editable
'
:
false
,
'
moderated
'
:
'
retracted
'
,
'
moderated_by
'
:
moderated
.
getAttribute
(
'
by
'
),
'
moderated_id
'
:
applies_to_id
,
'
moderation_reason
'
:
moderated
.
querySelector
(
'
reason
'
)?.
textContent
}
}
}
}
else
{
const
tombstone
=
sizzle
(
`> moderated[xmlns="
${
Strophe
.
NS
.
MODERATE
}
"]`
,
stanza
).
pop
();
if
(
tombstone
)
{
const
retracted
=
sizzle
(
`retracted[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
tombstone
).
pop
();
if
(
retracted
)
{
return
{
'
editable
'
:
false
,
'
is_tombstone
'
:
true
,
'
moderated_by
'
:
tombstone
.
getAttribute
(
'
by
'
),
'
retracted
'
:
tombstone
.
getAttribute
(
'
stamp
'
),
'
moderation_reason
'
:
tombstone
.
querySelector
(
'
reason
'
)?.
textContent
}
}
}
}
return
{};
}
/**
* @private
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @returns { Object }
*/
function
getRetractionAttributes
(
stanza
,
original_stanza
)
{
const
fastening
=
sizzle
(
`> apply-to[xmlns="
${
Strophe
.
NS
.
FASTEN
}
"]`
,
stanza
).
pop
();
if
(
fastening
)
{
const
applies_to_id
=
fastening
.
getAttribute
(
'
id
'
);
const
retracted
=
sizzle
(
`> retract[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
fastening
).
pop
();
if
(
retracted
)
{
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
time
=
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
(
new
Date
()).
toISOString
();
return
{
'
editable
'
:
false
,
'
retracted
'
:
time
,
'
retracted_id
'
:
applies_to_id
}
}
}
else
{
const
tombstone
=
sizzle
(
`> retracted[xmlns="
${
Strophe
.
NS
.
RETRACT
}
"]`
,
stanza
).
pop
();
if
(
tombstone
)
{
return
{
'
editable
'
:
false
,
'
is_tombstone
'
:
true
,
'
retracted
'
:
tombstone
.
getAttribute
(
'
stamp
'
)
}
}
}
return
{};
}
function
getReferences
(
stanza
)
{
const
text
=
stanza
.
querySelector
(
'
body
'
)?.
textContent
;
return
sizzle
(
`reference[xmlns="
${
Strophe
.
NS
.
REFERENCE
}
"]`
,
stanza
).
map
(
ref
=>
{
const
begin
=
ref
.
getAttribute
(
'
begin
'
);
const
end
=
ref
.
getAttribute
(
'
end
'
);
return
{
'
begin
'
:
begin
,
'
end
'
:
end
,
'
type
'
:
ref
.
getAttribute
(
'
type
'
),
'
value
'
:
text
.
slice
(
begin
,
end
),
'
uri
'
:
ref
.
getAttribute
(
'
uri
'
)
};
});
}
function
rejectMessage
(
stanza
,
text
)
{
// Reject an incoming message by replying with an error message of type "cancel".
api
.
send
(
$msg
({
'
to
'
:
stanza
.
getAttribute
(
'
from
'
),
'
type
'
:
'
error
'
,
'
id
'
:
stanza
.
getAttribute
(
'
id
'
)
}).
c
(
'
error
'
,
{
'
type
'
:
'
cancel
'
})
.
c
(
'
not-allowed
'
,
{
xmlns
:
"
urn:ietf:params:xml:ns:xmpp-stanzas
"
}).
up
()
.
c
(
'
text
'
,
{
xmlns
:
"
urn:ietf:params:xml:ns:xmpp-stanzas
"
}).
t
(
text
)
);
log
.
warn
(
`Rejecting message stanza with the following reason:
${
text
}
`
);
log
.
warn
(
stanza
);
}
/**
* Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
* @private
* @param { XMLElement } stanza - The message stanza
*/
function
getErrorAttributes
(
stanza
)
{
if
(
stanza
.
getAttribute
(
'
type
'
)
===
'
error
'
)
{
const
error
=
stanza
.
querySelector
(
'
error
'
);
const
text
=
sizzle
(
`text[xmlns="
${
Strophe
.
NS
.
STANZAS
}
"]`
,
error
).
pop
();
return
{
'
is_error
'
:
true
,
'
error_text
'
:
text
?.
textContent
,
'
error_type
'
:
error
.
getAttribute
(
'
type
'
),
'
error_condition
'
:
error
.
firstElementChild
.
nodeName
}
}
return
{};
}
class
StanzaParseError
extends
Error
{
constructor
(
message
,
stanza
)
{
super
(
message
,
stanza
);
this
.
name
=
'
StanzaParseError
'
;
this
.
stanza
=
stanza
;
}
}
function
rejectUnencapsulatedForward
(
stanza
)
{
const
bare_forward
=
sizzle
(
`message > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"]`
,
stanza
).
length
;
if
(
bare_forward
)
{
rejectMessage
(
stanza
,
'
Forwarded messages not part of an encapsulating protocol are not supported
'
);
const
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
return
new
StanzaParseError
(
`Ignoring unencapsulated forwarded message from
${
from_jid
}
`
,
stanza
);
}
}
/**
* The stanza utils object. Contains utility functions related to stanza processing.
* @namespace st
*/
const
st
=
{
isHeadline
(
stanza
)
{
return
stanza
.
getAttribute
(
'
type
'
)
===
'
headline
'
;
},
isServerMessage
(
stanza
)
{
const
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
if
(
stanza
.
getAttribute
(
'
type
'
)
!==
'
error
'
&&
from_jid
&&
!
from_jid
.
includes
(
'
@
'
))
{
// Some servers (e.g. Prosody) don't set the stanza
// type to "headline" when sending server messages.
// For now we check if an @ signal is included, and if not,
// we assume it's a headline stanza.
return
true
;
}
return
false
;
},
/**
* Determines whether the passed in stanza is a XEP-0333 Chat Marker
* @private
* @method st#getChatMarker
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
getChatMarker
(
stanza
)
{
// If we receive more than one marker (which shouldn't happen), we take
// the highest level of acknowledgement.
return
sizzle
(
`
acknowledged[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"],
displayed[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"],
received[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"]`
,
stanza
).
pop
();
},
/**
* Determines whether the passed in stanza is a XEP-0313 MAM stanza
* @private
* @method st#isArchived
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
isArchived
(
original_stanza
)
{
return
!!
sizzle
(
`message > result[xmlns="
${
Strophe
.
NS
.
MAM
}
"]`
,
original_stanza
).
pop
();
},
/**
* Returns an object containing all attribute names and values for a particular element.
* @method st#getAttributes
* @param { XMLElement } stanza
* @returns { Object }
*/
getAttributes
(
stanza
)
{
return
stanza
.
getAttributeNames
().
reduce
((
acc
,
name
)
=>
{
acc
[
name
]
=
Strophe
.
xmlunescape
(
stanza
.
getAttribute
(
name
))
return
acc
;
},
{});
},
/**
* Parses a passed in message stanza and returns an object of attributes.
* @method st#parseMessage
* @param { XMLElement } stanza - The message stanza
* @param { _converse } _converse
* @returns { (MessageAttributes|Error) }
*/
async
parseMessage
(
stanza
,
_converse
)
{
const
err
=
rejectUnencapsulatedForward
(
stanza
);
if
(
err
)
{
return
err
;
}
let
to_jid
=
stanza
.
getAttribute
(
'
to
'
);
const
to_resource
=
Strophe
.
getResourceFromJid
(
to_jid
);
if
(
api
.
settings
.
get
(
'
filter_by_resource
'
)
&&
(
to_resource
&&
to_resource
!==
_converse
.
resource
))
{
return
new
StanzaParseError
(
`Ignoring incoming message intended for a different resource:
${
to_jid
}
`
,
stanza
);
}
const
original_stanza
=
stanza
;
let
from_jid
=
stanza
.
getAttribute
(
'
from
'
)
||
_converse
.
bare_jid
;
if
(
isCarbon
(
stanza
))
{
if
(
from_jid
===
_converse
.
bare_jid
)
{
const
selector
=
`[xmlns="
${
Strophe
.
NS
.
CARBONS
}
"] > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"] > message`
;
stanza
=
sizzle
(
selector
,
stanza
).
pop
();
to_jid
=
stanza
.
getAttribute
(
'
to
'
);
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
}
else
{
// Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
rejectMessage
(
stanza
,
'
Rejecting carbon from invalid JID
'
);
return
new
StanzaParseError
(
`Rejecting carbon from invalid JID
${
to_jid
}
`
,
stanza
);
}
}
const
is_archived
=
st
.
isArchived
(
stanza
);
if
(
is_archived
)
{
if
(
from_jid
===
_converse
.
bare_jid
)
{
const
selector
=
`[xmlns="
${
Strophe
.
NS
.
MAM
}
"] > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"] > message`
;
stanza
=
sizzle
(
selector
,
stanza
).
pop
();
to_jid
=
stanza
.
getAttribute
(
'
to
'
);
from_jid
=
stanza
.
getAttribute
(
'
from
'
);
}
else
{
return
new
StanzaParseError
(
`Invalid Stanza: alleged MAM message from
${
stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
}
const
from_bare_jid
=
Strophe
.
getBareJidFromJid
(
from_jid
);
const
is_me
=
from_bare_jid
===
_converse
.
bare_jid
;
if
(
is_me
&&
to_jid
===
null
)
{
return
new
StanzaParseError
(
`Don't know how to handle message stanza without 'to' attribute.
${
stanza
.
outerHTML
}
`
,
stanza
);
}
const
is_headline
=
st
.
isHeadline
(
stanza
);
const
is_server_message
=
st
.
isServerMessage
(
stanza
);
let
contact
,
contact_jid
;
if
(
!
is_headline
&&
!
is_server_message
)
{
contact_jid
=
is_me
?
Strophe
.
getBareJidFromJid
(
to_jid
)
:
from_bare_jid
;
contact
=
await
api
.
contacts
.
get
(
contact_jid
);
if
(
contact
===
undefined
&&
!
api
.
settings
.
get
(
"
allow_non_roster_messaging
"
))
{
log
.
error
(
stanza
);
return
new
StanzaParseError
(
`Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`
,
stanza
);
}
}
/**
* @typedef { Object } MessageAttributes
* The object which {@link st.parseMessage} returns
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
* @property { Boolean } editable - Is this message editable via XEP-0308?
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
* @property { Boolean } is_error - Whether an error was received for this message
* @property { Boolean } is_headline - Is this a "headline" message?
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Object } encrypted - XEP-0384 encryption payload attributes
* @property { String } body - The contents of the <body> tag of the message stanza
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
* @property { String } contact_jid - The JID of the other person or entity
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
* @property { String } error_condition - The defined error condition
* @property { String } error_text - The error text received from the server
* @property { String } error_type - The type of error received from the server
* @property { String } from - The sender JID
* @property { String } fullname - The full name of the sender
* @property { String } marker - The XEP-0333 Chat Marker value
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
* @property { String } msgid - The root `id` attribute of the stanza
* @property { String } nick - The roster nickname of the sender
* @property { String } oob_desc - The description of the XEP-0066 out of band data
* @property { String } oob_url - The URL of the XEP-0066 out of band data
* @property { String } origin_id - The XEP-0359 Origin ID
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
* @property { String } received - An ISO8601 string recording the time that the message was received
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
* @property { String } spoiler_hint The XEP-0382 spoiler hint
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
* @property { String } subject - The <subject> element value
* @property { String } thread - The <thread> element value
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
* @property { String } to - The recipient JID
* @property { String } type - The type of message
*/
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
marker
=
st
.
getChatMarker
(
stanza
);
const
now
=
(
new
Date
()).
toISOString
();
let
attrs
=
Object
.
assign
({
contact_jid
,
is_archived
,
is_headline
,
is_server_message
,
'
body
'
:
stanza
.
querySelector
(
'
body
'
)?.
textContent
?.
trim
(),
'
chat_state
'
:
getChatState
(
stanza
),
'
from
'
:
Strophe
.
getBareJidFromJid
(
stanza
.
getAttribute
(
'
from
'
)),
'
is_carbon
'
:
isCarbon
(
original_stanza
),
'
is_delayed
'
:
!!
delay
,
'
is_markable
'
:
!!
sizzle
(
`markable[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"]`
,
stanza
).
length
,
'
is_marker
'
:
!!
marker
,
'
is_unstyled
'
:
!!
sizzle
(
`unstyled[xmlns="
${
Strophe
.
NS
.
STYLING
}
"]`
,
stanza
).
length
,
'
marker_id
'
:
marker
&&
marker
.
getAttribute
(
'
id
'
),
'
msgid
'
:
stanza
.
getAttribute
(
'
id
'
)
||
original_stanza
.
getAttribute
(
'
id
'
),
'
nick
'
:
contact
?.
attributes
?.
nickname
,
'
receipt_id
'
:
getReceiptId
(
stanza
),
'
received
'
:
(
new
Date
()).
toISOString
(),
'
references
'
:
getReferences
(
stanza
),
'
sender
'
:
is_me
?
'
me
'
:
'
them
'
,
'
subject
'
:
stanza
.
querySelector
(
'
subject
'
)?.
textContent
,
'
thread
'
:
stanza
.
querySelector
(
'
thread
'
)?.
textContent
,
'
time
'
:
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
now
,
'
to
'
:
stanza
.
getAttribute
(
'
to
'
),
'
type
'
:
stanza
.
getAttribute
(
'
type
'
)
},
getErrorAttributes
(
stanza
),
getOutOfBandAttributes
(
stanza
),
getSpoilerAttributes
(
stanza
),
getCorrectionAttributes
(
stanza
,
original_stanza
),
getStanzaIDs
(
stanza
,
original_stanza
),
getRetractionAttributes
(
stanza
,
original_stanza
),
getEncryptionAttributes
(
stanza
,
_converse
)
);
if
(
attrs
.
is_archived
)
{
const
from
=
original_stanza
.
getAttribute
(
'
from
'
);
if
(
from
&&
from
!==
_converse
.
bare_jid
)
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM message from
${
from
}
`
,
stanza
);
}
}
await
api
.
emojis
.
initialize
();
attrs
=
Object
.
assign
({
'
message
'
:
attrs
.
body
||
attrs
.
error
,
// TODO: Remove and use body and error attributes instead
'
is_only_emojis
'
:
attrs
.
body
?
u
.
isOnlyEmojis
(
attrs
.
body
)
:
false
,
'
is_valid_receipt_request
'
:
isValidReceiptRequest
(
stanza
,
attrs
)
},
attrs
);
// We prefer to use one of the XEP-0359 unique and stable stanza IDs
// as the Model id, to avoid duplicates.
attrs
[
'
id
'
]
=
attrs
[
'
origin_id
'
]
||
attrs
[
`stanza_id
${(
attrs
.
from
)}
`
]
||
u
.
getUniqueId
();
/**
* *Hook* which allows plugins to add additional parsing
* @event _converse#parseMessage
*/
return
api
.
hook
(
'
parseMessage
'
,
stanza
,
attrs
);
},
/**
* Parses a passed in message stanza and returns an object of attributes.
* @method st#parseMUCMessage
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @param { _converse.ChatRoom } chatbox
* @param { _converse } _converse
* @returns { Promise<MUCMessageAttributes|Error> }
*/
async
parseMUCMessage
(
stanza
,
chatbox
,
_converse
)
{
const
err
=
rejectUnencapsulatedForward
(
stanza
);
if
(
err
)
{
return
err
;
}
const
selector
=
`[xmlns="
${
NS
.
MAM
}
"] > forwarded[xmlns="
${
NS
.
FORWARD
}
"] > message`
;
const
original_stanza
=
stanza
;
stanza
=
sizzle
(
selector
,
stanza
).
pop
()
||
stanza
;
if
(
sizzle
(
`message > forwarded[xmlns="
${
Strophe
.
NS
.
FORWARD
}
"]`
,
stanza
).
length
)
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM groupchat message from
${
stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
const
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
original_stanza
).
pop
();
const
from
=
stanza
.
getAttribute
(
'
from
'
);
const
nick
=
Strophe
.
unescapeNode
(
Strophe
.
getResourceFromJid
(
from
));
const
marker
=
st
.
getChatMarker
(
stanza
);
const
now
=
(
new
Date
()).
toISOString
();
/**
* @typedef { Object } MUCMessageAttributes
* The object which {@link st.parseMUCMessage} returns
* @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else
* @property { Array<Object> } references - A list of objects representing XEP-0372 references
* @property { Boolean } editable - Is this message editable via XEP-0308?
* @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive?
* @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon?
* @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203?
* @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted?
* @property { Boolean } is_error - Whether an error was received for this message
* @property { Boolean } is_headline - Is this a "headline" message?
* @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker?
* @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker?
* @property { Boolean } is_only_emojis - Does the message body contain only emojis?
* @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message?
* @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone?
* @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored
* @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
* @property { Object } encrypted - XEP-0384 encryption payload attributes
* @property { String } body - The contents of the <body> tag of the message stanza
* @property { String } chat_state - The XEP-0085 chat state notification contained in this message
* @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308
* @property { String } error_condition - The defined error condition
* @property { String } error_text - The error text received from the server
* @property { String } error_type - The type of error received from the server
* @property { String } from - The sender JID (${muc_jid}/${nick})
* @property { String } from_muc - The JID of the MUC from which this message was sent
* @property { String } from_real_jid - The real JID of the sender, if available
* @property { String } fullname - The full name of the sender
* @property { String } marker - The XEP-0333 Chat Marker value
* @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker
* @property { String } moderated - The type of XEP-0425 moderation (if any) that was applied
* @property { String } moderated_by - The JID of the user that moderated this message
* @property { String } moderated_id - The XEP-0359 Stanza ID of the message that this one moderates
* @property { String } moderation_reason - The reason provided why this message moderates another
* @property { String } msgid - The root `id` attribute of the stanza
* @property { String } nick - The MUC nickname of the sender
* @property { String } oob_desc - The description of the XEP-0066 out of band data
* @property { String } oob_url - The URL of the XEP-0066 out of band data
* @property { String } origin_id - The XEP-0359 Origin ID
* @property { String } receipt_id - The `id` attribute of a XEP-0184 <receipt> element
* @property { String } received - An ISO8601 string recording the time that the message was received
* @property { String } replace_id - The `id` attribute of a XEP-0308 <replace> element
* @property { String } retracted - An ISO8601 string recording the time that the message was retracted
* @property { String } retracted_id - The `id` attribute of a XEP-424 <retracted> element
* @property { String } spoiler_hint The XEP-0382 spoiler hint
* @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
* @property { String } subject - The <subject> element value
* @property { String } thread - The <thread> element value
* @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
* @property { String } to - The recipient JID
* @property { String } type - The type of message
*/
let
attrs
=
Object
.
assign
({
from
,
nick
,
'
body
'
:
stanza
.
querySelector
(
'
body
'
)?.
textContent
?.
trim
(),
'
chat_state
'
:
getChatState
(
stanza
),
'
from_muc
'
:
Strophe
.
getBareJidFromJid
(
from
),
'
from_real_jid
'
:
chatbox
.
occupants
.
findOccupant
({
nick
})?.
get
(
'
jid
'
),
'
is_archived
'
:
st
.
isArchived
(
original_stanza
),
'
is_carbon
'
:
isCarbon
(
original_stanza
),
'
is_delayed
'
:
!!
delay
,
'
is_headline
'
:
st
.
isHeadline
(
stanza
),
'
is_markable
'
:
!!
sizzle
(
`markable[xmlns="
${
Strophe
.
NS
.
MARKERS
}
"]`
,
stanza
).
length
,
'
is_marker
'
:
!!
marker
,
'
is_unstyled
'
:
!!
sizzle
(
`unstyled[xmlns="
${
Strophe
.
NS
.
STYLING
}
"]`
,
stanza
).
length
,
'
marker_id
'
:
marker
&&
marker
.
getAttribute
(
'
id
'
),
'
msgid
'
:
stanza
.
getAttribute
(
'
id
'
)
||
original_stanza
.
getAttribute
(
'
id
'
),
'
receipt_id
'
:
getReceiptId
(
stanza
),
'
received
'
:
(
new
Date
()).
toISOString
(),
'
references
'
:
getReferences
(
stanza
),
'
subject
'
:
stanza
.
querySelector
(
'
subject
'
)?.
textContent
,
'
thread
'
:
stanza
.
querySelector
(
'
thread
'
)?.
textContent
,
'
time
'
:
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
now
,
'
to
'
:
stanza
.
getAttribute
(
'
to
'
),
'
type
'
:
stanza
.
getAttribute
(
'
type
'
),
},
getErrorAttributes
(
stanza
),
getOutOfBandAttributes
(
stanza
),
getSpoilerAttributes
(
stanza
),
getCorrectionAttributes
(
stanza
,
original_stanza
),
getStanzaIDs
(
stanza
,
original_stanza
),
getRetractionAttributes
(
stanza
,
original_stanza
),
getModerationAttributes
(
stanza
),
getEncryptionAttributes
(
stanza
,
_converse
)
);
await
api
.
emojis
.
initialize
();
attrs
=
Object
.
assign
({
'
is_only_emojis
'
:
attrs
.
body
?
u
.
isOnlyEmojis
(
attrs
.
body
)
:
false
,
'
is_valid_receipt_request
'
:
isValidReceiptRequest
(
stanza
,
attrs
),
'
message
'
:
attrs
.
body
||
attrs
.
error
,
// TODO: Remove and use body and error attributes instead
'
sender
'
:
attrs
.
nick
===
chatbox
.
get
(
'
nick
'
)
?
'
me
'
:
'
them
'
,
},
attrs
);
if
(
attrs
.
is_archived
&&
original_stanza
.
getAttribute
(
'
from
'
)
!==
attrs
.
from_muc
)
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM message from
${
original_stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
else
if
(
attrs
.
is_archived
&&
original_stanza
.
getAttribute
(
'
from
'
)
!==
chatbox
.
get
(
'
jid
'
))
{
return
new
StanzaParseError
(
`Invalid Stanza: Forged MAM groupchat message from
${
stanza
.
getAttribute
(
'
from
'
)}
`
,
stanza
);
}
else
if
(
attrs
.
is_carbon
)
{
return
new
StanzaParseError
(
"
Invalid Stanza: MUC messages SHOULD NOT be XEP-0280 carbon copied
"
,
stanza
);
}
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs
[
'
id
'
]
=
attrs
[
'
origin_id
'
]
||
attrs
[
`stanza_id
${(
attrs
.
from_muc
||
attrs
.
from
)}
`
]
||
u
.
getUniqueId
();
/**
* *Hook* which allows plugins to add additional parsing
* @event _converse#parseMUCMessage
*/
return
api
.
hook
(
'
parseMUCMessage
'
,
stanza
,
attrs
);
},
/**
* Parses a passed in MUC presence stanza and returns an object of attributes.
* @method st#parseMUCPresence
* @param { XMLElement } stanza - The presence stanza
* @returns { Object }
*/
parseMUCPresence
(
stanza
)
{
const
from
=
stanza
.
getAttribute
(
"
from
"
);
const
type
=
stanza
.
getAttribute
(
"
type
"
);
const
data
=
{
'
from
'
:
from
,
'
nick
'
:
Strophe
.
getResourceFromJid
(
from
),
'
type
'
:
type
,
'
states
'
:
[],
'
hats
'
:
[],
'
show
'
:
type
!==
'
unavailable
'
?
'
online
'
:
'
offline
'
};
Array
.
from
(
stanza
.
children
).
forEach
(
child
=>
{
if
(
child
.
matches
(
'
status
'
))
{
data
.
status
=
child
.
textContent
||
null
;
}
else
if
(
child
.
matches
(
'
show
'
))
{
data
.
show
=
child
.
textContent
||
'
online
'
;
}
else
if
(
child
.
matches
(
'
x
'
)
&&
child
.
getAttribute
(
'
xmlns
'
)
===
Strophe
.
NS
.
MUC_USER
)
{
Array
.
from
(
child
.
children
).
forEach
(
item
=>
{
if
(
item
.
nodeName
===
"
item
"
)
{
data
.
affiliation
=
item
.
getAttribute
(
"
affiliation
"
);
data
.
role
=
item
.
getAttribute
(
"
role
"
);
data
.
jid
=
item
.
getAttribute
(
"
jid
"
);
data
.
nick
=
item
.
getAttribute
(
"
nick
"
)
||
data
.
nick
;
}
else
if
(
item
.
nodeName
==
'
status
'
&&
item
.
getAttribute
(
"
code
"
))
{
data
.
states
.
push
(
item
.
getAttribute
(
"
code
"
));
}
});
}
else
if
(
child
.
matches
(
'
x
'
)
&&
child
.
getAttribute
(
'
xmlns
'
)
===
Strophe
.
NS
.
VCARDUPDATE
)
{
data
.
image_hash
=
child
.
querySelector
(
'
photo
'
)?.
textContent
;
}
else
if
(
child
.
matches
(
'
hats
'
)
&&
child
.
getAttribute
(
'
xmlns
'
)
===
Strophe
.
NS
.
MUC_HATS
)
{
data
[
'
hats
'
]
=
Array
.
from
(
child
.
children
).
map
(
c
=>
c
.
matches
(
'
hat
'
)
&&
{
'
title
'
:
c
.
getAttribute
(
'
title
'
),
'
uri
'
:
c
.
getAttribute
(
'
uri
'
)
});
}
});
return
data
;
}
}
export
default
st
;
src/modals/muc-list.js
View file @
e80afbfe
import
BootstrapModal
from
"
./base.js
"
;
import
log
from
"
@converse/headless/log
"
;
import
st
from
"
@converse/headless/utils/stanza
"
;
import
tpl_list_chatrooms_modal
from
"
./templates/muc-list.js
"
;
import
tpl_room_description
from
"
templates/room_description.html
"
;
import
tpl_spinner
from
"
templates/spinner.js
"
;
import
{
__
}
from
'
../i18n
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
{
getAttributes
}
from
'
@converse/headless/shared/parsers
'
;
import
{
head
}
from
"
lodash-es
"
;
const
{
Strophe
,
$iq
,
sizzle
}
=
converse
.
env
;
...
...
@@ -144,7 +144,7 @@ export default BootstrapModal.extend({
const
rooms
=
iq
?
sizzle
(
'
query item
'
,
iq
)
:
[];
if
(
rooms
.
length
)
{
this
.
model
.
set
({
'
feedback_text
'
:
__
(
'
Groupchats found
'
)},
{
'
silent
'
:
true
});
this
.
items
=
rooms
.
map
(
st
.
getAttributes
);
this
.
items
=
rooms
.
map
(
getAttributes
);
}
else
{
this
.
items
=
[];
this
.
model
.
set
({
'
feedback_text
'
:
__
(
'
No groupchats found
'
)},
{
'
silent
'
:
true
});
...
...
src/plugins/muc-views/index.js
View file @
e80afbfe
...
...
@@ -7,7 +7,6 @@
import
'
../../components/muc-sidebar
'
;
import
'
../chatview/index.js
'
;
import
'
../modal.js
'
;
import
'
@converse/headless/utils/muc
'
;
import
ChatRoomViewMixin
from
'
./muc.js
'
;
import
MUCConfigForm
from
'
./config-form.js
'
;
import
MUCPasswordForm
from
'
./password-form.js
'
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a 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