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
7f851208
Commit
7f851208
authored
Jan 26, 2021
by
JC Brand
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move converse-roster plugin into folder and split up
parent
7199e63f
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1162 additions
and
1121 deletions
+1162
-1121
src/headless/headless.js
src/headless/headless.js
+15
-15
src/headless/plugins/roster.js
src/headless/plugins/roster.js
+0
-1105
src/headless/plugins/roster/api.js
src/headless/plugins/roster/api.js
+73
-0
src/headless/plugins/roster/contact.js
src/headless/plugins/roster/contact.js
+182
-0
src/headless/plugins/roster/contacts.js
src/headless/plugins/roster/contacts.js
+465
-0
src/headless/plugins/roster/group.js
src/headless/plugins/roster/group.js
+18
-0
src/headless/plugins/roster/groups.js
src/headless/plugins/roster/groups.js
+58
-0
src/headless/plugins/roster/index.js
src/headless/plugins/roster/index.js
+222
-0
src/headless/plugins/roster/presence.js
src/headless/plugins/roster/presence.js
+84
-0
src/headless/plugins/roster/utils.js
src/headless/plugins/roster/utils.js
+44
-0
src/plugins/rosterview/index.js
src/plugins/rosterview/index.js
+1
-1
No files found.
src/headless/headless.js
View file @
7f851208
...
@@ -15,7 +15,7 @@ import "./plugins/mam/index.js"; // XEP-0313 Message Archive Management
...
@@ -15,7 +15,7 @@ import "./plugins/mam/index.js"; // XEP-0313 Message Archive Management
import
"
./plugins/muc/index.js
"
;
// XEP-0045 Multi-user chat
import
"
./plugins/muc/index.js
"
;
// XEP-0045 Multi-user chat
import
"
./plugins/ping.js
"
;
// XEP-0199 XMPP Ping
import
"
./plugins/ping.js
"
;
// XEP-0199 XMPP Ping
import
"
./plugins/pubsub.js
"
;
// XEP-0060 Pubsub
import
"
./plugins/pubsub.js
"
;
// XEP-0060 Pubsub
import
"
./plugins/roster
.js
"
;
// RFC-6121 Contacts Roster
import
"
./plugins/roster
/index.js
"
;
// RFC-6121 Contacts Roster
import
"
./plugins/smacks.js
"
;
// XEP-0198 Stream Management
import
"
./plugins/smacks.js
"
;
// XEP-0198 Stream Management
import
"
./plugins/status.js
"
;
// XEP-0199 XMPP Ping
import
"
./plugins/status.js
"
;
// XEP-0199 XMPP Ping
import
"
./plugins/vcard.js
"
;
// XEP-0054 VCard-temp
import
"
./plugins/vcard.js
"
;
// XEP-0054 VCard-temp
...
...
src/headless/plugins/roster.js
deleted
100644 → 0
View file @
7199e63f
/**
* @module converse-roster
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import
"
@converse/headless/plugins/status
"
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
invoke
,
isNaN
,
propertyOf
,
sum
}
from
"
lodash-es
"
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
log
from
"
../log.js
"
;
const
{
Strophe
,
$iq
,
$pres
,
dayjs
,
sizzle
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
converse
.
plugins
.
add
(
'
converse-roster
'
,
{
dependencies
:
[
'
converse-status
'
],
initialize
()
{
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const
{
__
}
=
_converse
;
api
.
settings
.
extend
({
'
allow_contact_requests
'
:
true
,
'
auto_subscribe
'
:
false
,
'
synchronize_availability
'
:
true
,
});
api
.
promises
.
add
([
'
cachedRoster
'
,
'
roster
'
,
'
rosterContactsFetched
'
,
'
rosterGroupsFetched
'
,
'
rosterInitialized
'
,
]);
_converse
.
HEADER_CURRENT_CONTACTS
=
__
(
'
My contacts
'
);
_converse
.
HEADER_PENDING_CONTACTS
=
__
(
'
Pending contacts
'
);
_converse
.
HEADER_REQUESTING_CONTACTS
=
__
(
'
Contact requests
'
);
_converse
.
HEADER_UNGROUPED
=
__
(
'
Ungrouped
'
);
_converse
.
HEADER_UNREAD
=
__
(
'
New messages
'
);
const
HEADER_WEIGHTS
=
{};
HEADER_WEIGHTS
[
_converse
.
HEADER_UNREAD
]
=
0
;
HEADER_WEIGHTS
[
_converse
.
HEADER_REQUESTING_CONTACTS
]
=
1
;
HEADER_WEIGHTS
[
_converse
.
HEADER_CURRENT_CONTACTS
]
=
2
;
HEADER_WEIGHTS
[
_converse
.
HEADER_UNGROUPED
]
=
3
;
HEADER_WEIGHTS
[
_converse
.
HEADER_PENDING_CONTACTS
]
=
4
;
_converse
.
registerPresenceHandler
=
function
()
{
_converse
.
unregisterPresenceHandler
();
_converse
.
presence_ref
=
_converse
.
connection
.
addHandler
(
presence
=>
{
_converse
.
roster
.
presenceHandler
(
presence
);
return
true
;
},
null
,
'
presence
'
,
null
);
};
/**
* Reject or cancel another user's subscription to our presence updates.
* @method rejectPresenceSubscription
* @private
* @memberOf _converse
* @param { String } jid - The Jabber ID of the user whose subscription is being canceled
* @param { String } message - An optional message to the user
*/
_converse
.
rejectPresenceSubscription
=
function
(
jid
,
message
)
{
const
pres
=
$pres
({
to
:
jid
,
type
:
"
unsubscribed
"
});
if
(
message
&&
message
!==
""
)
{
pres
.
c
(
"
status
"
).
t
(
message
);
}
api
.
send
(
pres
);
};
_converse
.
sendInitialPresence
=
function
()
{
if
(
_converse
.
send_initial_presence
)
{
api
.
user
.
presence
.
send
();
}
};
/**
* Fetch all the roster groups, and then the roster contacts.
* Emit an event after fetching is done in each case.
* @private
* @method _converse.populateRoster
* @param { Bool } ignore_cache - If set to to true, the local cache
* will be ignored it's guaranteed that the XMPP server
* will be queried for the roster.
*/
_converse
.
populateRoster
=
async
function
(
ignore_cache
=
false
)
{
if
(
ignore_cache
)
{
_converse
.
send_initial_presence
=
true
;
}
try
{
await
_converse
.
rostergroups
.
fetchRosterGroups
();
/**
* Triggered once roster groups have been fetched. Used by the
* `converse-rosterview.js` plugin to know when it can start alphabetically
* position roster groups.
* @event _converse#rosterGroupsFetched
* @example _converse.api.listen.on('rosterGroupsFetched', () => { ... });
* @example _converse.api.waitUntil('rosterGroupsFetched').then(() => { ... });
*/
api
.
trigger
(
'
rosterGroupsFetched
'
);
await
_converse
.
roster
.
fetchRosterContacts
();
api
.
trigger
(
'
rosterContactsFetched
'
);
}
catch
(
reason
)
{
log
.
error
(
reason
);
}
finally
{
_converse
.
sendInitialPresence
();
}
};
const
Resource
=
Model
.
extend
({
'
idAttribute
'
:
'
name
'
});
const
Resources
=
Collection
.
extend
({
'
model
'
:
Resource
});
_converse
.
Presence
=
Model
.
extend
({
defaults
:
{
'
show
'
:
'
offline
'
},
initialize
()
{
this
.
resources
=
new
Resources
();
const
id
=
`converse.identities-
${
this
.
get
(
'
jid
'
)}
`
;
this
.
resources
.
browserStorage
=
_converse
.
createStore
(
id
,
"
session
"
);
this
.
listenTo
(
this
.
resources
,
'
update
'
,
this
.
onResourcesChanged
);
this
.
listenTo
(
this
.
resources
,
'
change
'
,
this
.
onResourcesChanged
);
},
onResourcesChanged
()
{
const
hpr
=
this
.
getHighestPriorityResource
();
const
show
=
hpr
?.
attributes
?.
show
||
'
offline
'
;
if
(
this
.
get
(
'
show
'
)
!==
show
)
{
this
.
save
({
'
show
'
:
show
});
}
},
/**
* Return the resource with the highest priority.
* If multiple resources have the same priority, take the latest one.
* @private
*/
getHighestPriorityResource
()
{
return
this
.
resources
.
sortBy
(
r
=>
`
${
r
.
get
(
'
priority
'
)}
-
${
r
.
get
(
'
timestamp
'
)}
`
).
reverse
()[
0
];
},
/**
* Adds a new resource and it's associated attributes as taken
* from the passed in presence stanza.
* Also updates the presence if the resource has higher priority (and is newer).
* @private
* @param { XMLElement } presence: The presence stanza
*/
addResource
(
presence
)
{
const
jid
=
presence
.
getAttribute
(
'
from
'
),
name
=
Strophe
.
getResourceFromJid
(
jid
),
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
presence
).
pop
(),
priority
=
propertyOf
(
presence
.
querySelector
(
'
priority
'
))(
'
textContent
'
)
||
0
,
resource
=
this
.
resources
.
get
(
name
),
settings
=
{
'
name
'
:
name
,
'
priority
'
:
isNaN
(
parseInt
(
priority
,
10
))
?
0
:
parseInt
(
priority
,
10
),
'
show
'
:
propertyOf
(
presence
.
querySelector
(
'
show
'
))(
'
textContent
'
)
||
'
online
'
,
'
timestamp
'
:
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
(
new
Date
()).
toISOString
()
};
if
(
resource
)
{
resource
.
save
(
settings
);
}
else
{
this
.
resources
.
create
(
settings
);
}
},
/**
* Remove the passed in resource from the resources map.
* Also redetermines the presence given that there's one less
* resource.
* @private
* @param { string } name: The resource name
*/
removeResource
(
name
)
{
const
resource
=
this
.
resources
.
get
(
name
);
if
(
resource
)
{
resource
.
destroy
();
}
}
});
_converse
.
Presences
=
Collection
.
extend
({
model
:
_converse
.
Presence
,
});
/**
* @class
* @namespace _converse.RosterContact
* @memberOf _converse
*/
_converse
.
RosterContact
=
Model
.
extend
({
defaults
:
{
'
chat_state
'
:
undefined
,
'
image
'
:
_converse
.
DEFAULT_IMAGE
,
'
image_type
'
:
_converse
.
DEFAULT_IMAGE_TYPE
,
'
num_unread
'
:
0
,
'
status
'
:
undefined
,
},
async
initialize
(
attributes
)
{
this
.
initialized
=
u
.
getResolveablePromise
();
this
.
setPresence
();
const
{
jid
}
=
attributes
;
const
bare_jid
=
Strophe
.
getBareJidFromJid
(
jid
).
toLowerCase
();
attributes
.
jid
=
bare_jid
;
this
.
set
(
Object
.
assign
({
'
groups
'
:
[],
'
id
'
:
bare_jid
,
'
jid
'
:
bare_jid
,
'
user_id
'
:
Strophe
.
getNodeFromJid
(
jid
)
},
attributes
));
/**
* When a contact's presence status has changed.
* The presence status is either `online`, `offline`, `dnd`, `away` or `xa`.
* @event _converse#contactPresenceChanged
* @type { _converse.RosterContact }
* @example _converse.api.listen.on('contactPresenceChanged', contact => { ... });
*/
this
.
listenTo
(
this
.
presence
,
'
change:show
'
,
()
=>
api
.
trigger
(
'
contactPresenceChanged
'
,
this
));
this
.
listenTo
(
this
.
presence
,
'
change:show
'
,
()
=>
this
.
trigger
(
'
presenceChanged
'
));
/**
* Synchronous event which provides a hook for further initializing a RosterContact
* @event _converse#rosterContactInitialized
* @param { _converse.RosterContact } contact
*/
await
api
.
trigger
(
'
rosterContactInitialized
'
,
this
,
{
'
Synchronous
'
:
true
});
this
.
initialized
.
resolve
();
},
setPresence
()
{
const
jid
=
this
.
get
(
'
jid
'
);
this
.
presence
=
_converse
.
presences
.
findWhere
({
'
jid
'
:
jid
})
||
_converse
.
presences
.
create
({
'
jid
'
:
jid
});
},
openChat
()
{
const
attrs
=
this
.
attributes
;
api
.
chats
.
open
(
attrs
.
jid
,
attrs
,
true
);
},
/**
* Return a string of tab-separated values that are to be used when
* matching against filter text.
*
* The goal is to be able to filter against the VCard fullname,
* roster nickname and JID.
* @returns { String } Lower-cased, tab-separated values
*/
getFilterCriteria
()
{
const
nick
=
this
.
get
(
'
nickname
'
);
const
jid
=
this
.
get
(
'
jid
'
);
let
criteria
=
this
.
getDisplayName
();
criteria
=
!
criteria
.
includes
(
jid
)
?
criteria
.
concat
(
`
${
jid
}
`
)
:
criteria
;
criteria
=
!
criteria
.
includes
(
nick
)
?
criteria
.
concat
(
`
${
nick
}
`
)
:
criteria
;
return
criteria
.
toLowerCase
();
},
getDisplayName
()
{
// Gets overridden in converse-vcard where the fullname is may be returned
if
(
this
.
get
(
'
nickname
'
))
{
return
this
.
get
(
'
nickname
'
);
}
else
{
return
this
.
get
(
'
jid
'
);
}
},
getFullname
()
{
// Gets overridden in converse-vcard where the fullname may be returned
return
this
.
get
(
'
jid
'
);
},
/**
* Send a presence subscription request to this roster contact
* @private
* @method _converse.RosterContacts#subscribe
* @param { String } message - An optional message to explain the
* reason for the subscription request.
*/
subscribe
(
message
)
{
const
pres
=
$pres
({
to
:
this
.
get
(
'
jid
'
),
type
:
"
subscribe
"
});
if
(
message
&&
message
!==
""
)
{
pres
.
c
(
"
status
"
).
t
(
message
).
up
();
}
const
nick
=
_converse
.
xmppstatus
.
getNickname
()
||
_converse
.
xmppstatus
.
getFullname
();
if
(
nick
)
{
pres
.
c
(
'
nick
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
NICK
}).
t
(
nick
).
up
();
}
api
.
send
(
pres
);
this
.
save
(
'
ask
'
,
"
subscribe
"
);
// ask === 'subscribe' Means we have asked to subscribe to them.
return
this
;
},
/**
* Upon receiving the presence stanza of type "subscribed",
* the user SHOULD acknowledge receipt of that subscription
* state notification by sending a presence stanza of type
* "subscribe" to the contact
* @private
* @method _converse.RosterContacts#ackSubscribe
*/
ackSubscribe
()
{
api
.
send
(
$pres
({
'
type
'
:
'
subscribe
'
,
'
to
'
:
this
.
get
(
'
jid
'
)
}));
},
/**
* Upon receiving the presence stanza of type "unsubscribed",
* the user SHOULD acknowledge receipt of that subscription state
* notification by sending a presence stanza of type "unsubscribe"
* this step lets the user's server know that it MUST no longer
* send notification of the subscription state change to the user.
* @private
* @method _converse.RosterContacts#ackUnsubscribe
* @param { String } jid - The Jabber ID of the user who is unsubscribing
*/
ackUnsubscribe
()
{
api
.
send
(
$pres
({
'
type
'
:
'
unsubscribe
'
,
'
to
'
:
this
.
get
(
'
jid
'
)}));
this
.
removeFromRoster
();
this
.
destroy
();
},
/**
* Unauthorize this contact's presence subscription
* @private
* @method _converse.RosterContacts#unauthorize
* @param { String } message - Optional message to send to the person being unauthorized
*/
unauthorize
(
message
)
{
_converse
.
rejectPresenceSubscription
(
this
.
get
(
'
jid
'
),
message
);
return
this
;
},
/**
* Authorize presence subscription
* @private
* @method _converse.RosterContacts#authorize
* @param { String } message - Optional message to send to the person being authorized
*/
authorize
(
message
)
{
const
pres
=
$pres
({
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
"
subscribed
"
});
if
(
message
&&
message
!==
""
)
{
pres
.
c
(
"
status
"
).
t
(
message
);
}
api
.
send
(
pres
);
return
this
;
},
/**
* Instruct the XMPP server to remove this contact from our roster
* @private
* @method _converse.RosterContacts#
* @returns { Promise }
*/
removeFromRoster
()
{
const
iq
=
$iq
({
type
:
'
set
'
})
.
c
(
'
query
'
,
{
xmlns
:
Strophe
.
NS
.
ROSTER
})
.
c
(
'
item
'
,
{
jid
:
this
.
get
(
'
jid
'
),
subscription
:
"
remove
"
});
return
api
.
sendIQ
(
iq
);
}
});
/**
* @class
* @namespace _converse.RosterContacts
* @memberOf _converse
*/
_converse
.
RosterContacts
=
Collection
.
extend
({
model
:
_converse
.
RosterContact
,
comparator
(
contact1
,
contact2
)
{
// Groups are sorted alphabetically, ignoring case.
// However, Ungrouped, Requesting Contacts and Pending Contacts
// appear last and in that order.
const
status1
=
contact1
.
presence
.
get
(
'
show
'
)
||
'
offline
'
;
const
status2
=
contact2
.
presence
.
get
(
'
show
'
)
||
'
offline
'
;
if
(
_converse
.
STATUS_WEIGHTS
[
status1
]
===
_converse
.
STATUS_WEIGHTS
[
status2
])
{
const
name1
=
(
contact1
.
getDisplayName
()).
toLowerCase
();
const
name2
=
(
contact2
.
getDisplayName
()).
toLowerCase
();
return
name1
<
name2
?
-
1
:
(
name1
>
name2
?
1
:
0
);
}
else
{
return
_converse
.
STATUS_WEIGHTS
[
status1
]
<
_converse
.
STATUS_WEIGHTS
[
status2
]
?
-
1
:
1
;
}
},
onConnected
()
{
// Called as soon as the connection has been established
// (either after initial login, or after reconnection).
// Use the opportunity to register stanza handlers.
this
.
registerRosterHandler
();
this
.
registerRosterXHandler
();
},
registerRosterHandler
()
{
// Register a handler for roster IQ "set" stanzas, which update
// roster contacts.
_converse
.
connection
.
addHandler
(
iq
=>
{
_converse
.
roster
.
onRosterPush
(
iq
);
return
true
;
},
Strophe
.
NS
.
ROSTER
,
'
iq
'
,
"
set
"
);
},
registerRosterXHandler
()
{
// Register a handler for RosterX message stanzas, which are
// used to suggest roster contacts to a user.
let
t
=
0
;
_converse
.
connection
.
addHandler
(
function
(
msg
)
{
window
.
setTimeout
(
function
()
{
_converse
.
connection
.
flush
();
_converse
.
roster
.
subscribeToSuggestedItems
.
bind
(
_converse
.
roster
)(
msg
);
},
t
);
t
+=
msg
.
querySelectorAll
(
'
item
'
).
length
*
250
;
return
true
;
},
Strophe
.
NS
.
ROSTERX
,
'
message
'
,
null
);
},
/**
* Fetches the roster contacts, first by trying the browser cache,
* and if that's empty, then by querying the XMPP server.
* @private
* @returns {promise} Promise which resolves once the contacts have been fetched.
*/
async
fetchRosterContacts
()
{
const
result
=
await
new
Promise
((
resolve
,
reject
)
=>
{
this
.
fetch
({
'
add
'
:
true
,
'
silent
'
:
true
,
'
success
'
:
resolve
,
'
error
'
:
(
c
,
e
)
=>
reject
(
e
)
});
});
if
(
u
.
isErrorObject
(
result
))
{
log
.
error
(
result
);
// Force a full roster refresh
_converse
.
session
.
set
(
'
roster_cached
'
,
false
)
this
.
data
.
save
(
'
version
'
,
undefined
);
}
if
(
_converse
.
session
.
get
(
'
roster_cached
'
))
{
/**
* The contacts roster has been retrieved from the local cache (`sessionStorage`).
* @event _converse#cachedRoster
* @type { _converse.RosterContacts }
* @example _converse.api.listen.on('cachedRoster', (items) => { ... });
* @example _converse.api.waitUntil('cachedRoster').then(items => { ... });
*/
api
.
trigger
(
'
cachedRoster
'
,
result
);
}
else
{
_converse
.
send_initial_presence
=
true
;
return
_converse
.
roster
.
fetchFromServer
();
}
},
subscribeToSuggestedItems
(
msg
)
{
Array
.
from
(
msg
.
querySelectorAll
(
'
item
'
)).
forEach
(
item
=>
{
if
(
item
.
getAttribute
(
'
action
'
)
===
'
add
'
)
{
_converse
.
roster
.
addAndSubscribe
(
item
.
getAttribute
(
'
jid
'
),
_converse
.
xmppstatus
.
getNickname
()
||
_converse
.
xmppstatus
.
getFullname
()
);
}
});
return
true
;
},
isSelf
(
jid
)
{
return
u
.
isSameBareJID
(
jid
,
_converse
.
connection
.
jid
);
},
/**
* Add a roster contact and then once we have confirmation from
* the XMPP server we subscribe to that contact's presence updates.
* @private
* @method _converse.RosterContacts#addAndSubscribe
* @param { String } jid - The Jabber ID of the user being added and subscribed to.
* @param { String } name - The name of that user
* @param { Array.String } groups - Any roster groups the user might belong to
* @param { String } message - An optional message to explain the reason for the subscription request.
* @param { Object } attributes - Any additional attributes to be stored on the user's model.
*/
async
addAndSubscribe
(
jid
,
name
,
groups
,
message
,
attributes
)
{
const
contact
=
await
this
.
addContactToRoster
(
jid
,
name
,
groups
,
attributes
);
if
(
contact
instanceof
_converse
.
RosterContact
)
{
contact
.
subscribe
(
message
);
}
},
/**
* Send an IQ stanza to the XMPP server to add a new roster contact.
* @private
* @method _converse.RosterContacts#sendContactAddIQ
* @param { String } jid - The Jabber ID of the user being added
* @param { String } name - The name of that user
* @param { Array.String } groups - Any roster groups the user might belong to
* @param { Function } callback - A function to call once the IQ is returned
* @param { Function } errback - A function to call if an error occurred
*/
sendContactAddIQ
(
jid
,
name
,
groups
)
{
name
=
name
?
name
:
null
;
const
iq
=
$iq
({
'
type
'
:
'
set
'
})
.
c
(
'
query
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
ROSTER
})
.
c
(
'
item
'
,
{
jid
,
name
});
groups
.
forEach
(
g
=>
iq
.
c
(
'
group
'
).
t
(
g
).
up
());
return
api
.
sendIQ
(
iq
);
},
/**
* Adds a RosterContact instance to _converse.roster and
* registers the contact on the XMPP server.
* Returns a promise which is resolved once the XMPP server has responded.
* @private
* @method _converse.RosterContacts#addContactToRoster
* @param { String } jid - The Jabber ID of the user being added and subscribed to.
* @param { String } name - The name of that user
* @param { Array.String } groups - Any roster groups the user might belong to
* @param { Object } attributes - Any additional attributes to be stored on the user's model.
*/
async
addContactToRoster
(
jid
,
name
,
groups
,
attributes
)
{
await
api
.
waitUntil
(
'
rosterContactsFetched
'
);
groups
=
groups
||
[];
try
{
await
this
.
sendContactAddIQ
(
jid
,
name
,
groups
);
}
catch
(
e
)
{
log
.
error
(
e
);
alert
(
__
(
'
Sorry, there was an error while trying to add %1$s as a contact.
'
,
name
||
jid
));
return
e
;
}
return
this
.
create
(
Object
.
assign
({
'
ask
'
:
undefined
,
'
nickname
'
:
name
,
groups
,
jid
,
'
requesting
'
:
false
,
'
subscription
'
:
'
none
'
},
attributes
),
{
'
sort
'
:
false
});
},
async
subscribeBack
(
bare_jid
,
presence
)
{
const
contact
=
this
.
get
(
bare_jid
);
if
(
contact
instanceof
_converse
.
RosterContact
)
{
contact
.
authorize
().
subscribe
();
}
else
{
// Can happen when a subscription is retried or roster was deleted
const
nickname
=
sizzle
(
`nick[xmlns="
${
Strophe
.
NS
.
NICK
}
"]`
,
presence
).
pop
()?.
textContent
||
null
;
const
contact
=
await
this
.
addContactToRoster
(
bare_jid
,
nickname
,
[],
{
'
subscription
'
:
'
from
'
});
if
(
contact
instanceof
_converse
.
RosterContact
)
{
contact
.
authorize
().
subscribe
();
}
}
},
getNumOnlineContacts
()
{
const
ignored
=
[
'
offline
'
,
'
unavailable
'
];
return
sum
(
this
.
models
.
filter
(
m
=>
!
ignored
.
includes
(
m
.
presence
.
get
(
'
show
'
))));
},
/**
* Handle roster updates from the XMPP server.
* See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
* @private
* @method _converse.RosterContacts#onRosterPush
* @param { XMLElement } IQ - The IQ stanza received from the XMPP server.
*/
onRosterPush
(
iq
)
{
const
id
=
iq
.
getAttribute
(
'
id
'
);
const
from
=
iq
.
getAttribute
(
'
from
'
);
if
(
from
&&
from
!==
_converse
.
bare_jid
)
{
// https://tools.ietf.org/html/rfc6121#page-15
//
// A receiving client MUST ignore the stanza unless it has no 'from'
// attribute (i.e., implicitly from the bare JID of the user's
// account) or it has a 'from' attribute whose value matches the
// user's bare JID <user@domainpart>.
log
.
warn
(
`Ignoring roster illegitimate roster push message from
${
iq
.
getAttribute
(
'
from
'
)}
`
);
return
;
}
api
.
send
(
$iq
({
type
:
'
result
'
,
id
,
from
:
_converse
.
connection
.
jid
}));
const
query
=
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
ROSTER
}
"]`
,
iq
).
pop
();
this
.
data
.
save
(
'
version
'
,
query
.
getAttribute
(
'
ver
'
));
const
items
=
sizzle
(
`item`
,
query
);
if
(
items
.
length
>
1
)
{
log
.
error
(
iq
);
throw
new
Error
(
'
Roster push query may not contain more than one "item" element.
'
);
}
if
(
items
.
length
===
0
)
{
log
.
warn
(
iq
);
log
.
warn
(
'
Received a roster push stanza without an "item" element.
'
);
return
;
}
this
.
updateContact
(
items
.
pop
());
/**
* When the roster receives a push event from server (i.e. new entry in your contacts roster).
* @event _converse#rosterPush
* @type { XMLElement }
* @example _converse.api.listen.on('rosterPush', iq => { ... });
*/
api
.
trigger
(
'
rosterPush
'
,
iq
);
return
;
},
rosterVersioningSupported
()
{
return
api
.
disco
.
stream
.
getFeature
(
'
ver
'
,
'
urn:xmpp:features:rosterver
'
)
&&
this
.
data
.
get
(
'
version
'
);
},
/**
* Fetch the roster from the XMPP server
* @private
* @emits _converse#roster
* @returns {promise}
*/
async
fetchFromServer
()
{
const
stanza
=
$iq
({
'
type
'
:
'
get
'
,
'
id
'
:
u
.
getUniqueId
(
'
roster
'
)
}).
c
(
'
query
'
,
{
xmlns
:
Strophe
.
NS
.
ROSTER
});
if
(
this
.
rosterVersioningSupported
())
{
stanza
.
attrs
({
'
ver
'
:
this
.
data
.
get
(
'
version
'
)});
}
const
iq
=
await
api
.
sendIQ
(
stanza
,
null
,
false
);
if
(
iq
.
getAttribute
(
'
type
'
)
!==
'
error
'
)
{
const
query
=
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
ROSTER
}
"]`
,
iq
).
pop
();
if
(
query
)
{
const
items
=
sizzle
(
`item`
,
query
);
items
.
forEach
(
item
=>
this
.
updateContact
(
item
));
this
.
data
.
save
(
'
version
'
,
query
.
getAttribute
(
'
ver
'
));
}
}
else
if
(
!
u
.
isServiceUnavailableError
(
iq
))
{
// Some unknown error happened, so we will try to fetch again if the page reloads.
log
.
error
(
iq
);
log
.
error
(
"
Error while trying to fetch roster from the server
"
);
return
;
}
_converse
.
session
.
save
(
'
roster_cached
'
,
true
);
/**
* When the roster has been received from the XMPP server.
* See also the `cachedRoster` event further up, which gets called instead of
* `roster` if its already in `sessionStorage`.
* @event _converse#roster
* @type { XMLElement }
* @example _converse.api.listen.on('roster', iq => { ... });
* @example _converse.api.waitUntil('roster').then(iq => { ... });
*/
api
.
trigger
(
'
roster
'
,
iq
);
},
/* Update or create RosterContact models based on the given `item` XML
* node received in the resulting IQ stanza from the server.
* @private
* @param { XMLElement } item
*/
updateContact
(
item
)
{
const
jid
=
item
.
getAttribute
(
'
jid
'
);
const
contact
=
this
.
get
(
jid
);
const
subscription
=
item
.
getAttribute
(
"
subscription
"
);
const
ask
=
item
.
getAttribute
(
"
ask
"
);
const
groups
=
Array
.
from
(
item
.
getElementsByTagName
(
'
group
'
)).
map
(
e
=>
e
.
textContent
);
if
(
!
contact
)
{
if
((
subscription
===
"
none
"
&&
ask
===
null
)
||
(
subscription
===
"
remove
"
))
{
return
;
// We're lazy when adding contacts.
}
this
.
create
({
'
ask
'
:
ask
,
'
nickname
'
:
item
.
getAttribute
(
"
name
"
),
'
groups
'
:
groups
,
'
jid
'
:
jid
,
'
subscription
'
:
subscription
},
{
sort
:
false
});
}
else
{
if
(
subscription
===
"
remove
"
)
{
return
contact
.
destroy
();
}
// We only find out about requesting contacts via the
// presence handler, so if we receive a contact
// here, we know they aren't requesting anymore.
// see docs/DEVELOPER.rst
contact
.
save
({
'
subscription
'
:
subscription
,
'
ask
'
:
ask
,
'
nickname
'
:
item
.
getAttribute
(
"
name
"
),
'
requesting
'
:
null
,
'
groups
'
:
groups
});
}
},
createRequestingContact
(
presence
)
{
const
bare_jid
=
Strophe
.
getBareJidFromJid
(
presence
.
getAttribute
(
'
from
'
));
const
nickname
=
sizzle
(
`nick[xmlns="
${
Strophe
.
NS
.
NICK
}
"]`
,
presence
).
pop
()?.
textContent
||
null
;
const
user_data
=
{
'
jid
'
:
bare_jid
,
'
subscription
'
:
'
none
'
,
'
ask
'
:
null
,
'
requesting
'
:
true
,
'
nickname
'
:
nickname
};
/**
* Triggered when someone has requested to subscribe to your presence (i.e. to be your contact).
* @event _converse#contactRequest
* @type { _converse.RosterContact }
* @example _converse.api.listen.on('contactRequest', contact => { ... });
*/
api
.
trigger
(
'
contactRequest
'
,
this
.
create
(
user_data
));
},
handleIncomingSubscription
(
presence
)
{
const
jid
=
presence
.
getAttribute
(
'
from
'
),
bare_jid
=
Strophe
.
getBareJidFromJid
(
jid
),
contact
=
this
.
get
(
bare_jid
);
if
(
!
api
.
settings
.
get
(
'
allow_contact_requests
'
))
{
_converse
.
rejectPresenceSubscription
(
jid
,
__
(
"
This client does not allow presence subscriptions
"
)
);
}
if
(
api
.
settings
.
get
(
'
auto_subscribe
'
))
{
if
((
!
contact
)
||
(
contact
.
get
(
'
subscription
'
)
!==
'
to
'
))
{
this
.
subscribeBack
(
bare_jid
,
presence
);
}
else
{
contact
.
authorize
();
}
}
else
{
if
(
contact
)
{
if
(
contact
.
get
(
'
subscription
'
)
!==
'
none
'
)
{
contact
.
authorize
();
}
else
if
(
contact
.
get
(
'
ask
'
)
===
"
subscribe
"
)
{
contact
.
authorize
();
}
}
else
{
this
.
createRequestingContact
(
presence
);
}
}
},
handleOwnPresence
(
presence
)
{
const
jid
=
presence
.
getAttribute
(
'
from
'
),
resource
=
Strophe
.
getResourceFromJid
(
jid
),
presence_type
=
presence
.
getAttribute
(
'
type
'
);
if
((
_converse
.
connection
.
jid
!==
jid
)
&&
(
presence_type
!==
'
unavailable
'
)
&&
(
api
.
settings
.
get
(
'
synchronize_availability
'
)
===
true
||
api
.
settings
.
get
(
'
synchronize_availability
'
)
===
resource
))
{
// Another resource has changed its status and
// synchronize_availability option set to update,
// we'll update ours as well.
const
show
=
propertyOf
(
presence
.
querySelector
(
'
show
'
))(
'
textContent
'
)
||
'
online
'
;
_converse
.
xmppstatus
.
save
({
'
status
'
:
show
},
{
'
silent
'
:
true
});
const
status_message
=
propertyOf
(
presence
.
querySelector
(
'
status
'
))(
'
textContent
'
);
if
(
status_message
)
{
_converse
.
xmppstatus
.
save
({
'
status_message
'
:
status_message
});
}
}
if
(
_converse
.
jid
===
jid
&&
presence_type
===
'
unavailable
'
)
{
// XXX: We've received an "unavailable" presence from our
// own resource. Apparently this happens due to a
// Prosody bug, whereby we send an IQ stanza to remove
// a roster contact, and Prosody then sends
// "unavailable" globally, instead of directed to the
// particular user that's removed.
//
// Here is the bug report: https://prosody.im/issues/1121
//
// I'm not sure whether this might legitimately happen
// in other cases.
//
// As a workaround for now we simply send our presence again,
// otherwise we're treated as offline.
api
.
user
.
presence
.
send
();
}
},
presenceHandler
(
presence
)
{
const
presence_type
=
presence
.
getAttribute
(
'
type
'
);
if
(
presence_type
===
'
error
'
)
{
return
true
;
}
const
jid
=
presence
.
getAttribute
(
'
from
'
),
bare_jid
=
Strophe
.
getBareJidFromJid
(
jid
);
if
(
this
.
isSelf
(
bare_jid
))
{
return
this
.
handleOwnPresence
(
presence
);
}
else
if
(
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
MUC
}
"]`
,
presence
).
length
)
{
return
;
// Ignore MUC
}
const
status_message
=
propertyOf
(
presence
.
querySelector
(
'
status
'
))(
'
textContent
'
),
contact
=
this
.
get
(
bare_jid
);
if
(
contact
&&
(
status_message
!==
contact
.
get
(
'
status
'
)))
{
contact
.
save
({
'
status
'
:
status_message
});
}
if
(
presence_type
===
'
subscribed
'
&&
contact
)
{
contact
.
ackSubscribe
();
}
else
if
(
presence_type
===
'
unsubscribed
'
&&
contact
)
{
contact
.
ackUnsubscribe
();
}
else
if
(
presence_type
===
'
unsubscribe
'
)
{
return
;
}
else
if
(
presence_type
===
'
subscribe
'
)
{
this
.
handleIncomingSubscription
(
presence
);
}
else
if
(
presence_type
===
'
unavailable
'
&&
contact
)
{
const
resource
=
Strophe
.
getResourceFromJid
(
jid
);
contact
.
presence
.
removeResource
(
resource
);
}
else
if
(
contact
)
{
// presence_type is undefined
contact
.
presence
.
addResource
(
presence
);
}
}
});
_converse
.
RosterGroup
=
Model
.
extend
({
initialize
(
attributes
)
{
this
.
set
(
Object
.
assign
({
description
:
__
(
'
Click to hide these contacts
'
),
state
:
_converse
.
OPENED
},
attributes
));
// Collection of contacts belonging to this group.
this
.
contacts
=
new
_converse
.
RosterContacts
();
}
});
/**
* @class
* @namespace _converse.RosterGroups
* @memberOf _converse
*/
_converse
.
RosterGroups
=
Collection
.
extend
({
model
:
_converse
.
RosterGroup
,
comparator
(
a
,
b
)
{
a
=
a
.
get
(
'
name
'
);
b
=
b
.
get
(
'
name
'
);
const
WEIGHTS
=
HEADER_WEIGHTS
;
const
special_groups
=
Object
.
keys
(
HEADER_WEIGHTS
);
const
a_is_special
=
special_groups
.
includes
(
a
);
const
b_is_special
=
special_groups
.
includes
(
b
);
if
(
!
a_is_special
&&
!
b_is_special
)
{
return
a
.
toLowerCase
()
<
b
.
toLowerCase
()
?
-
1
:
(
a
.
toLowerCase
()
>
b
.
toLowerCase
()
?
1
:
0
);
}
else
if
(
a_is_special
&&
b_is_special
)
{
return
WEIGHTS
[
a
]
<
WEIGHTS
[
b
]
?
-
1
:
(
WEIGHTS
[
a
]
>
WEIGHTS
[
b
]
?
1
:
0
);
}
else
if
(
!
a_is_special
&&
b_is_special
)
{
const
a_header
=
_converse
.
HEADER_CURRENT_CONTACTS
;
return
WEIGHTS
[
a_header
]
<
WEIGHTS
[
b
]
?
-
1
:
(
WEIGHTS
[
a_header
]
>
WEIGHTS
[
b
]
?
1
:
0
);
}
else
if
(
a_is_special
&&
!
b_is_special
)
{
const
b_header
=
_converse
.
HEADER_CURRENT_CONTACTS
;
return
WEIGHTS
[
a
]
<
WEIGHTS
[
b_header
]
?
-
1
:
(
WEIGHTS
[
a
]
>
WEIGHTS
[
b_header
]
?
1
:
0
);
}
},
/**
* Fetches all the roster groups from sessionStorage.
* @private
* @method _converse.RosterGroups#fetchRosterGroups
* @returns { Promise } - A promise which resolves once the groups have been fetched.
*/
fetchRosterGroups
()
{
return
new
Promise
(
success
=>
{
this
.
fetch
({
success
,
// We need to first have all groups before
// we can start positioning them, so we set
// 'silent' to true.
silent
:
true
,
});
});
}
});
_converse
.
unregisterPresenceHandler
=
function
()
{
if
(
_converse
.
presence_ref
!==
undefined
)
{
_converse
.
connection
.
deleteHandler
(
_converse
.
presence_ref
);
delete
_converse
.
presence_ref
;
}
};
/******************** Event Handlers ********************/
function
updateUnreadCounter
(
chatbox
)
{
const
contact
=
_converse
.
roster
&&
_converse
.
roster
.
findWhere
({
'
jid
'
:
chatbox
.
get
(
'
jid
'
)});
if
(
contact
!==
undefined
)
{
contact
.
save
({
'
num_unread
'
:
chatbox
.
get
(
'
num_unread
'
)});
}
}
api
.
listen
.
on
(
'
chatBoxesInitialized
'
,
()
=>
{
_converse
.
chatboxes
.
on
(
'
change:num_unread
'
,
updateUnreadCounter
);
_converse
.
chatboxes
.
on
(
'
add
'
,
chatbox
=>
{
if
(
chatbox
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
)
{
chatbox
.
setRosterContact
(
chatbox
.
get
(
'
jid
'
));
}
});
});
api
.
listen
.
on
(
'
beforeTearDown
'
,
()
=>
_converse
.
unregisterPresenceHandler
());
api
.
waitUntil
(
'
rosterContactsFetched
'
).
then
(()
=>
{
_converse
.
roster
.
on
(
'
add
'
,
(
contact
)
=>
{
/* When a new contact is added, check if we already have a
* chatbox open for it, and if so attach it to the chatbox.
*/
const
chatbox
=
_converse
.
chatboxes
.
findWhere
({
'
jid
'
:
contact
.
get
(
'
jid
'
)});
if
(
chatbox
)
{
chatbox
.
setRosterContact
(
contact
.
get
(
'
jid
'
));
}
});
});
async
function
clearPresences
()
{
_converse
.
presences
&&
await
_converse
.
presences
.
clearStore
();
}
api
.
listen
.
on
(
'
streamResumptionFailed
'
,
()
=>
_converse
.
session
.
set
(
'
roster_cached
'
,
false
));
api
.
listen
.
on
(
'
clearSession
'
,
async
()
=>
{
await
clearPresences
();
if
(
_converse
.
shouldClearCache
())
{
if
(
_converse
.
rostergroups
)
{
await
_converse
.
rostergroups
.
clearStore
();
delete
_converse
.
rostergroups
;
}
if
(
_converse
.
roster
)
{
invoke
(
_converse
,
'
roster.data.destroy
'
);
await
_converse
.
roster
.
clearStore
();
delete
_converse
.
roster
;
}
}
});
api
.
listen
.
on
(
'
statusInitialized
'
,
async
reconnecting
=>
{
if
(
reconnecting
)
{
// When reconnecting and not resuming a previous session,
// we clear all cached presence data, since it might be stale
// and we'll receive new presence updates
!
_converse
.
connection
.
hasResumed
()
&&
await
clearPresences
();
}
else
{
_converse
.
presences
=
new
_converse
.
Presences
();
const
id
=
`converse.presences-
${
_converse
.
bare_jid
}
`
;
_converse
.
presences
.
browserStorage
=
_converse
.
createStore
(
id
,
"
session
"
);
// We might be continuing an existing session, so we fetch
// cached presence data.
_converse
.
presences
.
fetch
();
}
/**
* Triggered once the _converse.Presences collection has been
* initialized and its cached data fetched.
* Returns a boolean indicating whether this event has fired due to
* Converse having reconnected.
* @event _converse#presencesInitialized
* @type { bool }
* @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
*/
api
.
trigger
(
'
presencesInitialized
'
,
reconnecting
);
});
async
function
initRoster
()
{
// Initialize the Bakcbone collections that represent the contats
// roster and the roster groups.
await
api
.
waitUntil
(
'
VCardsInitialized
'
);
_converse
.
roster
=
new
_converse
.
RosterContacts
();
let
id
=
`converse.contacts-
${
_converse
.
bare_jid
}
`
;
_converse
.
roster
.
browserStorage
=
_converse
.
createStore
(
id
);
_converse
.
roster
.
data
=
new
Model
();
id
=
`converse-roster-model-
${
_converse
.
bare_jid
}
`
;
_converse
.
roster
.
data
.
id
=
id
;
_converse
.
roster
.
data
.
browserStorage
=
_converse
.
createStore
(
id
);
_converse
.
roster
.
data
.
fetch
();
id
=
`converse.roster.groups
${
_converse
.
bare_jid
}
`
;
_converse
.
rostergroups
=
new
_converse
.
RosterGroups
();
_converse
.
rostergroups
.
browserStorage
=
_converse
.
createStore
(
id
);
/**
* Triggered once the `_converse.RosterContacts` and `_converse.RosterGroups` have
* been created, but not yet populated with data.
* This event is useful when you want to create views for these collections.
* @event _converse#chatBoxMaximized
* @example _converse.api.listen.on('rosterInitialized', () => { ... });
* @example _converse.api.waitUntil('rosterInitialized').then(() => { ... });
*/
api
.
trigger
(
'
rosterInitialized
'
);
}
api
.
listen
.
on
(
'
presencesInitialized
'
,
async
(
reconnecting
)
=>
{
if
(
reconnecting
)
{
/**
* Similar to `rosterInitialized`, but instead pertaining to reconnection.
* This event indicates that the roster and its groups are now again
* available after Converse.js has reconnected.
* @event _converse#rosterReadyAfterReconnection
* @example _converse.api.listen.on('rosterReadyAfterReconnection', () => { ... });
*/
api
.
trigger
(
'
rosterReadyAfterReconnection
'
);
}
else
{
await
initRoster
();
}
_converse
.
roster
.
onConnected
();
_converse
.
registerPresenceHandler
();
_converse
.
populateRoster
(
!
_converse
.
connection
.
restored
);
});
/************************ API ************************/
// API methods only available to plugins
Object
.
assign
(
_converse
.
api
,
{
/**
* @namespace _converse.api.contacts
* @memberOf _converse.api
*/
contacts
:
{
/**
* This method is used to retrieve roster contacts.
*
* @method _converse.api.contacts.get
* @params {(string[]|string)} jid|jids The JID or JIDs of
* the contacts to be returned.
* @returns {promise} Promise which resolves with the
* _converse.RosterContact (or an array of them) representing the contact.
*
* @example
* // Fetch a single contact
* _converse.api.listen.on('rosterContactsFetched', function () {
* const contact = await _converse.api.contacts.get('buddy@example.com')
* // ...
* });
*
* @example
* // To get multiple contacts, pass in an array of JIDs:
* _converse.api.listen.on('rosterContactsFetched', function () {
* const contacts = await _converse.api.contacts.get(
* ['buddy1@example.com', 'buddy2@example.com']
* )
* // ...
* });
*
* @example
* // To return all contacts, simply call ``get`` without any parameters:
* _converse.api.listen.on('rosterContactsFetched', function () {
* const contacts = await _converse.api.contacts.get();
* // ...
* });
*/
async
get
(
jids
)
{
await
api
.
waitUntil
(
'
rosterContactsFetched
'
);
const
_getter
=
jid
=>
_converse
.
roster
.
get
(
Strophe
.
getBareJidFromJid
(
jid
));
if
(
jids
===
undefined
)
{
jids
=
_converse
.
roster
.
pluck
(
'
jid
'
);
}
else
if
(
typeof
jids
===
'
string
'
)
{
return
_getter
(
jids
);
}
return
jids
.
map
(
_getter
);
},
/**
* Add a contact.
*
* @method _converse.api.contacts.add
* @param {string} jid The JID of the contact to be added
* @param {string} [name] A custom name to show the user by in the roster
* @example
* _converse.api.contacts.add('buddy@example.com')
* @example
* _converse.api.contacts.add('buddy@example.com', 'Buddy')
*/
async
add
(
jid
,
name
)
{
await
api
.
waitUntil
(
'
rosterContactsFetched
'
);
if
(
typeof
jid
!==
'
string
'
||
!
jid
.
includes
(
'
@
'
))
{
throw
new
TypeError
(
'
contacts.add: invalid jid
'
);
}
return
_converse
.
roster
.
addAndSubscribe
(
jid
,
name
);
}
}
});
}
});
src/headless/plugins/roster/api.js
0 → 100644
View file @
7f851208
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
const
{
Strophe
}
=
converse
.
env
;
export
default
{
/**
* @namespace _converse.api.contacts
* @memberOf _converse.api
*/
contacts
:
{
/**
* This method is used to retrieve roster contacts.
*
* @method _converse.api.contacts.get
* @params {(string[]|string)} jid|jids The JID or JIDs of
* the contacts to be returned.
* @returns {promise} Promise which resolves with the
* _converse.RosterContact (or an array of them) representing the contact.
*
* @example
* // Fetch a single contact
* _converse.api.listen.on('rosterContactsFetched', function () {
* const contact = await _converse.api.contacts.get('buddy@example.com')
* // ...
* });
*
* @example
* // To get multiple contacts, pass in an array of JIDs:
* _converse.api.listen.on('rosterContactsFetched', function () {
* const contacts = await _converse.api.contacts.get(
* ['buddy1@example.com', 'buddy2@example.com']
* )
* // ...
* });
*
* @example
* // To return all contacts, simply call ``get`` without any parameters:
* _converse.api.listen.on('rosterContactsFetched', function () {
* const contacts = await _converse.api.contacts.get();
* // ...
* });
*/
async
get
(
jids
)
{
await
api
.
waitUntil
(
'
rosterContactsFetched
'
);
const
_getter
=
jid
=>
_converse
.
roster
.
get
(
Strophe
.
getBareJidFromJid
(
jid
));
if
(
jids
===
undefined
)
{
jids
=
_converse
.
roster
.
pluck
(
'
jid
'
);
}
else
if
(
typeof
jids
===
'
string
'
)
{
return
_getter
(
jids
);
}
return
jids
.
map
(
_getter
);
},
/**
* Add a contact.
*
* @method _converse.api.contacts.add
* @param {string} jid The JID of the contact to be added
* @param {string} [name] A custom name to show the user by in the roster
* @example
* _converse.api.contacts.add('buddy@example.com')
* @example
* _converse.api.contacts.add('buddy@example.com', 'Buddy')
*/
async
add
(
jid
,
name
)
{
await
api
.
waitUntil
(
'
rosterContactsFetched
'
);
if
(
typeof
jid
!==
'
string
'
||
!
jid
.
includes
(
'
@
'
))
{
throw
new
TypeError
(
'
contacts.add: invalid jid
'
);
}
return
_converse
.
roster
.
addAndSubscribe
(
jid
,
name
);
}
}
}
src/headless/plugins/roster/contact.js
0 → 100644
View file @
7f851208
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
const
{
Strophe
,
$iq
,
$pres
,
u
}
=
converse
.
env
;
/**
* @class
* @namespace RosterContact
*/
const
RosterContact
=
Model
.
extend
({
defaults
:
{
'
chat_state
'
:
undefined
,
'
image
'
:
_converse
.
DEFAULT_IMAGE
,
'
image_type
'
:
_converse
.
DEFAULT_IMAGE_TYPE
,
'
num_unread
'
:
0
,
'
status
'
:
undefined
,
},
async
initialize
(
attributes
)
{
this
.
initialized
=
u
.
getResolveablePromise
();
this
.
setPresence
();
const
{
jid
}
=
attributes
;
const
bare_jid
=
Strophe
.
getBareJidFromJid
(
jid
).
toLowerCase
();
attributes
.
jid
=
bare_jid
;
this
.
set
(
Object
.
assign
({
'
groups
'
:
[],
'
id
'
:
bare_jid
,
'
jid
'
:
bare_jid
,
'
user_id
'
:
Strophe
.
getNodeFromJid
(
jid
)
},
attributes
));
/**
* When a contact's presence status has changed.
* The presence status is either `online`, `offline`, `dnd`, `away` or `xa`.
* @event _converse#contactPresenceChanged
* @type { _converse.RosterContact }
* @example _converse.api.listen.on('contactPresenceChanged', contact => { ... });
*/
this
.
listenTo
(
this
.
presence
,
'
change:show
'
,
()
=>
api
.
trigger
(
'
contactPresenceChanged
'
,
this
));
this
.
listenTo
(
this
.
presence
,
'
change:show
'
,
()
=>
this
.
trigger
(
'
presenceChanged
'
));
/**
* Synchronous event which provides a hook for further initializing a RosterContact
* @event _converse#rosterContactInitialized
* @param { _converse.RosterContact } contact
*/
await
api
.
trigger
(
'
rosterContactInitialized
'
,
this
,
{
'
Synchronous
'
:
true
});
this
.
initialized
.
resolve
();
},
setPresence
()
{
const
jid
=
this
.
get
(
'
jid
'
);
this
.
presence
=
_converse
.
presences
.
findWhere
({
'
jid
'
:
jid
})
||
_converse
.
presences
.
create
({
'
jid
'
:
jid
});
},
openChat
()
{
const
attrs
=
this
.
attributes
;
api
.
chats
.
open
(
attrs
.
jid
,
attrs
,
true
);
},
/**
* Return a string of tab-separated values that are to be used when
* matching against filter text.
*
* The goal is to be able to filter against the VCard fullname,
* roster nickname and JID.
* @returns { String } Lower-cased, tab-separated values
*/
getFilterCriteria
()
{
const
nick
=
this
.
get
(
'
nickname
'
);
const
jid
=
this
.
get
(
'
jid
'
);
let
criteria
=
this
.
getDisplayName
();
criteria
=
!
criteria
.
includes
(
jid
)
?
criteria
.
concat
(
`
${
jid
}
`
)
:
criteria
;
criteria
=
!
criteria
.
includes
(
nick
)
?
criteria
.
concat
(
`
${
nick
}
`
)
:
criteria
;
return
criteria
.
toLowerCase
();
},
getDisplayName
()
{
// Gets overridden in converse-vcard where the fullname is may be returned
if
(
this
.
get
(
'
nickname
'
))
{
return
this
.
get
(
'
nickname
'
);
}
else
{
return
this
.
get
(
'
jid
'
);
}
},
getFullname
()
{
// Gets overridden in converse-vcard where the fullname may be returned
return
this
.
get
(
'
jid
'
);
},
/**
* Send a presence subscription request to this roster contact
* @private
* @method _converse.RosterContacts#subscribe
* @param { String } message - An optional message to explain the
* reason for the subscription request.
*/
subscribe
(
message
)
{
const
pres
=
$pres
({
to
:
this
.
get
(
'
jid
'
),
type
:
"
subscribe
"
});
if
(
message
&&
message
!==
""
)
{
pres
.
c
(
"
status
"
).
t
(
message
).
up
();
}
const
nick
=
_converse
.
xmppstatus
.
getNickname
()
||
_converse
.
xmppstatus
.
getFullname
();
if
(
nick
)
{
pres
.
c
(
'
nick
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
NICK
}).
t
(
nick
).
up
();
}
api
.
send
(
pres
);
this
.
save
(
'
ask
'
,
"
subscribe
"
);
// ask === 'subscribe' Means we have asked to subscribe to them.
return
this
;
},
/**
* Upon receiving the presence stanza of type "subscribed",
* the user SHOULD acknowledge receipt of that subscription
* state notification by sending a presence stanza of type
* "subscribe" to the contact
* @private
* @method _converse.RosterContacts#ackSubscribe
*/
ackSubscribe
()
{
api
.
send
(
$pres
({
'
type
'
:
'
subscribe
'
,
'
to
'
:
this
.
get
(
'
jid
'
)
}));
},
/**
* Upon receiving the presence stanza of type "unsubscribed",
* the user SHOULD acknowledge receipt of that subscription state
* notification by sending a presence stanza of type "unsubscribe"
* this step lets the user's server know that it MUST no longer
* send notification of the subscription state change to the user.
* @private
* @method _converse.RosterContacts#ackUnsubscribe
* @param { String } jid - The Jabber ID of the user who is unsubscribing
*/
ackUnsubscribe
()
{
api
.
send
(
$pres
({
'
type
'
:
'
unsubscribe
'
,
'
to
'
:
this
.
get
(
'
jid
'
)}));
this
.
removeFromRoster
();
this
.
destroy
();
},
/**
* Unauthorize this contact's presence subscription
* @private
* @method _converse.RosterContacts#unauthorize
* @param { String } message - Optional message to send to the person being unauthorized
*/
unauthorize
(
message
)
{
_converse
.
rejectPresenceSubscription
(
this
.
get
(
'
jid
'
),
message
);
return
this
;
},
/**
* Authorize presence subscription
* @private
* @method _converse.RosterContacts#authorize
* @param { String } message - Optional message to send to the person being authorized
*/
authorize
(
message
)
{
const
pres
=
$pres
({
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
"
subscribed
"
});
if
(
message
&&
message
!==
""
)
{
pres
.
c
(
"
status
"
).
t
(
message
);
}
api
.
send
(
pres
);
return
this
;
},
/**
* Instruct the XMPP server to remove this contact from our roster
* @private
* @method _converse.RosterContacts#
* @returns { Promise }
*/
removeFromRoster
()
{
const
iq
=
$iq
({
type
:
'
set
'
})
.
c
(
'
query
'
,
{
xmlns
:
Strophe
.
NS
.
ROSTER
})
.
c
(
'
item
'
,
{
jid
:
this
.
get
(
'
jid
'
),
subscription
:
"
remove
"
});
return
api
.
sendIQ
(
iq
);
}
});
export
default
RosterContact
;
src/headless/plugins/roster/contacts.js
0 → 100644
View file @
7f851208
import
RosterContact
from
'
./contact.js
'
;
import
log
from
"
@converse/headless/log
"
;
import
sum
from
'
lodash/sum
'
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
__
}
from
'
i18n
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
const
{
Strophe
,
$iq
,
sizzle
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
const
RosterContacts
=
Collection
.
extend
({
model
:
RosterContact
,
comparator
(
contact1
,
contact2
)
{
// Groups are sorted alphabetically, ignoring case.
// However, Ungrouped, Requesting Contacts and Pending Contacts
// appear last and in that order.
const
status1
=
contact1
.
presence
.
get
(
'
show
'
)
||
'
offline
'
;
const
status2
=
contact2
.
presence
.
get
(
'
show
'
)
||
'
offline
'
;
if
(
_converse
.
STATUS_WEIGHTS
[
status1
]
===
_converse
.
STATUS_WEIGHTS
[
status2
])
{
const
name1
=
(
contact1
.
getDisplayName
()).
toLowerCase
();
const
name2
=
(
contact2
.
getDisplayName
()).
toLowerCase
();
return
name1
<
name2
?
-
1
:
(
name1
>
name2
?
1
:
0
);
}
else
{
return
_converse
.
STATUS_WEIGHTS
[
status1
]
<
_converse
.
STATUS_WEIGHTS
[
status2
]
?
-
1
:
1
;
}
},
onConnected
()
{
// Called as soon as the connection has been established
// (either after initial login, or after reconnection).
// Use the opportunity to register stanza handlers.
this
.
registerRosterHandler
();
this
.
registerRosterXHandler
();
},
registerRosterHandler
()
{
// Register a handler for roster IQ "set" stanzas, which update
// roster contacts.
_converse
.
connection
.
addHandler
(
iq
=>
{
_converse
.
roster
.
onRosterPush
(
iq
);
return
true
;
},
Strophe
.
NS
.
ROSTER
,
'
iq
'
,
"
set
"
);
},
registerRosterXHandler
()
{
// Register a handler for RosterX message stanzas, which are
// used to suggest roster contacts to a user.
let
t
=
0
;
_converse
.
connection
.
addHandler
(
function
(
msg
)
{
window
.
setTimeout
(
function
()
{
_converse
.
connection
.
flush
();
_converse
.
roster
.
subscribeToSuggestedItems
.
bind
(
_converse
.
roster
)(
msg
);
},
t
);
t
+=
msg
.
querySelectorAll
(
'
item
'
).
length
*
250
;
return
true
;
},
Strophe
.
NS
.
ROSTERX
,
'
message
'
,
null
);
},
/**
* Fetches the roster contacts, first by trying the browser cache,
* and if that's empty, then by querying the XMPP server.
* @private
* @returns {promise} Promise which resolves once the contacts have been fetched.
*/
async
fetchRosterContacts
()
{
const
result
=
await
new
Promise
((
resolve
,
reject
)
=>
{
this
.
fetch
({
'
add
'
:
true
,
'
silent
'
:
true
,
'
success
'
:
resolve
,
'
error
'
:
(
c
,
e
)
=>
reject
(
e
)
});
});
if
(
u
.
isErrorObject
(
result
))
{
log
.
error
(
result
);
// Force a full roster refresh
_converse
.
session
.
set
(
'
roster_cached
'
,
false
)
this
.
data
.
save
(
'
version
'
,
undefined
);
}
if
(
_converse
.
session
.
get
(
'
roster_cached
'
))
{
/**
* The contacts roster has been retrieved from the local cache (`sessionStorage`).
* @event _converse#cachedRoster
* @type { _converse.RosterContacts }
* @example _converse.api.listen.on('cachedRoster', (items) => { ... });
* @example _converse.api.waitUntil('cachedRoster').then(items => { ... });
*/
api
.
trigger
(
'
cachedRoster
'
,
result
);
}
else
{
_converse
.
send_initial_presence
=
true
;
return
_converse
.
roster
.
fetchFromServer
();
}
},
subscribeToSuggestedItems
(
msg
)
{
Array
.
from
(
msg
.
querySelectorAll
(
'
item
'
)).
forEach
(
item
=>
{
if
(
item
.
getAttribute
(
'
action
'
)
===
'
add
'
)
{
_converse
.
roster
.
addAndSubscribe
(
item
.
getAttribute
(
'
jid
'
),
_converse
.
xmppstatus
.
getNickname
()
||
_converse
.
xmppstatus
.
getFullname
()
);
}
});
return
true
;
},
isSelf
(
jid
)
{
return
u
.
isSameBareJID
(
jid
,
_converse
.
connection
.
jid
);
},
/**
* Add a roster contact and then once we have confirmation from
* the XMPP server we subscribe to that contact's presence updates.
* @private
* @method _converse.RosterContacts#addAndSubscribe
* @param { String } jid - The Jabber ID of the user being added and subscribed to.
* @param { String } name - The name of that user
* @param { Array.String } groups - Any roster groups the user might belong to
* @param { String } message - An optional message to explain the reason for the subscription request.
* @param { Object } attributes - Any additional attributes to be stored on the user's model.
*/
async
addAndSubscribe
(
jid
,
name
,
groups
,
message
,
attributes
)
{
const
contact
=
await
this
.
addContactToRoster
(
jid
,
name
,
groups
,
attributes
);
if
(
contact
instanceof
_converse
.
RosterContact
)
{
contact
.
subscribe
(
message
);
}
},
/**
* Send an IQ stanza to the XMPP server to add a new roster contact.
* @private
* @method _converse.RosterContacts#sendContactAddIQ
* @param { String } jid - The Jabber ID of the user being added
* @param { String } name - The name of that user
* @param { Array.String } groups - Any roster groups the user might belong to
* @param { Function } callback - A function to call once the IQ is returned
* @param { Function } errback - A function to call if an error occurred
*/
sendContactAddIQ
(
jid
,
name
,
groups
)
{
name
=
name
?
name
:
null
;
const
iq
=
$iq
({
'
type
'
:
'
set
'
})
.
c
(
'
query
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
ROSTER
})
.
c
(
'
item
'
,
{
jid
,
name
});
groups
.
forEach
(
g
=>
iq
.
c
(
'
group
'
).
t
(
g
).
up
());
return
api
.
sendIQ
(
iq
);
},
/**
* Adds a RosterContact instance to _converse.roster and
* registers the contact on the XMPP server.
* Returns a promise which is resolved once the XMPP server has responded.
* @private
* @method _converse.RosterContacts#addContactToRoster
* @param { String } jid - The Jabber ID of the user being added and subscribed to.
* @param { String } name - The name of that user
* @param { Array.String } groups - Any roster groups the user might belong to
* @param { Object } attributes - Any additional attributes to be stored on the user's model.
*/
async
addContactToRoster
(
jid
,
name
,
groups
,
attributes
)
{
await
api
.
waitUntil
(
'
rosterContactsFetched
'
);
groups
=
groups
||
[];
try
{
await
this
.
sendContactAddIQ
(
jid
,
name
,
groups
);
}
catch
(
e
)
{
log
.
error
(
e
);
alert
(
__
(
'
Sorry, there was an error while trying to add %1$s as a contact.
'
,
name
||
jid
));
return
e
;
}
return
this
.
create
(
Object
.
assign
({
'
ask
'
:
undefined
,
'
nickname
'
:
name
,
groups
,
jid
,
'
requesting
'
:
false
,
'
subscription
'
:
'
none
'
},
attributes
),
{
'
sort
'
:
false
});
},
async
subscribeBack
(
bare_jid
,
presence
)
{
const
contact
=
this
.
get
(
bare_jid
);
if
(
contact
instanceof
_converse
.
RosterContact
)
{
contact
.
authorize
().
subscribe
();
}
else
{
// Can happen when a subscription is retried or roster was deleted
const
nickname
=
sizzle
(
`nick[xmlns="
${
Strophe
.
NS
.
NICK
}
"]`
,
presence
).
pop
()?.
textContent
||
null
;
const
contact
=
await
this
.
addContactToRoster
(
bare_jid
,
nickname
,
[],
{
'
subscription
'
:
'
from
'
});
if
(
contact
instanceof
_converse
.
RosterContact
)
{
contact
.
authorize
().
subscribe
();
}
}
},
getNumOnlineContacts
()
{
const
ignored
=
[
'
offline
'
,
'
unavailable
'
];
return
sum
(
this
.
models
.
filter
(
m
=>
!
ignored
.
includes
(
m
.
presence
.
get
(
'
show
'
))));
},
/**
* Handle roster updates from the XMPP server.
* See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
* @private
* @method _converse.RosterContacts#onRosterPush
* @param { XMLElement } IQ - The IQ stanza received from the XMPP server.
*/
onRosterPush
(
iq
)
{
const
id
=
iq
.
getAttribute
(
'
id
'
);
const
from
=
iq
.
getAttribute
(
'
from
'
);
if
(
from
&&
from
!==
_converse
.
bare_jid
)
{
// https://tools.ietf.org/html/rfc6121#page-15
//
// A receiving client MUST ignore the stanza unless it has no 'from'
// attribute (i.e., implicitly from the bare JID of the user's
// account) or it has a 'from' attribute whose value matches the
// user's bare JID <user@domainpart>.
log
.
warn
(
`Ignoring roster illegitimate roster push message from
${
iq
.
getAttribute
(
'
from
'
)}
`
);
return
;
}
api
.
send
(
$iq
({
type
:
'
result
'
,
id
,
from
:
_converse
.
connection
.
jid
}));
const
query
=
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
ROSTER
}
"]`
,
iq
).
pop
();
this
.
data
.
save
(
'
version
'
,
query
.
getAttribute
(
'
ver
'
));
const
items
=
sizzle
(
`item`
,
query
);
if
(
items
.
length
>
1
)
{
log
.
error
(
iq
);
throw
new
Error
(
'
Roster push query may not contain more than one "item" element.
'
);
}
if
(
items
.
length
===
0
)
{
log
.
warn
(
iq
);
log
.
warn
(
'
Received a roster push stanza without an "item" element.
'
);
return
;
}
this
.
updateContact
(
items
.
pop
());
/**
* When the roster receives a push event from server (i.e. new entry in your contacts roster).
* @event _converse#rosterPush
* @type { XMLElement }
* @example _converse.api.listen.on('rosterPush', iq => { ... });
*/
api
.
trigger
(
'
rosterPush
'
,
iq
);
return
;
},
rosterVersioningSupported
()
{
return
api
.
disco
.
stream
.
getFeature
(
'
ver
'
,
'
urn:xmpp:features:rosterver
'
)
&&
this
.
data
.
get
(
'
version
'
);
},
/**
* Fetch the roster from the XMPP server
* @private
* @emits _converse#roster
* @returns {promise}
*/
async
fetchFromServer
()
{
const
stanza
=
$iq
({
'
type
'
:
'
get
'
,
'
id
'
:
u
.
getUniqueId
(
'
roster
'
)
}).
c
(
'
query
'
,
{
xmlns
:
Strophe
.
NS
.
ROSTER
});
if
(
this
.
rosterVersioningSupported
())
{
stanza
.
attrs
({
'
ver
'
:
this
.
data
.
get
(
'
version
'
)});
}
const
iq
=
await
api
.
sendIQ
(
stanza
,
null
,
false
);
if
(
iq
.
getAttribute
(
'
type
'
)
!==
'
error
'
)
{
const
query
=
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
ROSTER
}
"]`
,
iq
).
pop
();
if
(
query
)
{
const
items
=
sizzle
(
`item`
,
query
);
items
.
forEach
(
item
=>
this
.
updateContact
(
item
));
this
.
data
.
save
(
'
version
'
,
query
.
getAttribute
(
'
ver
'
));
}
}
else
if
(
!
u
.
isServiceUnavailableError
(
iq
))
{
// Some unknown error happened, so we will try to fetch again if the page reloads.
log
.
error
(
iq
);
log
.
error
(
"
Error while trying to fetch roster from the server
"
);
return
;
}
_converse
.
session
.
save
(
'
roster_cached
'
,
true
);
/**
* When the roster has been received from the XMPP server.
* See also the `cachedRoster` event further up, which gets called instead of
* `roster` if its already in `sessionStorage`.
* @event _converse#roster
* @type { XMLElement }
* @example _converse.api.listen.on('roster', iq => { ... });
* @example _converse.api.waitUntil('roster').then(iq => { ... });
*/
api
.
trigger
(
'
roster
'
,
iq
);
},
/* Update or create RosterContact models based on the given `item` XML
* node received in the resulting IQ stanza from the server.
* @private
* @param { XMLElement } item
*/
updateContact
(
item
)
{
const
jid
=
item
.
getAttribute
(
'
jid
'
);
const
contact
=
this
.
get
(
jid
);
const
subscription
=
item
.
getAttribute
(
"
subscription
"
);
const
ask
=
item
.
getAttribute
(
"
ask
"
);
const
groups
=
Array
.
from
(
item
.
getElementsByTagName
(
'
group
'
)).
map
(
e
=>
e
.
textContent
);
if
(
!
contact
)
{
if
((
subscription
===
"
none
"
&&
ask
===
null
)
||
(
subscription
===
"
remove
"
))
{
return
;
// We're lazy when adding contacts.
}
this
.
create
({
'
ask
'
:
ask
,
'
nickname
'
:
item
.
getAttribute
(
"
name
"
),
'
groups
'
:
groups
,
'
jid
'
:
jid
,
'
subscription
'
:
subscription
},
{
sort
:
false
});
}
else
{
if
(
subscription
===
"
remove
"
)
{
return
contact
.
destroy
();
}
// We only find out about requesting contacts via the
// presence handler, so if we receive a contact
// here, we know they aren't requesting anymore.
// see docs/DEVELOPER.rst
contact
.
save
({
'
subscription
'
:
subscription
,
'
ask
'
:
ask
,
'
nickname
'
:
item
.
getAttribute
(
"
name
"
),
'
requesting
'
:
null
,
'
groups
'
:
groups
});
}
},
createRequestingContact
(
presence
)
{
const
bare_jid
=
Strophe
.
getBareJidFromJid
(
presence
.
getAttribute
(
'
from
'
));
const
nickname
=
sizzle
(
`nick[xmlns="
${
Strophe
.
NS
.
NICK
}
"]`
,
presence
).
pop
()?.
textContent
||
null
;
const
user_data
=
{
'
jid
'
:
bare_jid
,
'
subscription
'
:
'
none
'
,
'
ask
'
:
null
,
'
requesting
'
:
true
,
'
nickname
'
:
nickname
};
/**
* Triggered when someone has requested to subscribe to your presence (i.e. to be your contact).
* @event _converse#contactRequest
* @type { _converse.RosterContact }
* @example _converse.api.listen.on('contactRequest', contact => { ... });
*/
api
.
trigger
(
'
contactRequest
'
,
this
.
create
(
user_data
));
},
handleIncomingSubscription
(
presence
)
{
const
jid
=
presence
.
getAttribute
(
'
from
'
),
bare_jid
=
Strophe
.
getBareJidFromJid
(
jid
),
contact
=
this
.
get
(
bare_jid
);
if
(
!
api
.
settings
.
get
(
'
allow_contact_requests
'
))
{
_converse
.
rejectPresenceSubscription
(
jid
,
__
(
"
This client does not allow presence subscriptions
"
)
);
}
if
(
api
.
settings
.
get
(
'
auto_subscribe
'
))
{
if
((
!
contact
)
||
(
contact
.
get
(
'
subscription
'
)
!==
'
to
'
))
{
this
.
subscribeBack
(
bare_jid
,
presence
);
}
else
{
contact
.
authorize
();
}
}
else
{
if
(
contact
)
{
if
(
contact
.
get
(
'
subscription
'
)
!==
'
none
'
)
{
contact
.
authorize
();
}
else
if
(
contact
.
get
(
'
ask
'
)
===
"
subscribe
"
)
{
contact
.
authorize
();
}
}
else
{
this
.
createRequestingContact
(
presence
);
}
}
},
handleOwnPresence
(
presence
)
{
const
jid
=
presence
.
getAttribute
(
'
from
'
),
resource
=
Strophe
.
getResourceFromJid
(
jid
),
presence_type
=
presence
.
getAttribute
(
'
type
'
);
if
((
_converse
.
connection
.
jid
!==
jid
)
&&
(
presence_type
!==
'
unavailable
'
)
&&
(
api
.
settings
.
get
(
'
synchronize_availability
'
)
===
true
||
api
.
settings
.
get
(
'
synchronize_availability
'
)
===
resource
))
{
// Another resource has changed its status and
// synchronize_availability option set to update,
// we'll update ours as well.
const
show
=
presence
.
querySelector
(
'
show
'
)?.
textContent
||
'
online
'
;
_converse
.
xmppstatus
.
save
({
'
status
'
:
show
},
{
'
silent
'
:
true
});
const
status_message
=
presence
.
querySelector
(
'
status
'
)?.
textContent
;
if
(
status_message
)
{
_converse
.
xmppstatus
.
save
({
'
status_message
'
:
status_message
});
}
}
if
(
_converse
.
jid
===
jid
&&
presence_type
===
'
unavailable
'
)
{
// XXX: We've received an "unavailable" presence from our
// own resource. Apparently this happens due to a
// Prosody bug, whereby we send an IQ stanza to remove
// a roster contact, and Prosody then sends
// "unavailable" globally, instead of directed to the
// particular user that's removed.
//
// Here is the bug report: https://prosody.im/issues/1121
//
// I'm not sure whether this might legitimately happen
// in other cases.
//
// As a workaround for now we simply send our presence again,
// otherwise we're treated as offline.
api
.
user
.
presence
.
send
();
}
},
presenceHandler
(
presence
)
{
const
presence_type
=
presence
.
getAttribute
(
'
type
'
);
if
(
presence_type
===
'
error
'
)
{
return
true
;
}
const
jid
=
presence
.
getAttribute
(
'
from
'
),
bare_jid
=
Strophe
.
getBareJidFromJid
(
jid
);
if
(
this
.
isSelf
(
bare_jid
))
{
return
this
.
handleOwnPresence
(
presence
);
}
else
if
(
sizzle
(
`query[xmlns="
${
Strophe
.
NS
.
MUC
}
"]`
,
presence
).
length
)
{
return
;
// Ignore MUC
}
const
status_message
=
presence
.
querySelector
(
'
status
'
)?.
textContent
;
const
contact
=
this
.
get
(
bare_jid
);
if
(
contact
&&
(
status_message
!==
contact
.
get
(
'
status
'
)))
{
contact
.
save
({
'
status
'
:
status_message
});
}
if
(
presence_type
===
'
subscribed
'
&&
contact
)
{
contact
.
ackSubscribe
();
}
else
if
(
presence_type
===
'
unsubscribed
'
&&
contact
)
{
contact
.
ackUnsubscribe
();
}
else
if
(
presence_type
===
'
unsubscribe
'
)
{
return
;
}
else
if
(
presence_type
===
'
subscribe
'
)
{
this
.
handleIncomingSubscription
(
presence
);
}
else
if
(
presence_type
===
'
unavailable
'
&&
contact
)
{
const
resource
=
Strophe
.
getResourceFromJid
(
jid
);
contact
.
presence
.
removeResource
(
resource
);
}
else
if
(
contact
)
{
// presence_type is undefined
contact
.
presence
.
addResource
(
presence
);
}
}
});
export
default
RosterContacts
;
src/headless/plugins/roster/group.js
0 → 100644
View file @
7f851208
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
__
}
from
'
i18n
'
;
import
{
_converse
}
from
"
@converse/headless/core
"
;
const
RosterGroup
=
Model
.
extend
({
initialize
(
attributes
)
{
this
.
set
(
Object
.
assign
({
description
:
__
(
'
Click to hide these contacts
'
),
state
:
_converse
.
OPENED
},
attributes
));
// Collection of contacts belonging to this group.
this
.
contacts
=
new
_converse
.
RosterContacts
();
}
});
export
default
RosterGroup
;
src/headless/plugins/roster/groups.js
0 → 100644
View file @
7f851208
import
RosterGroup
from
'
./group.js
'
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
_converse
}
from
"
@converse/headless/core
"
;
/**
* @class
*/
const
RosterGroups
=
Collection
.
extend
({
model
:
RosterGroup
,
comparator
(
a
,
b
)
{
const
HEADER_WEIGHTS
=
{};
HEADER_WEIGHTS
[
_converse
.
HEADER_UNREAD
]
=
0
;
HEADER_WEIGHTS
[
_converse
.
HEADER_REQUESTING_CONTACTS
]
=
1
;
HEADER_WEIGHTS
[
_converse
.
HEADER_CURRENT_CONTACTS
]
=
2
;
HEADER_WEIGHTS
[
_converse
.
HEADER_UNGROUPED
]
=
3
;
HEADER_WEIGHTS
[
_converse
.
HEADER_PENDING_CONTACTS
]
=
4
;
a
=
a
.
get
(
'
name
'
);
b
=
b
.
get
(
'
name
'
);
const
WEIGHTS
=
HEADER_WEIGHTS
;
const
special_groups
=
Object
.
keys
(
HEADER_WEIGHTS
);
const
a_is_special
=
special_groups
.
includes
(
a
);
const
b_is_special
=
special_groups
.
includes
(
b
);
if
(
!
a_is_special
&&
!
b_is_special
)
{
return
a
.
toLowerCase
()
<
b
.
toLowerCase
()
?
-
1
:
(
a
.
toLowerCase
()
>
b
.
toLowerCase
()
?
1
:
0
);
}
else
if
(
a_is_special
&&
b_is_special
)
{
return
WEIGHTS
[
a
]
<
WEIGHTS
[
b
]
?
-
1
:
(
WEIGHTS
[
a
]
>
WEIGHTS
[
b
]
?
1
:
0
);
}
else
if
(
!
a_is_special
&&
b_is_special
)
{
const
a_header
=
_converse
.
HEADER_CURRENT_CONTACTS
;
return
WEIGHTS
[
a_header
]
<
WEIGHTS
[
b
]
?
-
1
:
(
WEIGHTS
[
a_header
]
>
WEIGHTS
[
b
]
?
1
:
0
);
}
else
if
(
a_is_special
&&
!
b_is_special
)
{
const
b_header
=
_converse
.
HEADER_CURRENT_CONTACTS
;
return
WEIGHTS
[
a
]
<
WEIGHTS
[
b_header
]
?
-
1
:
(
WEIGHTS
[
a
]
>
WEIGHTS
[
b_header
]
?
1
:
0
);
}
},
/**
* Fetches all the roster groups from sessionStorage.
* @private
* @method _converse.RosterGroups#fetchRosterGroups
* @returns { Promise } - A promise which resolves once the groups have been fetched.
*/
fetchRosterGroups
()
{
return
new
Promise
(
success
=>
{
this
.
fetch
({
success
,
// We need to first have all groups before
// we can start positioning them, so we set
// 'silent' to true.
silent
:
true
,
});
});
}
});
export
default
RosterGroups
;
src/headless/plugins/roster/index.js
0 → 100644
View file @
7f851208
/**
* @module converse-roster
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import
"
@converse/headless/plugins/status
"
;
import
RosterContact
from
'
./contact.js
'
;
import
RosterContacts
from
'
./contacts.js
'
;
import
RosterGroup
from
'
./group.js
'
;
import
RosterGroups
from
'
./groups.js
'
;
import
invoke
from
'
lodash/invoke
'
;
import
log
from
"
@converse/headless/log
"
;
import
roster_api
from
'
./api.js
'
;
import
{
Presence
,
Presences
}
from
'
./presence.js
'
;
import
{
__
}
from
'
i18n
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
{
clearPresences
,
initRoster
,
updateUnreadCounter
}
from
'
./utils.js
'
;
const
{
$pres
}
=
converse
.
env
;
converse
.
plugins
.
add
(
'
converse-roster
'
,
{
dependencies
:
[
'
converse-status
'
],
initialize
()
{
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api
.
settings
.
extend
({
'
allow_contact_requests
'
:
true
,
'
auto_subscribe
'
:
false
,
'
synchronize_availability
'
:
true
,
});
api
.
promises
.
add
([
'
cachedRoster
'
,
'
roster
'
,
'
rosterContactsFetched
'
,
'
rosterGroupsFetched
'
,
'
rosterInitialized
'
,
]);
// API methods only available to plugins
Object
.
assign
(
_converse
.
api
,
roster_api
);
_converse
.
HEADER_CURRENT_CONTACTS
=
__
(
'
My contacts
'
);
_converse
.
HEADER_PENDING_CONTACTS
=
__
(
'
Pending contacts
'
);
_converse
.
HEADER_REQUESTING_CONTACTS
=
__
(
'
Contact requests
'
);
_converse
.
HEADER_UNGROUPED
=
__
(
'
Ungrouped
'
);
_converse
.
HEADER_UNREAD
=
__
(
'
New messages
'
);
_converse
.
registerPresenceHandler
=
function
()
{
_converse
.
unregisterPresenceHandler
();
_converse
.
presence_ref
=
_converse
.
connection
.
addHandler
(
presence
=>
{
_converse
.
roster
.
presenceHandler
(
presence
);
return
true
;
},
null
,
'
presence
'
,
null
);
};
/**
* Reject or cancel another user's subscription to our presence updates.
* @method rejectPresenceSubscription
* @private
* @memberOf _converse
* @param { String } jid - The Jabber ID of the user whose subscription is being canceled
* @param { String } message - An optional message to the user
*/
_converse
.
rejectPresenceSubscription
=
function
(
jid
,
message
)
{
const
pres
=
$pres
({
to
:
jid
,
type
:
"
unsubscribed
"
});
if
(
message
&&
message
!==
""
)
{
pres
.
c
(
"
status
"
).
t
(
message
);
}
api
.
send
(
pres
);
};
_converse
.
sendInitialPresence
=
function
()
{
if
(
_converse
.
send_initial_presence
)
{
api
.
user
.
presence
.
send
();
}
};
/**
* Fetch all the roster groups, and then the roster contacts.
* Emit an event after fetching is done in each case.
* @private
* @method _converse.populateRoster
* @param { Bool } ignore_cache - If set to to true, the local cache
* will be ignored it's guaranteed that the XMPP server
* will be queried for the roster.
*/
_converse
.
populateRoster
=
async
function
(
ignore_cache
=
false
)
{
if
(
ignore_cache
)
{
_converse
.
send_initial_presence
=
true
;
}
try
{
await
_converse
.
rostergroups
.
fetchRosterGroups
();
/**
* Triggered once roster groups have been fetched. Used by the
* `converse-rosterview.js` plugin to know when it can start alphabetically
* position roster groups.
* @event _converse#rosterGroupsFetched
* @example _converse.api.listen.on('rosterGroupsFetched', () => { ... });
* @example _converse.api.waitUntil('rosterGroupsFetched').then(() => { ... });
*/
api
.
trigger
(
'
rosterGroupsFetched
'
);
await
_converse
.
roster
.
fetchRosterContacts
();
api
.
trigger
(
'
rosterContactsFetched
'
);
}
catch
(
reason
)
{
log
.
error
(
reason
);
}
finally
{
_converse
.
sendInitialPresence
();
}
};
_converse
.
Presence
=
Presence
;
_converse
.
Presences
=
Presences
;
_converse
.
RosterContact
=
RosterContact
;
_converse
.
RosterContacts
=
RosterContacts
;
_converse
.
RosterGroup
=
RosterGroup
;
_converse
.
RosterGroups
=
RosterGroups
;
_converse
.
unregisterPresenceHandler
=
function
()
{
if
(
_converse
.
presence_ref
!==
undefined
)
{
_converse
.
connection
.
deleteHandler
(
_converse
.
presence_ref
);
delete
_converse
.
presence_ref
;
}
};
/******************** Event Handlers ********************/
api
.
listen
.
on
(
'
chatBoxesInitialized
'
,
()
=>
{
_converse
.
chatboxes
.
on
(
'
change:num_unread
'
,
updateUnreadCounter
);
_converse
.
chatboxes
.
on
(
'
add
'
,
chatbox
=>
{
if
(
chatbox
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
)
{
chatbox
.
setRosterContact
(
chatbox
.
get
(
'
jid
'
));
}
});
});
api
.
listen
.
on
(
'
beforeTearDown
'
,
()
=>
_converse
.
unregisterPresenceHandler
());
api
.
waitUntil
(
'
rosterContactsFetched
'
).
then
(()
=>
{
_converse
.
roster
.
on
(
'
add
'
,
(
contact
)
=>
{
/* When a new contact is added, check if we already have a
* chatbox open for it, and if so attach it to the chatbox.
*/
const
chatbox
=
_converse
.
chatboxes
.
findWhere
({
'
jid
'
:
contact
.
get
(
'
jid
'
)});
if
(
chatbox
)
{
chatbox
.
setRosterContact
(
contact
.
get
(
'
jid
'
));
}
});
});
api
.
listen
.
on
(
'
streamResumptionFailed
'
,
()
=>
_converse
.
session
.
set
(
'
roster_cached
'
,
false
));
api
.
listen
.
on
(
'
clearSession
'
,
async
()
=>
{
await
clearPresences
();
if
(
_converse
.
shouldClearCache
())
{
if
(
_converse
.
rostergroups
)
{
await
_converse
.
rostergroups
.
clearStore
();
delete
_converse
.
rostergroups
;
}
if
(
_converse
.
roster
)
{
invoke
(
_converse
,
'
roster.data.destroy
'
);
await
_converse
.
roster
.
clearStore
();
delete
_converse
.
roster
;
}
}
});
api
.
listen
.
on
(
'
statusInitialized
'
,
async
reconnecting
=>
{
if
(
reconnecting
)
{
// When reconnecting and not resuming a previous session,
// we clear all cached presence data, since it might be stale
// and we'll receive new presence updates
!
_converse
.
connection
.
hasResumed
()
&&
await
clearPresences
();
}
else
{
_converse
.
presences
=
new
_converse
.
Presences
();
const
id
=
`converse.presences-
${
_converse
.
bare_jid
}
`
;
_converse
.
presences
.
browserStorage
=
_converse
.
createStore
(
id
,
"
session
"
);
// We might be continuing an existing session, so we fetch
// cached presence data.
_converse
.
presences
.
fetch
();
}
/**
* Triggered once the _converse.Presences collection has been
* initialized and its cached data fetched.
* Returns a boolean indicating whether this event has fired due to
* Converse having reconnected.
* @event _converse#presencesInitialized
* @type { bool }
* @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
*/
api
.
trigger
(
'
presencesInitialized
'
,
reconnecting
);
});
api
.
listen
.
on
(
'
presencesInitialized
'
,
async
(
reconnecting
)
=>
{
if
(
reconnecting
)
{
/**
* Similar to `rosterInitialized`, but instead pertaining to reconnection.
* This event indicates that the roster and its groups are now again
* available after Converse.js has reconnected.
* @event _converse#rosterReadyAfterReconnection
* @example _converse.api.listen.on('rosterReadyAfterReconnection', () => { ... });
*/
api
.
trigger
(
'
rosterReadyAfterReconnection
'
);
}
else
{
await
initRoster
();
}
_converse
.
roster
.
onConnected
();
_converse
.
registerPresenceHandler
();
_converse
.
populateRoster
(
!
_converse
.
connection
.
restored
);
});
}
});
src/headless/plugins/roster/presence.js
0 → 100644
View file @
7f851208
import
isNaN
from
"
lodash/isNaN
"
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
_converse
,
converse
}
from
"
@converse/headless/core
"
;
const
{
Strophe
,
dayjs
,
sizzle
}
=
converse
.
env
;
export
const
Resource
=
Model
.
extend
({
'
idAttribute
'
:
'
name
'
});
export
const
Resources
=
Collection
.
extend
({
'
model
'
:
Resource
});
export
const
Presence
=
Model
.
extend
({
defaults
:
{
'
show
'
:
'
offline
'
},
initialize
()
{
this
.
resources
=
new
Resources
();
const
id
=
`converse.identities-
${
this
.
get
(
'
jid
'
)}
`
;
this
.
resources
.
browserStorage
=
_converse
.
createStore
(
id
,
"
session
"
);
this
.
listenTo
(
this
.
resources
,
'
update
'
,
this
.
onResourcesChanged
);
this
.
listenTo
(
this
.
resources
,
'
change
'
,
this
.
onResourcesChanged
);
},
onResourcesChanged
()
{
const
hpr
=
this
.
getHighestPriorityResource
();
const
show
=
hpr
?.
attributes
?.
show
||
'
offline
'
;
if
(
this
.
get
(
'
show
'
)
!==
show
)
{
this
.
save
({
'
show
'
:
show
});
}
},
/**
* Return the resource with the highest priority.
* If multiple resources have the same priority, take the latest one.
* @private
*/
getHighestPriorityResource
()
{
return
this
.
resources
.
sortBy
(
r
=>
`
${
r
.
get
(
'
priority
'
)}
-
${
r
.
get
(
'
timestamp
'
)}
`
).
reverse
()[
0
];
},
/**
* Adds a new resource and it's associated attributes as taken
* from the passed in presence stanza.
* Also updates the presence if the resource has higher priority (and is newer).
* @private
* @param { XMLElement } presence: The presence stanza
*/
addResource
(
presence
)
{
const
jid
=
presence
.
getAttribute
(
'
from
'
),
name
=
Strophe
.
getResourceFromJid
(
jid
),
delay
=
sizzle
(
`delay[xmlns="
${
Strophe
.
NS
.
DELAY
}
"]`
,
presence
).
pop
(),
priority
=
presence
.
querySelector
(
'
priority
'
)?.
textContent
??
0
,
resource
=
this
.
resources
.
get
(
name
),
settings
=
{
'
name
'
:
name
,
'
priority
'
:
isNaN
(
parseInt
(
priority
,
10
))
?
0
:
parseInt
(
priority
,
10
),
'
show
'
:
presence
.
querySelector
(
'
show
'
)?.
textContent
??
'
online
'
,
'
timestamp
'
:
delay
?
dayjs
(
delay
.
getAttribute
(
'
stamp
'
)).
toISOString
()
:
(
new
Date
()).
toISOString
()
};
if
(
resource
)
{
resource
.
save
(
settings
);
}
else
{
this
.
resources
.
create
(
settings
);
}
},
/**
* Remove the passed in resource from the resources map.
* Also redetermines the presence given that there's one less
* resource.
* @private
* @param { string } name: The resource name
*/
removeResource
(
name
)
{
const
resource
=
this
.
resources
.
get
(
name
);
if
(
resource
)
{
resource
.
destroy
();
}
}
});
export
const
Presences
=
Collection
.
extend
({
'
model
'
:
Presence
});
src/headless/plugins/roster/utils.js
0 → 100644
View file @
7f851208
import
{
_converse
,
api
}
from
"
@converse/headless/core
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
export
async
function
initRoster
()
{
// Initialize the Bakcbone collections that represent the contats
// roster and the roster groups.
await
api
.
waitUntil
(
'
VCardsInitialized
'
);
_converse
.
roster
=
new
_converse
.
RosterContacts
();
let
id
=
`converse.contacts-
${
_converse
.
bare_jid
}
`
;
_converse
.
roster
.
browserStorage
=
_converse
.
createStore
(
id
);
_converse
.
roster
.
data
=
new
Model
();
id
=
`converse-roster-model-
${
_converse
.
bare_jid
}
`
;
_converse
.
roster
.
data
.
id
=
id
;
_converse
.
roster
.
data
.
browserStorage
=
_converse
.
createStore
(
id
);
_converse
.
roster
.
data
.
fetch
();
id
=
`converse.roster.groups
${
_converse
.
bare_jid
}
`
;
_converse
.
rostergroups
=
new
_converse
.
RosterGroups
();
_converse
.
rostergroups
.
browserStorage
=
_converse
.
createStore
(
id
);
/**
* Triggered once the `_converse.RosterContacts` and `_converse.RosterGroups` have
* been created, but not yet populated with data.
* This event is useful when you want to create views for these collections.
* @event _converse#chatBoxMaximized
* @example _converse.api.listen.on('rosterInitialized', () => { ... });
* @example _converse.api.waitUntil('rosterInitialized').then(() => { ... });
*/
api
.
trigger
(
'
rosterInitialized
'
);
}
export
function
updateUnreadCounter
(
chatbox
)
{
const
contact
=
_converse
.
roster
&&
_converse
.
roster
.
findWhere
({
'
jid
'
:
chatbox
.
get
(
'
jid
'
)});
if
(
contact
!==
undefined
)
{
contact
.
save
({
'
num_unread
'
:
chatbox
.
get
(
'
num_unread
'
)});
}
}
export
async
function
clearPresences
()
{
await
_converse
.
presences
?.
clearStore
();
}
src/plugins/rosterview/index.js
View file @
7f851208
...
@@ -5,7 +5,7 @@
...
@@ -5,7 +5,7 @@
*/
*/
import
"
../modal
"
;
import
"
../modal
"
;
import
"
@converse/headless/plugins/chatboxes
"
;
import
"
@converse/headless/plugins/chatboxes
"
;
import
"
@converse/headless/plugins/roster
"
;
import
"
@converse/headless/plugins/roster
/index.js
"
;
import
"
modals/add-contact.js
"
;
import
"
modals/add-contact.js
"
;
import
RosterContactView
from
'
./contactview.js
'
;
import
RosterContactView
from
'
./contactview.js
'
;
import
RosterGroupView
from
'
./groupview.js
'
;
import
RosterGroupView
from
'
./groupview.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