diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 423f9859e..7c4e11c3f 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -8,6 +8,7 @@ use App\Models\User; use App\Models\UserEmailVerification; use App\Models\UserTransfer; use App\Models\WebSocket; +use App\Module\AgoraIO\AgoraTokenGenerator; use App\Module\Base; use Arr; use Cache; @@ -760,4 +761,43 @@ class UsersController extends AbstractController return Base::retError('not exist'); } } + + /** + * @api {get} api/users/agoraio/token 16. 【agoraio】获取 token + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup users + * @apiName agoraio__token + * + * @apiParam {Number} dialog_id 会话ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function agoraio__token() + { + $user = User::auth(); + // + $appid = '342c604542484b0d9659527f79aefcdb'; + $app_certificate = '920eb911c1f549948366e44d6dcabcbe'; + $channel = "DooTask:" . md5(env("APP_KEY")); + $uid = $user->userid; + try { + $service = new AgoraTokenGenerator($appid, $app_certificate, $channel, $uid); + } catch (\Exception $e) { + return Base::retError($e->getMessage()); + } + $token = $service->buildToken(); + if (empty($token)) { + return Base::retError('Generated token failed'); + } + return Base::retSuccess('success', [ + 'appid' => $appid, + 'channel' => $channel, + 'uid' => $uid, + 'token' => $token + ]); + } } diff --git a/app/Module/AgoraIO/AccessToken.php b/app/Module/AgoraIO/AccessToken.php new file mode 100644 index 000000000..9bae17741 --- /dev/null +++ b/app/Module/AgoraIO/AccessToken.php @@ -0,0 +1,193 @@ + 1, + "kPublishAudioStream" => 2, + "kPublishVideoStream" => 3, + "kPublishDataStream" => 4, + "kPublishAudioCdn" => 5, + "kPublishVideoCdn" => 6, + "kRequestPublishAudioStream" => 7, + "kRequestPublishVideoStream" => 8, + "kRequestPublishDataStream" => 9, + "kInvitePublishAudioStream" => 10, + "kInvitePublishVideoStream" => 11, + "kInvitePublishDataStream" => 12, + "kAdministrateChannel" => 101 + ); + + public $appID, $appCertificate, $channelName, $uid; + + public $message; + + /** + * AccessToken constructor. + * @throws \Exception + */ + public function __construct() + { + $this->message = new Message(); + } + + /** + * @param $uid + */ + public function setUid($uid) + { + if ($uid === 0) { + $this->uid = ""; + } else { + $this->uid = $uid . ''; + } + } + + /** + * @param $name + * @param $str + * @return bool + */ + public function is_nonempty_string($name, $str) + { + if (is_string($str) && $str !== "") { + return true; + } + echo $name . " check failed, should be a non-empty string"; + return false; + } + + /** + * @param $appID + * @param $appCertificate + * @param $channelName + * @param $uid + * @return AccessToken|null + * @throws \Exception + */ + public static function init($appID, $appCertificate, $channelName, $uid) + { + $accessToken = new AccessToken(); + if (!$accessToken->is_nonempty_string("appID", $appID) || + !$accessToken->is_nonempty_string("appCertificate", $appCertificate) || + !$accessToken->is_nonempty_string("channelName", $channelName)) { + return null; + } + $accessToken->appID = $appID; + $accessToken->appCertificate = $appCertificate; + $accessToken->channelName = $channelName; + $accessToken->setUid($uid); + $accessToken->message = new Message(); + return $accessToken; + } + + /** + * @param $token + * @param $appCertificate + * @param $channel + * @param $uid + * @return AccessToken|null + * @throws \Exception + */ + public static function initWithToken($token, $appCertificate, $channel, $uid) + { + $accessToken = new AccessToken(); + if (!$accessToken->extract($token, $appCertificate, $channel, $uid)) { + return null; + } + return $accessToken; + } + + /** + * @param $key + * @param $expireTimestamp + * @return $this + */ + public function addPrivilege($key, $expireTimestamp) + { + $this->message->privileges[$key] = $expireTimestamp; + return $this; + } + + /** + * @param $token + * @param $appCertificate + * @param $channelName + * @param $uid + * @return bool + * @throws \Exception + */ + protected function extract($token, $appCertificate, $channelName, $uid) + { + $ver_len = 3; + $appid_len = 32; + $version = substr($token, 0, $ver_len); + if ($version !== "006") { + echo 'invalid version ' . $version; + return false; + } + if (!$this->is_nonempty_string("token", $token) || + !$this->is_nonempty_string("appCertificate", $appCertificate) || + !$this->is_nonempty_string("channelName", $channelName)) { + return false; + } + $appid = substr($token, $ver_len, $appid_len); + $content = (base64_decode(substr($token, $ver_len + $appid_len, strlen($token) - ($ver_len + $appid_len)))); + $pos = 0; + $len = unpack("v", $content . substr($pos, 2))[1]; + $pos += 2; + $sig = substr($content, $pos, $len); + $pos += $len; + $crc_channel = unpack("V", substr($content, $pos, 4))[1]; + $pos += 4; + $crc_uid = unpack("V", substr($content, $pos, 4))[1]; + $pos += 4; + $msgLen = unpack("v", substr($content, $pos, 2))[1]; + $pos += 2; + $msg = substr($content, $pos, $msgLen); + $this->appID = $appid; + $message = new Message(); + $message->unpackContent($msg); + $this->message = $message; + //non reversable values + $this->appCertificate = $appCertificate; + $this->channelName = $channelName; + $this->setUid($uid); + return true; + } + + /** + * @return string + */ + public function build() + { + $msg = $this->message->packContent(); + $val = array_merge(unpack("C*", $this->appID), unpack("C*", $this->channelName), unpack("C*", $this->uid), $msg); + + $sig = hash_hmac('sha256', implode(array_map("chr", $val)), $this->appCertificate, true); + $crc_channel_name = crc32($this->channelName) & 0xffffffff; + $crc_uid = crc32($this->uid) & 0xffffffff; + $content = array_merge(unpack("C*", $this->packString($sig)), unpack("C*", pack("V", $crc_channel_name)), unpack("C*", pack("V", $crc_uid)), unpack("C*", pack("v", count($msg))), $msg); + $version = "006"; + $ret = $version . $this->appID . base64_encode(implode(array_map("chr", $content))); + return $ret; + } + + /** + * @param $value + * @return string + */ + public function packString($value) + { + return pack("v", strlen($value)) . $value; + } +} diff --git a/app/Module/AgoraIO/AgoraTokenGenerator.php b/app/Module/AgoraIO/AgoraTokenGenerator.php new file mode 100644 index 000000000..9d0a00b86 --- /dev/null +++ b/app/Module/AgoraIO/AgoraTokenGenerator.php @@ -0,0 +1,122 @@ + 0, + AccessToken::Privileges["kPublishAudioStream"] => 0, + AccessToken::Privileges["kPublishVideoStream"] => 0, + AccessToken::Privileges["kPublishDataStream"] => 0 + ); + + + const PublisherPrivileges = array( + AccessToken::Privileges["kJoinChannel"] => 0, + AccessToken::Privileges["kPublishAudioStream"] => 0, + AccessToken::Privileges["kPublishVideoStream"] => 0, + AccessToken::Privileges["kPublishDataStream"] => 0, + AccessToken::Privileges["kPublishAudioCdn"] => 0, + AccessToken::Privileges["kPublishVideoCdn"] => 0, + AccessToken::Privileges["kInvitePublishAudioStream"] => 0, + AccessToken::Privileges["kInvitePublishVideoStream"] => 0, + AccessToken::Privileges["kInvitePublishDataStream"] => 0 + ); + + const SubscriberPrivileges = array( + AccessToken::Privileges["kJoinChannel"] => 0, + AccessToken::Privileges["kRequestPublishAudioStream"] => 0, + AccessToken::Privileges["kRequestPublishVideoStream"] => 0, + AccessToken::Privileges["kRequestPublishDataStream"] => 0 + ); + + const AdminPrivileges = array( + AccessToken::Privileges["kJoinChannel"] => 0, + AccessToken::Privileges["kPublishAudioStream"] => 0, + AccessToken::Privileges["kPublishVideoStream"] => 0, + AccessToken::Privileges["kPublishDataStream"] => 0, + AccessToken::Privileges["kAdministrateChannel"] => 0 + ); + const Role = array( + "kRoleAttendee" => 0, // for communication + "kRolePublisher" => 1, // for live broadcast + "kRoleSubscriber" => 2, // for live broadcast + "kRoleAdmin" => 101 + ); + + const RolePrivileges = array( + self::Role["kRoleAttendee"] => self::AttendeePrivileges, + self::Role["kRolePublisher"] => self::PublisherPrivileges, + self::Role["kRoleSubscriber"] => self::SubscriberPrivileges, + self::Role["kRoleAdmin"] => self::AdminPrivileges + ); + + public $token; + + /** + * AgoraTokenGenerator constructor. + * @param $appID + * @param $appCertificate + * @param $channelName + * @param $uid + * @throws \Exception + */ + public function __construct($appID, $appCertificate, $channelName, $uid){ + $this->token = new AccessToken(); + $this->token->appID = $appID; + $this->token->appCertificate = $appCertificate; + $this->token->channelName = $channelName; + $this->token->setUid($uid); + } + + /** + * @param $token + * @param $appCertificate + * @param $channel + * @param $uid + * @throws \Exception + */ + public function initWithToken($token, $appCertificate, $channel, $uid){ + $this->token = AccessToken::initWithToken($token, $appCertificate, $channel, $uid); + } + + /** + * @param $role + */ + public function initPrivilege($role){ + $p = self::RolePrivileges[$role]; + foreach($p as $key => $value){ + $this->setPrivilege($key, $value); + } + } + + /** + * @param $privilege + * @param $expireTimestamp + */ + public function setPrivilege($privilege, $expireTimestamp){ + $this->token->addPrivilege($privilege, $expireTimestamp); + } + + /** + * @param $privilege + */ + public function removePrivilege($privilege){ + unset($this->token->message->privileges[$privilege]); + } + + /** + * @return string + */ + public function buildToken(){ + return $this->token->build(); + } +} diff --git a/app/Module/AgoraIO/Message.php b/app/Module/AgoraIO/Message.php new file mode 100644 index 000000000..6634a2ada --- /dev/null +++ b/app/Module/AgoraIO/Message.php @@ -0,0 +1,70 @@ +salt = rand(0, 100000); + $date = new \DateTime("now", new \DateTimeZone('UTC')); + $this->ts = $date->getTimestamp() + 168 * 3600; // 7天时间 + $this->privileges = array(); + } + + /** + * @return array + */ + public function packContent() + { + $buffer = unpack("C*", pack("V", $this->salt)); + $buffer = array_merge($buffer, unpack("C*", pack("V", $this->ts))); + $buffer = array_merge($buffer, unpack("C*", pack("v", sizeof($this->privileges)))); + foreach ($this->privileges as $key => $value) { + $buffer = array_merge($buffer, unpack("C*", pack("v", $key))); + $buffer = array_merge($buffer, unpack("C*", pack("V", $value))); + } + return $buffer; + } + + /** + * @param $msg + */ + public function unpackContent($msg) + { + $pos = 0; + $salt = unpack("V", substr($msg, $pos, 4))[1]; + $pos += 4; + $ts = unpack("V", substr($msg, $pos, 4))[1]; + $pos += 4; + $size = unpack("v", substr($msg, $pos, 2))[1]; + $pos += 2; + $privileges = array(); + for ($i = 0; $i < $size; $i++) { + $key = unpack("v", substr($msg, $pos, 2)); + $pos += 2; + $value = unpack("V", substr($msg, $pos, 4)); + $pos += 4; + $privileges[$key[1]] = $value[1]; + } + $this->salt = $salt; + $this->ts = $ts; + $this->privileges = $privileges; + } +} diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index f80d81979..573518811 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -350,6 +350,9 @@ + + + @@ -376,9 +379,11 @@ import {Store} from "le5le-store"; import MobileBack from "../components/Mobile/Back"; import TaskMenu from "./manage/components/TaskMenu"; import MobileNotification from "../components/Mobile/Notification"; +import MeetingManager from "./manage/components/MeetingManager"; export default { components: { + MeetingManager, MobileNotification, TaskMenu, MobileBack, diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index a653baf53..e1ed99a70 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -51,13 +51,17 @@ +
+ + {{$L('新会议')}} +
- - {{$L('图片')}} + + {{$L('发送图片')}}
- - {{$L('文件')}} + + {{$L('上传文件')}}
@@ -112,6 +116,7 @@ import ChatEmoji from "./emoji"; import touchmouse from "../../../../directives/touchmouse"; import TransferDom from "../../../../directives/transfer-dom"; import clickoutside from "../../../../directives/clickoutside"; +import {Store} from "le5le-store"; export default { name: 'ChatInput', @@ -781,6 +786,12 @@ export default { this.openMenu("#"); break; + case 'meeting': + Store.set('addMeeting', { + userids: [this.userId] + }); // todo 加入当前会话人员 + break; + case 'image': case 'file': this.$emit('on-more', action) diff --git a/resources/assets/js/pages/manage/components/MeetingManager.vue b/resources/assets/js/pages/manage/components/MeetingManager.vue new file mode 100644 index 000000000..721db9eb4 --- /dev/null +++ b/resources/assets/js/pages/manage/components/MeetingManager.vue @@ -0,0 +1,238 @@ + + + diff --git a/resources/assets/js/pages/manage/components/MeetingPlayer.vue b/resources/assets/js/pages/manage/components/MeetingPlayer.vue new file mode 100644 index 000000000..3fc8601bb --- /dev/null +++ b/resources/assets/js/pages/manage/components/MeetingPlayer.vue @@ -0,0 +1,46 @@ + + + diff --git a/resources/assets/sass/pages/components/_.scss b/resources/assets/sass/pages/components/_.scss index 2f16a813a..61a68d32f 100755 --- a/resources/assets/sass/pages/components/_.scss +++ b/resources/assets/sass/pages/components/_.scss @@ -2,6 +2,7 @@ @import "dialog-group-info"; @import "dialog-wrapper"; @import "file-content"; +@import "meeting-manager"; @import "project-archived"; @import "project-dialog"; @import "project-gantt"; diff --git a/resources/assets/sass/pages/components/meeting-manager.scss b/resources/assets/sass/pages/components/meeting-manager.scss new file mode 100644 index 000000000..616edc6b2 --- /dev/null +++ b/resources/assets/sass/pages/components/meeting-manager.scss @@ -0,0 +1,43 @@ +body { + .ivu-modal-wrap { + &.meeting-manager { + .ivu-modal { + .ivu-modal-content { + .ivu-modal-body { + padding: 16px 12px 0; + > ul { + display: grid; + justify-content: center; + grid-template-columns: repeat(auto-fill, 180px); + grid-gap: 16px; + > li { + list-style: none; + width: 180px; + height: 230px; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + .meeting-player { + flex: 1; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + .player { + flex: 1; + width: 100%; + background-color: #f1f1f1; + } + .common-avatar { + margin-top: 12px; + } + } + } + } + } + } + } + } + } +} diff --git a/resources/assets/statics/public/css/fonts/taskfont.ttf b/resources/assets/statics/public/css/fonts/taskfont.ttf index 4701aab06..8490faf02 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.ttf and b/resources/assets/statics/public/css/fonts/taskfont.ttf differ diff --git a/resources/assets/statics/public/css/fonts/taskfont.woff b/resources/assets/statics/public/css/fonts/taskfont.woff index 4829fa215..fbe5cc655 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.woff and b/resources/assets/statics/public/css/fonts/taskfont.woff differ diff --git a/resources/assets/statics/public/css/fonts/taskfont.woff2 b/resources/assets/statics/public/css/fonts/taskfont.woff2 index ed2d0c544..53ff52cac 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.woff2 and b/resources/assets/statics/public/css/fonts/taskfont.woff2 differ