Browse Source

Multiparty Jingle

- Add support of XEP-0482: Call Invites
- Add support of XEP-0272: Multiparty Jingle
- Create Muji tables in the database
- Add Moxl Muji and Call Invites classes
- Refactoring to allow multiple Jingles at the same time
- Refactor the Jingle XMPP payload to use Packets
- Generalize the from parameter in all the Jingle events
- Create/delete dynamicaly the remote audio and video tags per Jingle session
- Initiate individual Jingle sessions in a MujiCall
- Refactor the presence handling to initiate properly the individual Jingle sessions
- Add adjustments to fit with Dino
- Move the audio/video toggle and screen sharing features to MovimJingles
- Handle properly Call Invite retractation
- Handle state where there is several Muji calls invites at the same time
- Add Ongoing Call in the title bar
- Handle mute/unmute in Muji calls
- Create new status messages to track the calls in the MUCs
- Complete DOAP
pull/1368/head
Timothée Jaussoin 1 year ago
parent
commit
9837032d27
  1. 2
      CHANGELOG.md
  2. 16
      app/Conference.php
  3. 9
      app/Helpers/StringHelper.php
  4. 6
      app/Helpers/UtilsHelper.php
  5. 61
      app/MujiCall.php
  6. 34
      app/MujiCallParticipant.php
  7. 6
      app/Presence.php
  8. 5
      app/Session.php
  9. 66
      app/Widgets/Chat/Chat.php
  10. 51
      app/Widgets/Chat/_chat_header.tpl
  11. 1
      app/Widgets/Chat/_chat_muji_propose.tpl
  12. 1
      app/Widgets/Chat/_chat_muji_retract.tpl
  13. 2
      app/Widgets/Chat/chat.js
  14. 2
      app/Widgets/Chat/locales.ini
  15. 17
      app/Widgets/Chats/Chats.php
  16. 11
      app/Widgets/Chats/_chats_calls.tpl
  17. 5
      app/Widgets/Chats/chats.tpl
  18. 12
      app/Widgets/ContactActions/ContactActions.php
  19. 2
      app/Widgets/ContactActions/_contactactions_drawer.tpl
  20. 2
      app/Widgets/Notif/_notif.tpl
  21. 13
      app/Widgets/Notif/notif.js
  22. 6
      app/Widgets/Presence/Presence.php
  23. 4
      app/Widgets/Publish/Publish.php
  24. 2
      app/Widgets/PublishStories/PublishStories.php
  25. 51
      app/Widgets/Rooms/Rooms.php
  26. 17
      app/Widgets/Rooms/_rooms_room.tpl
  27. 2
      app/Widgets/Rooms/locales.ini
  28. 8
      app/Widgets/Rooms/rooms.css
  29. 10
      app/Widgets/Rooms/rooms.js
  30. 4
      app/Widgets/Search/Search.php
  31. 2
      app/Widgets/Search/_search_roster.tpl
  32. 6
      app/Widgets/Search/search.js
  33. 2
      app/Widgets/Stickers/_stickers_emojis.tpl
  34. 454
      app/Widgets/Visio/Visio.php
  35. 126
      app/Widgets/Visio/_visio_lobby.tpl
  36. 9
      app/Widgets/Visio/locales.ini
  37. 175
      app/Widgets/Visio/visio.css
  38. 436
      app/Widgets/Visio/visio.js
  39. 20
      app/Widgets/Visio/visio.tpl
  40. 183
      app/Widgets/Visio/visio_utils.js
  41. 61
      database/migrations/20250329110303_create_muji_calls_table.php
  42. 1850
      doap.xml
  43. 2
      locales/locales.ini
  44. 408
      public/scripts/movim_jingles.js
  45. 2
      public/scripts/movim_rpc.js
  46. 11
      public/scripts/movim_utils.js
  47. 290
      public/scripts/movim_visio.js
  48. 5
      public/theme/css/form.css
  49. 4
      src/Movim/Controller/Base.php
  50. 49
      src/Movim/CurrentCall.php
  51. 11
      src/Movim/Librairies/JingletoSDP.php
  52. 61
      src/Movim/Librairies/SDPtoJingle.php
  53. 8
      src/Moxl/API.php
  54. 2
      src/Moxl/Stanza/Confirm.php
  55. 19
      src/Moxl/Stanza/Jingle.php
  56. 76
      src/Moxl/Stanza/JingleCallInvite.php
  57. 99
      src/Moxl/Stanza/Message.php
  58. 23
      src/Moxl/Stanza/Muc.php
  59. 17
      src/Moxl/Stanza/Presence.php
  60. 4
      src/Moxl/Stanza/Stream.php
  61. 2
      src/Moxl/Utils.php
  62. 2
      src/Moxl/Xec/Action/Jingle/SessionAccept.php
  63. 2
      src/Moxl/Xec/Action/Jingle/SessionTerminate.php
  64. 18
      src/Moxl/Xec/Action/JingleCallInvite/Accept.php
  65. 33
      src/Moxl/Xec/Action/JingleCallInvite/Invite.php
  66. 18
      src/Moxl/Xec/Action/JingleCallInvite/Left.php
  67. 18
      src/Moxl/Xec/Action/JingleCallInvite/Reject.php
  68. 34
      src/Moxl/Xec/Action/JingleCallInvite/Retract.php
  69. 22
      src/Moxl/Xec/Action/Muc/CreateMujiRoom.php
  70. 40
      src/Moxl/Xec/Action/Presence/Muc.php
  71. 14
      src/Moxl/Xec/Action/Presence/Unavailable.php
  72. 7
      src/Moxl/Xec/Handler.php
  73. 25
      src/Moxl/Xec/Payload/CallInviteAccept.php
  74. 27
      src/Moxl/Xec/Payload/CallInviteLeft.php
  75. 64
      src/Moxl/Xec/Payload/CallInvitePropose.php
  76. 39
      src/Moxl/Xec/Payload/CallInviteRetract.php
  77. 5
      src/Moxl/Xec/Payload/Carbons.php
  78. 75
      src/Moxl/Xec/Payload/Jingle.php
  79. 5
      src/Moxl/Xec/Payload/JingleAccept.php
  80. 8
      src/Moxl/Xec/Payload/JingleProceed.php
  81. 3
      src/Moxl/Xec/Payload/JinglePropose.php
  82. 5
      src/Moxl/Xec/Payload/JingleReject.php
  83. 25
      src/Moxl/Xec/Payload/JingleRetract.php
  84. 4
      src/Moxl/Xec/Payload/Payload.php
  85. 20
      src/Moxl/Xec/Payload/Presence.php
  86. 2
      src/Moxl/Xec/Payload/SASL2Challenge.php
  87. 2
      src/Moxl/Xec/Payload/SASLChallenge.php

2
CHANGELOG.md

@ -4,6 +4,8 @@ Movim Changelog
v0.30 (master)
---------------------------
* Limit the jid, name and group name to 256 character max in the Roster to allow it to be saved in the DB
* Implement XEP-0272: Multiparty Jingle (Muji)
* Implement XEP-0482: Call Invites
v0.29.2
---------------------------

16
app/Conference.php

