support speaker request

pull/6/head
Namekuji 2022-12-09 22:16:43 -05:00
rodzic 544b0a54c1
commit ac16bc409a
9 zmienionych plików z 387 dodań i 69 usunięć

Wyświetl plik

@ -9,6 +9,7 @@ export const validators = {
}; };
export function webfinger(user) { export function webfinger(user) {
if (!user) return "";
const url = new URL(user.url); const url = new URL(user.url);
return `${user.acct}@${url.host}`; return `${user.acct}@${url.host}`;
} }

Wyświetl plik

@ -14,21 +14,26 @@ export default {
} }
}, },
computed: { computed: {
isHostOrCohost() { canSpeak() {
return this.type === "host" || this.type === "cohost"; return this.type === "host" || this.type === "cohost" || this.type === "speaker";
}, },
badgeProps() { badgeProps() {
switch (this.type) { switch (this.type) {
case "host": case "host":
return { return {
content: "Host", content: "Host",
colour: "primary", colour: "deep-orange",
}; };
case "cohost": case "cohost":
return { return {
content: "Cohost", content: "Cohost",
colour: "secondary", colour: "indigo",
}; };
case "speaker":
return {
content: "Speaker",
colour: ""
}
default: default:
return { return {
content: "", content: "",
@ -43,7 +48,7 @@ export default {
<template> <template>
<v-col sm="3" cols="4" class="text-center"> <v-col sm="3" cols="4" class="text-center">
<v-badge <v-badge
v-if="isHostOrCohost" v-if="canSpeak"
:content="badgeProps.content" :content="badgeProps.content"
location="top" location="top"
:color="badgeProps.colour" :color="badgeProps.colour"
@ -59,8 +64,8 @@ export default {
> >
<v-img :src="data?.avatar"></v-img> <v-img :src="data?.avatar"></v-img>
</v-avatar> </v-avatar>
<h4 :class="isHostOrCohost ? 'mt-1' : 'mt-2'"> <h4 :class="canSpeak ? 'mt-1' : 'mt-2'">
<v-icon v-if="isHostOrCohost" :icon="muted ? mdiMicrophoneOff : mdiMicrophone"></v-icon> <v-icon v-if="canSpeak" :icon="muted ? mdiMicrophoneOff : mdiMicrophone"></v-icon>
<a :href="data?.url" target="_blank">{{ data?.displayName }}</a> <a :href="data?.url" target="_blank">{{ data?.displayName }}</a>
</h4> </h4>
</v-col> </v-col>

Wyświetl plik

@ -239,6 +239,8 @@ export default {
v-model="searchError.enabled" v-model="searchError.enabled"
color="error" color="error"
:timeout="searchError.timeout" :timeout="searchError.timeout"
position="sticky"
location="top"
> >
{{ searchError.message }} {{ searchError.message }}
</v-snackbar> </v-snackbar>
@ -276,8 +278,8 @@ export default {
></v-text-field> ></v-text-field>
<v-card class="mt-3" variant="outlined"> <v-card class="mt-3" variant="outlined">
<v-card-title class="text-subtitle-1">共同ホスト</v-card-title> <v-card-title class="text-subtitle-1">共同ホスト</v-card-title>
<v-card-text v-if="cohosts.length > 0 || searchResult"> <v-card-text v-if="cohosts.length > 0 || searchResult" class="py-0">
<div v-if="cohosts.length > 0"> <template v-if="cohosts.length > 0">
<v-list lines="two" variant="tonal"> <v-list lines="two" variant="tonal">
<v-list-item <v-list-item
v-for="(cohost, index) in cohosts" v-for="(cohost, index) in cohosts"
@ -308,8 +310,8 @@ export default {
</template> </template>
</v-list-item> </v-list-item>
</v-list> </v-list>
</div> </template>
<div v-if="searchResult"> <template v-if="searchResult">
<v-divider></v-divider> <v-divider></v-divider>
<v-list lines="two"> <v-list lines="two">
<v-list-item <v-list-item
@ -335,7 +337,7 @@ export default {
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
</v-list> </v-list>
</div> </template>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-text-field <v-text-field

Wyświetl plik

@ -30,9 +30,6 @@ export default {
}; };
}, },
computed: { computed: {
lastPath() {
return this.$route.query.l ?? "";
},
serverErrors() { serverErrors() {
const errors = this.v$.server.$errors; const errors = this.v$.server.$errors;
const messages = map(errors, (e) => e.$message); const messages = map(errors, (e) => e.$message);
@ -50,7 +47,7 @@ export default {
} }
try { try {
const response = await axios.postForm("/app/login", { const response = await axios.postForm("/app/login", {
redir: this.lastPath, redir: this.$route.query.l ?? "/",
server: this.server, server: this.server,
}); });
if (response.status === 201) { if (response.status === 201) {

Wyświetl plik

@ -1,8 +1,8 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import { pushNotFound } from "../assets/utils"; import { pushNotFound, webfinger } from "../assets/utils";
import { useMastodonStore } from "../stores/mastodon"; import { useMastodonStore } from "../stores/mastodon";
import { map, some, omit } from "lodash-es"; import { map, some, omit, filter } from "lodash-es";
import Participant from "../components/Participant.vue"; import Participant from "../components/Participant.vue";
import { import {
mdiMicrophone, mdiMicrophone,
@ -11,14 +11,25 @@ import {
mdiMicrophoneQuestion, mdiMicrophoneQuestion,
mdiDoorClosed, mdiDoorClosed,
mdiVolumeOff, mdiVolumeOff,
mdiClose,
mdiCheck,
mdiAccountVoice,
} from "@mdi/js"; } from "@mdi/js";
import { Room, RoomEvent, Track, DisconnectReason } from "livekit-client"; import {
Room,
RoomEvent,
Track,
DisconnectReason,
DataPacket_Kind,
} from "livekit-client";
import { login } from "masto"; import { login } from "masto";
export default { export default {
setup() { setup() {
return { return {
donStore: useMastodonStore(), donStore: useMastodonStore(),
decoder: new TextDecoder(),
encoder: new TextEncoder(),
}; };
}, },
components: { components: {
@ -26,12 +37,15 @@ export default {
}, },
data() { data() {
return { return {
mdiAccountVoice,
mdiMicrophone, mdiMicrophone,
mdiMicrophoneOff, mdiMicrophoneOff,
mdiPhoneRemove, mdiPhoneRemove,
mdiMicrophoneQuestion, mdiMicrophoneQuestion,
mdiDoorClosed, mdiDoorClosed,
mdiVolumeOff, mdiVolumeOff,
mdiClose,
mdiCheck,
roomID: this.$route.params.id, roomID: this.$route.params.id,
loading: false, loading: false,
mainHeight: 600, mainHeight: 600,
@ -41,6 +55,7 @@ export default {
description: "", description: "",
host: null, host: null,
cohosts: [], cohosts: [],
speakers: [],
createdAt: null, createdAt: null,
}, },
participants: {}, participants: {},
@ -49,6 +64,10 @@ export default {
mutedSpeakerIDs: new Set(), mutedSpeakerIDs: new Set(),
micGranted: false, micGranted: false,
autoplayDisabled: false, autoplayDisabled: false,
speakRequests: new Set(),
showRequestNotification: false,
showRequestDialog: false,
showRequestedNotification: false,
}; };
}, },
created() { created() {
@ -70,7 +89,7 @@ export default {
iamMuted() { iamMuted() {
const myAudonID = this.donStore.oauth.audon_id; const myAudonID = this.donStore.oauth.audon_id;
return ( return (
(this.iamHost || this.iamCohost) && (this.iamHost || this.iamCohost || this.iamSpeaker) &&
this.micGranted && this.micGranted &&
this.mutedSpeakerIDs.has(myAudonID) this.mutedSpeakerIDs.has(myAudonID)
); );
@ -87,6 +106,12 @@ export default {
return this.isCohost({ remote_id: myInfo.id, remote_url: myInfo.url }); return this.isCohost({ remote_id: myInfo.id, remote_url: myInfo.url });
}, },
iamSpeaker() {
const myAudonID = this.donStore.oauth.audon_id;
if (!myAudonID) return false;
return this.isSpeaker(myAudonID);
},
micStatusIcon() { micStatusIcon() {
if (!this.micGranted) { if (!this.micGranted) {
return mdiMicrophoneQuestion; return mdiMicrophoneQuestion;
@ -98,6 +123,7 @@ export default {
}, },
}, },
methods: { methods: {
webfinger,
async joinRoom() { async joinRoom() {
if (!this.donStore.authorized) return; if (!this.donStore.authorized) return;
this.loading = true; this.loading = true;
@ -153,8 +179,6 @@ export default {
}) })
.on(RoomEvent.AudioPlaybackStatusChanged, () => { .on(RoomEvent.AudioPlaybackStatusChanged, () => {
if (!room.canPlaybackAudio) { if (!room.canPlaybackAudio) {
// FIXME: popup a dialog to ask user to allow audio playback
// alert("autoplay not permitted");
self.autoplayDisabled = true; self.autoplayDisabled = true;
} }
}) })
@ -177,6 +201,47 @@ export default {
alert(message); alert(message);
} }
self.$router.push({ name: "home" }); self.$router.push({ name: "home" });
})
.on(RoomEvent.DataReceived, (payload, participant, kind) => {
try {
/* data should be like
{ "kind": "speak_request" }
{ "kind": "chat", "data": "..." }
{ "kind": "request_declined", "audon_id": "..."}
*/
const strData = self.decoder.decode(payload);
const jsonData = JSON.parse(strData);
const metadata = JSON.parse(participant.metadata);
switch (jsonData?.kind) {
case "speak_request": // someone is wanting to be a speaker
self.onSpeakRequestReceived(participant);
break;
case "request_declined":
if (
self.isHost(participant.identity) ||
self.isCohost(metadata)
) {
self.speakRequests.delete(jsonData.audon_id);
}
break;
}
} catch (error) {
console.log(
"invalida data received from: ",
participant.identity
);
}
})
.on(RoomEvent.RoomMetadataChanged, (metadata) => {
self.roomInfo = JSON.parse(metadata);
for (const speakers of self.roomInfo.speakers) {
self.speakRequests.delete(speakers.audon_id);
}
if (self.iamSpeaker || !self.micGranted) {
self.roomClient.localParticipant.setMicrophoneEnabled(true).then((v) => {
self.micGranted = true;
})
}
}); });
await room.connect(resp.data.url, resp.data.token); await room.connect(resp.data.url, resp.data.token);
this.roomClient = room; this.roomClient = room;
@ -194,7 +259,7 @@ export default {
this.fetchMastoData(key, value); this.fetchMastoData(key, value);
} }
} }
if (this.iamHost || this.iamCohost) { if (this.iamHost || this.iamCohost || this.iamSpeaker) {
try { try {
await room.localParticipant.setMicrophoneEnabled(true); await room.localParticipant.setMicrophoneEnabled(true);
} catch { } catch {
@ -241,6 +306,59 @@ export default {
}) })
); );
}, },
isSpeaker(identity) {
return identity && some(this.roomInfo.speakers, { audon_id: identity });
},
isTalking(identity) {
return (
this.activeSpeakerIDs.has(identity) &&
!this.mutedSpeakerIDs.has(identity)
);
},
onSpeakRequestReceived(participant) {
if (this.iamHost || this.iamCohost) {
if (this.speakRequests.has(participant.identity)) return;
this.speakRequests.add(participant.identity);
this.showRequestNotification = true;
}
},
async onAcceptRequest(identity) {
// promote user to a speaker
// the livekit server will update room metadata
try {
await axios.put(`/api/room/${this.roomID}/${identity}`);
} catch (reqError) {
console.log("permission update request error: ", reqError);
}
},
async onDeclineRequest(identity) {
// share declined identity with host and other cohosts
if (!this.speakRequests.delete(identity)) return;
const data = { kind: "request_declined", audon_id: identity };
await this.publishDataToHostAndCohosts(data);
},
async requestSpeak() {
if (confirm("通話をリクエストしますか?")) {
await this.publishDataToHostAndCohosts({ kind: "speak_request" });
this.showRequestedNotification = true;
}
},
async publishDataToHostAndCohosts(data) {
const payload = this.encoder.encode(JSON.stringify(data));
// participants - speakers
const hostandcohosts = filter(
Array.from(this.roomClient.participants.values()),
(p) => {
const metadata = JSON.parse(p.metadata);
return this.isHost(p.identity) || this.isCohost(metadata);
}
);
await this.roomClient.localParticipant.publishData(
payload,
DataPacket_Kind.RELIABLE,
hostandcohosts
);
},
addParticipant(participant) { addParticipant(participant) {
const metadata = participant.metadata const metadata = participant.metadata
? JSON.parse(participant.metadata) ? JSON.parse(participant.metadata)
@ -276,7 +394,7 @@ export default {
const myTrack = this.roomClient.localParticipant.getTrack( const myTrack = this.roomClient.localParticipant.getTrack(
Track.Source.Microphone Track.Source.Microphone
); );
if (this.iamHost || this.iamCohost) { if (this.iamHost || this.iamCohost || this.iamSpeaker) {
try { try {
if (!this.micGranted) { if (!this.micGranted) {
await this.roomClient.localParticipant.setMicrophoneEnabled(true); await this.roomClient.localParticipant.setMicrophoneEnabled(true);
@ -289,7 +407,8 @@ export default {
alert("ブラウザが録音を許可していません"); alert("ブラウザが録音を許可していません");
} }
} else { } else {
alert("リクエストはアップデートで実装予定です!"); // alert("");
this.requestSpeak();
} }
}, },
async onRoomClose() { async onRoomClose() {
@ -329,6 +448,97 @@ export default {
</div> </div>
</v-alert> </v-alert>
</v-dialog> </v-dialog>
<v-dialog persistent v-model="showRequestDialog" max-width="500">
<v-card max-height="600" class="d-flex flex-column">
<v-card-title>通話リクエスト</v-card-title>
<v-card-text class="flex-grow-1 overflow-auto py-0">
<v-list v-if="speakRequests.size > 0" lines="two" variant="tonal">
<v-list-item
v-for="id of Array.from(speakRequests)"
:key="id"
:title="cachedMastoData[id]?.displayName"
class="my-1"
rounded
>
<template v-slot:prepend>
<v-avatar class="rounded">
<v-img :src="cachedMastoData[id]?.avatar"></v-img>
</v-avatar>
</template>
<template v-slot:append>
<v-btn
class="mr-2"
size="small"
variant="text"
:icon="mdiCheck"
@click="onAcceptRequest(id)"
></v-btn>
<v-btn
size="small"
variant="text"
:icon="mdiClose"
@click="onDeclineRequest(id)"
></v-btn>
</template>
<v-list-item-subtitle>
<a
:href="cachedMastoData[id]?.url"
class="text-body"
style="text-decoration: inherit; color: inherit"
target="_blank"
>{{ webfinger(cachedMastoData[id]) }}</a
>
</v-list-item-subtitle>
</v-list-item>
</v-list>
<p class="text-center py-3" v-else></p>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="justify-end">
<v-btn @click="showRequestDialog = false">閉じる</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
location="top"
:timeout="5000"
v-model="showRequestedNotification"
color="info"
>
<strong>通話リクエストを送信しました</strong>
<template v-slot:actions>
<v-btn
variant="text"
@click="showRequestedNotification = false"
:icon="mdiClose"
size="small"
></v-btn>
</template>
</v-snackbar>
<v-snackbar
location="top"
:timeout="-1"
v-model="showRequestNotification"
color="info"
>
<div
style="cursor: pointer"
@click="
showRequestDialog = true;
showRequestNotification = false;
"
>
<strong>新しい通話リクエストがあります</strong>
</div>
<template v-slot:actions>
<v-btn
variant="text"
@click="showRequestNotification = false"
:icon="mdiClose"
size="small"
></v-btn>
</template>
</v-snackbar>
<div class="d-none" ref="audioDOM"></div> <div class="d-none" ref="audioDOM"></div>
<main class="fill-height" v-resize="onResize"> <main class="fill-height" v-resize="onResize">
<v-card :height="mainHeight" :loading="loading" class="d-flex flex-column"> <v-card :height="mainHeight" :loading="loading" class="d-flex flex-column">
@ -361,26 +571,34 @@ export default {
<template v-for="(value, key) of participants" :key="key"> <template v-for="(value, key) of participants" :key="key">
<Participant <Participant
v-if="isHost(key)" v-if="isHost(key)"
:talking="activeSpeakerIDs.has(key)" :talking="isTalking(key)"
type="host" type="host"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)" :muted="mutedSpeakerIDs.has(key)"
></Participant> ></Participant>
<Participant <Participant
v-if="isCohost(value)" v-if="isCohost(value)"
:talking="activeSpeakerIDs.has(key)" :talking="isTalking(key)"
type="cohost" type="cohost"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)" :muted="mutedSpeakerIDs.has(key)"
></Participant> ></Participant>
<Participant
v-if="isSpeaker(key)"
:talking="isTalking(key)"
type="speaker"
:data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)"
>
</Participant>
</template> </template>
</v-row> </v-row>
<v-row> <v-row justify="start">
<template v-for="(value, key) of participants" :key="key"> <template v-for="(value, key) of participants" :key="key">
<Participant <Participant
v-if="!isHost(key) && !isCohost(value)" v-if="!isHost(key) && !isCohost(value) && !isSpeaker(key)"
:talking="activeSpeakerIDs.has(key)"
:data="cachedMastoData[key]" :data="cachedMastoData[key]"
type="listener"
></Participant> ></Participant>
</template> </template>
</v-row> </v-row>
@ -399,6 +617,23 @@ export default {
@click="roomClient.disconnect()" @click="roomClient.disconnect()"
variant="flat" variant="flat"
></v-btn> ></v-btn>
<v-badge
v-if="iamHost || iamCohost"
color="info"
:model-value="speakRequests.size > 0"
:content="speakRequests.size"
>
<v-btn
:icon="mdiAccountVoice"
variant="flat"
color="white"
@click="
showRequestDialog = true;
showRequestNotification = false;
"
>
</v-btn>
</v-badge>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</main> </main>

Wyświetl plik

@ -56,6 +56,9 @@ func loginHandler(c echo.Context) (err error) {
Scheme: "https", Scheme: "https",
Path: "/", Path: "/",
} }
if req.Redirect == "" {
req.Redirect = "/"
}
appConfig, err := getAppConfig(serverURL.String(), req.Redirect) appConfig, err := getAppConfig(serverURL.String(), req.Redirect)
if err != nil { if err != nil {
@ -103,15 +106,15 @@ func oauthHandler(c echo.Context) (err error) {
} }
return echo.NewHTTPError(http.StatusBadRequest, "auth_code_required") return echo.NewHTTPError(http.StatusBadRequest, "auth_code_required")
} }
if req.Redirect == "" { // if req.Redirect == "" {
req.Redirect = "/" // req.Redirect = "/"
} // }
data, err := getSessionData(c) data, err := getSessionData(c)
if err != nil { if err != nil {
return err return err
} }
appConf, err := getAppConfig(data.MastodonConfig.Server, "/") appConf, err := getAppConfig(data.MastodonConfig.Server, req.Redirect)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} }

90
room.go
Wyświetl plik

@ -129,7 +129,24 @@ func joinRoomHandler(c echo.Context) (err error) {
} }
} }
token, err := getRoomToken(room, user, room.IsHost(user) || room.IsCoHost(user)) // host and cohost can talk from the beginning canTalk := room.IsHost(user) || room.IsCoHost(user) // host and cohost can talk from the beginning
roomMetadata := &RoomMetadata{Room: room}
// Allows the user to talk if the user is a speaker
lkRoom, _ := getRoomInLivekit(c.Request().Context(), room.RoomID) // lkRoom will be nil if it doesn't exist
if lkRoom != nil {
if existingMetadata, _ := getRoomMetadataFromLivekitRoom(lkRoom); existingMetadata != nil {
roomMetadata = existingMetadata
for _, speaker := range existingMetadata.Speakers {
if speaker.AudonID == user.AudonID {
canTalk = true
break
}
}
}
}
token, err := getRoomToken(room, user, canTalk)
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError)
@ -142,15 +159,16 @@ func joinRoomHandler(c echo.Context) (err error) {
} }
// Create room in LiveKit if it doesn't exist // Create room in LiveKit if it doesn't exist
metadata, _ := json.Marshal(room) metadata, _ := json.Marshal(roomMetadata)
if lkRoom == nil {
_, err = lkRoomServiceClient.CreateRoom(c.Request().Context(), &livekit.CreateRoomRequest{ _, err = lkRoomServiceClient.CreateRoom(c.Request().Context(), &livekit.CreateRoomRequest{
Name: room.RoomID, Name: room.RoomID,
Metadata: string(metadata), Metadata: string(metadata),
}) })
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusConflict) return echo.NewHTTPError(http.StatusConflict)
}
} }
return c.JSON(http.StatusOK, resp) return c.JSON(http.StatusOK, resp)
@ -194,14 +212,13 @@ func closeRoomHandler(c echo.Context) error {
func updatePermissionHandler(c echo.Context) error { func updatePermissionHandler(c echo.Context) error {
roomID := c.Param("room") roomID := c.Param("room")
// look up room in livekit // look up lkRoom in livekit
room, exists := getRoomInLivekit(c.Request().Context(), roomID) lkRoom, exists := getRoomInLivekit(c.Request().Context(), roomID)
if !exists { if !exists {
return ErrRoomNotFound return ErrRoomNotFound
} }
audonRoom := new(Room) lkRoomMetadata, err := getRoomMetadataFromLivekitRoom(lkRoom)
err := json.Unmarshal([]byte(room.Metadata), audonRoom)
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError)
@ -209,25 +226,56 @@ func updatePermissionHandler(c echo.Context) error {
iam := c.Get("user").(*AudonUser) iam := c.Get("user").(*AudonUser)
if !(audonRoom.IsHost(iam) || audonRoom.IsCoHost(iam)) { if !(lkRoomMetadata.IsHost(iam) || lkRoomMetadata.IsCoHost(iam)) {
return ErrOperationNotPermitted return ErrOperationNotPermitted
} }
tgtAudonID := c.Param("user") tgtAudonID := c.Param("user")
if !lkRoomMetadata.IsUserInLivekitRoom(c.Request().Context(), tgtAudonID) {
if !audonRoom.IsUserInLivekitRoom(c.Request().Context(), tgtAudonID) {
return ErrUserNotFound return ErrUserNotFound
} }
tgtUser, err := findUserByID(c.Request().Context(), tgtAudonID)
if err != nil {
return ErrUserNotFound
}
if lkRoomMetadata.IsHost(tgtUser) || lkRoomMetadata.IsCoHost(tgtUser) {
return ErrOperationNotPermitted
}
permission := new(livekit.ParticipantPermission) newPermission := &livekit.ParticipantPermission{
if err := c.Bind(permission); err != nil { CanPublishData: true,
return ErrInvalidRequestFormat CanSubscribe: true,
}
// promote user to a speaker
if c.Request().Method == http.MethodPut {
newPermission.CanPublish = true
for _, speaker := range lkRoomMetadata.Speakers {
if speaker.Equal(tgtUser) {
return echo.NewHTTPError(http.StatusConflict, "already_speaking")
}
}
lkRoomMetadata.Speakers = append(lkRoomMetadata.Speakers, tgtUser)
}
newMetadata, err := json.Marshal(lkRoomMetadata)
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
_, err = lkRoomServiceClient.UpdateRoomMetadata(c.Request().Context(), &livekit.UpdateRoomMetadataRequest{
Room: roomID,
Metadata: string(newMetadata),
})
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
} }
info, err := lkRoomServiceClient.UpdateParticipant(c.Request().Context(), &livekit.UpdateParticipantRequest{ info, err := lkRoomServiceClient.UpdateParticipant(c.Request().Context(), &livekit.UpdateParticipantRequest{
Room: roomID, Room: roomID,
Identity: tgtAudonID, Identity: tgtAudonID,
Permission: permission, Permission: newPermission,
}) })
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)

Wyświetl plik

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"time" "time"
"github.com/livekit/protocol/livekit" "github.com/livekit/protocol/livekit"
@ -25,16 +26,21 @@ type (
CreatedAt time.Time `bson:"created_at" json:"created_at"` CreatedAt time.Time `bson:"created_at" json:"created_at"`
} }
RoomMetadata struct {
*Room
Speakers []*AudonUser `json:"speakers"`
}
Room struct { Room struct {
RoomID string `bson:"room_id" json:"room_id" validate:"required,printascii"` RoomID string `bson:"room_id" json:"room_id" validate:"required,printascii"`
Title string `bson:"title" json:"title" validate:"required,max=100,printascii|multibyte"` Title string `bson:"title" json:"title" validate:"required,max=100,printascii|multibyte"`
Description string `bson:"description" json:"description" validate:"max=500,ascii|multibyte"` Description string `bson:"description" json:"description" validate:"max=500,ascii|multibyte"`
Host *AudonUser `bson:"host" json:"host"` Host *AudonUser `bson:"host" json:"host"`
CoHosts []*AudonUser `bson:"cohost" json:"cohosts,omitempty"` CoHosts []*AudonUser `bson:"cohost" json:"cohosts"`
FollowingOnly bool `bson:"following_only" json:"following_only"` FollowingOnly bool `bson:"following_only" json:"following_only"`
FollowerOnly bool `bson:"follower_only" json:"follower_only"` FollowerOnly bool `bson:"follower_only" json:"follower_only"`
MutualOnly bool `bson:"mutual_only" json:"mutual_only"` MutualOnly bool `bson:"mutual_only" json:"mutual_only"`
Kicked []*AudonUser `bson:"kicked" json:"kicked,omitempty"` Kicked []*AudonUser `bson:"kicked" json:"kicked"`
ScheduledAt time.Time `bson:"scheduled_at" json:"scheduled_at"` ScheduledAt time.Time `bson:"scheduled_at" json:"scheduled_at"`
EndedAt time.Time `bson:"ended_at" json:"ended_at"` EndedAt time.Time `bson:"ended_at" json:"ended_at"`
CreatedAt time.Time `bson:"created_at" json:"created_at"` CreatedAt time.Time `bson:"created_at" json:"created_at"`
@ -78,7 +84,25 @@ func (r *Room) IsHost(u *AudonUser) bool {
return r != nil && r.Host.Equal(u) return r != nil && r.Host.Equal(u)
} }
func getRoomMetadataFromLivekitRoom(lkRoom *livekit.Room) (*RoomMetadata, error) {
metadata := new(RoomMetadata)
if err := json.Unmarshal([]byte(lkRoom.GetMetadata()), metadata); err != nil {
return nil, err
}
return metadata, nil
}
func (r *Room) ExistsInLivekit(ctx context.Context) bool {
lkRooms, _ := lkRoomServiceClient.ListRooms(ctx, &livekit.ListRoomsRequest{Names: []string{r.RoomID}})
return len(lkRooms.GetRooms()) > 0
}
func (r *Room) IsUserInLivekitRoom(ctx context.Context, userID string) bool { func (r *Room) IsUserInLivekitRoom(ctx context.Context, userID string) bool {
if r == nil {
return false
}
participantsInfo, _ := lkRoomServiceClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{Room: r.RoomID}) participantsInfo, _ := lkRoomServiceClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{Room: r.RoomID})
participants := participantsInfo.GetParticipants() participants := participantsInfo.GetParticipants()

Wyświetl plik

@ -132,12 +132,12 @@ func main() {
api.POST("/room", createRoomHandler) api.POST("/room", createRoomHandler)
api.GET("/room/:id", joinRoomHandler) api.GET("/room/:id", joinRoomHandler)
api.DELETE("/room/:id", closeRoomHandler) api.DELETE("/room/:id", closeRoomHandler)
api.PATCH("/room/:room/:user", updatePermissionHandler) api.PUT("/room/:room/:user", updatePermissionHandler)
e.Static("/assets", "audon-fe/dist/assets") e.Static("/assets", "audon-fe/dist/assets")
e.File("/*", "audon-fe/dist/index.html") e.File("/*", "audon-fe/dist/index.html")
e.Logger.Debug(e.Start(":1323")) e.Logger.Debug(e.Start(":8100"))
} }
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
@ -152,16 +152,19 @@ func (cv *CustomValidator) Validate(i interface{}) error {
} }
func getAppConfig(server string, redirPath string) (*mastodon.AppConfig, error) { func getAppConfig(server string, redirPath string) (*mastodon.AppConfig, error) {
if mastAppConfigBase != nil { // if mastAppConfigBase != nil {
return &mastodon.AppConfig{ // return &mastodon.AppConfig{
Server: server, // Server: server,
ClientName: mastAppConfigBase.ClientName, // ClientName: mastAppConfigBase.ClientName,
Scopes: mastAppConfigBase.Scopes, // Scopes: mastAppConfigBase.Scopes,
Website: mastAppConfigBase.Website, // Website: mastAppConfigBase.Website,
RedirectURIs: mastAppConfigBase.RedirectURIs, // RedirectURIs: mastAppConfigBase.RedirectURIs,
}, nil // }, nil
} // }
if redirPath == "" {
redirPath = "/"
}
redirectURI := "urn:ietf:wg:oauth:2.0:oob" redirectURI := "urn:ietf:wg:oauth:2.0:oob"
u := &url.URL{ u := &url.URL{
Host: mainConfig.LocalDomain, Host: mainConfig.LocalDomain,
@ -181,7 +184,7 @@ func getAppConfig(server string, redirPath string) (*mastodon.AppConfig, error)
RedirectURIs: redirectURI, RedirectURIs: redirectURI,
} }
mastAppConfigBase = conf // mastAppConfigBase = conf
return &mastodon.AppConfig{ return &mastodon.AppConfig{
Server: server, Server: server,