add role update

peertube
Namekuji 2023-01-28 22:16:21 -05:00
rodzic 55ca35c3c8
commit cff4f9eaab
9 zmienionych plików z 244 dodań i 79 usunięć

Wyświetl plik

@ -1,12 +1,12 @@
{
"name": "audon-fe",
"version": "0.2.3",
"version": "0.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audon-fe",
"version": "0.2.3",
"version": "0.2.4",
"dependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@picmo/popup-picker": "^5.7.2",

Wyświetl plik

@ -18,6 +18,7 @@ export default {
muted: Boolean,
emoji: String,
preview: Boolean,
enableMenu: Boolean,
},
computed: {
showEmoji() {
@ -34,17 +35,17 @@ export default {
switch (this.type) {
case "host":
return {
content: "Host",
content: this.$t("role.host"),
colour: "deep-orange",
};
case "cohost":
return {
content: "Cohost",
content: this.$t("role.cohost"),
colour: "indigo",
};
case "speaker":
return {
content: "Speaker",
content: this.$t("role.speaker"),
colour: "",
};
default:
@ -78,7 +79,11 @@ export default {
>
<span>{{ emoji }}</span>
</v-overlay>
<v-img :src="data?.avatar"></v-img>
<v-img
:class="{ cursorPointer: enableMenu }"
:id="`mod-${data?.identity}`"
:src="data?.avatar"
></v-img>
</v-avatar>
</v-badge>
<v-avatar
@ -97,8 +102,34 @@ export default {
>
<span>{{ emoji }}</span>
</v-overlay>
<v-img :src="data?.avatar"></v-img>
<v-img
:class="{ cursorPointer: enableMenu }"
:id="`mod-${data?.identity}`"
:src="data?.avatar"
></v-img>
</v-avatar>
<v-menu v-if="enableMenu" :activator="`#mod-${data?.identity}`">
<v-list>
<v-list-item
:title="$t('moderation.promote', { role: $t('role.cohost') })"
@click="$emit('moderate', this.data?.identity, 'cohost')"
></v-list-item>
<v-list-item
v-if="type !== 'speaker'"
:title="$t('moderation.promote', { role: $t('role.speaker') })"
@click="$emit('moderate', this.data?.identity, 'speaker')"
></v-list-item>
<v-list-item
v-else
:title="$t('moderation.demote')"
@click="$emit('moderate', this.data?.identity, 'demote')"
></v-list-item>
<v-list-item
:title="$t('moderation.kick')"
@click="$emit('moderate', this.data?.identity, 'kick')"
></v-list-item>
</v-list>
</v-menu>
<h4 :class="canSpeak ? 'mt-1' : 'mt-2'">
<v-icon
v-if="canSpeak && !preview"
@ -121,4 +152,8 @@ export default {
color: white;
text-align: center;
}
.cursorPointer {
cursor: pointer;
}
</style>

Wyświetl plik

@ -25,6 +25,7 @@ editRoom: "Room Edit"
comingFuture: "Coming with future update!"
processing: "Processing now...<br />Keep this window open!"
lostWarning: "Unsaved data will be lost if you leave the page, are you sure?"
cannotUndone: "This process cannot be undone. Are you sure?"
staticLink:
title: "Your Audon Link"
hint: "Other participants can join to your room via this personal link while you're hosting."
@ -33,8 +34,8 @@ form:
titleRequired: "Room title required"
description: "Description"
restriction: "Who can join"
cohosts: "Cohosts"
cohostCanAlwaysJoin: "Cohosts can join regardless of this setting."
cohosts: "CoHosts"
cohostCanAlwaysJoin: "CoHosts can join regardless of this setting."
schedule: "Schedule at"
advertise: "Allow the bot ({bot}) to advertise your room"
relationships:
@ -43,7 +44,7 @@ form:
follower: "Followers-only"
knowing: "Followed accounts and/or followers"
mutual: "Your mutuals"
private: "Cohosts only"
private: "CoHosts only"
shareRoomMessage: "Join my Audon room!\n{link}\n\nTitle: {title}"
roomReady:
header: "Your room is ready!"
@ -82,6 +83,15 @@ speakRequest:
microphoneBlocked: "Your browser has blocked access to the microphone. Check permission settings of your device and browser."
closeRoomConfirm: "Are you sure you want to close this room?"
roomEvent:
closedByHost: "Host has closed this room."
closedByHost: "This room has been closed."
removed: "You have been requested to leave."
disconnected: "Disconnected from this room."
moderation:
promote: "Promote to {role}"
demote: "Demote to listener"
kick: "Kick out"
role:
host: "Host"
cohost: "CoHost"
speaker: "Speaker"
listener: "Listener"

Wyświetl plik

@ -25,6 +25,7 @@ editRoom: "部屋の編集"
comingFuture: "今後のアップデートで追加予定"
processing: "処理中です。<br />画面を閉じないでください。"
lostWarning: "この画面を閉じると保存前の内容が失われます。構いませんか?"
cannotUndone: "この操作は取り消せません。続行しますか?"
staticLink:
title: "Audon リンク"
hint: "あなたが部屋をホストしたとき、他の人はこの固定 URL からでも参加できます。"
@ -82,6 +83,15 @@ speakRequest:
microphoneBlocked: "マイクが禁止されています。ブラウザやデバイスの設定からマイクの使用を許可してください。"
closeRoomConfirm: "この部屋を閉じますか?"
roomEvent:
closedByHost: "ホストにより部屋が閉じられました。"
closedByHost: "部屋が閉じられました。"
removed: "リクエストにより部屋から退去しました。"
disconneced: "切断されました。"
moderation:
promote: "{role} にする"
demote: "リスナー に戻す"
kick: "追い出す"
role:
host: "ホスト"
cohost: "共同ホスト"
speaker: "スピーカー"
listener: "リスナー"

Wyświetl plik

@ -2,7 +2,7 @@
import axios from "axios";
import { pushNotFound, webfinger } from "../assets/utils";
import { useMastodonStore } from "../stores/mastodon";
import { map, some, omit, filter, trim, clone } from "lodash-es";
import { map, some, omit, filter, trim, clone, differenceBy } from "lodash-es";
import { darkTheme } from "picmo";
import { createPopup } from "@picmo/popup-picker";
import { Howl } from "howler";
@ -126,7 +126,6 @@ export default {
publish: {
audioBitrate: AudioPresets.music,
forceStereo: false,
source: Track.Source.Microphone,
},
},
roomInfo: {
@ -163,6 +162,7 @@ export default {
showRequestDialog: false,
showRequestedNotification: false,
isEditLoading: false,
isRequestLoading: false,
closeLoading: false,
showEditDialog: false,
timeElapsed: "",
@ -245,7 +245,10 @@ export default {
return this.isSpeaker(myAudonID);
},
micStatusIcon() {
if (!this.micGranted) {
if (
!this.micGranted ||
!(this.iamHost || this.iamCohost || this.iamSpeaker)
) {
return mdiMicrophoneQuestion;
}
if (this.iamMuted) {
@ -385,15 +388,39 @@ export default {
}
})
.on(RoomEvent.RoomMetadataChanged, (metadata) => {
self.roomInfo = JSON.parse(metadata);
const newRoominfo = JSON.parse(metadata);
const myAudonID = self.donStore.oauth.audon.audon_id;
const iamNewCohost = some(
differenceBy(
newRoominfo.cohosts,
self.roomInfo.cohosts,
"audon_id"
),
(v) => v.audon_id === myAudonID
);
if (iamNewCohost) {
self.closeLoading = true;
self.roomClient.disconnect();
self.donStore.revertAvatar().finally(() => {
window.location.reload();
});
}
const iamNewSpeaker = some(
differenceBy(
newRoominfo.speakers,
self.roomInfo.speakers,
"audon_id"
),
(v) => v.audon_id === myAudonID
);
self.roomInfo = newRoominfo;
self.editingRoomInfo = clone(self.roomInfo);
if (!self.roomInfo.speakers) return;
for (const speakers of self.roomInfo.speakers) {
self.speakRequests.delete(speakers.audon_id);
if (self.speakRequests.size < 1)
self.showRequestNotification = false;
}
if (self.iamSpeaker && !self.micGranted) {
if (self.iamSpeaker && iamNewSpeaker) {
self.roomClient.localParticipant
.setMicrophoneEnabled(
true,
@ -405,6 +432,7 @@ export default {
})
.finally(() => {
self.roomClient.localParticipant.setMicrophoneEnabled(false);
self.mutedSpeakerIDs.add(myAudonID);
});
}
});
@ -480,13 +508,19 @@ export default {
this.sounds.request.play();
}
},
async onAcceptRequest(identity) {
// promote user to a speaker
// the livekit server will update room metadata
async onModerate(identity, op) {
if (!identity) return;
if (op === "kick" || op === "cohost") {
if (!confirm(this.$t("cannotUndone"))) {
return;
}
}
this.isRequestLoading = true;
try {
await axios.put(`/api/room/${this.roomID}/${identity}`);
} catch (reqError) {
console.log("permission update request error: ", reqError);
await axios.put(`/api/room/${this.roomID}`, { identity, op });
} finally {
this.isRequestLoading = false;
this.speakRequests.delete(identity);
}
},
async onDeclineRequest(identity) {
@ -585,6 +619,7 @@ export default {
displayName: account.displayName,
avatar: account.avatar,
url: account.url,
identity,
};
if (resp.data.avatar) {
info.avatar = `/storage/${resp.data.audon_id}/avatar/${resp.data.avatar}`;
@ -736,7 +771,7 @@ export default {
@connect.once="joinRoom"
></JoinDialog>
<v-dialog v-model="showRequestDialog" max-width="500">
<v-card class="d-flex flex-column">
<v-card :loading="isRequestLoading" class="d-flex flex-column">
<v-card-title>{{ $t("speakRequest.label") }}</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">
@ -758,12 +793,14 @@ export default {
size="small"
variant="text"
:icon="mdiCheck"
@click="onAcceptRequest(id)"
:disabled="isRequestLoading"
@click.once="onModerate(id, 'speaker')"
></v-btn>
<v-btn
size="small"
variant="text"
:icon="mdiClose"
:disabled="isRequestLoading"
@click="onDeclineRequest(id)"
></v-btn>
</template>
@ -884,6 +921,8 @@ export default {
:data="cachedMastoData[key]"
:muted="mutedSpeakerIDs.has(key)"
:emoji="emojiReactions[key]?.emoji"
@moderate="onModerate"
:enable-menu="iamHost || iamCohost"
>
</Participant>
</template>
@ -895,6 +934,8 @@ export default {
:data="cachedMastoData[key]"
type="listener"
:emoji="emojiReactions[key]?.emoji"
@moderate="onModerate"
:enable-menu="iamHost || iamCohost"
></Participant>
</template>
</v-row>

137
room.go
Wyświetl plik

@ -34,7 +34,7 @@ func createRoomHandler(c echo.Context) error {
host := c.Get("user").(*AudonUser)
room.Host = host
// check if user is already hosting
// check if user is already hosting or cohosting
lkRooms, err := host.GetCurrentLivekitRooms(c.Request().Context())
if err != nil {
c.Logger().Error(err)
@ -46,7 +46,7 @@ func createRoomHandler(c echo.Context) error {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
if meta.Host.Equal(host) {
if meta.IsHost(host) || meta.IsCoHost(host) {
return ErrOperationNotPermitted
}
}
@ -81,7 +81,7 @@ func createRoomHandler(c echo.Context) error {
}
// Create livekit room
roomMetadata := &RoomMetadata{Room: room, MastodonAccounts: make(map[string]*MastodonAccount)}
roomMetadata := &RoomMetadata{Room: room, Speakers: []*AudonUser{}, Kicked: []*AudonUser{}, MastodonAccounts: make(map[string]*MastodonAccount)}
metadata, _ := json.Marshal(roomMetadata)
_, err = lkRoomServiceClient.CreateRoom(c.Request().Context(), &livekit.CreateRoomRequest{
Name: room.RoomID,
@ -257,13 +257,14 @@ func joinRoomHandler(c echo.Context) (err error) {
return ErrAlreadyEnded
}
// return 403 if one has been kicked
for _, kicked := range room.Kicked {
if kicked.Equal(user) {
return echo.NewHTTPError(http.StatusForbidden)
}
lkRoom, _ := getRoomInLivekit(c.Request().Context(), room.RoomID) // lkRoom will be nil if it doesn't exist
if lkRoom == nil {
return ErrRoomNotFound
}
roomMetadata, _ := getRoomMetadataFromLivekitRoom(lkRoom)
room = roomMetadata.Room
canTalk := room.IsHost(user) || room.IsCoHost(user) // host and cohost can talk from the beginning
// check room restriction
@ -301,19 +302,18 @@ func joinRoomHandler(c echo.Context) (err error) {
}
}
roomMetadata := &RoomMetadata{Room: room, MastodonAccounts: make(map[string]*MastodonAccount)}
// return 403 if one has been kicked
for _, kicked := range roomMetadata.Kicked {
if kicked.Equal(user) {
return echo.NewHTTPError(http.StatusForbidden)
}
}
// 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
}
}
for _, speaker := range roomMetadata.Speakers {
if speaker.AudonID == user.AudonID {
canTalk = true
break
}
}
@ -401,13 +401,8 @@ func joinRoomHandler(c echo.Context) (err error) {
}
// Update room metadata
currentMeta, err := getRoomMetadataFromLivekitRoom(lkRoom)
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
currentMeta.MastodonAccounts[user.AudonID] = mastoAccount
newMetadata, err := json.Marshal(currentMeta)
roomMetadata.MastodonAccounts[user.AudonID] = mastoAccount
newMetadata, err := json.Marshal(roomMetadata)
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
@ -429,7 +424,7 @@ func joinRoomHandler(c echo.Context) (err error) {
return c.JSON(http.StatusOK, resp)
}
// intended to be called by room's host
// intended to be called by room's host or cohost
func closeRoomHandler(c echo.Context) error {
roomID := c.Param("id")
if err := mainValidator.Var(&roomID, "required,printascii"); err != nil {
@ -444,15 +439,19 @@ func closeRoomHandler(c echo.Context) error {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
// return 410 if the room has already ended
if !room.EndedAt.IsZero() {
return ErrAlreadyEnded
}
meta := room.getRoomMetadata(c.Request().Context())
if meta == nil {
return ErrRoomNotFound
}
// only host or cohost can close the room
user := c.Get("user").(*AudonUser)
if !room.IsHost(user) && !room.IsCoHost(user) {
if !meta.IsHost(user) && !meta.IsCoHost(user) {
return ErrOperationNotPermitted
}
@ -481,8 +480,8 @@ func leaveRoomHandler(c echo.Context) error {
return c.NoContent(http.StatusOK)
}
func updatePermissionHandler(c echo.Context) error {
roomID := c.Param("room")
func updateRoleHandler(c echo.Context) error {
roomID := c.Param("id")
// look up lkRoom in livekit
lkRoom, exists := getRoomInLivekit(c.Request().Context(), roomID)
@ -502,7 +501,12 @@ func updatePermissionHandler(c echo.Context) error {
return ErrOperationNotPermitted
}
audonID := c.Param("user")
params := make(map[string]string)
if err := c.Bind(&params); err != nil {
return ErrInvalidRequestFormat
}
audonID := params["identity"]
operation := params["op"]
if !lkRoomMetadata.IsUserInLivekitRoom(c.Request().Context(), audonID) {
return ErrUserNotFound
}
@ -517,17 +521,48 @@ func updatePermissionHandler(c echo.Context) error {
newPermission := &livekit.ParticipantPermission{
CanPublishData: true,
CanSubscribe: true,
CanPublish: true,
}
// promote user to a speaker
if c.Request().Method == http.MethodPut {
newPermission.CanPublish = true
if operation == "speaker" {
for _, speaker := range lkRoomMetadata.Speakers {
if speaker.Equal(tgtUser) {
return echo.NewHTTPError(http.StatusConflict, "already_speaking")
}
}
lkRoomMetadata.Speakers = append(lkRoomMetadata.Speakers, tgtUser)
} else if operation == "cohost" {
lkRoomMetadata.CoHosts = append(lkRoomMetadata.CoHosts, tgtUser)
coll := mainDB.Collection(COLLECTION_ROOM)
if _, err = coll.UpdateOne(c.Request().Context(),
bson.D{{Key: "room_id", Value: roomID}},
bson.D{{Key: "$set", Value: bson.D{{
Key: "cohosts",
Value: lkRoomMetadata.CoHosts,
}}}}); err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
} else if operation == "kick" {
lkRoomMetadata.Kicked = append(lkRoomMetadata.Kicked, tgtUser)
lkRoomServiceClient.RemoveParticipant(c.Request().Context(), &livekit.RoomParticipantIdentity{
Room: roomID,
Identity: tgtUser.AudonID,
})
} else if operation == "demote" {
newPermission.CanPublish = false
} else {
return ErrInvalidRequestFormat
}
if operation == "demote" || operation == "cohost" {
newSpeakers := make([]*AudonUser, 0, len(lkRoomMetadata.Speakers))
for _, v := range lkRoomMetadata.Speakers {
if v.AudonID != tgtUser.AudonID {
newSpeakers = append(newSpeakers, v)
}
}
lkRoomMetadata.Speakers = newSpeakers
}
newMetadata, err := json.Marshal(lkRoomMetadata)
@ -535,26 +570,28 @@ func updatePermissionHandler(c echo.Context) error {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
_, err = lkRoomServiceClient.UpdateRoomMetadata(c.Request().Context(), &livekit.UpdateRoomMetadataRequest{
Room: roomID,
Metadata: string(newMetadata),
})
if operation != "kick" {
_, err = lkRoomServiceClient.UpdateParticipant(c.Request().Context(), &livekit.UpdateParticipantRequest{
Room: roomID,
Identity: audonID,
Permission: newPermission,
})
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
}
metadataRequest := &livekit.UpdateRoomMetadataRequest{Room: roomID, Metadata: string(newMetadata)}
_, err = lkRoomServiceClient.UpdateRoomMetadata(context.Background(), metadataRequest)
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
info, err := lkRoomServiceClient.UpdateParticipant(c.Request().Context(), &livekit.UpdateParticipantRequest{
Room: roomID,
Identity: audonID,
Permission: newPermission,
})
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, info)
return c.NoContent(http.StatusOK)
}
func getRoomToken(room *Room, user *AudonUser, canTalk bool) (string, error) {

Wyświetl plik

@ -31,6 +31,7 @@ type (
RoomMetadata struct {
*Room
Speakers []*AudonUser `json:"speakers"`
Kicked []*AudonUser `json:"kicked"`
MastodonAccounts map[string]*MastodonAccount `json:"accounts"`
}
@ -41,7 +42,6 @@ type (
Host *AudonUser `bson:"host" json:"host"`
CoHosts []*AudonUser `bson:"cohosts" json:"cohosts"`
Restriction JoinRestriction `bson:"restriction" json:"restriction"`
Kicked []*AudonUser `bson:"kicked" json:"kicked"`
EndedAt time.Time `bson:"ended_at" json:"ended_at"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
Advertise string `bson:"advertise" json:"advertise"`
@ -90,6 +90,11 @@ func (a *AudonUser) GetCurrentLivekitRooms(ctx context.Context) ([]*livekit.Room
break
}
}
// check host
room, _ := getRoomMetadataFromLivekitRoom(r)
if room.IsHost(a) {
current = append(current, r)
}
}
return current, nil
}
@ -150,6 +155,18 @@ func getRoomMetadataFromLivekitRoom(lkRoom *livekit.Room) (*RoomMetadata, error)
return metadata, nil
}
func (r *Room) getRoomMetadata(ctx context.Context) *RoomMetadata {
lkRoom, _ := getRoomInLivekit(ctx, r.RoomID)
if lkRoom == nil {
return nil
}
meta, err := getRoomMetadataFromLivekitRoom(lkRoom)
if err != nil {
return nil
}
return meta
}
func (r *Room) ExistsInLivekit(ctx context.Context) bool {
lkRooms, _ := lkRoomServiceClient.ListRooms(ctx, &livekit.ListRoomsRequest{Names: []string{r.RoomID}})

Wyświetl plik

@ -179,7 +179,7 @@ func main() {
api.POST("/room/:id", joinRoomHandler)
api.PATCH("/room/:id", updateRoomHandler)
api.DELETE("/room/:id", closeRoomHandler)
api.PUT("/room/:room/:user", updatePermissionHandler)
api.PUT("/room/:id", updateRoleHandler)
e.Static("/assets", "audon-fe/dist/assets")
e.Static("/static", "audon-fe/dist/static")

17
user.go
Wyświetl plik

@ -82,9 +82,24 @@ func redirectUserHandler(c echo.Context) error {
opts := options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}})
var room Room
if err := coll.FindOne(c.Request().Context(), bson.D{{Key: "host.audon_id", Value: user.AudonID}}, opts).Decode(&room); err == nil {
if err := coll.FindOne(c.Request().Context(), bson.D{
{Key: "host.audon_id", Value: user.AudonID},
}, opts).Decode(&room); err == nil {
if room.ExistsInLivekit(c.Request().Context()) {
// redirect to the hosting room if online
return c.Redirect(http.StatusFound, fmt.Sprintf("/r/%s", room.RoomID))
} else {
// redirect to the first cohosting room if online
status, err := user.GetCurrentRoomStatus(c.Request().Context())
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
for _, v := range status {
if v.Role == "cohost" {
return c.Redirect(http.StatusFound, fmt.Sprintf("/r/%s", v.RoomID))
}
}
}
}