@ -5,13 +5,14 @@ namespace App;
use Movim\ImageSize;
use Awobaz\Compoships\Database\Eloquent\Model;
use Movim\CurrentCall;
class Conference extends Model
{
public $incrementing = false;
protected $primaryKey = ['session_id', 'conference'];
protected $fillable = ['conference', 'name', 'nick', 'autojoin', 'pinned'];
protected $with = ['contact'];
protected $with = ['contact', 'mujiCalls'];
public static $xmlnsNotifications = 'xmpp:movim.eu/notifications:0';
public static $xmlnsPinned = 'urn:xmpp:bookmarks-pinning:0';
@ -46,6 +47,12 @@ class Conference extends Model
->orderBy('resource');
}
public function mujiCalls()
{
return $this->hasMany('App\MujiCall', ['jidfrom', 'session_id'], ['conference', 'session_id'])
->where('isfromconference', true);
}
public function otherPresences()
{
return $this->presences()->where('mucjid', '!=', \App\User::me()->id);
@ -222,7 +229,7 @@ class Conference extends Model
}
// https://docs.modernxmpp.org/client/groupchat/#types-of-chat
public function isGroupChat()
public function isGroupChat(): bool
{
if ($this->info) {
return $this->info->mucmembersonly && !$this->info->mucsemianonymous;
@ -231,6 +238,11 @@ class Conference extends Model
return false;
}
public function isInCall(): bool
{
return CurrentCall::getInstance()->isJidInCall($this->conference);
}
public function toArray()
{
$now = \Carbon\Carbon::now();

9
app/Helpers/StringHelper.php

@ -153,15 +153,6 @@ function unechap($string): string
return str_replace("\\\\", "\\", $string);
}
/**
* @desc Clean the resource of a jid
*/
function cleanJid($jid): string
{
$explode = explode('/', $jid);
return reset($explode);
}
/**
* @desc Extract the CID
*/

6
app/Helpers/UtilsHelper.php

@ -631,9 +631,11 @@ function generateUUID($string = false)
* @desc Generate a simple random key
* @params The size of the key
*/
function generateKey(?int $size = 16): string
function generateKey(?int $size = 16, bool $withCapitals = true): string
{
$hashChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$hashChars = 'abcdefghijklmnopqrstuvwxyz';
if ($withCapitals) $hashChars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$hash = '';
for ($i = 0; $i < $size; $i++) {

61
app/MujiCall.php

@ -0,0 +1,61 @@
<?php
namespace App;
use Awobaz\Compoships\Database\Eloquent\Model;
class MujiCall extends Model
{
use \Awobaz\Compoships\Compoships;
public $incrementing = false;
protected $primaryKey = ['session_id', 'id'];
protected $fillable = ['id', 'muc', 'jidfrom', 'video', 'isfromconference'];
protected $with = ['participants', 'presences'];
protected $attributes = [
'session_id' => SESSION_ID
];
public function session()
{
return $this->hasOne('App\Session');
}
public function conference()
{
return $this->hasOne('App\Conference', 'conference', 'jidfrom')
->where('session_id', $this->session_id);
}
public function presences()
{
return $this->hasMany('App\Presence', 'jid', 'muc')
->where('session_id', $this->session_id)
->where('value', '<', '5')
->where('resource', '!=', '');
}
public function presence()
{
return $this->hasOne('App\Presence', ['jid', 'session_id'], ['muc', 'session_id'])
->where('value', '<', 5)
->where('mucjid', \App\User::me()->id);
}
public function participants()
{
return $this->hasMany('App\MujiCallParticipant', 'muji_call_id', 'id')
->where('session_id', $this->session_id);
}
public function getIconAttribute()
{
return $this->video ? 'videocam' : 'call';
}
public function getendIconAttribute()
{
return $this->video ? 'videocam_off' : 'call_end';
}
}

34
app/MujiCallParticipant.php

@ -0,0 +1,34 @@
<?php
namespace App;
use Movim\Image;
use Movim\Model;
class MujiCallParticipant extends Model
{
public $incrementing = false;
protected $primaryKey = ['session_id', 'muji_call_id', 'jid'];
protected $fillable = ['session_id', 'muji_call_id', 'jid', 'left_at', 'inviter'];
protected $attributes = [
'session_id' => SESSION_ID
];
public function session()
{
return $this->hasOne('App\Session');
}
public function mujiCall()
{
return $this->belongsTo('App\MujiCall', 'id', 'muji_call_id')
->where('session_id', $this->session_id);
}
public function getConferencePictureAttribute(): string
{
return Image::getOrCreate($this->jid, 120) ?? avatarPlaceholder($this->jid . 'groupchat');
}
}

6
app/Presence.php

@ -159,7 +159,9 @@ class Presence extends Model
if (!empty($c->xpath("//status[@code='110']"))) {
$this->mucjid = \App\User::me()->id;
} elseif ($c->item->attributes()->jid) {
$this->mucjid = cleanJid((string)$c->item->attributes()->jid);
$jid = explodeJid((string)$c->item->attributes()->jid);
$this->mucjid = $jid['jid'];
$this->mucjidresource = $jid['resource'];
} else {
$this->mucjid = (string)$stanza->attributes()->from;
}
@ -206,6 +208,7 @@ class Presence extends Model
public function toArray()
{
$now = \Carbon\Carbon::now();
return [
'session_id' => $this->attributes['session_id'] ?? null,
'jid' => $this->attributes['jid'] ?? null,
@ -220,6 +223,7 @@ class Presence extends Model
'idle' => $this->attributes['idle'] ?? null,
'muc' => $this->attributes['muc'] ?? null,
'mucjid' => $this->attributes['mucjid'] ?? '',
'mucjidresource' => $this->attributes['mucjidresource'] ?? null,
'mucaffiliation' => $this->attributes['mucaffiliation'] ?? null,
'mucrole' => $this->attributes['mucrole'] ?? null,
'created_at' => $this->attributes['created_at'] ?? $now,

5
app/Session.php

@ -44,6 +44,11 @@ class Session extends Model
return $this->hasMany('App\Roster');
}
public function mujiCalls()
{
return $this->hasMany('App\MujiCall', 'session_id', 'id');
}
public function topContacts()
{
return $this->contacts()->join(DB::raw('(

66
app/Widgets/Chat/Chat.php

@ -39,10 +39,13 @@ class Chat extends \Movim\Widget\Base
private $_pagination = 50;
private $_wrapper = [];
private $_messageTypes = [
'chat', 'headline', 'invitation', 'jingle_incoming',
'jingle_outgoing', 'jingle_end', 'jingle_retract', 'jingle_reject'
'chat', 'headline', 'invitation',
'jingle_incoming', 'jingle_outgoing', 'jingle_end', 'jingle_retract', 'jingle_reject'
];
private $_messageTypesMuc = [
'groupchat', 'muji_propose', 'muji_retract',
'muc_owner', 'muc_admin', 'muc_outcast', 'muc_member'
];
private $_messageTypesMuc = ['groupchat', 'muc_owner', 'muc_admin', 'muc_outcast', 'muc_member'];
private $_mucPresences = [];
public function load()
@ -70,6 +73,7 @@ class Chat extends \Movim\Widget\Base
$this->registerEvent('chat_counter', 'onCounter', 'chat');
$this->registerEvent('jingle_message', 'onJingleMessage');
$this->registerEvent('muji_message', 'onMujiMessage');
$this->registerEvent('muc_event_message', 'onMucEventMessage');
$this->registerEvent('bob_request_handle', 'onSticker');
@ -77,6 +81,12 @@ class Chat extends \Movim\Widget\Base
$this->registerEvent('currentcall_started', 'onCallEvent', 'chat');
$this->registerEvent('currentcall_stopped', 'onCallEvent', 'chat');
$this->registerEvent('callinvitepropose', 'onCallInvite');
$this->registerEvent('callinviteaccept', 'onCallInvite');
$this->registerEvent('callinviteleft', 'onCallInvite');
$this->registerEvent('callinviteretract', 'onCallInvite');
$this->registerEvent('presence_muji_event', 'onCallInvite');
}
public function onPresence($packet)
@ -90,6 +100,15 @@ class Chat extends \Movim\Widget\Base
}
}
public function onCallInvite($packet)
{
$muji = $packet->content;
if ($muji->jidfrom && $muji->conference) {
$this->ajaxGetHeader($muji->jidfrom, $muji->isfromconference);
}
}
public function onCallEvent($packet)
{
$this->ajaxGetHeader($packet[0]);
@ -100,6 +119,11 @@ class Chat extends \Movim\Widget\Base
$this->onMessage($packet, false, false);
}
public function onMujiMessage($packet)
{
$this->onMessage($packet, false, false);
}
public function onMucEventMessage($packet)
{
$this->onMessage($packet, false, false);
@ -153,8 +177,9 @@ class Chat extends \Movim\Widget\Base
if (
$message->isEmpty() && !in_array($message->type, [
'jingle_incoming', 'jingle_outgoing', 'jingle_end', 'muc_owner', 'jingle_retract',
'jingle_reject', 'muc_admin', 'muc_outcast', 'muc_member'
'jingle_incoming', 'jingle_outgoing', 'jingle_end', 'muc_owner', 'jingle_retract', 'jingle_reject',
'muji_propose', 'muji_retract',
'muc_admin', 'muc_outcast', 'muc_member'
])
) {
return;
@ -465,6 +490,10 @@ class Chat extends \Movim\Widget\Base
(new Dictaphone)->ajaxHttpGet();
}
if (CurrentCall::getInstance()->isStarted()) {
$this->rpc('MovimVisio.moveToChat', CurrentCall::getInstance()->getBareJid());
}
$this->rpc('Chat.setObservers');
$this->rpc('Chat.resetCurrentDateTime'); // TODO, not call there all the time ?!
$this->prepareMessages($room, true);
@ -1549,9 +1578,11 @@ class Chat extends \Movim\Widget\Base
: $view->draw('_chat_invitation');
}
// Internal messages
if (in_array($message->type, [
'muc_owner', 'muc_admin', 'muc_outcast', 'jingle_reject',
'muc_member', 'jingle_incoming', 'jingle_outgoing', 'jingle_retract'
'muc_owner', 'muc_admin', 'muc_outcast', 'muc_member',
'jingle_reject', 'jingle_incoming', 'jingle_outgoing', 'jingle_retract',
'muji_propose'
])) {
$view = $this->tpl();
$view->assign('message', $message);
@ -1563,7 +1594,7 @@ class Chat extends \Movim\Widget\Base
$view->assign('message', $message);
$view->assign('diff', false);
$start = Message::where('thread', $message->thread)
$start = $this->user->messages()->where('thread', $message->thread)
->whereIn('type', ['jingle_incoming', 'jingle_outgoing'])
->first();
@ -1577,6 +1608,25 @@ class Chat extends \Movim\Widget\Base
$message->body = trim((string)$view->draw('_chat_jingle_end'));
}
if ($message->type == 'muji_retract') {
$view = $this->tpl();
$view->assign('message', $message);
$view->assign('diff', false);
$start = $this->user->messages()->where('thread', $message->thread)
->whereIn('type', ['muji_propose'])
->first();
if ($start) {
$diff = (new \DateTime($start->created_at))
->diff(new \DateTime($message->created_at));
$view->assign('diff', $diff);
}
$message->body = trim((string)$view->draw('_chat_muji_retract'));
}
return $this->_wrapper;
}

51
app/Widgets/Chat/_chat_header.tpl

@ -31,13 +31,24 @@
{/if}
</span>
{if="$conference && $conference->info && $conference->info->related"}
{$related = $conference->info->related}
<span
title="{$c->__('page.communities')}{$related->name}"
onclick="MovimUtils.reload('{$c->route('community', [$related->server, $related->node])}')"
class="control icon bubble active small">
<img src="{$related->getPicture(\Movim\ImageSize::M)}"/>
{if="$conference->isGroupChat()"}
{if="$conference && $conference->info && $conference->info->related"}
{$related = $conference->info->related}
<span
title="{$c->__('page.communities')}{$related->name}"
onclick="MovimUtils.reload('{$c->route('community', [$related->server, $related->node])}')"
class="control icon bubble active small">
<img src="{$related->getPicture(\Movim\ImageSize::M)}"/>
</span>
{/if}
{/if}
{if="$conference->mujiCalls->isEmpty()"}
<span class="control icon active {if="$incall"}disabled{/if}" onclick="Visio_ajaxGetMujiLobby('{$conference->conference}', true, true);">
<i class="material-symbols">videocam</i>
</span>
<span class="control icon active {if="$incall"}disabled{/if}" onclick="Visio_ajaxGetMujiLobby('{$conference->conference}', true, false);">
<i class="material-symbols">call</i>
</span>
{/if}
@ -48,6 +59,18 @@
</span>
<div>
{if="$conference->mujiCalls->isNotEmpty()"}
{$muji = $conference->mujiCalls->first()}
{if="!$contactincall"}
<button class="button oppose color blue {if="$incall"}disabled{/if}" onclick="Visio_ajaxJoinMuji('{$muji->id}', {if="$muji->video"}true{else}false{/if});">
<i class="material-symbols {if="!$incall"}blink{/if}">{$muji->icon}</i>
</button>
{else}
<button class="button oppose color red" onclick="Visio_ajaxLeaveMuji('{$muji->id}')">
<i class="material-symbols">{$muji->icon}</i>
</button>
{/if}
{/if}
<p class="line active" title="{$jid|echapJS}" onclick="RoomsUtils_ajaxGetDrawer('{$jid|echapJS}')">
{if="$conference && $conference->title"}
{$conference->title}
@ -87,6 +110,20 @@
<p class="compose first line" id="{$jid|cleanupId}-state"></p>
<p class="line active">
{if="$conference->mujiCalls->isNotEmpty()"}
<i class="material-symbols icon {if="$conference->isInCall()"}green{else}blue{/if} blink">
{$conference->mujiCalls->first()->icon}
</i>
{if="$conference->isInCall()"}
{$conference->mujiCalls->first()->presences->count()}
{else}
{$conference->mujiCalls->first()->participants->count()}
{/if}
<i class="material-symbols">people</i>
{$c->__('visio.in_call')}
{/if}
{if="$conference"}
{if="!$conference->connected"}
{$c->__('button.connecting')}

1
app/Widgets/Chat/_chat_muji_propose.tpl

@ -0,0 +1 @@
<i class="material-symbols icon green">call</i> {$c->__('chat.muji_propose')}

1
app/Widgets/Chat/_chat_muji_retract.tpl

@ -0,0 +1 @@
<i class="material-symbols icon red">call_end</i> {$c->__('chat.muji_retract')}{if="$diff"}{if="$diff->h > 0"}{$c->__('chat.jingle_hours', $diff->h, $diff->i)}{elseif="$diff->i > 0"}{$c->__('chat.jingle_minutes', $diff->i, $diff->s)}{else}{$c->__('chat.jingle_seconds', $diff->s)}{/if}{/if}

2
app/Widgets/Chat/chat.js

@ -38,7 +38,7 @@ var Chat = {
groupChatMembers: [],
// Jingle types
jingleTypes: ['jingle_incoming', 'jingle_outgoing', 'jingle_end'],
jingleTypes: ['jingle_incoming', 'jingle_outgoing', 'jingle_end', 'muji_propose', 'muji_retract'],
// Keep track of replaced messages hash when loading history or refreshing
replacedHash: [],

2
app/Widgets/Chat/locales.ini

@ -37,6 +37,8 @@ jingle_end = Call ended
jingle_hours = %s hours %s minutes
jingle_minutes = %s minutes and %s seconds
jingle_seconds = %s seconds
muji_propose = A conference call has started
muji_retract = The conference call has ended
muc_admin = %s is now admin
muc_owner = %s is now owner
muc_outcast = %s is now banned

17
app/Widgets/Chats/Chats.php

@ -54,6 +54,10 @@ class Chats extends Base
$this->registerEvent('currentcall_started', 'onCallEvent', 'chat');
$this->registerEvent('currentcall_stopped', 'onCallEvent', 'chat');
$this->registerEvent('callinvitepropose', 'onCallInvite');
$this->registerEvent('callinviteaccept', 'onCallInvite');
$this->registerEvent('callinviteleft', 'onCallInvite');
}
public function onStart($packet)
@ -62,6 +66,11 @@ class Chats extends Base
$tpl->cacheClear('_chats_item');
}
public function onCallInvite($packet)
{
$this->rpc('MovimTpl.fill', '#chats_calls_list', $this->prepareCalls());
}
public function onMessage($packet)
{
$message = $packet->content;
@ -253,6 +262,14 @@ class Chats extends Base
}
}
public function prepareCalls()
{
$view = $this->tpl();
$view->assign('calls', $this->user->session->mujiCalls()->where('isfromconference', false)->get());
return $view->draw('_chats_calls');
}
public function prepareChats()
{
$chats = $this->resolveChats();

11
app/Widgets/Chats/_chats_calls.tpl

@ -0,0 +1,11 @@
{loop="$calls"}
<li>
<span class="primary icon gray">
<i class="material-symbols">interpreter_mode</i>
</span>
<div>
<p>{$value->id}</p>
<p><i class="material-symbols icon green blink">phone_in_talk</i> {$c->__('visio.in_call')}</p>
</div>
</li>
{/loop}

5
app/Widgets/Chats/chats.tpl

@ -1,3 +1,8 @@
<ul id="chats_calls_list" class="list middle active divided spaced">
{autoescape="off"}
{$c->prepareCalls()}
{/autoescape}
</ul>
<ul id="chats_widget_header" class="list" data-filter="{$filter}">
<li class="subheader">
<div>

12
app/Widgets/ContactActions/ContactActions.php

@ -132,16 +132,20 @@ class ContactActions extends Base
$this->rpc('ContactActions.resolveSessionsStates', $jid);
}
public function ajaxChat($jid)
public function ajaxChat(string $jid, bool $muc = false)
{
if (!validateJid($jid)) {
return;
}
$c = new Chats();
$c->ajaxOpen($jid);
if ($muc) {
$this->rpc('MovimUtils.reload', $this->route('chat', [$jid, 'room']));
} else {
$c = new Chats();
$c->ajaxOpen($jid);
$this->rpc('MovimUtils.reload', $this->route('chat', $jid));
$this->rpc('MovimUtils.reload', $this->route('chat', $jid));
}
}
public function ajaxHttpGetPictures($jid, $page = 0)

2
app/Widgets/ContactActions/_contactactions_drawer.tpl

@ -18,7 +18,7 @@
</span>
{/if}
{if="!$contact->isMe()"}
<span class="control icon active divided" onclick="Search.chat('{$contact->id|echapJS}'); Drawer.clear();">
<span class="control icon active divided" onclick="Search.chat('{$contact->id|echapJS}', false); Drawer.clear();">
<i class="material-symbols">comment</i>
</span>
{if="$roster && $roster->presences->count() > 0 && !$incall"}

2
app/Widgets/Notif/_notif.tpl

@ -2,7 +2,7 @@
{if="isset($onclick)"}
onclick="{$onclick}; Notif.snackbarClear();"
{elseif="isset($action)"}
onclick="MovimUtils.softRedirect('{$action}')"
onclick="MovimUtils.reload('{$action}')"
{/if}
>
<li>

13
app/Widgets/Notif/notif.js

@ -1,6 +1,7 @@
var Notif = {
inhibed: false,
focused: false,
call_status: null,
tab_counter1: 0,
tab_counter2: 0,
tab_counter1_key: 'chat',
@ -92,10 +93,18 @@ var Notif = {
Notif.document_title = title;
Notif.displayTab();
},
setCallStatus: function (status) {
Notif.call_status = status;
Notif.displayTab();
},
displayTab: function () {
document.title = (Notif.call_status != null)
? Notif.call_status + ' | '
: '';
if (Notif.tab_counter1 == 0 && Notif.tab_counter2 == 0) {
MovimFavicon.counter(0, 0);
document.title = Notif.document_title;
document.title += Notif.document_title;
if (typeof window.electron !== 'undefined')
window.electron.notification(false);
@ -103,7 +112,7 @@ var Notif = {
if (typeof window.rambox !== 'undefined')
window.rambox.setUnreadCount(0);
} else {
document.title =
document.title +=
Notif.tab_counter1
+ '∣'
+ Notif.tab_counter2

6
app/Widgets/Presence/Presence.php

@ -19,6 +19,8 @@ use App\BundleCapabilityResolver;
use App\Post;
use App\Widgets\Chats\Chats;
use App\Widgets\Dialog\Dialog;
use App\Widgets\Visio\Visio;
use Movim\CurrentCall;
use Moxl\Xec\Action\Blocking\Request;
class Presence extends Base
@ -94,6 +96,10 @@ class Presence extends Base
$this->user->encryptedPasswords()->delete();
if (CurrentCall::getInstance()->isStarted()) {
//(new Visio)->ajaxEnd(CurrentCall::getInstance()->jid, CurrentCall::getInstance()->id);
}
$p = new Unavailable;
$p->setType('terminate')
->setResource($this->user->session->resource)

4
app/Widgets/Publish/Publish.php

@ -44,9 +44,9 @@ class Publish extends Base
}
if ($node == AppPost::MICROBLOG_NODE) {
$this->rpc('MovimUtils.softRedirect', $this->route('news'));
$this->rpc('MovimUtils.reload', $this->route('news'));
} elseif ($node != AppPost::STORIES_NODE) {
$this->rpc('MovimUtils.softRedirect', $this->route('community', [$to, $node]));
$this->rpc('MovimUtils.reload', $this->route('community', [$to, $node]));
}
}

2
app/Widgets/PublishStories/PublishStories.php

@ -35,7 +35,7 @@ class PublishStories extends Base
->request();
}
$this->rpc('MovimUtils.softRedirect', $this->route('chat'));
$this->rpc('MovimUtils.reload', $this->route('chat'));
}
$this->rpc('PublishStories.close');

51
app/Widgets/Rooms/Rooms.php

@ -12,8 +12,11 @@ use Movim\ChatStates;
use Movim\Widget\Base;
use App\Conference;
use App\Widgets\Notif\Notif;
use App\Widgets\Toast\Toast;
use Movim\ChatroomPings;
use Movim\CurrentCall;
use Moxl\Xec\Payload\Packet;
class Rooms extends Base
{
@ -46,6 +49,12 @@ class Rooms extends Base
$this->registerEvent('presence_muc_errornotacceptable', 'onNotAcceptable');
$this->registerEvent('presence_muc_errorserviceunavailable', 'onServiceUnavailable');
$this->registerEvent('callinvitepropose', 'onCallInvitePropose');
$this->registerEvent('callinviteaccept', 'onCallInvite');
$this->registerEvent('callinviteleft', 'onCallInvite');
$this->registerEvent('callinviteretract', 'onCallInvite');
$this->registerEvent('presence_muji_event', 'onCallInvite');
// Bug: In Chat::ajaxGet, Notif.current might come after this event
// so we don't set the filter
$this->registerEvent('chat_open_room', 'onChatOpen'/*, 'chat'*/);
@ -61,6 +70,37 @@ class Rooms extends Base
$this->setState($array[0], isset($array[1]));
}
public function onCallInvitePropose(Packet $packet)
{
$muji = $packet->content;
if ($muji->jidfrom && $muji->conference) {
Notif::append(
'chat|' . $muji->jidfrom,
($muji->conference != null && $muji->conference->name)
? $muji->conference->name
: $muji->jidfrom,
($muji->video)
? "📹 " . __('muji.call_video_invite')
: "📞 " . __('muji.call_audio_invite'),
$muji->conference->getPicture(),
5,
$this->route('chat', [$muji->jidfrom, 'room'])
);
$this->onCallInvite($packet);
}
}
public function onCallInvite(Packet $packet)
{
$muji = $packet->content;
if ($muji->jidfrom && $muji->conference) {
$this->onPresence($muji->jidfrom);
}
}
public function onMessage($packet)
{
$message = $packet->content;
@ -166,7 +206,7 @@ class Rooms extends Base
}
}
private function onPresence(string $room)
public function onPresence(string $room, bool $callSecond = true)
{
$conference = $this->user->session->conferences()
->where('conference', $room)
@ -175,10 +215,15 @@ class Rooms extends Base
->first();
if ($conference) {
$this->rpc('Rooms.setRoom', \cleanupId($conference->conference), $this->prepareConference($conference));
$this->rpc('Rooms.setRoom', \cleanupId($conference->conference), $this->prepareConference($conference), $callSecond);
}
}
public function ajaxSecondGet(string $room)
{
$this->onPresence($room, callSecond: false);
}
public function ajaxHttpGet()
{
$conferences = $this->user->session->conferences()
@ -189,7 +234,7 @@ class Rooms extends Base
$this->rpc('Rooms.clearRooms');
foreach ($conferences as $conference) {
$this->rpc('Rooms.setRoom', \cleanupId($conference->conference), $this->prepareConference($conference));
$this->rpc('Rooms.setRoom', \cleanupId($conference->conference), $this->prepareConference($conference), true);
}
$this->rpc('Rooms.refresh');

17
app/Widgets/Rooms/_rooms_room.tpl

@ -6,6 +6,7 @@
{if="$conference->pinned"}pinned{/if}
{if="$conference->isGroupChat()"}groupchat{/if}
{if="$conference->unreads_count > 0 || $conference->quoted_count > 0"}unread{/if}
{if="$conference->mujiCalls->isNotEmpty()"}muc_call{/if}
">
<span class="primary icon bubble small"
id="{$conference->conference|cleanupId}-rooms-primary"
@ -59,6 +60,22 @@
{/if}
</span>
</p>
{if="$conference->mujiCalls->isNotEmpty()"}
<p data-mujiid="{$conference->mujiCalls->first()->id}">
<i class="material-symbols icon {if="$conference->isInCall()"}green{else}blue{/if} blink">
{$conference->mujiCalls->first()->icon}
</i>
{if="$conference->isInCall()"}
{$conference->mujiCalls->first()->presences->count()}
{else}
{$conference->mujiCalls->first()->participants->count()}
{/if}
<i class="material-symbols">people</i>
{$c->__('visio.in_call')}
</p>
{/if}
</div>
<span class="control icon active gray" onclick="event.stopPropagation(); RoomsUtils_ajaxRemove('{$conference->conference|echapJS}');">
<i class="material-symbols">delete</i>

2
app/Widgets/Rooms/locales.ini

@ -102,4 +102,4 @@ affiliation_none_changed = Affiliation removed for the user
[rooms_filter]
all = All
connected = Connected
connected = Connected

8
app/Widgets/Rooms/rooms.css

@ -42,6 +42,14 @@
line-height: initial;
}
#rooms_widget ul li.muc_call p:first-child {
padding-top: 0.75rem;
}
#rooms_widget ul li.muc_call p:nth-child(2) {
padding: 1rem 0;
}
#rooms_widget ul.list.rooms:empty ~ ul.toggle_show,
#rooms_widget ul.list.rooms:not(:empty):not(.all) ~ ul.toggle_show li span.primary i:first-child,
#rooms_widget ul.list.rooms:not(:empty).all ~ ul.toggle_show li span.primary i:last-child,

10
app/Widgets/Rooms/rooms.js

@ -67,7 +67,7 @@ var Rooms = {
}
},
refresh: function () {
refresh: function (callSecond) {
Rooms.displayToggleButton();
var list = document.querySelector('#rooms_widget ul.list.rooms');
@ -84,6 +84,10 @@ var Rooms = {
}
}
// If we have a room with a call we do a second daemon refresh to get the live status
if (items[i].classList.contains('muc_call') && callSecond == true) {
Rooms_ajaxSecondGet(items[i].dataset.jid);
}
if (
i >= 1
@ -109,7 +113,7 @@ var Rooms = {
document.querySelector('#rooms_widget ul.list.rooms').innerHTML = '';
},
setRoom: function (id, html) {
setRoom: function (id, html, noSecondRefresh) {
var listSelector = '#rooms_widget ul.list.rooms ';
var list = document.querySelector(listSelector);
var element = list.querySelector('#' + id);
@ -132,7 +136,7 @@ var Rooms = {
MovimTpl.append(listSelector, html);
}
Rooms.refresh();
Rooms.refresh(noSecondRefresh);
},
clearAllActives: function() {

4
app/Widgets/Search/Search.php

@ -144,10 +144,10 @@ class Search extends Base
$this->rpc('Search.searchClear');
}
public function ajaxChat($jid)
public function ajaxChat(string $jid, bool $muc = false)
{
$contact = new ContactActions();
$contact->ajaxChat($jid);
$contact->ajaxChat($jid, $muc);
}
public function prepareTicket(Post $post)

2
app/Widgets/Search/_search_roster.tpl

@ -39,7 +39,7 @@
{/if}
{/loop}
{/if}
<span class="control icon active gray divided" onclick="Search.chat('{$value->jid|echapJS}')">
<span class="control icon active gray divided" onclick="Search.chat('{$value->jid|echapJS}', false)">
<i class="material-symbols">comment</i>
</span>
<div>

6
app/Widgets/Search/search.js

@ -58,12 +58,12 @@ var Search = {
Search.searchCurrent();
},
chat : function(jid) {
if (MovimUtils.urlParts().page === 'chat') {
chat : function(jid, muc) {
if (MovimUtils.urlParts().page === 'chat' && muc == false) {
Chats_ajaxOpen(jid, true);
Drawer.clear();
} else {
Search_ajaxChat(jid);
Search_ajaxChat(jid, muc);
}
},

2
app/Widgets/Stickers/_stickers_emojis.tpl

@ -31,7 +31,7 @@
{if="$gotemojis"}
{if="$favorites->isEmpty()"}
<ul class="list thick active">
<li onclick="MovimUtils.softRedirect('{$c->route('conf')}')">
<li onclick="MovimUtils.reload('{$c->route('conf')}')">
<span class="primary icon yellow">
<i class="material-symbols">family_star</i>
</span>

454
app/Widgets/Visio/Visio.php

@ -2,9 +2,12 @@
namespace App\Widgets\Visio;
use App\MujiCall;
use App\Widgets\Dialog\Dialog;
use App\Widgets\Notif\Notif;
use App\Widgets\Rooms\Rooms;
use Movim\CurrentCall;
use Movim\ImageSize;
use Movim\Librairies\JingletoSDP;
use Movim\Librairies\SDPtoJingle;
use Moxl\Xec\Action\Jingle\SessionPropose;
@ -17,6 +20,13 @@ use Moxl\Xec\Action\Jingle\SessionUnmute;
use Movim\Widget\Base;
use Moxl\Xec\Action\Jingle\SessionReject;
use Moxl\Xec\Action\Jingle\SessionRetract;
use Moxl\Xec\Action\JingleCallInvite\Accept;
use Moxl\Xec\Action\JingleCallInvite\Invite;
use Moxl\Xec\Action\JingleCallInvite\Retract;
use Moxl\Xec\Action\Muc\CreateMujiRoom;
use Moxl\Xec\Action\Presence\Muc;
use Moxl\Xec\Action\Presence\Unavailable;
use Moxl\Xec\Payload\Packet;
class Visio extends Base
{
@ -24,18 +34,17 @@ class Visio extends Base
{
$this->addcss('visio.css');
$this->addcss('visio_lobby.css');
$this->addjs('visio.js');
$this->addjs('visio_utils.js');
$this->addjs('visio_dtmf.js');
$this->registerEvent('jinglepropose', 'onPropose');
$this->registerEvent('jingleproceed', 'onProceed');
$this->registerEvent('jingleaccept', 'onAccept');
$this->registerEvent('jingleretract', 'onTerminateRetract');
$this->registerEvent('jinglereject', 'onTerminateReject');
$this->registerEvent('jingleretract', 'onRetract');
$this->registerEvent('jinglereject', 'onReject');
$this->registerEvent('jingle_sessioninitiate', 'onInitiateSDP');
$this->registerEvent('jingle_sessioninitiate_erroritemnotfound', 'onTerminateNotFound');
//$this->registerEvent('jingle_sessioninitiate_erroritemnotfound', 'onNotFound');
$this->registerEvent('jingle_sessionaccept', 'onAcceptSDP');
$this->registerEvent('jingle_transportinfo', 'onCandidate');
$this->registerEvent('jingle_sessionterminate', 'onTerminate');
@ -50,6 +59,11 @@ class Visio extends Base
$this->registerEvent('externalservices_get_error', 'onExternalServicesError');
$this->registerEvent('session_down', 'onSessionDown');
$this->registerEvent('presence_muji', 'onMujiPresence');
$this->registerEvent('presence_muc_muji_preparing', 'onMucMujiPreparing');
$this->registerEvent('presence_muc_create_muji_handle', 'onMucMujiCreated');
$this->registerEvent('callinviteretract', 'onCallInviteRetract');
}
public function onSessionDown()
@ -58,15 +72,34 @@ class Visio extends Base
if ($currentCall->isStarted()) {
$st = new SessionTerminate;
$st->setTo($currentCall->to)
->setJingleSid($currentCall->id)
->setReason('failed-application')
->request();
$st->setTo($currentCall->jid)
->setJingleSid($currentCall->id)
->setReason('failed-application')
->request();
$currentCall->stop();
$currentCall->stop($currentCall->jid, $currentCall->id);
}
}
public function onMucMujiCreated($packet)
{
$presence = $packet->content;
$createMujiRoom = new CreateMujiRoom;
$createMujiRoom->setTo($presence->jid)
->request();
}
public function onMucMujiPreparing($packet)
{
$this->ajaxMujiTrigger();
}
public function onCallInviteRetract($packet)
{
$this->rpc('MovimVisio.clear');
}
public function onExternalServices($packet)
{
$externalServices = [];
@ -79,8 +112,8 @@ class Visio extends Base
if ($service['type'] == 'turn' && $turn) continue;
if ($service['type'] == 'turn') $turn = true;
$url = $service['type'].':'.$service['host'];
$url .= !empty($service['port']) ? ':'.$service['port'] : '';
$url = $service['type'] . ':' . $service['host'];
$url .= !empty($service['port']) ? ':' . $service['port'] : '';
$item = ['urls' => $url];
if (isset($service['username']) && isset($service['password'])) {
@ -93,161 +126,257 @@ class Visio extends Base
}
if (!empty($externalServices)) {
$this->rpc('Visio.setServices', $externalServices);
$this->rpc('MovimVisio.setServices', $externalServices);
} else {
$this->setDefaultServices();
}
}
public function onMujiPresence($packet)
{
list($stanza, $presence) = $packet->content;
if ($stanza && $stanza->muji && $stanza->muji->attributes()->xmlns == 'urn:xmpp:jingle:muji:0'
&& $stanza->muji->content && $stanza->muji->content->attributes()->xmlns == 'urn:xmpp:jingle:1') {
$contact = \App\Contact::firstOrNew(['id' => \baseJid($packet->from)]);
$this->rpc('MovimJingles.initSession',
\baseJid($packet->from),
$packet->from,
null,
$contact->truename,
$contact->getPicture(ImageSize::L)
);
}
}
public function onExternalServicesError($packet)
{
$this->setDefaultServices();
//$this->rpc('MovimJingles.onInitiateSDP', \baseJid($packet->from), $jts->generate());
}
public function onPropose($packet)
/**
* Session events
*/
public function onPropose(Packet $packet)
{
$data = $packet->content;
$this->ajaxGetLobby($data['from'], false, $data['withVideo'], $data['id']);
(CurrentCall::getInstance())->start(\baseJid($packet->from), $data['id']);
$this->ajaxGetLobby($packet->from, false, $data['withVideo'], $data['id']);
}
public function onInitiateSDP($data)
public function onProceed(Packet $packet)
{
list($stanza, $from) = $data;
$jts = new JingletoSDP($stanza);
$this->rpc('Visio.onInitiateSDP', $jts->generate());
$this->rpc('MovimJingles.onProceed', \baseJid($packet->from), $packet->from, $packet->content /* id */);
}
public function onContentAdd($stanza)
public function onAccept($packet)
{
$jts = new JingletoSDP($stanza);
$this->rpc('Notif.incomingAnswer');
$this->rpc('Visio.onContentAdd', $jts->generate());
(new Dialog)->ajaxClear();
}
public function onProceed($packet)
public function onRetract(Packet $packet)
{
$data = $packet->content;
$this->rpc('Visio.onProceed', $data['from'], $data['id']);
$this->onTerminate($packet);
}
public function onAccept($packet)
public function onReject(Packet $packet)
{
$this->rpc('Notif.incomingAnswer');
(new Dialog)->ajaxClear();
$this->onTerminate($packet);
}
public function onAcceptSDP($stanza)
/*public function onNotFound(Packet $packet)
{
$jts = new JingletoSDP($stanza);
$this->rpc('Visio.onAcceptSDP', $jts->generate());
}
(CurrentCall::getInstance())->stop(\baseJid($packet->from), $packet->content);
$this->onTerminate('notfound');
}*/
public function onCandidate($stanza)
public function onTerminate(Packet $packet)
{
$jts = new JingletoSDP($stanza);
$sdp = $jts->generate();
(CurrentCall::getInstance())->stop(\baseJid($packet->from), $packet->content);
// Stop calling sound and clear the Dialog if there
$this->rpc('Notif.incomingAnswer');
(new Dialog)->ajaxClear();
$this->rpc('Visio.onCandidate', $sdp, (string)$jts->name, $jts->name);
$this->rpc('MovimJingles.onTerminate', \baseJid($packet->from));
}
public function onTerminateRetract()
/**
* Jingle events
*/
public function onInitiateSDP(Packet $packet)
{
$this->onTerminate('retract');
$jts = new JingletoSDP($packet->content);
$this->rpc('MovimJingles.onInitiateSDP', \baseJid($packet->from), $jts->generate(), $jts->sid);
}
public function onTerminateReject()
public function onContentAdd(Packet $packet)
{
$this->onTerminate('reject');
$jts = new JingletoSDP($packet->content);
$this->rpc('MovimJingles.onContentAdd', \baseJid($packet->from), $jts->generate());
}
public function onTerminateNotFound()
public function onAcceptSDP(Packet $packet)
{
$this->onTerminate('notfound');
$jts = new JingletoSDP($packet->content);
$this->rpc('MovimJingles.onAcceptSDP', \baseJid($packet->from), $jts->generate());
}
public function onTerminate($reason)
public function onCandidate(Packet $packet)
{
// Stop calling sound and clear the Dialog if there
$this->rpc('Notif.incomingAnswer');
(new Dialog)->ajaxClear();
if (CurrentCall::getInstance()->isStarted()) {
CurrentCall::getInstance()->stop();
}
$jts = new JingletoSDP($packet->content);
$sdp = $jts->generate();
$this->rpc('Visio.goodbye', $reason);
$this->rpc('MovimJingles.onCandidate', \baseJid($packet->from), $sdp, (string)$jts->name, $jts->name);
}
public function onMute($name)
public function onMute(Packet $packet)
{
$this->rpc('Visio.onMute', $name);
$this->rpc('MovimJingles.onMute', \baseJid($packet->from), $packet->content);
}
public function onUnmute($name)
public function onUnmute(Packet $packet)
{
$this->rpc('Visio.onUnmute', $name);
$this->rpc('MovimJingles.onUnmute', \baseJid($packet->from), $packet->content);
}
public function ajaxPropose($to, $id, $withVideo = false)
public function ajaxPropose(string $to, string $id, $withVideo = false)
{
(CurrentCall::getInstance())->start(\baseJid($to), $id);
$p = new SessionPropose;
$p->setTo($to)
->setId($id)
->setWithVideo($withVideo)
->request();
->setId($id)
->setWithVideo($withVideo)
->request();
}
public function ajaxAccept($to, $id)
public function ajaxAccept(string $to, string $id)
{
(CurrentCall::getInstance())->start(\baseJid($to), $id);
$p = new SessionAccept;
$p->setTo($to)
->setId($id)
->request();
->setId($id)
->request();
}
public function ajaxReject($to, $id)
{
$this->rpc('Notification.incomingAnswer');
(CurrentCall::getInstance())->stop($to, $id);
$this->rpc('Notifs.incomingAnswer');
$reject = new SessionReject;
$reject->setTo($to)
->setId($id)
->request();
->setId($id)
->request();
}
public function ajaxMute($to, $id, $name)
{
$p = new SessionMute;
$p->setTo($to)
->setId($id)
->setName($name)
->request();
->setId($id)
->setName($name)
->request();
}
public function ajaxUnmute($to, $id, $name)
{
$p = new SessionUnmute;
$p->setTo($to)
->setId($id)
->setName($name)
->request();
->setId($id)
->setName($name)
->request();
}
/** Muji */
public function ajaxJoinMuji(string $mujiId, ?bool $withVideo = false)
{
$muji = $this->user->session->mujiCalls()
->where('id', $mujiId)
->with('conference')
->first();
if ($muji) {
$this->ajaxGetMujiLobby($muji->jidfrom, false, $withVideo, $muji->id);
}
}
public function ajaxLeaveMuji(string $mujiId)
{
$muji = $this->user->session->mujiCalls()
->where('id', $mujiId)
->with('conference')
->first();
if ($muji) {
CurrentCall::getInstance()->stop($muji->jidfrom, $muji->id);
$resource = $muji->presence?->resource;
if ($resource) {
$pu = new Unavailable;
$pu->setTo($muji->muc)
->setResource($resource)
->request();
$this->user->session->mujiCalls()->where('id', $mujiId)->delete();
(new Rooms)->onPresence($muji->jidfrom);
// If we were the inviter, we also retract the call
$participant = $muji->participants->firstWhere('jid', $muji->jidfrom . '/' . $resource);
if ($participant && $participant->inviter) {
$retract = new Retract;
$retract->setTo($muji->jidfrom)
->setId($muji->id)
->request();
}
}
$this->rpc('MovimJingles.terminateAll');
}
}
public function ajaxGetMujiLobby(string $jid, bool $calling = false, ?bool $withVideo = false, ?string $id = null)
{
$view = $this->tpl();
$view->assign('conference', $this->user->session
->conferences()->where('conference', $jid)
->first());
$view->assign('calling', $calling);
$view->assign('withvideo', $withVideo);
$view->assign('id', $id);
Dialog::fill($view->draw('_visio_lobby'), false, true);
$this->rpc('MovimVisio.getUserMedia', $withVideo);
}
public function ajaxGetLobby(string $jid, bool $calling = false, ?bool $withVideo = false, ?string $id = null)
{
$contact = \App\Contact::firstOrNew(['id' => \explodeJid($jid)['jid']]);
$contact = \App\Contact::firstOrNew(['id' => \baseJid($jid)]);
$view = $this->tpl();
$view->assign('contact', $contact);
$view->assign('jid', $jid);
$view->assign('calling', $calling);
$view->assign('withvideo', $withVideo);
$view->assign('id', $id);
$view->assign('fullJid', $jid);
Dialog::fill($view->draw('_visio_lobby'), false, true);
$this->rpc('Visio.lobbySetup', $withVideo);
$this->rpc('MovimVisio.getUserMedia', $withVideo);
if ($calling == false) {
$this->rpc('Notif.incomingCall');
@ -262,20 +391,113 @@ class Visio extends Base
}
}
public function ajaxSessionInitiate($sdp, $to, $id)
public function ajaxMujiAccept(string $mujiId)
{
$muji = $this->user->session->mujiCalls()
->where('id', $mujiId)
->with('conference')
->first();
if ($muji) {
$accept = new Accept;
$accept->setTo($muji->jidfrom)
->setId($muji->id)
->request();
CurrentCall::getInstance()->start($muji->jidfrom, $muji->id, mujiRoom: $muji->muc);
$muc = new Muc;
$muc->setTo($muji->muc)
->setNickname($muji->conference ? $muji->conference->nickname : $this->user->nickname)
->enableMujiPreparing()
->noNotify()
->request();
$this->rpc('Chat_ajaxGetHeader', $muji->jidfrom, true);
}
}
public function ajaxMujiTrigger()
{
$muji = $this->user->session->mujiCalls()->first();
$this->rpc('MovimVisio.init', $muji->jidfrom, $muji->jidfrom, $muji->id, $muji->video, true);
}
public function ajaxMujiInit(string $mujiId, $sdp)
{
$muji = $this->user->session->mujiCalls()
->where('id', $mujiId)
->with('conference')
->first();
if ($muji) {
$stj = new SDPtoJingle($sdp->sdp, $this->user->id, $mujiId, true);
$muc = new Muc;
$muc->setTo($muji->muc)
->setNickname($muji->conference ? $muji->conference->nickname : $this->user->nickname)
->setMuji($stj->generate())
->noNotify()
->request();
$this->rpc('MovimJingles.startCalls', $muji->muc);
}
}
public function ajaxMujiCreate(string $to, bool $withVideo = false)
{
$conference = $this->user->session
->conferences()->where('conference', $to)
->first();
if ($conference) {
$mujiId = generateUUID();
$mujiConference = generateKey(withCapitals: false);
$mujiConferenceJid = $mujiConference . '@conference.movim.eu';
CurrentCall::getInstance()->start($to, $mujiId, mujiRoom: $mujiConferenceJid);
$muc = new Muc;
$muc->setTo($mujiConferenceJid)
->setNickname($conference->nickname)
->enableCreate()
->enableMujiPreparing()
->noNotify()
->request();
$invite = new Invite;
$invite->setTo($to)
->setId($mujiId)
->setRoom($mujiConferenceJid);
if ($withVideo) {
$invite->enableVideo();
}
$invite->request();
}
}
public function ajaxSessionInitiate(string $jid, $sdp, string $id, ?string $mujiRoom = null)
{
$stj = new SDPtoJingle(
$sdp->sdp,
$this->user->id,
$to,
$id,
false,
$jid,
'session-initiate'
);
$stj->setSessionId($id);
if ($mujiRoom) {
$stj->setMujiRoom($mujiRoom);
}
$si = new SessionInitiate;
$si->setTo($to)
->setOffer($stj->generate())
->request();
$si->setTo($jid)
->setOffer($stj->generate())
->request();
}
public function ajaxResolveServices()
@ -283,12 +505,12 @@ class Visio extends Base
if (!$this->user->session) return;
$info = \App\Info::where('server', $this->user->session->host)
->where('node', '')
->first();
->where('node', '')
->first();
if ($info && $info->hasExternalServices()) {
$c = new \Moxl\Xec\Action\ExternalServices\Get;
$c->setTo($this->user->session->host)
->request();
->request();
} else {
$this->setDefaultServices();
}
@ -296,7 +518,7 @@ class Visio extends Base
public function ajaxPrepare(string $jid)
{
$bareJid =\explodeJid($jid)['jid'];
$bareJid = \baseJid($jid);
$contact = \App\Contact::firstOrNew(['id' => $bareJid]);
$view = $this->tpl();
@ -304,7 +526,6 @@ class Visio extends Base
$this->rpc('MovimVisio.moveToChat', $bareJid);
$this->rpc('MovimTpl.fill', '#visio_contact', $view->draw('_visio_contact'));
$this->rpc('Visio.init', $bareJid);
}
public function setDefaultServices()
@ -318,73 +539,94 @@ class Visio extends Base
];
shuffle($servers);
$this->rpc('Visio.setServices', [['urls' => array_slice($servers, 0, 2)]]);
$this->rpc('MovimVisio.setServices', [['urls' => array_slice($servers, 0, 2)]]);
}
public function ajaxGetStates()
{
$this->rpc('Visio.setStates', [
$this->rpc('MovimVisio.setStates', [
'calling' => $this->__('visio.calling'),
'ringing' => $this->__('visio.ringing'),
'in_call' => $this->__('visio.in_call'),
'failed' => $this->__('visio.failed'),
'connecting' => $this->__('visio.connecting'),
'ended' => $this->__('visio.ended'),
'declined' => $this->__('visio.declined')
'declined' => $this->__('visio.declined'),
'no_participants_left' => $this->__('visio.no_participants_left'),
]);
}
public function ajaxSessionAccept($sdp, string $to, string $id)
public function ajaxSessionAccept(string $to, string $id, $sdp)
{
$stj = new SDPtoJingle(
$sdp->sdp,
$this->user->id,
$id,
false,
$to,
'session-accept'
);
$stj->setSessionId($id);
$si = new SessionInitiate;
$si->setTo($to)
->setOffer($stj->generate())
->request();
->setOffer($stj->generate())
->request();
}
public function ajaxCandidate($sdp, string $to, string $id)
public function ajaxCandidate(string $to, string $id, $sdp)
{
// Firefox is passing the ufrag as an argument, Chrome as a parameter in the candidate
$ufrag = $sdp->usernameFragment ?? null;
$stj = new SDPtoJingle(
'a='.$sdp->candidate,
'a=' . $sdp->candidate,
$this->user->id,
$id,
false,
$to,
'transport-info',
$sdp->sdpMid,
$ufrag
);
$stj->setSessionId($id);
$si = new SessionInitiate;
$si->setTo($to)
->setOffer($stj->generate())
->request();
->setOffer($stj->generate())
->request();
}
public function ajaxEnd(string $to, string $sid, $reason = 'success')
public function ajaxTerminate(string $to, string $sid, ?string $reason = 'success')
{
$st = new SessionTerminate;
$st->setTo($to)
->setJingleSid($sid)
->setReason($reason)
->request();
}
/**
* Close a one-to-one call
*/
public function ajaxGoodbye(string $to, string $sid, ?string $reason = 'success') {
CurrentCall::getInstance()->stop($to, $sid);
$this->rpc('MovimJingles.terminateAll', $reason);
}
/*public function ajaxEnd(string $to, string $sid, $reason = 'success')
{
if (CurrentCall::getInstance()->isStarted()) {
CurrentCall::getInstance()->stop();
CurrentCall::getInstance()->stop($to, $sid);
$st = new SessionTerminate;
$st->setTo($to)
->setJingleSid($sid)
->setReason($reason)
->request();
->setJingleSid($sid)
->setReason($reason)
->request();
} else {
$sr = new SessionRetract;
$sr->setTo($to)
->setId($sid)
->request();
->setId($sid)
->request();
}
}
}*/
}

126
app/Widgets/Visio/_visio_lobby.tpl

@ -1,21 +1,39 @@
<section id="visio_lobby">
<ul class="list thick">
<li>
<span class="primary icon bubble">
<img src="{$contact->getPicture(\Movim\ImageSize::O)}">
</span>
<div>
<p class="normal">
{if="$calling"}
<i class="material-symbols icon blue">call</i>
{$c->__('visiolobby.calling', $contact->truename)}
{else}
<i class="material-symbols icon blue">phone_callback</i>
{$c->__('visiolobby.called', $contact->truename)}
{/if}
</p>
<p>{$c->__('visiolobby.setup')}</p>
</div>
{if="isset($contact)"}
<span class="primary icon bubble">
<img src="{$contact->getPicture(\Movim\ImageSize::O)}">
</span>
<div>
<p class="normal">
{if="$calling"}
<i class="material-symbols icon blue">call</i>
{$c->__('visiolobby.calling', $contact->truename)}
{else}
<i class="material-symbols icon blue">phone_callback</i>
{$c->__('visiolobby.called', $contact->truename)}
{/if}
</p>
<p>{$c->__('visiolobby.setup')}</p>
</div>
{else}
<span class="primary icon bubble">
<img src="{$conference->getPicture(\Movim\ImageSize::O)}">
</span>
<div>
<p class="normal">
{if="$calling"}
<i class="material-symbols icon blue">call</i>
{$c->__('visiolobby.muji_create', $conference->title)}
{else}
<i class="material-symbols icon blue">phone_callback</i>
{$c->__('visiolobby.muji_join', $conference->title)}
{/if}
</p>
<p>{$c->__('visiolobby.setup')}</p>
</div>
{/if}
</li>
</ul>
<div class="placeholder">
@ -78,31 +96,59 @@
</div>
</form>
</section>
<footer>
{if="$calling"}
<button onclick="Visio.goodbye(); Dialog_ajaxClear()" class="button flat">
{$c->__('button.cancel')}
</button>
<button id="lobby_start" onclick="Visio.prepare('{$jid}', null, {if="$withvideo"}true{else}false{/if}); Dialog_ajaxClear();" class="button color green disabled">
{if="$withvideo"}
<i class="material-symbols">videocam</i>
{else}
<i class="material-symbols">call</i>
{/if}
{$c->__('button.call')}
</button>
<div>
{if="isset($contact)"}
{if="$calling"}
<button onclick="MovimVisio.clear(); Dialog_ajaxClear()" class="button flat">
{$c->__('button.cancel')}
</button>
<button id="lobby_start" onclick="MovimVisio.init('{$fullJid}', '{$contact->id}', null, {if="$withvideo"}true{else}false{/if}); Dialog_ajaxClear();" class="button color green disabled">
{if="$withvideo"}
<i class="material-symbols">videocam</i>
{else}
<i class="material-symbols">call</i>
{/if}
{$c->__('button.call')}
</button>
{else}
<button onclick="Visio_ajaxReject('{$contact->id|echapJS}', '{$id}'); MovimVisio.clear(); Dialog_ajaxClear()" class="button color red">
<i class="material-symbols">call_end</i>
{$c->__('button.refuse')}
</button>
<button id="lobby_start" onclick="MovimVisio.init('{$fullJid}', '{$contact->id}', '{$id}', {if="$withvideo"}true{else}false{/if}); Dialog_ajaxClear();" class="button color green disabled">
{if="$withvideo"}
<i class="material-symbols">videocam</i>
{else}
<i class="material-symbols">call</i>
{/if}
{$c->__('button.reply')}
</button>
{/if}
{else}
<button onclick="Visio_ajaxReject('{$jid|echapJS}', '{$id}'); Visio.goodbye(); Dialog_ajaxClear()" class="button color red">
<i class="material-symbols">call_end</i>
{$c->__('button.refuse')}
</button>
<button id="lobby_start" onclick="Visio.prepare('{$jid}', '{$id}', {if="$withvideo"}true{else}false{/if}); Dialog_ajaxClear();" class="button color green disabled">
{if="$withvideo"}
<i class="material-symbols">videocam</i>
{else}
<i class="material-symbols">call</i>
{/if}
{$c->__('button.reply')}
</button>
{if="$calling"}
<button onclick="MovimVisio.clear(); Dialog_ajaxClear()" class="button flat">
{$c->__('button.cancel')}
</button>
<button id="lobby_start" onclick="Visio_ajaxMujiCreate('{$conference->conference}', {if="$withvideo"}true{else}false{/if}); Dialog_ajaxClear()" class="button color green disabled">
{if="$withvideo"}
<i class="material-symbols">videocam</i>
{else}
<i class="material-symbols">call</i>
{/if}
{$c->__('button.create')}
</button>
{else}
<button onclick="Dialog_ajaxClear()" class="button flat">
{$c->__('button.cancel')}
</button>
<button id="lobby_start" onclick="Visio_ajaxMujiAccept('{$id}'); Dialog_ajaxClear()" class="button color green disabled">
{if="$withvideo"}
<i class="material-symbols">videocam</i>
{else}
<i class="material-symbols">call</i>
{/if}
{$c->__('button.join')}
</button>
{/if}
{/if}
</footer>

9
app/Widgets/Visio/locales.ini

@ -4,14 +4,17 @@ video_call = Incoming video call
audio_call = Incoming audio call
ringing = …ringing
declined = Declined
in_call = In call
in_call = Ongoing call
failed = Failed
connecting = …connecting
ended = Call ended
no_participants_left = All participants have left the call
[visiolobby]
called = "%s is calling you"
calling = "Calling %s"
muji_create = "Creating a conference call in %s"
muji_join = "Joining the conference call"
setup = "Setting up your camera and microphone"
devices_disco = "Please allow your browser to share your devices to configure them"
microphone_label= Default microphone
@ -20,3 +23,7 @@ default_microphone_changed = Default microphone changed
default_camera_changed = Default camera changed
no_mic_sound = "No sound detected from your microphone"
no_mic_sound2 = "Try to select another source or check your system settings"
[muji]
call_audio_invite = You are invited in an audio conference call
call_video_invite = You are invited in a video conference call

175
app/Widgets/Visio/visio.css

@ -1,14 +1,32 @@
#visio {
display: block;
position: fixed;
bottom: 8rem;
right: 3rem;
bottom: 7rem;
right: 2rem;
width: 40rem;
height: 25rem;
background-color: #111;
border-radius: 1rem;
transition: opacity 0.3s ease-in-out;
z-index: 2;
transition: opacity 0.3s ease-in-out,
bottom 0.3s ease-in-out,
right 0.3s ease-in-out,
width 0.3s ease-in-out,
height 0.3s ease-in-out,
transform 0.3s ease-in-out;
z-index: 3;
}
@media screen and (max-width: 800px) {
#visio {
right: 1rem;
width: 34rem;
height: 22rem;
}
}
#dialog:not(:empty) ~ #visio,
#drawer:not(:empty) ~ #visio {
transform: translateX(110%);
}
#chat_widget #visio {
@ -24,8 +42,11 @@
body > #visio:not(:fullscreen) #main,
body > #visio:not(:fullscreen) #toggle_dtmf,
body > #visio:not(:fullscreen) .participant:before,
body > #visio:not(:fullscreen) .participant:after,
#chat_widget #visio #switch_chat,
#visio:not([data-from]),
#visio:not([data-type]),
#visio:not([data-type]) #toggle_audio,
#visio[data-type=video] #toggle_dtmf,
#visio[data-type=video] #dtmf,
@ -33,17 +54,10 @@ body > #visio:not(:fullscreen) #toggle_dtmf,
#visio:not([data-type=video]) #switch_camera,
#visio:not([data-type=video]) #screen_sharing,
#visio:not([data-type=video]) #local_video,
#visio:not([data-type=video]) #remote_video,
#visio:not([data-type=video]) #screen_sharing_video,
#visio:not([data-type=video]) #video {
#visio:not([data-type=video]) #screen_sharing_video {
display: none;
}
#visio video {
width: 100%;
height: 100%;
}
#visio #visio_source {
display: none;
}
@ -91,6 +105,10 @@ body > #visio:not(:fullscreen) #toggle_dtmf,
bottom: 2rem; /* Override global CSS */
}
#visio[data-muji='true'] #main {
display: none;
}
#visio .infos #remote_level {
overflow: hidden;
border-radius: 100%;
@ -119,7 +137,8 @@ body > #visio:not(:fullscreen) #toggle_dtmf,
bottom: 1rem;
width: 25rem;
height: auto;
border-radius: 0.25rem;
border-radius: 0.5rem;
box-shadow: var(--elevation-4);
background-image: url('../theme/img/movim_cloud.svg');
background-position: center;
@ -136,12 +155,14 @@ body > #visio:not(:fullscreen) video#screen_sharing_video.sharing {
bottom: 0;
}
body > #visio:not(:fullscreen) #visio_contact {
body > #visio:not(:fullscreen) #visio_contact,
body > #visio:not(:fullscreen) .participant img.avatar {
transform: scale(0.7);
}
@media screen and (max-width: 800px) {
#chat_widget #visio:not(:fullscreen) #visio_contact {
#chat_widget #visio:not(:fullscreen) #visio_contact,
body > #visio:not(:fullscreen) .participant img.avatar {
transform: scale(0.7);
}
}
@ -162,15 +183,20 @@ body > #visio:not(:fullscreen) #visio_contact {
position: absolute;
width: 100%;
z-index: 1;
background-color: transparent;
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), transparent);
}
#visio header ul.list li {
justify-content: space-between;
padding-right: 0;
}
#visio header ul.list li span#switch_chat {
flex: 0 0 5rem;
}
#visio header i {
text-shadow: 0 0 3rem black;
text-shadow: 0 0 3rem #333, 0 0 0.5rem #777;
}
#visio header #no_mic_sound.disabled {
@ -181,7 +207,6 @@ body > #visio:not(:fullscreen) #visio_contact {
animation: fadein 0.3s;
font-size: 1.5rem;
line-height: 2.5rem;
margin: 0 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -197,26 +222,116 @@ body > #visio:not(:fullscreen) #visio_contact {
animation: Rotate 2000ms infinite;
}
#visio #remote_state {
position: absolute;
bottom: 1rem;
left: 1rem;
#visio #participants {
display: flex;
flex-wrap: wrap;
height: 100%;
justify-content: center;
}
#visio #local_video,
#visio .participant video {
transition: opacity 0.5s ease-in-out;
}
#visio .participant {
position: relative;
padding: 0.5rem;
box-sizing: border-box;
max-height: 100%;
display: flex;
align-items: center;
flex: 1 auto;
outline: 0.5rem solid rgba(255, 255, 255, var(--level));
outline-offset: -0.75rem;
}
#visio #remote_state i {
#visio .participant video {
max-width: 100%;
height: 100%;
margin: 0 auto;
display: block;
border-radius: 0.5rem;
}
#visio #participants:has(.participant:nth-child(2)) .participant {
flex: 1 0 calc(50% - 0.5rem);
max-width: 50%;
}
#visio #participants:has(.participant:nth-child(2)) .participant video {
max-height: 800px;
}
#visio #participants:has(.participant:nth-child(3)) .participant {
height: 50%;
}
#visio #participants:has(.participant:nth-child(5)) .participant {
height: 33.33%;
}
#visio #participants:has(.participant:nth-child(7)) .participant {
height: 25%;
}
#visio .participant::after,
#visio .participant::before {
position: absolute;
bottom: 5.5rem;
left: 2rem;
line-height: 1.5rem;
display: inline-block;
color: white;
text-shadow: 0 0 1rem rgba(0, 0, 0, 0.85);
font-size: 2rem;
margin-right: 1rem;
}
#visio .participant::after {
font-family: 'Material Symbols';
font-variation-settings: 'FILL' 1;
content: '\e029';
letter-spacing: 1.5rem;
opacity: 0.5;
color: white;
}
#visio #local_video,
#visio #remote_video {
#visio .participant img.avatar {
position: absolute;
width: 15rem;
height: 15rem;
left: calc(50% - 7.5rem);
top: calc(50% - 7.5rem);
border-radius: 50%;
transition: opacity 0.5s ease-in-out;
opacity: 0;
}
#visio .participant.video_off img.avatar {
opacity: 1;
}
#visio .participant[data-name]::before {
bottom: 2rem;
content: attr(data-name);
}
#visio .participant:not([data-name])::after {
bottom: 2rem;
}
#visio .participant.audio_off::after {
content: '\e02b';
}
#visio .participant.audio_off.video_off::after {
content: '\e02b \f83b';
}
#visio #local_video.muted,
#visio #remote_video.muted {
#visio[data-muji='true'] #participants:not(:empty) ~ .infos,
#visio #local_video.video_off,
#visio .participant.video_off video {
opacity: 0;
}

436
app/Widgets/Visio/visio.js

@ -1,436 +0,0 @@
function logError(error) {
console.log(error.name + ': ' + error.message);
console.error(error);
}
var Visio = {
calling: false,
videoSelect: undefined,
switchCamera: undefined,
inboundStream: null,
states: null,
tracksTypes: [],
prepare: function (from, id, withVideo) {
if (!MovimVisio.localStream) return;
Visio_ajaxPrepare(from);
MovimVisio.from = from;
MovimVisio.id = id;
MovimVisio.withVideo = withVideo ?? false;
},
init: function (bareFrom) {
let visio = document.querySelector('#visio');
visio.dataset.from = bareFrom;
delete visio.dataset.type;
visio.dataset.type = (MovimVisio.withVideo) ? 'video' : 'audio';
MovimVisio.load();
MovimVisio.pc = new RTCPeerConnection({ 'iceServers': MovimVisio.services });
MovimVisio.pc.ontrack = event => {
var srcObject = null;
if (event.streams && event.streams[0]) {
srcObject = event.streams[0];
} else {
if (!MovimVisio.inboundStream) {
MovimVisio.inboundStream = new MediaStream();
MovimVisio.remoteAudio.srcObject = MovimVisio.inboundStream;
}
MovimVisio.inboundStream.addTrack(event.track);
srcObject = MovimVisio.inboundStream;
}
VisioUtils.setRemoteVideoState('');
if (event.track.kind == 'audio') {
MovimVisio.remoteAudio.srcObject = srcObject;
VisioUtils.setRemoteAudioState('mic');
} else if (event.track.kind == 'video') {
MovimVisio.remoteVideo.srcObject = srcObject;
VisioUtils.setRemoteVideoState('videocam');
}
VisioUtils.handleRemoteAudio();
Visio.tracksTypes['mid' + event.transceiver.mid] = event.track.kind;
};
MovimVisio.pc.onicecandidate = event => {
let candidate = event.candidate;
if (candidate && candidate.candidate && candidate.candidate.length > 0) {
Visio_ajaxCandidate(event.candidate, MovimVisio.from, MovimVisio.id);
}
};
MovimVisio.pc.oniceconnectionstatechange = () => VisioUtils.toggleMainButton();
MovimVisio.pc.onicegatheringstatechange = function (event) {
// When we didn't receive the WebRTC termination before Jingle
if (MovimVisio.pc.iceConnectionState == 'disconnected') {
Visio.onTerminate();
}
VisioUtils.toggleMainButton();
};
if (MovimVisio.withVideo) {
VisioUtils.pcReplaceTrack(MovimVisio.localStream);
}
VisioUtils.toggleMainButton();
MovimVisio.localStream.getTracks().forEach(track => {
MovimVisio.pc.addTrack(track, MovimVisio.localStream);
});
if (MovimVisio.withVideo) {
VisioUtils.switchCameraInCall();
}
if (MovimVisio.id) {
Visio_ajaxAccept(MovimVisio.from, MovimVisio.id);
} else {
MovimVisio.id = crypto.randomUUID();
Visio.calling = true;
VisioUtils.toggleMainButton();
Visio_ajaxPropose(MovimVisio.from, MovimVisio.id, MovimVisio.withVideo);
}
},
onMute: function (name) {
if (Visio.tracksTypes[name]) {
if (Visio.tracksTypes[name] == 'audio') {
VisioUtils.setRemoteAudioState('mic_off');
}
if (Visio.tracksTypes[name] == 'video') {
document.querySelector('#remote_video').classList.add('muted');
VisioUtils.setRemoteVideoState('videocam_off');
}
}
},
onUnmute: function (name) {
if (Visio.tracksTypes[name]) {
if (Visio.tracksTypes[name] == 'audio') {
VisioUtils.setRemoteAudioState('mic');
}
if (Visio.tracksTypes[name] == 'video') {
document.querySelector('#remote_video').classList.remove('muted');
VisioUtils.setRemoteVideoState('videocam');
}
}
},
setServices: function (services) {
MovimVisio.services = services;
},
setStates: function (states) {
Visio.states = states;
},
lobbySetup: function (withVideo) {
Visio.getUserMedia(withVideo);
},
getUserMedia: function(withVideo) {
var constraints = {
audio: true,
video: false,
};
if (withVideo) {
constraints.video = {
facingMode: 'user',
width: { ideal: 4096 },
height: { ideal: 4096 }
}
if (localStorage.defaultCamera) {
constraints.video = {
deviceId: localStorage.defaultCamera
};
}
}
if (localStorage.defaultMicrophone) {
constraints.audio = {
deviceId: localStorage.defaultMicrophone
}
}
MovimVisio.load();
let lobby = document.querySelector('#visio_lobby');
if (lobby) {
VisioUtils.disableLobbyCallButton();
}
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
MovimVisio.localStream = stream;
if (lobby) {
lobby.classList.add('configure');
} else {
MovimVisio.clear();
return;
}
stream.getTracks().forEach(track => {
if (lobby) {
VisioUtils.enableLobbyCallButton();
}
if (track.kind == 'audio') {
MovimVisio.localAudio.srcObject = stream;
} else if (withVideo && track.kind == 'video') {
MovimVisio.localVideo.srcObject = stream;
if (lobby) {
let cameraPreview = lobby.querySelector('video#camera_preview');
cameraPreview.addEventListener('loadeddata', () => cameraPreview.play());
cameraPreview.srcObject = stream;
cameraPreview.disablePictureInPicture = true;
}
}
});
VisioUtils.handleAudio();
if (withVideo) {
VisioUtils.enableScreenSharingButton();
}
navigator.mediaDevices.enumerateDevices().then(devices => Visio.gotDevices(withVideo, devices));
});
},
gotDevices: function(withVideo, devicesInfo) {
microphoneFound = false;
cameraFound = false;
let microphoneSelect = document.querySelector('select[name=default_microphone]');
microphoneSelect.onchange = (e) => {
localStorage.defaultMicrophone = e.target.value;
Visio.getUserMedia(withVideo);
};
microphoneSelect.innerText = '';
VisioUtils.handleAudio();
let cameraSelect = document.querySelector('select[name=default_camera]');
if (cameraSelect) {
cameraSelect.addEventListener('change', e => {
localStorage.defaultCamera = e.target.value;
let cameraPreview = document.querySelector('video#camera_preview');
if (cameraPreview.srcObject) {
cameraPreview.srcObject.getTracks().forEach(track => track.stop());
}
cameraPreview.srcObject = null;
Visio.getUserMedia(withVideo);
});
cameraSelect.innerText = '';
}
for (const deviceInfo of devicesInfo) {
if (deviceInfo.kind === 'audioinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `Microphone ${microphoneSelect.length + 1}`;
if (deviceInfo.deviceId == localStorage.defaultMicrophone) {
option.selected = true;
microphoneFound = true;
}
microphoneSelect.appendChild(option);
}
if (withVideo && deviceInfo.kind === 'videoinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `Camera ${microphoneSelect.length + 1}`;
if (deviceInfo.deviceId == localStorage.defaultCamera) {
option.selected = true;
cameraFound = true;
}
// Sometimes we can have two devices with the same id
if (!cameraSelect.querySelector('option[value="' + deviceInfo.deviceId + '"]')) {
cameraSelect.appendChild(option);
}
}
}
if (microphoneFound == false) {
localStorage.defaultMicrophone = microphoneSelect.value;
}
if (withVideo && cameraFound == false) {
localStorage.defaultCamera = cameraSelect.value;
}
},
gotQuickStream: function () {
VisioUtils.pcReplaceTrack(MovimVisio.localVideo.srcObject);
},
gotScreen: function () {
VisioUtils.pcReplaceTrack(MovimVisio.screenSharing.srcObject);
},
onCandidate: function (candidate, mid, mlineindex) {
// filter the a=candidate lines
var filtered = candidate.split(/\n/).filter(line => {
return line.startsWith('a=candidate');
});
MovimVisio.pc.addIceCandidate(new RTCIceCandidate({
'candidate': filtered.join('').substring(2),
'sdpMid': mid,
'sdpMLineIndex': mlineindex
}), () => { }, logError);
},
onProceed: function (from, id) {
if (from.substring(0, MovimVisio.from.length) == MovimVisio.from && MovimVisio.id == id) {
// We set the remote resource
MovimVisio.from = from;
MovimVisio.pc.createOffer().then(function (offer) {
Visio.calling = false;
VisioUtils.toggleMainButton();
return MovimVisio.pc.setLocalDescription(offer);
})
.then(function () {
Visio_ajaxSessionInitiate(MovimVisio.pc.localDescription, MovimVisio.from, MovimVisio.id);
});
} else {
console.error('Wrong call')
}
},
onInitiateSDP: function (sdp) {
MovimVisio.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' }), () => {
MovimVisio.pc.createAnswer().then(function (answer) {
return MovimVisio.pc.setLocalDescription(answer);
}).then(function () {
Visio_ajaxSessionAccept(MovimVisio.pc.localDescription, MovimVisio.from, MovimVisio.id);
}).catch(logError);
}, logError);
},
onContentAdd: function (sdp) {
MovimVisio.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' }), () => {
}, logError);
},
onAcceptSDP: function (sdp) {
MovimVisio.pc.setRemoteDescription(
new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'answer' }), () => { },
(error) => {
Visio.goodbye('incompatible-parameters');
logError(error)
}
);
},
onTerminate: (reason) => {
if (MovimVisio.localAudio) {
let localStream = MovimVisio.localAudio.srcObject;
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
MovimVisio.localAudio.srcObject = null;
}
if (MovimVisio.remoteAudio) {
let remoteStream = MovimVisio.remoteAudio.srcObject;
if (remoteStream) {
remoteStream.getTracks().forEach(track => track.stop());
}
MovimVisio.remoteAudio.srcObject = null;
}
if (MovimVisio.localVideo) {
let localStream = MovimVisio.localVideo.srcObject;
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
MovimVisio.localVideo.srcObject = null;
}
if (MovimVisio.remoteVideo) {
let remoteStream = MovimVisio.remoteVideo.srcObject;
if (remoteStream) {
remoteStream.getTracks().forEach(track => track.stop());
}
MovimVisio.remoteVideo.srcObject = null;
}
if (VisioUtils.audioContext) {
VisioUtils.audioContext.close();
VisioUtils.audioContext = null;
}
if (VisioUtils.remoteAudioContext) {
VisioUtils.remoteAudioContext.close();
VisioUtils.remoteAudioContext = null;
}
},
goodbye: (reason) => {
Visio.onTerminate(reason);
let visio = document.querySelector('#visio');
delete visio.dataset.from;
delete visio.dataset.type;
if (document.fullscreenElement) {
document.exitFullscreen();
}
if (MovimVisio.id) {
Visio_ajaxEnd(MovimVisio.from, MovimVisio.id, reason);
}
MovimVisio.clear();
},
}
MovimWebsocket.attach(() => {
if (MovimVisio.services.length == 0) {
Visio_ajaxResolveServices();
}
Visio_ajaxGetStates();
});

20
app/Widgets/Visio/visio.tpl

@ -12,16 +12,16 @@
<i class="material-symbols">dialpad</i>
</span>
<span id="toggle_audio" class="divided control icon color transparent active" onclick="VisioUtils.toggleAudio()">
<i class="material-symbols">mic_off</i>
<i class="material-symbols">mic</i>
</span>
<span id="toggle_video" class="control icon color transparent active" onclick="VisioUtils.toggleVideo()">
<i class="material-symbols">videocam_off</i>
<i class="material-symbols">videocam</i>
</span>
<span id="switch_camera" class="control icon color transparent active">
<i class="material-symbols">switch_camera</i>
</span>
<span id="screen_sharing" class="control icon color transparent active" onclick="VisioUtils.toggleScreenSharing()">
<span id="screen_sharing" class="control icon color transparent active toggleable" onclick="VisioUtils.toggleScreenSharing()">
<i class="material-symbols">screen_share</i>
</span>
@ -62,27 +62,21 @@
<p class="dtmf"></p>
</div>
<div id="participants"></div>
<ul class="list infos" class="list middle">
<li>
<div id="visio_contact"></div>
</li>
</ul>
<audio id="remote_audio" autoplay></audio>
<audio id="local_audio" autoplay muted></audio>
<video id="remote_video" autoplay poster="{$c->baseUri}theme/img/empty.png"></video>
<video id="screen_sharing_video" autoplay muted poster="{$c->baseUri}theme/img/empty.png"></video>
<video id="local_video" autoplay muted poster="{$c->baseUri}theme/img/empty.png"></video>
<span id="remote_state">
<i class="voice material-symbols"></i>
<i class="webcam material-symbols"></i>
</span>
<div class="controls">
<a id="main" class="button action color gray">
<i class="material-symbols">phone</i>
<a id="main" class="button action color red" onclick="MovimVisio.goodbye()">
<i class="material-symbols">call_end</i>
</a>
</div>
</div>

183
app/Widgets/Visio/visio_utils.js

@ -17,7 +17,7 @@ var VisioUtils = {
MovimVisio.localAudio.srcObject
);
} catch (error) {
logError(error);
MovimUtils.logError(error);
return;
}
@ -49,7 +49,7 @@ var VisioUtils = {
VisioUtils.maxLevel = Math.max(VisioUtils.maxLevel, instant);
var base = (instant / VisioUtils.maxLevel);
var level = (base > 0.01) ? base ** .3 : 0;
var level = (base > 0.05) ? base ** .3 : 0;
let step = 0;
if (level == 0) {
@ -87,58 +87,7 @@ var VisioUtils = {
});
}
mainButton.style.outlineColor = 'rgba(255, 255, 255, ' + level + ')';
}
},
handleRemoteAudio: function () {
if (VisioUtils.remoteAudioContext) {
VisioUtils.remoteAudioContext.close();
VisioUtils.remoteAudioContext = null;
}
VisioUtils.remoteAudioContext = new AudioContext();
try {
var remoteMicrophone = VisioUtils.remoteAudioContext.createMediaStreamSource(
MovimVisio.remoteAudio.srcObject
);
} catch (error) {
logError(error);
return;
}
var remoteJavascriptNode = VisioUtils.remoteAudioContext.createScriptProcessor(2048, 1, 1);
var remoteMeter = document.querySelector('#visio #remote_level');
let isMuteStep = 0;
remoteMicrophone.connect(remoteJavascriptNode);
remoteJavascriptNode.connect(VisioUtils.remoteAudioContext.destination);
remoteJavascriptNode.onaudioprocess = function (event) {
var inpt = event.inputBuffer.getChannelData(0);
var instant = 0.0;
var sum = 0.0;
for (var i = 0; i < inpt.length; ++i) {
sum += inpt[i] * inpt[i];
}
instant = Math.sqrt(sum / inpt.length);
VisioUtils.remoteMaxLevel = Math.max(VisioUtils.remoteMaxLevel, instant);
var base = (instant / VisioUtils.remoteMaxLevel);
var level = (base > 0.01) ? base ** .3 : 0;
// Fallback in case we don't have the proper signalisation
if (level == 0) {
isMuteStep++;
} else {
isMuteStep = 0;
}
VisioUtils.setRemoteAudioState(isMuteStep > 250 ? 'mic_off' : 'mic');
remoteMeter.style.borderColor = 'rgba(255, 255, 255, ' + level + ')';
mainButton.style.outlineColor = 'rgba(255, 255, 255, ' + level.toFixed(2) + ')';
}
},
@ -162,25 +111,35 @@ var VisioUtils = {
toggleAudio: function () {
var button = document.querySelector('#toggle_audio i');
var rtc = MovimVisio.pc.getSenders().find(rtc => rtc.track && rtc.track.kind == 'audio');
var mid = MovimVisio.pc.getTransceivers().filter(t => t.sender.track.id == rtc.track.id)[0].mid;
if (rtc && rtc.track.enabled == 1) {
rtc.track.enabled = 0;
button.innerText = 'mic_off';
Visio_ajaxUnmute(MovimVisio.from, MovimVisio.id, 'mid' + mid);
} else if (rtc) {
rtc.track.enabled = 1;
if (button.innerText == 'mic_off') {
MovimJingles.enableAudio(true);
button.innerText = 'mic';
Visio_ajaxMute(MovimVisio.from, MovimVisio.id, 'mid' + mid);
} else {
MovimJingles.enableAudio(false);
button.innerText = 'mic_off';
}
},
toggleVideo: function () {
var button = document.querySelector('#toggle_video i');
if (button.innerText == 'videocam_off') {
MovimJingles.enableVideo(true);
document.querySelector('#local_video').classList.remove('video_off');
button.innerText = 'videocam';
} else {
MovimJingles.enableVideo(false);
document.querySelector('#local_video').classList.add('video_off');
button.innerText = 'videocam_off';
}
},
switchChat: function () {
var from = document.querySelector('#visio').dataset.from;
let visio = document.querySelector('#visio');
if (from) {
Search.chat(from);
if (visio.dataset.jid) {
Search.chat(visio.dataset.jid, (visio.dataset.muji == 'true'));
}
},
@ -206,36 +165,6 @@ var VisioUtils = {
document.querySelector('#dtmf p.dtmf').innerHTML = '';
},
toggleVideo: function () {
var button = document.querySelector('#toggle_video i');
var rtc = MovimVisio.pc.getSenders().find(rtc => rtc.track && rtc.track.kind == 'video');
var mid = MovimVisio.pc.getTransceivers().filter(t => t.sender.track.id == rtc.track.id)[0].mid;
if (rtc) {
if (rtc.track.enabled == 1) {
rtc.track.enabled = 0;
button.innerText = 'videocam_off';
document.querySelector('#video').classList.add('muted');
Visio_ajaxUnmute(MovimVisio.from, MovimVisio.id, 'mid' + mid);
} else {
rtc.track.enabled = 1;
button.innerText = 'videocam';
document.querySelector('#video').classList.remove('muted');
Visio_ajaxMute(MovimVisio.from, MovimVisio.id, 'mid' + mid);
}
}
},
setRemoteAudioState: function (icon) {
var voice = document.querySelector('#remote_state i.voice');
voice.innerHTML = icon;
},
setRemoteVideoState: function (icon) {
var webcam = document.querySelector('#remote_state i.webcam');
webcam.innerHTML = icon;
},
toggleMainButton: function () {
button = document.getElementById('main');
state = document.querySelector('p.state');
@ -261,13 +190,13 @@ var VisioUtils = {
} else if (MovimVisio.pc.iceConnectionState == 'new') {
//if (MovimVisio.pc.iceGatheringState == 'gathering'
//|| MovimVisio.pc.iceGatheringState == 'complete') {
if (Visio.calling) {
if (MovimVisio.calling) {
button.classList.add('orange');
i.className = 'material-symbols ring';
i.innerText = 'call';
state.innerText = Visio.states.ringing;
state.innerText = MovimVisio.states.ringing;
button.onclick = function () { Visio.goodbye('cancel'); };
button.onclick = function () { MovimJingles.terminateAll('cancel'); };
} else {
button.classList.add('green');
button.classList.add('disabled');
@ -277,13 +206,13 @@ var VisioUtils = {
button.classList.add('blue');
i.className = 'material-symbols disabled';
i.innerText = 'more_horiz';
state.innerText = Visio.states.connecting;
state.innerText = MovimVisio.states.connecting;
} else if (MovimVisio.pc.iceConnectionState == 'closed') {
button.classList.add('gray');
button.classList.remove('disabled');
i.innerText = 'call_end';
button.onclick = function () { Visio.goodbye(); };
button.onclick = function () { MovimJingles.terminateAll(); };
} else if (MovimVisio.pc.iceConnectionState == 'connected'
|| MovimVisio.pc.iceConnectionState == 'complete'
|| MovimVisio.pc.iceConnectionState == 'failed') {
@ -292,19 +221,19 @@ var VisioUtils = {
i.innerText = 'call_end';
if (MovimVisio.pc.iceConnectionState == 'failed') {
state.innerText = Visio.states.failed;
state.innerText = MovimVisio.states.failed;
} else {
state.innerText = Visio.states.in_call;
state.innerText = MovimVisio.states.in_call;
}
button.onclick = () => Visio.goodbye();
button.onclick = () => MovimJingles.terminateAll();
}
} else {
button.classList.add('red');
i.className = 'material-symbols';
i.innerText = 'close';
button.onclick = () => Visio.goodbye();
button.onclick = () => MovimJingles.terminateAll();
}
},
@ -313,11 +242,11 @@ var VisioUtils = {
},
enableSwitchCameraButton: function () {
Visio.switchCamera.classList.remove('disabled');
MovimVisio.switchCamera.classList.remove('disabled');
},
disableSwitchCameraButton: function () {
Visio.switchCamera.classList.add('disabled');
MovimVisio.switchCamera.classList.add('disabled');
},
enableLobbyCallButton: function () {
@ -329,7 +258,7 @@ var VisioUtils = {
},
toggleScreenSharing: async function () {
Visio.switchCamera = document.querySelector("#visio #switch_camera");
MovimVisio.switchCamera = document.querySelector("#visio #switch_camera");
var button = document.querySelector('#screen_sharing i');
if (MovimVisio.screenSharing.srcObject == null) {
@ -345,7 +274,7 @@ var VisioUtils = {
VisioUtils.disableSwitchCameraButton();
button.innerText = 'stop_screen_share';
Visio.gotScreen();
MovimVisio.gotScreen();
} catch (err) {
console.error("Error: " + err);
}
@ -357,39 +286,40 @@ var VisioUtils = {
button.innerText = 'screen_share';
Visio.gotQuickStream();
MovimVisio.gotQuickStream();
}
},
switchCameraInCall: function () {
Visio.videoSelect = document.querySelector('#visio select#visio_source');
Visio.switchCamera = document.querySelector("#visio #switch_camera");
// TODO Use MovimVisio.getDevices
/*switchCameraInCall: function () {
MovimVisio.videoSelect = document.querySelector('#visio select#visio_source');
MovimVisio.switchCamera = document.querySelector("#visio #switch_camera");
navigator.mediaDevices.enumerateDevices().then(devicesInfo => {
Visio.videoSelect.innerText = '';
MovimVisio.videoSelect.innerText = '';
for (const deviceInfo of devicesInfo) {
if (deviceInfo.kind === 'videoinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || 'Camera ' + Visio.videoSelect.length + 1;
option.text = deviceInfo.label || 'Camera ' + MovimVisio.videoSelect.length + 1;
if (!Visio.videoSelect.querySelector('option[value="' + deviceInfo.deviceId + '"]')) {
Visio.videoSelect.appendChild(option);
MovimVisio.videoSelect.appendChild(option);
}
}
}
if (Visio.videoSelect.options.length >= 2) {
Visio.switchCamera.classList.add('enabled');
MovimVisio.switchCamera.classList.add('enabled');
}
});
Visio.switchCamera.onclick = () => {
Visio.videoSelect.selectedIndex++;
MovimVisio.switchCamera.onclick = () => {
MovimVisio.videoSelect.selectedIndex++;
if (Visio.videoSelect.selectedIndex == -1) {
Visio.videoSelect.selectedIndex++;
MovimVisio.videoSelect.selectedIndex++;
}
Toast.send(Visio.videoSelect.options[Visio.videoSelect.selectedIndex].label);
@ -399,7 +329,7 @@ var VisioUtils = {
};
constraints.video = {
deviceId: Visio.videoSelect.options[Visio.videoSelect.selectedIndex].value,
deviceId: MovimVisio.videoSelect.options[Visio.videoSelect.selectedIndex].value,
width: { ideal: 4096 },
height: { ideal: 4096 }
};
@ -430,14 +360,5 @@ var VisioUtils = {
VisioUtils.toggleMainButton();
}, logError);
};
},
pcReplaceTrack: function (stream) {
let videoTrack = stream.getVideoTracks()[0];
var sender = MovimVisio.pc.getSenders().find(s => s.track && s.track.kind == videoTrack.kind);
if (sender) {
sender.replaceTrack(videoTrack);
}
}
},*/
}

61
database/migrations/20250329110303_create_muji_calls_table.php

@ -0,0 +1,61 @@
<?php
use Movim\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateMujiCallsTable extends Migration
{
public function up()
{
$this->schema->create('muji_calls', function (Blueprint $table) {
$table->string('id', 256);
$table->string('muc', 256);
$table->string('session_id', 64);
$table->string('jidfrom', 128);
$table->boolean('isfromconference')->default(false);
$table->boolean('video', false);
$table->foreign('session_id')
->references('id')->on('sessions')
->onDelete('cascade');
$table->primary(['id', 'session_id']);
$table->timestamps();
});
$this->schema->create('muji_call_participants', function (Blueprint $table) {
$table->string('session_id', 64);
$table->string('muji_call_id', 256);
$table->string('jid', 256);
$table->boolean('inviter')->default(false);
$table->dateTime('left_at')->nullable();
$table->primary(['session_id', 'muji_call_id', 'jid']);
$table->foreign('session_id')
->references('id')->on('sessions')
->onDelete('cascade');
$table->foreign(['session_id', 'muji_call_id'])
->references(['session_id', 'id'])->on('muji_calls')
->onDelete('cascade');
$table->timestamps();
});
$this->schema->table('presences', function (Blueprint $table) {
$table->string('mucjidresource')->nullable();
});
}
public function down()
{
$this->schema->drop('muji_call_participants');
$this->schema->drop('muji_calls');
$this->schema->table('presences', function (Blueprint $table) {
$table->dropColumn('mucjidresource');
});
}
}

1850
doap.xml
File diff suppressed because it is too large
View File

2
locales/locales.ini

@ -55,6 +55,7 @@ destroy = Destroy
remove = Remove
cancel = Cancel
close = Close
create = Create
update = Update
updating = Updating
submit = Submit
@ -64,6 +65,7 @@ register = Register
reply = Reply
unregister = Unregister
save = Save
join = Join
clear = Clear
upload = Upload
connecting = Connecting

408
public/scripts/movim_jingles.js

@ -0,0 +1,408 @@
var MovimJingleSession = function (jid, fullJid, id, name, avatarUrl) {
this.id = id ?? crypto.randomUUID();
this.jid = jid;
this.fullJid = fullJid;
this.tracksTypes = {};
this.name = name;
this.avatarUrl = avatarUrl;
this.pc = new RTCPeerConnection({ 'iceServers': MovimVisio.services });
this.participant = document.createElement('div');
this.participant.dataset.jid = this.jid;
this.participant.classList.add('participant');
document.querySelector('#participants').appendChild(this.participant);
this.participant.classList.add('video_off');
this.participant.classList.add('audio_off');
this.remoteVideo = document.createElement('video');
this.remoteVideo.autoplay = true;
this.remoteVideo.disablePictureInPicture = true;
this.remoteVideo.poster = BASE_URI + 'theme/img/empty.png';
this.participant.appendChild(this.remoteVideo);
this.remoteAudio = document.createElement('audio');
this.remoteAudio.autoplay = true;
this.participant.appendChild(this.remoteAudio);
if (this.name) {
this.participant.dataset.name = this.name;
}
if (this.avatarUrl) {
let background = document.createElement('img');
background.classList.add('avatar');
background.src = this.avatarUrl;
this.participant.appendChild(background);
}
this.pc.ontrack = event => {
var srcObject = null;
if (event.streams && event.streams[0]) {
srcObject = event.streams[0];
} else {
if (!this.inboundStream) {
this.inboundStream = new MediaStream();
this.remoteAudio.srcObject = this.inboundStream;
}
this.inboundStream.addTrack(event.track);
srcObject = this.inboundStream;
}
if (event.track.kind == 'audio') {
this.remoteAudio.srcObject = srcObject;
this.participant.classList.remove('audio_off');
} else if (event.track.kind == 'video') {
this.remoteVideo.srcObject = srcObject;
this.participant.classList.remove('video_off');
}
this.tracksTypes['mid' + event.transceiver.mid] = event.track.kind;
this.handleRemoteAudio();
}
this.pc.onicecandidate = event => {
let candidate = event.candidate;
if (candidate && candidate.candidate && candidate.candidate.length > 0) {
Visio_ajaxCandidate(this.fullJid, this.id, event.candidate);
}
};
MovimVisio.localStream.getTracks().forEach(track => {
this.pc.addTrack(track, MovimVisio.localStream);
});
}
MovimJingleSession.prototype.handleRemoteAudio = function () {
this.remoteAudioContext = new AudioContext();
try {
var remoteMicrophone = this.remoteAudioContext.createMediaStreamSource(
this.remoteAudio.srcObject
);
} catch (error) {
MovimUtils.logError(error);
return;
}
var remoteJavascriptNode = this.remoteAudioContext.createScriptProcessor(2048, 1, 1);
this.isMuteStep = 0;
this.remoteMaxLevel = 0;
remoteMicrophone.connect(remoteJavascriptNode);
remoteJavascriptNode.connect(this.remoteAudioContext.destination);
remoteJavascriptNode.onaudioprocess = (event) => {
var inpt = event.inputBuffer.getChannelData(0);
var instant = 0.0;
var sum = 0.0;
for (var i = 0; i < inpt.length; ++i) {
sum += inpt[i] * inpt[i];
}
instant = Math.sqrt(sum / inpt.length);
this.remoteMaxLevel = Math.max(this.remoteMaxLevel, instant);
var base = (instant / this.remoteMaxLevel);
var level = (base > 0.05) ? base ** .3 : 0;
// Fallback in case we don't have the proper signalisation
if (level == 0) {
this.isMuteStep++;
} else {
this.isMuteStep = 0;
}
if (this.isMuteStep > 250) {
this.remoteVideo.classList.add('audio_off');
} else {
this.remoteVideo.classList.remove('audio_off');
}
this.participant.style.setProperty('--level', level.toFixed(2));
}
}
MovimJingleSession.prototype.terminate = function (reason) {
Visio_ajaxTerminate(this.fullJid, this.id, reason);
this.close();
}
MovimJingleSession.prototype.close = function () {
if (this.pc) {
this.pc.close();
this.pc = null;
}
if (this.remoteAudioContext) {
this.remoteAudioContext.close();
this.remoteAudioContext = null;
}
let participant = document.querySelector('.participant[data-jid="' + this.jid + '"]');
if (participant) participant.remove();
let remoteVideoStream = this.remoteVideo.srcObject;
if (remoteVideoStream) {
remoteVideoStream.getTracks().forEach(track => track.stop());
remoteVideoStream = null;
}
let remoteAudioStream = this.remoteAudio.srcObject;
if (remoteAudioStream) {
remoteAudioStream.getTracks().forEach(track => track.stop());
remoteAudioStream = null;
}
}
MovimJingleSession.prototype.onCandidate = function (candidate, mid, mlineindex) {
this.pc.addIceCandidate(new RTCIceCandidate({
// filter the a=candidate lines
'candidate': candidate.split(/\n/).filter(line => {
return line.startsWith('a=candidate');
}).join('').substring(2),
'sdpMid': mid,
'sdpMLineIndex': mlineindex
})).catch(error => MovimUtils.logError(error));
}
MovimJingleSession.prototype.onContentAdd = function (sdp) {
this.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' }))
.catch(error => MovimUtils.logError(error));
}
MovimJingleSession.prototype.sessionInitiate = function (fullJid, id, mujiRoom) {
this.id = id;
this.fullJid = fullJid;
this.pc.createOffer()
.then(offer => this.pc.setLocalDescription(offer))
.then(() => Visio_ajaxSessionInitiate(this.fullJid, this.pc.localDescription, id, mujiRoom));
}
MovimJingleSession.prototype.onAcceptSDP = function (sdp) {
this.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'answer' }))
.catch(error => {
this.terminate('incompatible-parameters');
MovimUtils.logError(error)
});
}
MovimJingleSession.prototype.onInitiateSDP = function (sdp) {
this.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' }))
.then(() => {
this.pc.createAnswer()
.then(answer => this.pc.setLocalDescription(answer))
.then(() => Visio_ajaxSessionAccept(this.fullJid, this.id, this.pc.localDescription))
.catch(MovimUtils.logError);
}).catch(error => MovimUtils.logError(error));
}
MovimJingleSession.prototype.onReplaceTrack = function (videoTrack) {
var sender = this.pc.getSenders().find(s => s.track && s.track.kind == videoTrack.kind);
if (sender) {
sender.replaceTrack(videoTrack);
}
}
MovimJingleSession.prototype.enableTrack = function (enable = true, kind) {
let rtc = this.pc.getSenders().find(rtc => rtc.track && rtc.track.kind == kind);
let mid = this.pc.getTransceivers().filter(t => t.sender.track.id == rtc.track.id)[0].mid;
if (rtc) {
if (enable) {
Visio_ajaxMute(this.fullJid, this.id, 'mid' + mid);
} else {
Visio_ajaxUnmute(this.fullJid, this.id, 'mid' + mid);
}
}
}
MovimJingleSession.prototype.onMute = function (name) {
if (this.tracksTypes[name]) {
if (this.tracksTypes[name] == 'audio') {
this.participant.classList.add('audio_off');
}
if (this.tracksTypes[name] == 'video') {
this.participant.classList.add('video_off');
}
}
}
MovimJingleSession.prototype.onUnmute = function (name) {
if (this.tracksTypes[name]) {
if (this.tracksTypes[name] == 'audio') {
this.participant.classList.remove('audio_off');
}
if (this.tracksTypes[name] == 'video') {
this.participant.classList.remove('video_off');
}
}
}
MovimJingleSession.prototype.replaceLocalStream = function (stream) {
let videoTrack = stream.getVideoTracks()[0];
var sender = this.pc.getSenders().find(s => s.track && videoTrack && s.track.kind == videoTrack.kind);
if (sender) {
sender.replaceTrack(videoTrack);
} else {
stream.getTracks().forEach(track => {
this.pc.addTrack(track, stream);
});
}
}
var MovimJingles = {
sessions: {},
startCalls: function (mujiRoom) {
for (jid of Object.keys(MovimJingles.sessions)) {
MovimJingles.onProceed(jid, MovimJingles.sessions[jid].fullJid, MovimJingles.sessions[jid].id, mujiRoom);
}
},
initSession: function (jid, fullJid, id, name, avatarUrl) {
if (Object.keys(MovimJingles.sessions).includes(jid)) {
return;
}
if (!MovimVisio.localStream) {
throw Error('localStream is not ready');
};
MovimJingles.sessions[jid] = new MovimJingleSession(jid, fullJid, id, name, avatarUrl);
return MovimJingles.sessions[jid].id;
},
enableAudio: function (enable = true) {
MovimVisio.localStream.getTracks().filter(track => track.kind == 'audio').forEach(track => {
track.enabled = enable;
});
for (jid of Object.keys(MovimJingles.sessions)) {
MovimJingles.sessions[jid].enableTrack(enable, 'audio');
}
},
enableVideo: function (enable = true) {
MovimVisio.localStream.getTracks().filter(track => track.kind == 'video').forEach(track => {
track.enabled = enable;
});
for (jid of Object.keys(MovimJingles.sessions)) {
MovimJingles.sessions[jid].enableTrack(enable, 'video');
}
},
replaceLocalStream: function (stream) {
for (session of Object.values(MovimJingles.sessions)) {
session.replaceLocalStream(stream);
}
},
onCandidate: function (jid, candidate, mid, mlineindex) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Candidate from a non initiated session ' + jid);
}
MovimJingles.sessions[jid].onCandidate(candidate, mid, mlineindex);
},
onInitiateSDP: function (jid, sdp, sid) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Initiate SDP from a non initiated session ' + jid);
}
// Put the real sid
MovimJingles.sessions[jid].id = sid;
MovimJingles.sessions[jid].onInitiateSDP(sdp);
},
onAcceptSDP: function (jid, sdp) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Accept SDP from a non initiated session ' + jid);
}
MovimJingles.sessions[jid].onAcceptSDP(sdp);
},
onProceed: function (jid, fullJid, id, mujiRoom) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Proceed from a non initiated session ' + jid);
}
MovimJingles.sessions[jid].sessionInitiate(fullJid, id, mujiRoom);
},
onMute: function (jid, name) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Mute from a non initiated session ' + jid);
}
MovimJingles.sessions[jid].onMute(name);
},
onUnmute: function (jid, name) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Unmute from a non initiated session ' + jid);
}
MovimJingles.sessions[jid].onUnmute(name);
},
onContentAdd: function (jid, sdp) {
if (MovimJingles.sessions[jid] == undefined) {
throw Error('Content add from a non initiated session ' + jid);
}
MovimJingles.sessions[jid].onContentAdd(sdp);
},
onReplaceTrack: function (stream) {
let videoTrack = stream.getVideoTracks()[0];
for (jid of Object.keys(MovimJingles.sessions)) {
MovimJingles.sessions[jid].onReplaceTrack(videoTrack);
}
},
terminate: function (jid, reason) {
MovimJingles.sessions[jid].terminate(reason);
delete MovimJingles.sessions[jid];
},
onTerminate: function (jid) {
if (MovimJingles.sessions[jid] == undefined) return;
MovimJingles.sessions[jid].close();
delete MovimJingles.sessions[jid];
let visio = document.querySelector('#visio');
// No sessions left
if (Object.keys(MovimJingles.sessions).length == 0) {
if (visio.dataset.muji == 'false') {
MovimVisio.clear();
} else {
let state = document.querySelector('p.state');
state.innerText = MovimVisio.states.no_participants_left;
}
}
},
terminateAll: function (reason) {
for (jid of Object.keys(MovimJingles.sessions)) {
MovimJingles.terminate(jid, reason);
}
MovimVisio.clear();
}
}

2
public/scripts/movim_rpc.js

@ -82,6 +82,8 @@ var MovimRPC = {
if (typeof window[called] == 'object'
&& typeof window[funcs[0]][funcs[1]] != 'undefined') {
window[funcs[0]][funcs[1]].apply(null, funcall.p);
} else {
throw Error('Function not found: ' + funcall.func);
}
}
}

11
public/scripts/movim_utils.js

@ -108,13 +108,6 @@ var MovimUtils = {
redirect: function (url) {
window.location.href = url;
},
softRedirect: function (url) {
var location = window.location.href;
if (location.substring(0, location.indexOf('#')) !== url) {
window.location.href = url;
}
},
openInNew: function (url) {
window.open(url, '_blank');
},
@ -481,5 +474,9 @@ var MovimUtils = {
replacedText = replacedText.replace(replacePattern2, '<a href="mailto:$1">$1</a>');
return replacedText;
},
logError: function(error) {
console.log(error.name + ': ' + error.message);
console.error(error);
}
};

290
public/scripts/movim_visio.js

@ -1,44 +1,286 @@
var MovimVisio = {
from: null,
id: null,
withVideo: false,
calling: false,
pc: null,
states: null,
services: [],
localStream: null,
localVideo: null,
remoteVideo: null,
localAudio: null,
remoteAudio: null,
screenSharing: null,
inboundStream: null,
observer: null,
load: function() {
load: function () {
MovimVisio.localVideo = document.getElementById('local_video');
MovimVisio.localVideo.addEventListener('loadeddata', () => {
MovimVisio.localVideo.play()
});
MovimVisio.remoteVideo = document.getElementById('remote_video');
MovimVisio.remoteVideo.disablePictureInPicture = true;
MovimVisio.screenSharing = document.getElementById('screen_sharing_video');
MovimVisio.screenSharing = document.getElementById('screen_sharing_video');
MovimVisio.localAudio = document.getElementById('local_audio');
MovimVisio.remoteAudio = document.getElementById('remote_audio');
},
init: function (fullJid, jid, id, withVideo, isMuji) {
Visio_ajaxPrepare(jid);
MovimVisio.id = id;
let visio = document.querySelector('#visio');
delete visio.dataset.type;
visio.dataset.jid = jid;
visio.dataset.type = (withVideo) ? 'video' : 'audio';
visio.dataset.muji = isMuji ? 'true' : 'false';
if (isMuji == true) {
let pc = new RTCPeerConnection({ 'iceServers': MovimVisio.services });
var constraints = {
audio: true,
video: false,
};
if (withVideo) {
constraints.video = true;
}
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
pc.createOffer().then(function (offer) {
//VisioUtils.toggleMainButton();
Visio_ajaxMujiInit(MovimVisio.id, offer);
pc.close();
});
});
} else {
MovimJingles.initSession(jid, fullJid, id);
// Called
if (MovimVisio.id) {
Visio_ajaxAccept(fullJid, MovimVisio.id);
// Calling
} else {
MovimVisio.id = crypto.randomUUID();
MovimVisio.calling = true; // TODO, remove me ?
//VisioUtils.toggleMainButton();
Visio_ajaxPropose(jid, MovimVisio.id, withVideo);
}
}
Notif.setCallStatus(MovimVisio.states.in_call);
},
gotQuickStream: function () {
MovimJingles.onReplaceTrack(MovimVisio.localVideo.srcObject);
},
gotScreen: function () {
MovimJingles.onReplaceTrack(MovimVisio.screenSharing.srcObject);
},
setStates: function (states) {
MovimVisio.states = states;
},
setServices: function (services) {
MovimVisio.services = services;
},
getUserMedia: function (withVideo) {
var constraints = {
audio: true,
video: false,
};
if (withVideo) {
constraints.video = {
facingMode: 'user',
width: { ideal: 1920 },
height: { ideal: 1920 }
}
if (localStorage.defaultCamera) {
constraints.video = {
deviceId: localStorage.defaultCamera
};
}
}
if (localStorage.defaultMicrophone) {
constraints.audio = {
deviceId: localStorage.defaultMicrophone
}
}
MovimVisio.load();
let lobby = document.querySelector('#visio_lobby');
if (lobby) {
VisioUtils.disableLobbyCallButton();
}
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
MovimVisio.localStream = stream;
if (lobby) {
lobby.classList.add('configure');
} else {
MovimVisio.clear();
return;
}
stream.getTracks().forEach(track => {
if (lobby) {
VisioUtils.enableLobbyCallButton();
}
if (track.kind == 'audio') {
MovimVisio.localAudio.srcObject = stream;
} else if (withVideo && track.kind == 'video') {
MovimVisio.localVideo.srcObject = stream;
if (lobby) {
let cameraPreview = lobby.querySelector('video#camera_preview');
cameraPreview.addEventListener('loadeddata', () => cameraPreview.play());
cameraPreview.srcObject = stream;
cameraPreview.disablePictureInPicture = true;
}
}
});
VisioUtils.handleAudio();
if (withVideo) {
VisioUtils.enableScreenSharingButton();
}
navigator.mediaDevices.enumerateDevices().then(devices => MovimVisio.gotDevices(withVideo, devices));
});
},
gotDevices: function (withVideo, devicesInfo) {
microphoneFound = false;
cameraFound = false;
let microphoneSelect = document.querySelector('select[name=default_microphone]');
microphoneSelect.onchange = (e) => {
localStorage.defaultMicrophone = e.target.value;
MovimVisio.getUserMedia(withVideo);
};
microphoneSelect.innerText = '';
VisioUtils.handleAudio();
let cameraSelect = document.querySelector('select[name=default_camera]');
if (cameraSelect) {
cameraSelect.addEventListener('change', e => {
localStorage.defaultCamera = e.target.value;
let cameraPreview = document.querySelector('video#camera_preview');
if (cameraPreview.srcObject) {
cameraPreview.srcObject.getTracks().forEach(track => track.stop());
}
cameraPreview.srcObject = null;
MovimVisio.getUserMedia(withVideo);
});
cameraSelect.innerText = '';
}
for (const deviceInfo of devicesInfo) {
if (deviceInfo.kind === 'audioinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `Microphone ${microphoneSelect.length + 1}`;
if (deviceInfo.deviceId == localStorage.defaultMicrophone) {
option.selected = true;
microphoneFound = true;
}
microphoneSelect.appendChild(option);
}
if (withVideo && deviceInfo.kind === 'videoinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `Camera ${microphoneSelect.length + 1}`;
if (deviceInfo.deviceId == localStorage.defaultCamera) {
option.selected = true;
cameraFound = true;
}
// Sometimes we can have two devices with the same id
if (!cameraSelect.querySelector('option[value="' + deviceInfo.deviceId + '"]')) {
cameraSelect.appendChild(option);
}
}
}
if (microphoneFound == false) {
localStorage.defaultMicrophone = microphoneSelect.value;
}
if (withVideo && cameraFound == false) {
localStorage.defaultCamera = cameraSelect.value;
}
},
goodbye: function () {
let visio = document.querySelector('#visio');
Visio_ajaxGoodbye(visio.dataset.jid, this.id);
},
clear: function () {
MovimVisio.from = null;
MovimVisio.id = null;
MovimVisio.withVideo = false;
if (MovimVisio.pc) {
MovimVisio.pc.close();
MovimVisio.pc = null;
Notif.setCallStatus(null);
let visio = document.querySelector('#visio');
delete visio.dataset.type;
delete visio.dataset.jid;
delete visio.dataset.muji;
if (document.fullscreenElement) {
document.exitFullscreen();
}
if (VisioUtils.audioContext) {
VisioUtils.audioContext.close();
VisioUtils.audioContext = null;
}
if (MovimVisio.localAudio) {
let localStream = MovimVisio.localAudio.srcObject;
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
}
if (MovimVisio.localVideo) {
let localStream = MovimVisio.localVideo.srcObject;
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
}
if (MovimVisio.localStream) {
@ -49,12 +291,8 @@ var MovimVisio = {
}
MovimVisio.localAudio = null;
MovimVisio.remoteAudio = null;
MovimVisio.localVideo = null;
MovimVisio.remoteVideo = null;
MovimVisio.screenSharing = null;
MovimVisio.inboundStream = null;
},
moveToChat: function (jid) {
@ -81,4 +319,12 @@ var MovimVisio = {
MovimVisio.observer = new MutationObserver(callback);
MovimVisio.observer.observe(body, { childList: true, subtree: true });
}
}
}
MovimWebsocket.attach(() => {
if (MovimVisio.services.length == 0) {
Visio_ajaxResolveServices();
}
Visio_ajaxGetStates();
});

5
public/theme/css/form.css

@ -102,6 +102,11 @@ form > div textarea {
outline-width: 0;
}
form > div .select select:has(option:only-child) {
opacity: 0.5;
pointer-events: none;
}
form > div .select,
form > div input:not([type=submit]),
form > div textarea {

4
src/Movim/Controller/Base.php

@ -94,8 +94,8 @@ class Base
$this->page->addScript('movim_utils.js');
$this->page->addScript('movim_events.js');
$this->page->addScript('movim_jingles.js');
$this->page->addScript('movim_e2ee.js');
$this->page->addScript('movim_visio.js');
$this->page->addScript('movim_base.js');
$this->page->addScript('movim_favicon.js');
$this->page->addScript('movim_avatar.js');
@ -109,6 +109,8 @@ class Base
$this->page->addScript('movim_websocket.js');
}
$this->page->addScript('movim_visio.js');
$content = new Builder;
$headers = getallheaders();

49
src/Movim/CurrentCall.php

@ -8,18 +8,33 @@ namespace Movim;
use Carbon\Carbon;
use DOMDocument;
use DOMXPath;
use Movim\Widget\Wrapper;
use SimpleXMLElement;
class CurrentMujiCall
{
public ?string $jid = null;
public ?string $id = null;
public ?string $mujiRoom = null;
public ?Carbon $startTime = null;
public function __construct(string $jid, string $id)
{
$this->jid = $jid;
$this->id = $id;
$this->startTime = Carbon::now();
}
}
/**
* This class handle the current Jitsi call
*/
class CurrentCall
{
protected static $instance;
public ?string $to = null;
public ?string $jid = null;
public ?string $id = null;
public ?string $mujiRoom = null;
public ?Carbon $startTime = null;
private array $contents = [];
@ -33,25 +48,39 @@ class CurrentCall
return self::$instance;
}
public function start(string $to, string $id)
public function start(string $jid, string $id, ?string $mujiRoom = null): bool
{
$this->to = $to;
if ($this->isStarted()) return false;
$this->jid = $jid;
$this->id = $id;
$this->mujiRoom = $mujiRoom;
$this->startTime = Carbon::now();
$wrapper = Wrapper::getInstance();
$wrapper->iterate('currentcall_started', [$this->getBareJid(), $id]);
return true;
}
public function stop()
public function stop(string $jid, string $id): bool
{
if ($this->jid != $jid || $this->id != $id) return false;
$jid = $this->getBareJid();
$id = $this->id;
$this->to = $this->id = $this->startTime = null;
$this->jid = $this->id = $this->mujiRoom = $this->startTime = null;
$wrapper = Wrapper::getInstance();
$wrapper->iterate('currentcall_stopped', [$jid, $id]);
return true;
}
public function hasId(string $id): bool
{
return $this->id == $id;
}
public function isJidInCall(string $jid): bool
@ -61,20 +90,20 @@ class CurrentCall
public function isStarted(): bool
{
return $this->to != null && $this->id != null;
return $this->jid != null && $this->id != null;
}
public function getBareJid(): ?string
{
if (!$this->isStarted()) return null;
return explodeJid($this->to)['jid'];
return \baseJid($this->jid);
}
/**
* Content management
*/
public function setContent(SimpleXMLElement $jingleStanza): SimpleXMLElement
/*public function setContent(SimpleXMLElement $jingleStanza): SimpleXMLElement
{
if ($jingleStanza->attributes()->sid == $this->id) {
$contentIds = [];
@ -132,5 +161,5 @@ class CurrentCall
}
return $jingleStanza;
}
}*/
}

11
src/Movim/Librairies/JingletoSDP.php

@ -2,13 +2,13 @@
namespace Movim\Librairies;
use Movim\CurrentCall;
use SimpleXMLElement;
class JingletoSDP
{
private string $sdp = '';
private SimpleXMLElement $jingle;
public ?string $sid = null;
private string $action;
@ -33,17 +33,12 @@ class JingletoSDP
$this->jingle = $jingle;
if (isset($this->jingle->attributes()->sid)) {
CurrentCall::getInstance()->id = (string)$this->jingle->attributes()->sid;
$this->sid = (string)$this->jingle->attributes()->sid;
}
$this->action = (string)$this->jingle->attributes()->action;
}
public function getSessionId()
{
return substr(base_convert(hash('sha256', CurrentCall::getInstance()->id), 30, 10), 0, 6);
}
public function generate()
{
if ($this->jingle->attributes()->initiator) {
@ -53,7 +48,7 @@ class JingletoSDP
$username = '-';
}
$this->values['session_sdp_id'] = $this->getSessionId();
$this->values['session_sdp_id'] = substr(base_convert(hash('sha256', $this->sid), 30, 10), 0, 6);
$sdpVersion =
'v=0';

61
src/Movim/Librairies/SDPtoJingle.php

@ -2,6 +2,7 @@
namespace Movim\Librairies;
use DOMElement;
use Movim\CurrentCall;
use Movim\Session;
@ -19,7 +20,8 @@ class SDPtoJingle
private $ufrag = null;
private $mid = null;
private $msid = null;
private $sid;
private $sid = null;
private $mujiRoom = null;
// Move the global fingerprint into each medias
private $globalFingerprint = [];
@ -52,10 +54,19 @@ class SDPtoJingle
'media' => "/^m=(audio|video|application|data)/i"
];
public function __construct($sdp, $initiator, $responder = false, $action = false, $mid = null, $ufrag = null)
{
public function __construct(
string $sdp,
string $initiator,
string $sid,
bool $muji = false,
$responder = false,
$action = false,
$mid = null,
$ufrag = null
) {
$this->sdp = $sdp;
$this->arr = explode("\n", $this->sdp);
$this->sid = $sid;
if ($mid !== null) {
$this->mid = $mid;
@ -65,9 +76,15 @@ class SDPtoJingle
$this->ufrag = $ufrag;
}
$this->jingle = new \SimpleXMLElement('<jingle></jingle>');
$this->jingle->addAttribute('xmlns', 'urn:xmpp:jingle:1');
$this->jingle->addAttribute('initiator', $initiator);
if ($muji) {
$this->jingle = new \SimpleXMLElement('<muji></muji>');
$this->jingle->addAttribute('xmlns', 'urn:xmpp:jingle:muji:0');
} else {
$this->jingle = new \SimpleXMLElement('<jingle></jingle>');
$this->jingle->addAttribute('xmlns', 'urn:xmpp:jingle:1');
$this->jingle->addAttribute('initiator', $initiator);
}
if ($action) {
$this->jingle->addAttribute('action', $action);
@ -79,20 +96,9 @@ class SDPtoJingle
$this->action = $action;
}
public function setSessionId(string $sid)
public function setMujiRoom(string $mujiRoom)
{
$this->sid = $sid;
}
private function getSessionId()
{
if ($sid = CurrentCall::getInstance()->id) {
return $sid;
} else {
$o = $this->arr[1];
$sid = explode(" ", $o);
return substr(base_convert($sid[1], 30, 10), 0, 6);
}
$this->mujiRoom = $mujiRoom;
}
private function initContent($force = false)
@ -106,6 +112,11 @@ class SDPtoJingle
$this->transport->addAttribute('xmlns', "urn:xmpp:jingle:transports:ice-udp:1");
$this->content->addAttribute('creator', 'initiator'); // FIXME
$this->msid = null;
// A hack to ensure that Dino is returning complete Muji content proposal
if ($this->jingle->getName() == 'muji') {
$this->content->addAttribute('xmlns', 'urn:xmpp:jingle:1');
}
}
}
@ -150,14 +161,20 @@ class SDPtoJingle
}
}
public function generate()
public function generate(): DOMElement
{
if ($this->mujiRoom) {
$muji = $this->jingle->addChild('muji');
$muji->addAttribute('xmlns', 'urn:xmpp:jingle:muji:0');
$muji->addAttribute('room', $this->mujiRoom);
}
foreach ($this->arr as $l) {
foreach ($this->regex as $key => $r) {
if (preg_match($r, $l, $matches)) {
switch ($key) {
case 'sess_id':
$this->jingle->addAttribute('sid', $this->sid ?? $this->getSessionId());
$this->jingle->addAttribute('sid', $this->sid);
break;
case 'media':
$this->initContent(true);
@ -389,7 +406,7 @@ class SDPtoJingle
$this->initContent();
$this->addName();
$this->jingle->addAttribute('sid', $this->sid ?? $this->getSessionId());
$this->jingle->addAttribute('sid', $this->sid);
$candidate = $this->transport->addChild('candidate');
$candidate->addAttribute('foundation', $matches[1]);

8
src/Moxl/API.php

@ -50,6 +50,14 @@ class API
return $dom->saveXML($dom->documentElement);
}
/**
* Request a DomDocument
*/
public static function sendDom(\DOMDocument $dom)
{
API::request($dom->saveXML($dom->documentElement));
}
/*
* Call the request class with the correct XML
*/

2
src/Moxl/Stanza/Confirm.php

@ -31,6 +31,6 @@ class Confirm
$error->appendChild($notauth);
}
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
}

19
src/Moxl/Stanza/Jingle.php

@ -25,7 +25,7 @@ class Jingle
$description->setAttribute('media', 'audio');
$propose->appendChild($description);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function sessionAccept($id)
@ -38,7 +38,7 @@ class Jingle
$accept->setAttribute('id', $id);
$message->appendChild($accept);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function sessionProceed($to, $id)
@ -52,7 +52,7 @@ class Jingle
$proceed->setAttribute('id', $id);
$message->appendChild($proceed);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function sessionRetract($to, $id)
@ -72,7 +72,7 @@ class Jingle
$reason->appendChild($dom->createElement('cancel'));
$reason->appendChild($dom->createElement('text', 'Retracted'));
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function sessionReject($id, $to = false)
@ -88,11 +88,15 @@ class Jingle
$proceed->setAttribute('id', $id);
$message->appendChild($proceed);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function sessionInitiate($to, $offer)
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$jingle = $dom->createElementNS('urn:xmpp:jingle:1', 'jingle');
$jingle->setAttribute('action', 'session-terminate');
\Moxl\API::request(\Moxl\API::iqWrapper($offer, $to, 'set'));
}
@ -156,8 +160,9 @@ class Jingle
$error = $dom->createElement('error');
$error->setAttribute('type', 'cancel');
$fni = $dom->createElementNS('urn:xmpp:jingle:errors:1', 'unknown-session');
$error->appendChild($fni);
$us = $dom->createElement('unknown-session');
$us->setAttribute('xmlns', 'urn:xmpp:jingle:errors:1');
$error->appendChild($us);
\Moxl\API::request(\Moxl\API::iqWrapper($error, $to, 'error', $id));
}

76
src/Moxl/Stanza/JingleCallInvite.php

@ -0,0 +1,76 @@
<?php
namespace Moxl\Stanza;
class JingleCallInvite
{
public static function invite(string $to, string $id, string $room, bool $video = false)
{
$dom = Message::factory($to, 'groupchat');
$invite = $dom->createElementNS('urn:xmpp:call-invites:0', 'invite');
$invite->setAttribute('id', $id);
if ($video) {
$invite->setAttribute('video', 'true');
}
$muji = $dom->createElement('muji');
$muji->setAttribute('xmlns', 'urn:xmpp:jingle:muji:0');
$muji->setAttribute('room', $room);
$invite->appendChild($muji);
$dom->documentElement->appendChild($invite);
\Moxl\API::sendDom($dom);
}
public static function retract(string $to, string $id)
{
$dom = Message::factory($to, 'groupchat');
$retract = $dom->createElementNS('urn:xmpp:call-invites:0', 'retract');
$retract->setAttribute('id', $id);
$dom->documentElement->appendChild($retract);
\Moxl\API::sendDom($dom);
}
public static function accept(string $to, string $id)
{
$dom = Message::factory($to, 'groupchat');
$accept = $dom->createElementNS('urn:xmpp:call-invites:0', 'accept');
$accept->setAttribute('id', $id);
$dom->documentElement->appendChild($accept);
\Moxl\API::sendDom($dom);
}
public static function reject(string $to, string $id)
{
$dom = Message::factory($to, 'groupchat');
$reject = $dom->createElementNS('urn:xmpp:call-invites:0', 'reject');
$reject->setAttribute('id', $id);
$dom->documentElement->appendChild($reject);
\Moxl\API::sendDom($dom);
}
public static function left(string $to, string $id)
{
$dom = Message::factory($to, 'groupchat');
$left = $dom->createElementNS('urn:xmpp:call-invites:0', 'left');
$left->setAttribute('id', $id);
$dom->documentElement->appendChild($left);
\Moxl\API::sendDom($dom);
}
}

99
src/Moxl/Stanza/Message.php

@ -8,32 +8,18 @@ use Movim\Session;
class Message
{
public static function maker(
public static function factory(
string $to,
?string $content = null,
?string $html = null,
?string $type = null,
?string $chatstates = null,
?string $receipts = null,
?string $id = null,
?string $replace = null,
?MessageFile $file = null,
$invite = false,
$parentId = false,
array $reactions = [],
$originId = false,
$threadId = false,
$replyId = false,
$replyTo = false,
$replyQuotedBodyLength = 0,
?MessageOmemoHeader $messageOMEMO = null
) {
?string $receipts = null
): \DOMDocument {
$session = Session::instance();
$dom = new \DOMDocument('1.0', 'UTF-8');
$root = $dom->createElementNS('jabber:client', 'message');
$dom->appendChild($root);
$root->setAttribute('to', $to);
$root->setAttribute('to', str_replace(' ', '\40', $to));
if ($type != null) {
$root->setAttribute('type', $type);
@ -47,19 +33,44 @@ class Message
$root->setAttribute('id', $session->get('id'));
}
return $dom;
}
public static function maker(
string $to,
?string $content = null,
?string $html = null,
?string $type = null,
?string $chatstates = null,
?string $receipts = null,
?string $id = null,
?string $replace = null,
?MessageFile $file = null,
$invite = false,
$parentId = false,
array $reactions = [],
$originId = false,
$threadId = false,
$replyId = false,
$replyTo = false,
$replyQuotedBodyLength = 0,
?MessageOmemoHeader $messageOMEMO = null
) {
$dom = Message::factory($to, $type, $id, $receipts);
/**
* https://xmpp.org/extensions/xep-0045.html#privatemessage
* Resource on the to, we assume that it's a MUC PM
*/
if (explodeJid($to)['resource'] !== null) {
$xuser = $dom->createElementNS('http://jabber.org/protocol/muc#user', 'x');
$root->appendChild($xuser);
$dom->documentElement->appendChild($xuser);
}
// Thread
if ($threadId) {
$thread = $dom->createElement('thread', $threadId);
$root->appendChild($thread);
$dom->documentElement->appendChild($thread);
}
// Message replies
@ -67,7 +78,7 @@ class Message
$reply = $dom->createElementNS('urn:xmpp:reply:0', 'reply');
$reply->setAttribute('id', $replyId);
$reply->setAttribute('to', $replyTo);
$root->appendChild($reply);
$dom->documentElement->appendChild($reply);
if ($replyQuotedBodyLength > 0) {
$fallback = $dom->createElementNS('urn:xmpp:fallback:0', 'fallback');
@ -79,19 +90,19 @@ class Message
$fallback->appendChild($fallbackBody);
$root->appendChild($fallback);
$dom->documentElement->appendChild($fallback);
}
}
// Chatstates
if ($chatstates != null && $content == null) {
$chatstate = $dom->createElementNS('http://jabber.org/protocol/chatstates', $chatstates);
$root->appendChild($chatstate);
$dom->documentElement->appendChild($chatstate);
}
if ($content != null) {
$chatstate = $dom->createElementNS('http://jabber.org/protocol/chatstates', 'active');
$root->appendChild($chatstate);
$dom->documentElement->appendChild($chatstate);
$body = $dom->createElement('body');
@ -100,13 +111,13 @@ class Message
? $dom->createCDATASection($content)
: $dom->createTextNode($content);
$body->appendChild($bodyContent);
$root->appendChild($body);
$dom->documentElement->appendChild($body);
}
if ($replace != null) {
$rep = $dom->createElementNS('urn:xmpp:message-correct:0', 'replace');
$rep->setAttribute('id', $replace);
$root->appendChild($rep);
$dom->documentElement->appendChild($rep);
}
if ($html != null) {
@ -121,7 +132,7 @@ class Message
$body->appendChild($dom->importNode($bar, true));
$xhtml->appendChild($body);
$root->appendChild($xhtml);
$dom->documentElement->appendChild($xhtml);
}
if ($receipts != null) {
@ -131,21 +142,21 @@ class Message
$request = $dom->createElement('received');
$request->setAttribute('id', $id);
$request->setAttribute('xmlns', 'urn:xmpp:receipts');
$root->appendChild($request);
$dom->documentElement->appendChild($request);
} elseif ($receipts == 'displayed') {
$request = $dom->createElement('displayed');
$request->setAttribute('id', $id);
$request->setAttribute('xmlns', 'urn:xmpp:chat-markers:0');
}
$root->appendChild($request);
$dom->documentElement->appendChild($request);
if ($receipts == 'received') {
$nostore = $dom->createElementNS('urn:xmpp:hints', 'no-store');
$root->appendChild($nostore);
$dom->documentElement->appendChild($nostore);
$nocopy = $dom->createElementNS('urn:xmpp:hints', 'no-copy');
$root->appendChild($nocopy);
$dom->documentElement->appendChild($nocopy);
}
}
@ -154,7 +165,7 @@ class Message
&& $chatstates == 'active'
) {
$markable = $dom->createElementNS('urn:xmpp:chat-markers:0', 'markable');
$root->appendChild($markable);
$dom->documentElement->appendChild($markable);
}
if ($file != null) {
@ -215,20 +226,20 @@ class Message
$reference->setAttribute('uri', $file->url);
}
$root->appendChild($reference);
$dom->documentElement->appendChild($reference);
// OOB
$x = $dom->createElement('x');
$x->setAttribute('xmlns', 'jabber:x:oob');
$x->appendChild($dom->createElement('url', $file->url));
$root->appendChild($x);
$dom->documentElement->appendChild($x);
}
if ($invite != false) {
$x = $dom->createElement('x');
$x->setAttribute('xmlns', 'http://jabber.org/protocol/muc#user');
$root->appendChild($x);
$dom->documentElement->appendChild($x);
$xinvite = $dom->createElement('invite');
$xinvite->setAttribute('to', $invite);
@ -244,19 +255,19 @@ class Message
$reaction = $dom->createElement('reaction', $emoji);
$reactionsn->appendChild($reaction);
}
$root->appendChild($reactionsn);
$dom->documentElement->appendChild($reactionsn);
// Force the storage of the reactions in the archive
$store = $dom->createElement('store');
$store->setAttribute('xmlns', 'urn:xmpp:hints');
$root->appendChild($store);
$dom->documentElement->appendChild($store);
}
if ($originId != false) {
$origin = $dom->createElement('origin-id');
$origin->setAttribute('xmlns', 'urn:xmpp:sid:0');
$origin->setAttribute('id', $originId);
$root->appendChild($origin);
$dom->documentElement->appendChild($origin);
}
// OMEMO
@ -265,13 +276,13 @@ class Message
$encryption->setAttribute('xmlns', 'urn:xmpp:eme:0');
$encryption->setAttribute('name', 'OMEMOE');
$encryption->setAttribute('namespace', 'eu.siacs.conversations.axolotl');
$root->appendChild($encryption);
$dom->documentElement->appendChild($encryption);
$messageOMEMOXML = $dom->importNode($messageOMEMO->getDom(), true);
$root->appendChild($messageOMEMOXML);
$dom->documentElement->appendChild($messageOMEMOXML);
}
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function message(
@ -316,8 +327,8 @@ class Message
string $to,
?string $content = null,
?string $html = null,
string $id = null,
string $replace = null,
?string $id = null,
?string $replace = null,
?MessageFile $file = null,
$parentId = false,
array $reactions = [],
@ -414,7 +425,7 @@ class Message
$body->appendChild($bodyContent);
$root->appendChild($body);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function moderate(string $to, string $stanzaId)

23
src/Moxl/Stanza/Muc.php

@ -70,7 +70,7 @@ class Muc
$message->appendChild($dom->createElement('subject', $subject));
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function destroy($to)
@ -140,6 +140,27 @@ class Muc
\Moxl\API::request(\Moxl\API::iqWrapper($query, $to, 'set'));
}
public static function createMujiChat($to)
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$query = $dom->createElementNS('http://jabber.org/protocol/muc#owner', 'query');
$dom->appendChild($query);
$x = $dom->createElementNS('jabber:x:data', 'x');
$x->setAttribute('type', 'submit');
$query->appendChild($x);
\Moxl\Utils::injectConfigInX($x, [
'FORM_TYPE' => 'http://jabber.org/protocol/muc#roomconfig',
'muc#roomconfig_persistentroom' => 'false',
'muc#roomconfig_membersonly' => 'false',
'muc#roomconfig_whois' => 'anyone',
]);
\Moxl\API::request(\Moxl\API::iqWrapper($query, $to, 'set'));
}
public static function createGroupChat($to, $name)
{
$dom = new \DOMDocument('1.0', 'UTF-8');

17
src/Moxl/Stanza/Presence.php

@ -2,6 +2,7 @@
namespace Moxl\Stanza;
use DOMElement;
use Movim\Session;
class Presence
@ -17,6 +18,8 @@ class Presence
$type = false,
bool $muc = false,
bool $mam = false,
bool $mujiPreparing = false,
?DOMElement $muji = null,
$last = 0
) {
$session = Session::instance();
@ -56,6 +59,10 @@ class Presence
$root->appendChild($priority);
}
if ($muji != null) {
$root->append($dom->importNode($muji, true));
}
// https://xmpp.org/extensions/xep-0319.html#last-interact
if ($last > 0) {
$timestamp = time() - $last;
@ -64,6 +71,12 @@ class Presence
$root->appendChild($idle);
}
if ($mujiPreparing) {
$muji = $dom->createElementNS('urn:xmpp:jingle:muji:0', 'muji');
$muji->appendChild($dom->createElement('preparing'));
$root->appendChild($muji);
}
if ($muc) {
$x = $dom->createElementNS('http://jabber.org/protocol/muc', 'x');
@ -136,9 +149,9 @@ class Presence
/*
* Enter a chat room
*/
public static function muc($to, $nickname = false, $mam = false)
public static function muc($to, $nickname = false, $mam = false, $mujiPreparing = false, ?DOMElement $muji = null)
{
\Moxl\API::request(self::maker($to . '/' . $nickname, muc: true, mam: $mam));
\Moxl\API::request(self::maker($to . '/' . $nickname, muc: true, mam: $mam, mujiPreparing: $mujiPreparing, muji: $muji));
}
/*

4
src/Moxl/Stanza/Stream.php

@ -34,7 +34,7 @@ class Stream
$starttls = $dom->createElementNS('urn:ietf:params:xml:ns:xmpp-tls', 'starttls');
$dom->appendChild($starttls);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function bindSet($resource)
@ -70,7 +70,7 @@ class Stream
$dom->appendChild($authenticate);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
public static function sessionStart($to)

2
src/Moxl/Utils.php

@ -63,6 +63,8 @@ class Utils
'urn:xmpp:jingle:transports:ice-udp:1',
'urn:xmpp:jingle:apps:rtp:rtcp-fb:0',
'urn:xmpp:jingle-message:0',
'urn:xmpp:jingle:muji:0',
'urn:xmpp:call-invites:0',
'http://jabber.org/protocol/muc',
'http://jabber.org/protocol/nick',

2
src/Moxl/Xec/Action/Jingle/SessionAccept.php

@ -13,8 +13,6 @@ class SessionAccept extends Action
public function request()
{
CurrentCall::getInstance()->start($this->_to, $this->_id);
$this->store();
Jingle::sessionAccept($this->_id);
Jingle::sessionProceed($this->_to, $this->_id);

2
src/Moxl/Xec/Action/Jingle/SessionTerminate.php

@ -30,8 +30,6 @@ class SessionTerminate extends Action
$message->type = 'jingle_end';
$message->save();
$this->event('jingle_sessionterminate', $this->_reason);
$this->pack($message);
$this->event('jingle_message');
}

18
src/Moxl/Xec/Action/JingleCallInvite/Accept.php

@ -0,0 +1,18 @@
<?php
namespace Moxl\Xec\Action\JingleCallInvite;
use Moxl\Stanza\JingleCallInvite;
use Moxl\Xec\Action;
class Accept extends Action
{
protected string $_to;
protected string $_id;
public function request()
{
$this->store();
JingleCallInvite::accept($this->_to, $this->_id);
}
}

33
src/Moxl/Xec/Action/JingleCallInvite/Invite.php

@ -0,0 +1,33 @@
<?php
namespace Moxl\Xec\Action\JingleCallInvite;
use Moxl\Stanza\JingleCallInvite;
use Moxl\Xec\Action;
use Moxl\Xec\Payload\CallInvitePropose;
class Invite extends Action
{
protected string $_to;
protected string $_id;
protected ?string $_room = null;
protected bool $_video = false;
public function request()
{
$this->store();
JingleCallInvite::invite($this->_to, $this->_id, $this->_room, $this->_video);
}
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
$propose = new CallInvitePropose;
$propose->handle($stanza->invite, $stanza);
}
public function enableVideo()
{
$this->_video = true;
return $this;
}
}

18
src/Moxl/Xec/Action/JingleCallInvite/Left.php

@ -0,0 +1,18 @@
<?php
namespace Moxl\Xec\Action\JingleCallInvite;
use Moxl\Stanza\JingleCallInvite;
use Moxl\Xec\Action;
class Left extends Action
{
protected string $_to;
protected string $_id;
public function request()
{
$this->store();
JingleCallInvite::left($this->_to, $this->_id);
}
}

18
src/Moxl/Xec/Action/JingleCallInvite/Reject.php

@ -0,0 +1,18 @@
<?php
namespace Moxl\Xec\Action\JingleCallInvite;
use Moxl\Stanza\JingleCallInvite;
use Moxl\Xec\Action;
class Reject extends Action
{
protected string $_to;
protected string $_id;
public function request()
{
$this->store();
JingleCallInvite::reject($this->_to, $this->_id);
}
}

34
src/Moxl/Xec/Action/JingleCallInvite/Retract.php

@ -0,0 +1,34 @@
<?php
namespace Moxl\Xec\Action\JingleCallInvite;
use App\Message;
use Moxl\Stanza\JingleCallInvite;
use Moxl\Xec\Action;
use SimpleXMLElement;
class Retract extends Action
{
protected string $_to;
protected string $_id;
public function request()
{
$this->store();
JingleCallInvite::retract($this->_to, $this->_id);
}
public function handle(?SimpleXMLElement $stanza = null, ?SimpleXMLElement $parent = null)
{
$message = Message::eventMessageFactory(
'jingle',
baseJid($this->_to),
$this->_id
);
$message->type = 'muji_retract';
$message->save();
$this->pack($message);
$this->event('muji_message');
}
}

22
src/Moxl/Xec/Action/Muc/CreateMujiRoom.php

@ -0,0 +1,22 @@
<?php
namespace Moxl\Xec\Action\Muc;
use Moxl\Xec\Action;
use Moxl\Stanza\Muc;
class CreateMujiRoom extends Action
{
protected $_to;
public function request()
{
$this->store();
Muc::createMujiChat($this->_to);
}
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
$this->deliver();
}
}

40
src/Moxl/Xec/Action/Presence/Muc.php

@ -6,7 +6,7 @@ use Moxl\Xec\Action;
use Moxl\Stanza\Presence;
use Movim\Session;
use App\PresenceBuffer;
use DOMElement;
use Illuminate\Database\Capsule\Manager as DB;
class Muc extends Action
@ -18,6 +18,8 @@ class Muc extends Action
protected $_mam = false;
protected $_mam2 = false;
protected $_create = false;
protected $_mujiPreparing = false;
protected ?DOMElement $_muji = null;
// Disable the event
protected $_notify = true;
@ -42,7 +44,7 @@ class Muc extends Action
*/
$session->set(self::$mucId . $this->_to . '/' . $this->_nickname, $this->stanzaId);
Presence::muc($this->_to, $this->_nickname, $this->_mam);
Presence::muc($this->_to, $this->_nickname, $this->_mam, $this->_mujiPreparing, $this->_muji);
}
public function enableCreate()
@ -63,6 +65,18 @@ class Muc extends Action
return $this;
}
public function enableMujiPreparing()
{
$this->_mujiPreparing = true;
return $this;
}
public function setMuji(DOMElement $muji)
{
$this->_muji = $muji;
return $this;
}
public function noNotify()
{
$this->_notify = false;
@ -111,15 +125,27 @@ class Muc extends Action
if ($this->_create) {
$presence->save();
if ($this->_mujiPreparing) {
$this->method('create_muji_handle');
$this->pack($presence);
$this->deliver();
}
$this->method('create_handle');
$this->pack($presence);
$this->deliver();
} else {
PresenceBuffer::getInstance()->append($presence, function () use ($presence) {
$this->pack([$presence, $this->_notify]);
$this->deliver();
});
}
PresenceBuffer::getInstance()->append($presence, function () use ($presence) {
if ($this->_mujiPreparing) {
$this->method('muji_preparing');
$this->deliver();
}
$this->method('handle'); // Reset
$this->pack([$presence, $this->_notify]);
$this->deliver();
});
}
public function errorRegistrationRequired(string $errorId, ?string $message = null)

14
src/Moxl/Xec/Action/Presence/Unavailable.php

@ -2,6 +2,8 @@
namespace Moxl\Xec\Action\Presence;
use App\Presence as DBPresence;
use App\PresenceBuffer;
use Moxl\Xec\Action;
use Moxl\Stanza\Presence;
@ -15,13 +17,19 @@ class Unavailable extends Action
public function request()
{
$this->store();
Presence::unavailable($this->_to.'/'.$this->_resource, $this->_status, $this->_type);
Presence::unavailable($this->_to . '/' . $this->_resource, $this->_status, $this->_type);
}
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
$this->pack($this->_to);
$this->deliver();
$presence = DBPresence::findByStanza($stanza);
$presence->set($stanza);
PresenceBuffer::getInstance()->append($presence, function () {
$this->pack($this->_to);
$this->deliver();
});
}
public function error(string $errorId, ?string $message = null)

7
src/Moxl/Xec/Handler.php

@ -174,6 +174,13 @@ class Handler
'44d0c16e222fcdee6961c8939b647e15' => 'JingleReject',
'd84d4b89d43e88a244197ccf499de8d8' => 'Jingle',
// TODO: Update the handlers to XEP-0482 when Dino is updated
'f157a6eb56b1ad5d1e4b9ae9101464d7' => 'CallInvitePropose',
'c633e665374419257db0c4f8e2624798' => 'CallInviteAccept',
//'b7b53756a85b4d0c27c8797b3dcbaf6f' => 'CallInviteReject',
'ddbf7b88f16d982ddfb129c18aaf94dc' => 'CallInviteRetract',
'a6e8ce859ac26a7bc1c728a9c829ddcc' => 'CallInviteLeft',
'09ef1b34cf40fdd954f10d6e5075ee5c' => 'Carbons', // sent
'201fa54dd93e3403611830213f5f9fbc' => 'Carbons', // received

25
src/Moxl/Xec/Payload/CallInviteAccept.php

@ -0,0 +1,25 @@
<?php
namespace Moxl\Xec\Payload;
use App\MujiCallParticipant;
class CallInviteAccept extends Payload
{
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
if ($parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') {
$muji = \App\User::me()->session->mujiCalls()->where('id', (string)$stanza->attributes()->id)->first();
if ($muji) {
MujiCallParticipant::firstOrCreate([
'muji_call_id' => (string)$stanza->attributes()->id,
'jid' => (string)$parent->attributes()->from,
]);
$this->pack($muji);
$this->deliver();
}
}
}
}

27
src/Moxl/Xec/Payload/CallInviteLeft.php

@ -0,0 +1,27 @@
<?php
namespace Moxl\Xec\Payload;
use App\MujiCallParticipant;
use Carbon\Carbon;
class CallInviteLeft extends Payload
{
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
if ($parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') {
$muji = \App\User::me()->session->mujiCalls()->where('id', (string)$stanza->attributes()->id)->first();
if ($muji) {
MujiCallParticipant::firstOrCreate([
'muji_call_id' => (string)$stanza->attributes()->id,
'jid' => (string)$parent->attributes()->from,
'left_at' => Carbon::now(),
]);
$this->pack($muji);
$this->deliver();
}
}
}
}

64
src/Moxl/Xec/Payload/CallInvitePropose.php

@ -0,0 +1,64 @@
<?php
namespace Moxl\Xec\Payload;
use App\Message;
use App\MujiCallParticipant;
use Movim\CurrentCall;
use Moxl\Xec\Action\JingleCallInvite\Reject;
class CallInvitePropose extends Payload
{
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null, bool $carbon = false)
{
// Another session is already started
if (CurrentCall::getInstance()->isStarted()
&& CurrentCall::getInstance()->isJidInCall(baseJid($parent->attributes()->from))) {
$conference = \App\User::me()->session->conferences()->where('conference', \baseJid((string)$parent->attributes()->from))->first();
if ($conference) {
// If the propose is from another person
if (!$conference->presence || $conference->presence->resource != \explodeJid((string)$parent->attributes()->from)['resource']) {
$reject = new Reject;
$reject->setTo(\baseJid((string)$parent->attributes()->from))
->setId((string)$stanza->attributes()->id)
->request();
return;
}
}
}
if ($stanza->muji && $stanza->muji->attributes()->xmlns == 'urn:xmpp:jingle:muji:0'
&& $parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') {
$muji = \App\MujiCall::firstOrCreate([
'id' => (string)$stanza->attributes()->id,
'muc' => (string)$stanza->muji->attributes()->room,
'jidfrom' => $carbon
? (string)$parent->attributes()->to
: (string)$parent->{'stanza-id'}->attributes()->by,
'isfromconference' => ((string)$parent->attributes()->type == 'groupchat'),
'video' => ((string)$stanza->attributes()->video == 'true'),
]);
MujiCallParticipant::firstOrCreate([
'muji_call_id' => (string)$stanza->attributes()->id,
'jid' => (string)$parent->attributes()->from,
'inviter' => true
]);
$message = Message::eventMessageFactory(
'muji_propose',
baseJid((string)$parent->attributes()->from),
(string)$stanza->attributes()->id
);
$message->save();
$this->pack($message);
$this->event('muji_message');
$this->pack($muji);
$this->deliver();
}
}
}

39
src/Moxl/Xec/Payload/CallInviteRetract.php

@ -0,0 +1,39 @@
<?php
namespace Moxl\Xec\Payload;
use App\Message;
use App\MujiCall;
use Movim\CurrentCall;
class CallInviteRetract extends Payload
{
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
if ($parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') {
$muji = \App\User::me()->session->mujiCalls()->where('id', (string)$stanza->attributes()->id)->first();
if ($muji) {
$participant = $muji->participants->firstWhere('jid', $parent->attributes()->from);
if ($participant && $participant->inviter) {
$message = Message::eventMessageFactory(
'muji_retract',
baseJid((string)$parent->attributes()->from),
(string)$stanza->attributes()->id
);
$message->save();
$this->pack($message);
$this->event('muji_message');
CurrentCall::getInstance()->stop(baseJid((string)$parent->attributes()->from), $muji->id);
MujiCall::where('id', $muji->id)->where('session_id', $muji->session_id)->delete();
$this->pack($muji);
$this->deliver();
}
}
}
}
}

5
src/Moxl/Xec/Payload/Carbons.php

@ -14,6 +14,10 @@ class Carbons extends Payload
&& $message->retract->attributes()->xmlns == 'urn:xmpp:message-retract:1') {
$retracted = new Retracted;
$retracted->handle($message->retract, $message);
} elseif ($message->invite
&& $message->invite->attributes()->xmlns == 'urn:xmpp:call-invites:0') {
$callInvite = new CallInvitePropose;
$callInvite->handle($message->invite, $message, carbon: true);
} elseif ($message->body || $message->subject
|| ($message->reactions && $message->reactions->attributes()->xmlns == 'urn:xmpp:reactions:0')) {
$m = \App\Message::findByStanza($message);
@ -37,6 +41,7 @@ class Carbons extends Payload
$displayed->handle($message->displayed, $message);
} elseif (count($jingle_messages = $stanza->xpath('//*[@xmlns="urn:xmpp:jingle-message:0"]')) >= 1) {
$callto = baseJid((string)$message->attributes()->to);
if ($callto == \App\User::me()->id || $callto == "") {
// We get carbons for calls other clients make as well as calls other clients receive
// So make sure we only ring when we see a call _to_ us

75
src/Moxl/Xec/Payload/Jingle.php

@ -22,65 +22,80 @@ class Jingle extends Payload
(string)$stanza->attributes()->sid
);
$sid = CurrentCall::getInstance()->id;
if ($sid == $message->thread) {
//if (CurrentCall::getInstance()->hasId($message->thread)) {
Ack::send($from, $id);
switch ($action) {
case 'session-initiate':
$message->type = 'jingle_incoming';
$message->save();
if (!$stanza->muji && CurrentCall::getInstance()->hasId($stanza->attributes()->sid)) {
$message->type = 'jingle_incoming';
$message->save();
$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->pack($message);
$this->event('jingle_message');
}
$this->event('jingle_sessioninitiate', [$stanza, $from]);
//$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->pack($message);
$this->event('jingle_message');
$this->pack($stanza, $from);
$this->event('jingle_sessioninitiate');
break;
case 'session-info':
if ($stanza->mute) {
$this->event('jingle_sessionmute', 'mid' . (string)$stanza->mute->attributes()->name);
$this->pack('mid' . (string)$stanza->mute->attributes()->name, $from);
$this->event('jingle_sessionmute');
}
if ($stanza->unmute) {
$this->event('jingle_sessionunmute', 'mid' . (string)$stanza->unmute->attributes()->name);
$this->pack('mid' . (string)$stanza->unmute->attributes()->name, $from);
$this->event('jingle_sessionunmute');
}
break;
case 'transport-info':
$this->event('jingle_transportinfo', $stanza);
$this->pack($stanza, $from);
$this->event('jingle_transportinfo');
break;
case 'session-terminate':
$message->type = 'jingle_end';
$message->save();
$this->event('jingle_sessionterminate', (string)$stanza->reason->children()[0]->getName());
if (!$stanza->muji && CurrentCall::getInstance()->hasId($stanza->attributes()->sid)) {
$message->type = 'jingle_end';
$message->save();
$this->pack($message);
$this->event('jingle_message');
}
$this->pack($message);
$this->event('jingle_message');
$this->pack((string)$stanza->attributes()->sid, $from);
$this->event('jingle_sessionterminate'/*, (string)$stanza->reason->children()[0]->getName()*/);
break;
case 'session-accept':
$message->type = 'jingle_outgoing';
$message->save();
$this->event('jingle_sessionaccept', $stanza);
if (!$stanza->muji && CurrentCall::getInstance()->hasId($stanza->attributes()->sid)) {
$message->type = 'jingle_outgoing';
$message->save();
$this->pack($message);
$this->event('jingle_message');
}
$this->pack($message);
$this->event('jingle_message');
$this->pack($stanza, $from);
$this->event('jingle_sessionaccept');
break;
case 'content-add':
$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->event('jingle_contentadd', $stanza);
//$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->pack($stanza, $from);
$this->event('jingle_contentadd');
break;
case 'content-modify':
$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->event('jingle_contentmodify', $stanza);
//$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->pack($stanza, $from);
$this->event('jingle_contentmodify');
break;
case 'content-remove':
$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->event('jingle_contentremove', $stanza);
//$stanza = CurrentCall::getInstance()->setContent($stanza);
$this->pack($stanza, $from);
$this->event('jingle_contentremove');
break;
}
} else {
/*} else {
JingleStanza::unknownSession($from, $id);
}
}*/
}
}

5
src/Moxl/Xec/Payload/JingleAccept.php

@ -6,10 +6,7 @@ class JingleAccept extends Payload
{
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
$this->pack([
'from' => (string)$parent->attributes()->from,
'id' => (string)$stanza->attributes()->id
]);
$this->pack((string)$stanza->attributes()->id, (string)$parent->attributes()->from);
$this->deliver();
}
}

8
src/Moxl/Xec/Payload/JingleProceed.php

@ -11,13 +11,7 @@ class JingleProceed extends Payload
$from = (string)$parent->attributes()->from;
$id = (string)$stanza->attributes()->id;
CurrentCall::getInstance()->start($from, $id);
$this->pack([
'from' => $from,
'id' => $id
]);
$this->pack($id, $from);
$this->deliver();
}
}

3
src/Moxl/Xec/Payload/JinglePropose.php

@ -24,10 +24,9 @@ class JinglePropose extends Payload
}
$this->pack([
'from' => (string)$parent->attributes()->from,
'id' => (string)$stanza->attributes()->id,
'withVideo' => $withVideo
]);
], (string)$parent->attributes()->from);
$this->deliver();
}

5
src/Moxl/Xec/Payload/JingleReject.php

@ -12,10 +12,7 @@ class JingleReject extends Payload
$jingleSid = CurrentCall::getInstance()->id;
if ($jingleSid && (string)$stanza->attributes()->id != $jingleSid) return;
$this->pack([
'from' => (string)$parent->attributes()->from,
'id' => (string)$stanza->attributes()->id
]);
$this->pack((string)$stanza->attributes()->id, (string)$parent->attributes()->from);
$this->deliver();
}
}

25
src/Moxl/Xec/Payload/JingleRetract.php

@ -10,21 +10,20 @@ class JingleRetract extends Payload
{
$from = (string)$parent->attributes()->from;
$message = Message::eventMessageFactory(
'jingle',
baseJid($from),
(string)$stanza->attributes()->id
);
$message->type = 'jingle_retract';
$message->save();
if (!$stanza->muji) {
$message = Message::eventMessageFactory(
'jingle',
baseJid($from),
(string)$stanza->attributes()->id
);
$message->type = 'jingle_retract';
$message->save();
$this->pack($message);
$this->event('jingle_message');
$this->pack($message);
$this->event('jingle_message');
}
$this->pack([
'from' => $from,
'id' => (string)$stanza->attributes()->id
]);
$this->pack((string)$stanza->attributes()->id, $from);
$this->deliver();
}
}

4
src/Moxl/Xec/Payload/Payload.php

@ -83,9 +83,9 @@ abstract class Payload
*
* @return void
*/
final public function method(string $method)
final public function method(?string $method = null)
{
$this->method = strtolower($method);
$this->method = $method ? strtolower($method) : null;
}
/**

20
src/Moxl/Xec/Payload/Presence.php

@ -6,6 +6,7 @@ use App\Presence as DBPresence;
use App\PresenceBuffer;
use Movim\Session;
use Movim\ChatroomPings;
use Movim\CurrentCall;
use Moxl\Xec\Action\Presence\Muc;
use Moxl\Xec\Handler;
@ -14,6 +15,7 @@ class Presence extends Payload
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
$jid = explodeJid($stanza->attributes()->from);
if (\App\User::me()->hasBlocked($jid['jid'])) {
return;
}
@ -27,7 +29,23 @@ class Presence extends Payload
$presence = DBPresence::findByStanza($stanza);
$presence->set($stanza);
PresenceBuffer::getInstance()->append($presence, function () use ($presence, $stanza) {
if (CurrentCall::getInstance()->isStarted() && CurrentCall::getInstance()->mujiRoom == $jid['jid']) {
$muji = \App\User::me()->session->mujiCalls()
->where('muc', $jid['jid'])
->first();
if ($muji) {
$this->pack($muji);
$this->method('muji_event');
$this->deliver();
$this->pack([$stanza, $presence], $presence->mucjid . '/' . $presence->mucjidresource);
$this->method('muji');
$this->deliver();
}
}
PresenceBuffer::getInstance()->append($presence, function () use ($presence, $stanza, $jid) {
if ((string)$stanza->attributes()->type == 'subscribe') {
$this->event('subscribe', (string)$stanza->attributes()->from);
}

2
src/Moxl/Xec/Payload/SASL2Challenge.php

@ -16,6 +16,6 @@ class SASL2Challenge extends Payload
$auth = $dom->createElementNS('urn:xmpp:sasl:2', 'response', $response);
$dom->appendChild($auth);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
}

2
src/Moxl/Xec/Payload/SASLChallenge.php

@ -17,6 +17,6 @@ class SASLChallenge extends Payload
$auth = $dom->createElementNS('urn:ietf:params:xml:ns:xmpp-sasl', 'response', $response);
$dom->appendChild($auth);
\Moxl\API::request($dom->saveXML($dom->documentElement));
\Moxl\API::sendDom($dom);
}
}
Loading…
Cancel
Save