fix annoying avatar change

peertube
Namekuji 2023-01-25 01:37:31 -05:00
rodzic 44e0b2b3d7
commit cc4337ebcb
19 zmienionych plików z 554 dodań i 316 usunięć

Wyświetl plik

@ -20,7 +20,7 @@ webhook:
room: room:
auto_create: false auto_create: false
empty_timeout: 30 empty_timeout: 3600
max_participants: 0 max_participants: 0
max_metadata_size: 0 max_metadata_size: 0

Wyświetl plik

@ -0,0 +1,173 @@
<script>
import { Room } from "livekit-client";
import { useMastodonStore } from "../stores/mastodon";
import { mdiArrowRightBold } from "@mdi/js";
import { pushNotFound } from "../assets/utils";
import axios from "axios";
export default {
setup() {
return {
mdiArrowRightBold,
donStore: useMastodonStore(),
};
},
data() {
return {
roomID: this.$route.params.id,
isLoading: true,
roomToken: null,
dialogEnabled: true,
uploading: false,
};
},
emits: ["connect"],
props: {
roomId: String,
roomClient: Room,
},
computed: {
uploadEnabled() {
return this.roomToken?.original && this.roomToken?.indicator;
},
},
async mounted() {
try {
await this.donStore.fetchToken();
const resp = await axios.post(
`/api/room/${this.roomID}`,
this.donStore.userinfo
);
this.roomToken = resp.data;
} catch (error) {
this.dialogEnabled = false;
if (error.response?.status === 401) {
return;
}
let message = "";
switch (error.response?.status) {
case 403:
switch (error.response?.data) {
case "following":
message = this.$t("errors.restriction.following");
break;
case "follower":
message = this.$t("errors.restriction.follower");
break;
case "knowing":
message = this.$t("errors.restriction.knowing");
break;
case "mutual":
message = this.$t("errors.restriction.mutual");
break;
case "private":
message = this.$t("errors.restriction.private");
break;
default:
message = this.$t("errors.restriction.default");
}
alert(message);
break;
case 404:
pushNotFound(this.$route);
break;
case 406:
alert(this.$t("errors.alreadyConnected"));
break;
case 410:
alert(this.$t("errors.alreadyClosed"));
break;
default:
alert(error);
}
this.$router.push({ name: "home" });
} finally {
this.isLoading = false;
}
},
methods: {
async joining(indicator) {
try {
await this.roomClient.startAudio();
if (indicator && this.uploadEnabled) {
this.uploading = true;
this.donStore.avatar = this.roomToken.original;
try {
await this.donStore.updateAvatar(this.roomToken.indicator);
} finally {
this.uploading = false;
this.dialogEnabled = false;
this.$emit("connect", this.roomToken);
}
} else {
this.dialogEnabled = false;
this.$emit("connect", this.roomToken);
}
} catch {
alert(this.$t("errors.connectionFailed"));
}
},
},
};
</script>
<template>
<v-overlay
:model-value="isLoading || uploading"
persistent
class="align-center justify-center"
>
<v-progress-circular indeterminate size="40"></v-progress-circular>
</v-overlay>
<v-dialog
v-if="!isLoading"
v-model="dialogEnabled"
max-width="500"
persistent
>
<v-alert v-if="uploadEnabled" color="deep-purple-darken-2">
<div>
{{ $t("onlineIndicator.message") }}
</div>
<div class="mt-3 text-center">
<v-avatar class="rounded" size="80">
<v-img :src="roomToken.indicator"></v-img>
</v-avatar>
</div>
<v-alert
class="mt-3"
density="compact"
type="success"
color="white"
variant="tonal"
>
{{ $t("onlineIndicator.hint") }}
</v-alert>
<v-alert
class="mt-3"
border="start"
density="compact"
type="warning"
variant="outlined"
>
{{ $t("onlineIndicator.warning") }}
</v-alert>
<div class="mt-3 mb-1 d-flex align-center justify-space-around">
<v-btn @click="joining(false)">{{ $t("onlineIndicator.nope") }}</v-btn>
<v-btn color="indigo" @click="joining(true)">{{
$t("onlineIndicator.sure")
}}</v-btn>
</div>
</v-alert>
<v-alert v-else color="indigo">
<div class="mb-5">
{{ $t("browserMuted") }}
</div>
<div class="text-center mb-1">
<v-btn color="gray" @click="joining(false)">{{
$t("startListening")
}}</v-btn>
</div>
</v-alert>
</v-dialog>
</template>

Wyświetl plik

@ -1,3 +1,4 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script> <script>
import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js"; import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js";
import { webfinger } from "../assets/utils"; import { webfinger } from "../assets/utils";
@ -103,7 +104,7 @@ export default {
:icon="muted ? mdiMicrophoneOff : mdiMicrophone" :icon="muted ? mdiMicrophoneOff : mdiMicrophone"
></v-icon> ></v-icon>
<a :href="data?.url" class="plain" target="_blank">{{ <a :href="data?.url" class="plain" target="_blank">{{
!data?.displayName ? data?.acct : data?.displayName !data?.displayName ? webfinger(data) : data?.displayName
}}</a> }}</a>
</h4> </h4>
</v-col> </v-col>

Wyświetl plik

@ -6,6 +6,8 @@ logoutConfirm: "Are you sure you want to sign out from Audon?"
loginRequired: "You need to sign in to view this page." loginRequired: "You need to sign in to view this page."
create: "Create" create: "Create"
cancel: "Cancel" cancel: "Cancel"
sure: "Yes"
nope: "No"
edit: "Edit" edit: "Edit"
save: "Save" save: "Save"
share: "Share" share: "Share"
@ -21,6 +23,8 @@ addressRequired: "Enter your instance address"
createNewRoom: "Create a New Room" createNewRoom: "Create a New Room"
editRoom: "Room Edit" editRoom: "Room Edit"
comingFuture: "Coming with future update!" 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?"
form: form:
title: "Title" title: "Title"
titleRequired: "Room title required" titleRequired: "Room title required"
@ -57,7 +61,13 @@ errors:
private: "Only cohosts can join." private: "Only cohosts can join."
default: "You are not allowed to join." default: "You are not allowed to join."
startListening: "Start Listening" startListening: "Start Listening"
browserMuted: "Your sound is muted by the browser. Press @:startListening to continue." browserMuted: "To protect your ears, sound is muted by the browser. Press @:startListening to continue."
onlineIndicator:
message: "Do you want to add the online indicator to your account's avatar like this?"
hint: "Audon will remove the indicator after this room is closed."
warning: "Your instance may take a while to reflect the indicator."
sure: "Yes"
nope: "No"
speakRequest: speakRequest:
label: "Speaker Requests" label: "Speaker Requests"
dialog: "Are you sure you want to send a request to be a speaker?" dialog: "Are you sure you want to send a request to be a speaker?"

Wyświetl plik

@ -6,6 +6,8 @@ logoutConfirm: "Audon からログアウトしますか?"
loginRequired: "続行するにはログインする必要があります。" loginRequired: "続行するにはログインする必要があります。"
create: "作成" create: "作成"
cancel: "キャンセル" cancel: "キャンセル"
sure: "はい"
nope: "いいえ"
edit: "編集" edit: "編集"
save: "保存" save: "保存"
share: "シェア" share: "シェア"
@ -21,6 +23,8 @@ addressRequired: "アドレスを入力してください"
createNewRoom: "部屋を作成" createNewRoom: "部屋を作成"
editRoom: "部屋の編集" editRoom: "部屋の編集"
comingFuture: "今後のアップデートで追加予定" comingFuture: "今後のアップデートで追加予定"
processing: "処理中です。<br />画面を閉じないでください。"
lostWarning: "この画面を閉じると保存前の内容が失われます。構いませんか?"
form: form:
title: "タイトル" title: "タイトル"
titleRequired: "部屋の名前を入力してください" titleRequired: "部屋の名前を入力してください"
@ -57,7 +61,13 @@ errors:
private: "この部屋は共同ホスト限定です。" private: "この部屋は共同ホスト限定です。"
default: "入室が許可されていません。" default: "入室が許可されていません。"
startListening: "視聴を始める" startListening: "視聴を始める"
browserMuted: "ブラウザの設定により無音になっています。続行するには @:startListening ボタンを押してください。" browserMuted: "大きな音であなたが驚かないよう、無音になっています。続行するには @:startListening ボタンを押してください。"
onlineIndicator:
message: "あなたが部屋をホスト中であることを表示しますか?「表示する」を選ぶとアカウントのアバターがこのようになります。"
hint: "部屋を閉じた後、アバターは自動で元に戻ります。"
warning: "サーバーが変更を反映するまで時間がかかることがあります。"
sure: "表示する"
nope: "表示しない"
speakRequest: speakRequest:
label: "発言リクエスト" label: "発言リクエスト"
dialog: "発言をリクエストしますか?" dialog: "発言をリクエストしますか?"

Wyświetl plik

@ -64,7 +64,7 @@ router.beforeEach(async (to) => {
} }
} }
}); });
router.afterEach((to, from) => { router.afterEach((to) => {
const donStore = useMastodonStore(); const donStore = useMastodonStore();
if (!to.meta.noauth && !donStore.authorized) { if (!to.meta.noauth && !donStore.authorized) {
const query = to.name !== "home" ? { l: to.path } : {}; const query = to.name !== "home" ? { l: to.path } : {};

Wyświetl plik

@ -14,6 +14,7 @@ export const useMastodonStore = defineStore("mastodon", {
}, },
client: null, client: null,
userinfo: null, userinfo: null,
avatar: "",
}; };
}, },
getters: { getters: {
@ -38,30 +39,19 @@ export const useMastodonStore = defineStore("mastodon", {
this.userinfo = user; this.userinfo = user;
this.authorized = true; this.authorized = true;
}, },
async updateAvatar(img) { async updateAvatar(img, filename) {
if (this.client === null) return; if (this.client === null) return;
const avatarBlob = await (await fetch(img)).blob(); const avatarBlob = await (await fetch(img)).blob();
this.userinfo = await this.client.v1.accounts.updateCredentials({ this.userinfo = await this.client.v1.accounts.updateCredentials({
avatar: new File([avatarBlob], `${Date.now()}.gif`), avatar: new File([avatarBlob], `${Date.now()}_${filename}`),
}); });
}, },
async revertAvatar() { async revertAvatar() {
const t = setTimeout(async () => { const token = await axios.get("/api/token");
const token = await axios.get("/api/token"); if (this.avatar && token.data.audon.avatar) {
const oldAvatar = sessionStorage.getItem("avatar_old_data"); await this.updateAvatar(this.avatar, token.data.audon.avatar);
sessionStorage.removeItem("avatar_old_data"); };
sessionStorage.removeItem("avatar_timeout"); await axios.delete("/api/room");
if (this.client === null || !oldAvatar || !token.data.audon.avatar)
return;
const resp = await axios.delete("/api/room");
if (resp.status === 200) {
const avatarBlob = await (await fetch(oldAvatar)).blob();
this.userinfo = await this.client.v1.accounts.updateCredentials({
avatar: new File([avatarBlob], token.data.audon.avatar),
});
}
}, 1500);
sessionStorage.setItem("avatar_timeout", t.toString());
}, },
}, },
}); });

Wyświetl plik

@ -13,6 +13,12 @@ export default {
query: "", query: "",
}; };
}, },
mounted() {
removeEventListener("beforeunload", (event) => {
event.preventDefault();
return (event.returnValue = "");
});
},
methods: { methods: {
async onLogout() { async onLogout() {
// if (!confirm(this.$t("logoutConfirm"))) return; // if (!confirm(this.$t("logoutConfirm"))) return;

Wyświetl plik

@ -88,9 +88,7 @@ export default {
alt="Branding Wordmark" alt="Branding Wordmark"
style="width: 100%; max-width: 200px" style="width: 100%; max-width: 200px"
/> />
<p class="mt-2"> <p class="mt-2">Audio spaces for Mastodon</p>
Audio spaces for Mastodon
</p>
</div> </div>
<v-alert v-if="$route.query.l" type="warning" variant="text"> <v-alert v-if="$route.query.l" type="warning" variant="text">
<div>{{ $t("loginRequired") }}</div> <div>{{ $t("loginRequired") }}</div>

Wyświetl plik

@ -7,6 +7,7 @@ import { darkTheme } from "picmo";
import { createPopup } from "@picmo/popup-picker"; import { createPopup } from "@picmo/popup-picker";
import { Howl } from "howler"; import { Howl } from "howler";
import Participant from "../components/Participant.vue"; import Participant from "../components/Participant.vue";
import JoinDialog from "../components/JoinDialog.vue";
import { import {
mdiMicrophone, mdiMicrophone,
mdiMicrophoneOff, mdiMicrophoneOff,
@ -28,7 +29,6 @@ import {
DataPacket_Kind, DataPacket_Kind,
AudioPresets, AudioPresets,
} from "livekit-client"; } from "livekit-client";
import { createClient } from "masto";
import { useVuelidate } from "@vuelidate/core"; import { useVuelidate } from "@vuelidate/core";
import { helpers, maxLength, required } from "@vuelidate/validators"; import { helpers, maxLength, required } from "@vuelidate/validators";
import NoSleep from "@uriopass/nosleep.js"; import NoSleep from "@uriopass/nosleep.js";
@ -96,6 +96,7 @@ export default {
}, },
components: { components: {
Participant, Participant,
JoinDialog,
}, },
validations() { validations() {
return { return {
@ -116,7 +117,7 @@ export default {
data() { data() {
return { return {
roomID: this.$route.params.id, roomID: this.$route.params.id,
loading: true, loading: false,
mainHeight: 700, mainHeight: 700,
roomInfo: { roomInfo: {
title: this.$t("connecting"), title: this.$t("connecting"),
@ -126,6 +127,7 @@ export default {
cohosts: [], cohosts: [],
speakers: [], speakers: [],
created_at: null, created_at: null,
accounts: {},
}, },
editingRoomInfo: { editingRoomInfo: {
title: "", title: "",
@ -146,12 +148,12 @@ export default {
activeSpeakerIDs: new Set(), activeSpeakerIDs: new Set(),
mutedSpeakerIDs: new Set(), mutedSpeakerIDs: new Set(),
micGranted: false, micGranted: false,
autoplayDisabled: false,
speakRequests: new Set(), speakRequests: new Set(),
showRequestNotification: false, showRequestNotification: false,
showRequestDialog: false, showRequestDialog: false,
showRequestedNotification: false, showRequestedNotification: false,
isEditLoading: false, isEditLoading: false,
closeLoading: false,
showEditDialog: false, showEditDialog: false,
timeElapsed: "", timeElapsed: "",
preview: false, preview: false,
@ -172,7 +174,7 @@ export default {
this.mutedSpeakerIDs = new Set(Object.keys(this.participants)); this.mutedSpeakerIDs = new Set(Object.keys(this.participants));
for (const [key, value] of Object.entries(this.participants)) { for (const [key, value] of Object.entries(this.participants)) {
if (value !== null) { if (value !== null) {
this.fetchMastoData(key, value); this.fetchMastoData(key);
} }
} }
} catch (error) { } catch (error) {
@ -198,13 +200,6 @@ export default {
} }
} }
} }
if (!this.preview) {
try {
await this.joinRoom();
} finally {
this.loading = false;
}
}
setInterval(this.refreshRemoteMuteStatus, 100); setInterval(this.refreshRemoteMuteStatus, 100);
setInterval(this.refreshTimeElapsed, 1000); setInterval(this.refreshTimeElapsed, 1000);
}, },
@ -258,219 +253,175 @@ export default {
const delta = now.diff(createdAt); const delta = now.diff(createdAt);
this.timeElapsed = delta.toFormat("hh:mm:ss"); this.timeElapsed = delta.toFormat("hh:mm:ss");
}, },
async joinRoom() { async joinRoom(token) {
if (!this.donStore.authorized) return; if (!this.donStore.authorized) {
this.$router.replace({ name: "home" });
}
try { try {
const timeout = sessionStorage.getItem("avatar_timeout"); this.loading = true;
if (timeout) { await this.connectLivekit(token);
const timeoutID = parseInt(timeout); } catch (error) {
clearTimeout(timeoutID); alert(this.$t("errors.connectionFailed"));
sessionStorage.removeItem("avatar_timeout"); } finally {
} this.loading = false;
const token = await axios.get("/api/token"); }
this.donStore.oauth = token.data; },
let avatarURL = this.donStore.userinfo.avatar; async connectLivekit(payload) {
if (this.donStore.oauth.audon?.avatar) { const self = this;
avatarURL = ""; this.roomClient
} .on(RoomEvent.TrackSubscribed, (track) => {
const resp = await axios.postForm(`/api/room/${this.roomID}`, { if (track.kind === Track.Kind.Audio) {
avatar: avatarURL, const element = track.attach();
}); self.$refs.audioDOM.appendChild(element);
sessionStorage.setItem("avatar_old_data", resp.data.original);
if (resp.data.indicator && !timeout) {
try {
await this.donStore.updateAvatar(resp.data.indicator);
} catch (err) {
console.log(err);
} }
} })
const room = new Room(); .on(RoomEvent.TrackUnsubscribed, (track) => {
const self = this; track.detach();
room })
.on(RoomEvent.TrackSubscribed, (track) => { .on(RoomEvent.LocalTrackPublished, () => {
if (track.kind === Track.Kind.Audio) { self.micGranted = true;
const element = track.attach(); })
self.$refs.audioDOM.appendChild(element); .on(RoomEvent.LocalTrackUnpublished, (publication) => {
} publication.track?.detach();
}) })
.on(RoomEvent.TrackUnsubscribed, (track) => { .on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
track.detach(); self.activeSpeakerIDs = new Set(map(speakers, (p) => p.identity));
}) })
.on(RoomEvent.LocalTrackPublished, () => { .on(RoomEvent.ParticipantConnected, (participant) => {
self.micGranted = true; if (self.iamHost || self.iamCohost) self.sounds.boop.play();
}) const metadata = self.addParticipant(participant);
.on(RoomEvent.LocalTrackUnpublished, (publication) => { if (metadata !== null) {
publication.track?.detach(); self.fetchMastoData(participant.identity);
}) }
.on(RoomEvent.ActiveSpeakersChanged, (speakers) => { })
self.activeSpeakerIDs = new Set(map(speakers, (p) => p.identity)); .on(RoomEvent.ParticipantDisconnected, (participant) => {
}) self.participants = omit(self.participants, participant.identity);
.on(RoomEvent.ParticipantConnected, (participant) => { })
if (self.iamHost || self.iamCohost) self.sounds.boop.play(); .on(RoomEvent.Disconnected, async (reason) => {
const metadata = self.addParticipant(participant); // TODO: change this from alert to a vuetify thing
if (metadata !== null) { self.noSleep.disable();
self.fetchMastoData(participant.identity, metadata); if (reason === DisconnectReason.PARTICIPANT_REMOVED) {
} alert(self.$t("roomEvent.removed"));
}) self.$router.push({ name: "home" });
.on(RoomEvent.ParticipantDisconnected, (participant) => { } else {
self.participants = omit(self.participants, participant.identity); let message = "";
}) switch (reason) {
.on(RoomEvent.AudioPlaybackStatusChanged, () => { case DisconnectReason.ROOM_DELETED:
if (!room.canPlaybackAudio) { if (self.iamHost || self.iamCohost) {
self.autoplayDisabled = true; self.closeLoading = true;
} try {
}) await self.donStore.revertAvatar();
.on(RoomEvent.Disconnected, (reason) => { } catch (error) {
// TODO: change this from alert to a vuetify thing console.log(error);
self.noSleep.disable(); } finally {
if (reason === DisconnectReason.PARTICIPANT_REMOVED) { self.closeLoading = false;
alert(self.$t("roomEvent.removed")); }
self.$router.push({ name: "home" });
} else {
self.donStore.revertAvatar().finally(() => {
let message = "";
switch (reason) {
case DisconnectReason.ROOM_DELETED:
message = self.$t("roomEvent.closedByHost");
break;
case DisconnectReason.CLIENT_INITIATED:
break;
default:
message = "Disconnected due to unknown reasons";
} }
if (message !== "") { message = self.$t("roomEvent.closedByHost");
alert(message); break;
case DisconnectReason.CLIENT_INITIATED:
if (self.iamCohost) {
self.closeLoading = true;
try {
await self.donStore.revertAvatar();
} catch (error) {
console.log(error);
} finally {
self.closeLoading = false;
}
} }
self.$router.push({ name: "home" }); break;
}); default:
message = "Disconnected due to unknown reasons";
} }
}) if (message !== "") {
.on(RoomEvent.DataReceived, (payload, participant) => { alert(message);
try { }
/* data should be like self.$router.push({ name: "home" });
}
})
.on(RoomEvent.DataReceived, (payload, participant) => {
try {
/* data should be like
{ "kind": "speak_request" } { "kind": "speak_request" }
{ "kind": "chat", "data": "..." } { "kind": "chat", "data": "..." }
{ "kind": "request_declined", "audon_id": "..."} { "kind": "request_declined", "audon_id": "..."}
{ "kind": "emoji", "emoji": "..." } { "kind": "emoji", "emoji": "..." }
*/ */
const strData = self.decoder.decode(payload); const strData = self.decoder.decode(payload);
const jsonData = JSON.parse(strData); const jsonData = JSON.parse(strData);
const metadata = JSON.parse(participant.metadata); const metadata = JSON.parse(participant.metadata);
switch (jsonData?.kind) { switch (jsonData?.kind) {
case "emoji": case "emoji":
self.addEmojiReaction(participant.identity, jsonData.emoji); self.addEmojiReaction(participant.identity, jsonData.emoji);
break; break;
case "speak_request": // someone is wanting to be a speaker case "speak_request": // someone is wanting to be a speaker
self.onSpeakRequestReceived(participant); self.onSpeakRequestReceived(participant);
break; break;
case "request_declined": case "request_declined":
if ( if (
self.isHost(participant.identity) || self.isHost(participant.identity) ||
self.isCohost(metadata) self.isCohost(metadata)
) { ) {
self.speakRequests.delete(jsonData.audon_id); self.speakRequests.delete(jsonData.audon_id);
if (self.speakRequests.size < 1) if (self.speakRequests.size < 1)
self.showRequestNotification = false; self.showRequestNotification = false;
} }
break; break;
}
} catch (error) {
console.log(
"invalida data received from: ",
participant.identity
);
} }
}) } catch (error) {
.on(RoomEvent.RoomMetadataChanged, (metadata) => { console.log("invalida data received from: ", participant.identity);
self.roomInfo = JSON.parse(metadata);
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) {
self.roomClient.localParticipant
.setMicrophoneEnabled(true, captureOpts, publishOpts)
.then(() => {
self.micGranted = true;
})
.finally(() => {
self.roomClient.localParticipant.setMicrophoneEnabled(false);
});
}
});
await room.connect(resp.data.url, resp.data.token);
this.roomClient = room;
this.roomInfo = JSON.parse(room.metadata);
this.editingRoomInfo = clone(this.roomInfo);
this.addParticipant(room.localParticipant);
for (const part of room.participants.values()) {
this.addParticipant(part);
}
this.mutedSpeakerIDs.add(this.donStore.oauth.audon.audon_id);
this.activeSpeakerIDs = new Set(
map(room.activeSpeakers, (p) => p.identity)
);
// cache mastodon data of current participants
for (const [key, value] of Object.entries(this.participants)) {
if (value !== null) {
this.fetchMastoData(key, value);
} }
} })
if (this.iamHost || this.iamCohost || this.iamSpeaker) { .on(RoomEvent.RoomMetadataChanged, (metadata) => {
try { self.roomInfo = JSON.parse(metadata);
await room.localParticipant.setMicrophoneEnabled( self.editingRoomInfo = clone(self.roomInfo);
true, if (!self.roomInfo.speakers) return;
captureOpts, for (const speakers of self.roomInfo.speakers) {
publishOpts self.speakRequests.delete(speakers.audon_id);
); if (self.speakRequests.size < 1)
} catch { self.showRequestNotification = false;
alert(this.$t("microphoneBlocked"));
} finally {
await room.localParticipant.setMicrophoneEnabled(false);
} }
if (self.iamSpeaker && !self.micGranted) {
self.roomClient.localParticipant
.setMicrophoneEnabled(true, captureOpts, publishOpts)
.then(() => {
self.micGranted = true;
})
.finally(() => {
self.roomClient.localParticipant.setMicrophoneEnabled(false);
});
}
});
await this.roomClient.connect(payload.url, payload.token);
this.roomInfo = JSON.parse(this.roomClient.metadata);
this.editingRoomInfo = clone(this.roomInfo);
this.addParticipant(this.roomClient.localParticipant);
for (const part of this.roomClient.participants.values()) {
this.addParticipant(part);
}
this.mutedSpeakerIDs.add(this.donStore.oauth.audon.audon_id);
this.activeSpeakerIDs = new Set(
map(this.roomClient.activeSpeakers, (p) => p.identity)
);
// cache mastodon data of current participants
for (const [key, value] of Object.entries(this.participants)) {
if (value !== null) {
this.fetchMastoData(key);
} }
} catch (error) { }
let message = ""; if (this.iamHost || this.iamCohost || this.iamSpeaker) {
switch (error.response?.status) { try {
case 403: await this.roomClient.localParticipant.setMicrophoneEnabled(
switch (error.response?.data) { true,
case "following": captureOpts,
message = this.$t("errors.restriction.following"); publishOpts
break; );
case "follower": } catch {
message = this.$t("errors.restriction.follower"); alert(this.$t("microphoneBlocked"));
break; } finally {
case "knowing": await this.roomClient.localParticipant.setMicrophoneEnabled(false);
message = this.$t("errors.restriction.knowing");
break;
case "mutual":
message = this.$t("errors.restriction.mutual");
break;
case "private":
message = this.$t("errors.restriction.private");
break;
default:
message = this.$t("errors.restriction.default");
}
alert(message);
break;
case 404:
pushNotFound(this.$route);
break;
case 406:
alert(this.$t("errors.alreadyConnected"));
break;
case 410:
alert(this.$t("errors.alreadyClosed"));
break;
default:
alert(error);
} }
this.noSleep.disable();
this.$router.push({ name: "home" });
} }
}, },
refreshRemoteMuteStatus() { refreshRemoteMuteStatus() {
@ -612,17 +563,24 @@ export default {
} }
return metadata; return metadata;
}, },
async fetchMastoData(identity, { remote_id, remote_url }) { async fetchMastoData(identity) {
if (this.cachedMastoData[identity] !== undefined) return; if (
this.cachedMastoData[identity] !== undefined ||
this.roomInfo.accounts[identity] === undefined
)
return;
try { try {
const url = new URL(remote_url);
const mastoClient = createClient({
url: url.origin,
disableVersionCheck: true,
});
const info = await mastoClient.v1.accounts.fetch(remote_id);
const resp = await axios.get(`/app/user/${identity}`); const resp = await axios.get(`/app/user/${identity}`);
info.avatar = `/storage/${resp.data.audon_id}/avatar/${resp.data.avatar}`; const account = this.roomInfo.accounts[identity];
const info = {
acct: account.acct,
displayName: account.displayName,
avatar: account.avatar,
url: account.url,
};
if (resp.data.avatar) {
info.avatar = `/storage/${resp.data.audon_id}/avatar/${resp.data.avatar}`;
}
this.cachedMastoData[identity] = info; this.cachedMastoData[identity] = info;
} catch (error) { } catch (error) {
// FIXME: display error snackbar // FIXME: display error snackbar
@ -680,15 +638,6 @@ export default {
async onLeave() { async onLeave() {
await this.roomClient.disconnect(); await this.roomClient.disconnect();
}, },
async onStartListening() {
try {
await this.roomClient.startAudio();
this.autoplayDisabled = false;
} catch {
alert(this.$t("errors.connectionFailed"));
await this.roomClient.disconnect();
}
},
async onEditSubmit() { async onEditSubmit() {
this.editingRoomInfo.title = trim(this.editingRoomInfo.title); this.editingRoomInfo.title = trim(this.editingRoomInfo.title);
this.editingRoomInfo.description = trim(this.editingRoomInfo.description); this.editingRoomInfo.description = trim(this.editingRoomInfo.description);
@ -718,6 +667,20 @@ export default {
</script> </script>
<template> <template>
<v-overlay
:model-value="closeLoading"
persistent
class="align-center justify-center"
>
<div class="mb-8 text-center">
<v-progress-circular indeterminate size="40"></v-progress-circular>
</div>
<div>
<v-alert variant="flat" class="text-center">
<span v-html="$t('processing')"></span>
</v-alert>
</div>
</v-overlay>
<v-dialog v-model="showEditDialog" max-width="500" persistent> <v-dialog v-model="showEditDialog" max-width="500" persistent>
<v-card :loading="isEditLoading"> <v-card :loading="isEditLoading">
<v-card-title>{{ $t("editRoom") }}</v-card-title> <v-card-title>{{ $t("editRoom") }}</v-card-title>
@ -760,23 +723,12 @@ export default {
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-model="autoplayDisabled" max-width="500" persistent> <JoinDialog
<v-alert color="indigo"> v-if="!preview"
<div class="mb-5"> :room-id="roomID"
{{ $t("browserMuted") }} :room-client="roomClient"
</div> @connect.once="joinRoom"
<div class="text-center mb-3"> ></JoinDialog>
<v-btn color="gray" @click="onStartListening">{{
$t("startListening")
}}</v-btn>
</div>
<div class="text-center">
<v-btn variant="text" @click="roomClient.disconnect()">{{
$t("leaveRoom")
}}</v-btn>
</div>
</v-alert>
</v-dialog>
<v-dialog v-model="showRequestDialog" max-width="500"> <v-dialog v-model="showRequestDialog" max-width="500">
<v-card max-height="600" class="d-flex flex-column"> <v-card max-height="600" class="d-flex flex-column">
<v-card-title>{{ $t("speakRequest.label") }}</v-card-title> <v-card-title>{{ $t("speakRequest.label") }}</v-card-title>

Wyświetl plik

@ -177,7 +177,7 @@ func oauthHandler(c echo.Context) (err error) {
// return c.Redirect(http.StatusFound, "http://localhost:5173") // return c.Redirect(http.StatusFound, "http://localhost:5173")
} }
func getOAuthTokenHandler(c echo.Context) (err error) { func getUserTokenHandler(c echo.Context) (err error) {
data, ok := c.Get("data").(*SessionData) data, ok := c.Get("data").(*SessionData)
if !ok { if !ok {
return ErrInvalidSession return ErrInvalidSession

Wyświetl plik

@ -47,9 +47,6 @@ func (u *AudonUser) GetIndicator(ctx context.Context, fnew []byte) ([]byte, erro
if err := os.WriteFile(saved, fnew, 0664); err != nil { if err := os.WriteFile(saved, fnew, 0664); err != nil {
return nil, err return nil, err
} }
if u.AvatarFile != "" {
// os.Remove(u.getAvatarImagePath(u.AvatarFile))
}
isAvatarNew = true isAvatarNew = true
} }
@ -94,7 +91,7 @@ func (u *AudonUser) createGIF(avatar image.Image) ([]byte, error) {
baseFrame := image.NewRGBA(avatarPNG.Bounds()) baseFrame := image.NewRGBA(avatarPNG.Bounds())
draw.Draw(baseFrame, baseFrame.Bounds(), image.Black, image.Point{}, draw.Src) draw.Draw(baseFrame, baseFrame.Bounds(), image.Black, image.Point{}, draw.Src)
draw.Copy(baseFrame, image.Point{}, avatarPNG, avatarPNG.Bounds(), draw.Over, nil) draw.Copy(baseFrame, image.Point{}, avatarPNG, avatarPNG.Bounds(), draw.Over, nil)
draw.Draw(baseFrame, baseFrame.Bounds(), mainConfig.LogoImageBack, image.Point{-35, -35}, draw.Over) draw.Draw(baseFrame, baseFrame.Bounds(), mainConfig.LogoImageBack, image.Point{-55, -105}, draw.Over)
anim := webpanimation.NewWebpAnimation(150, 150, 0) anim := webpanimation.NewWebpAnimation(150, 150, 0)
defer anim.ReleaseMemory() defer anim.ReleaseMemory()
@ -115,7 +112,7 @@ func (u *AudonUser) createGIF(avatar image.Image) ([]byte, error) {
} }
mask := image.NewUniform(color.Alpha{alpha}) mask := image.NewUniform(color.Alpha{alpha})
draw.DrawMask(frame, frame.Bounds(), mainConfig.LogoImageFront, image.Point{-35, -35}, mask, image.Point{}, draw.Over) draw.DrawMask(frame, frame.Bounds(), mainConfig.LogoImageFront, image.Point{-55, -105}, mask, image.Point{}, draw.Over)
if err := anim.AddFrame(frame, 1000/count*i, webpConf); err != nil { if err := anim.AddFrame(frame, 1000/count*i, webpConf); err != nil {
return nil, err return nil, err

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 5.9 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.6 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 16 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 11 KiB

68
room.go
Wyświetl plik

@ -299,7 +299,7 @@ func joinRoomHandler(c echo.Context) (err error) {
} }
} }
roomMetadata := &RoomMetadata{Room: room} roomMetadata := &RoomMetadata{Room: room, MastodonAccounts: make(map[string]*MastodonAccount)}
// Allows the user to talk if the user is a speaker // 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 lkRoom, _ := getRoomInLivekit(c.Request().Context(), room.RoomID) // lkRoom will be nil if it doesn't exist
@ -326,21 +326,43 @@ func joinRoomHandler(c echo.Context) (err error) {
Token: token, Token: token,
Audon: user, Audon: user,
} }
if user.AvatarFile != "" {
orig, err := os.ReadFile(user.getAvatarImagePath(user.AvatarFile)) mastoAccount := new(MastodonAccount)
if err == nil && orig != nil { if err := c.Bind(&mastoAccount); err != nil {
resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(orig), base64.StdEncoding.EncodeToString(orig)) c.Logger().Error(err)
} return ErrInvalidRequestFormat
} }
avatarLink := c.FormValue("avatar") roomMetadata.MastodonAccounts[user.AudonID] = mastoAccount
if avatarLink != "" {
// Get ready to change avatar if user is host or cohost
if room.IsHost(user) || room.IsCoHost(user) {
// Get user's stored avatar if exists
if user.AvatarFile != "" {
orig, err := os.ReadFile(user.getAvatarImagePath(user.AvatarFile))
if err == nil && orig != nil {
resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(orig), base64.StdEncoding.EncodeToString(orig))
} else if orig == nil {
user.AvatarFile = ""
}
// icon, err := os.ReadFile(user.GetGIFAvatarPath())
// if err == nil && icon != nil {
// resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(icon))
// }
}
avatarLink := mastoAccount.Avatar
if err := mainValidator.Var(&avatarLink, "required"); err != nil {
return wrapValidationError(err)
}
avatarURL, err := url.Parse(avatarLink) avatarURL, err := url.Parse(avatarLink)
if err != nil { if err != nil {
c.Logger().Error(err)
return ErrInvalidRequestFormat return ErrInvalidRequestFormat
} }
if online, err := user.InLivekit(c.Request().Context()); !online && err == nil { // Retrieve user's current avatar if the old one doesn't exist in Audon.
// Skips if user is still in another room.
if already, err := user.InLivekit(c.Request().Context()); !already && err == nil && user.AvatarFile == "" {
// Download user's avatar // Download user's avatar
req, err := http.NewRequest(http.MethodGet, avatarURL.String(), nil) req, err := http.NewRequest(http.MethodGet, avatarURL.String(), nil)
if err != nil { if err != nil {
@ -362,12 +384,14 @@ func joinRoomHandler(c echo.Context) (err error) {
return echo.NewHTTPError(http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError)
} }
// Generate indicator GIF
indicator, err := user.GetIndicator(c.Request().Context(), fnew) indicator, err := user.GetIndicator(c.Request().Context(), fnew)
if err != nil { if err != nil {
c.Logger().Warn(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
} }
resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(indicator))
resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(fnew), base64.StdEncoding.EncodeToString(fnew)) resp.Original = fmt.Sprintf("data:%s;base64,%s", mimetype.Detect(fnew), base64.StdEncoding.EncodeToString(fnew))
resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(indicator))
} else if err != nil { } else if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
} }
@ -392,6 +416,26 @@ func joinRoomHandler(c echo.Context) (err error) {
c.Logger().Error(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusConflict) return echo.NewHTTPError(http.StatusConflict)
} }
} else {
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)
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)
}
} }
// Store user's session data in cache // Store user's session data in cache
@ -444,7 +488,7 @@ func leaveRoomHandler(c echo.Context) error {
c.Logger().Error(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError)
} else if still { } else if still {
return c.NoContent(http.StatusAccepted) return c.NoContent(http.StatusConflict)
} }
if err := user.ClearUserAvatar(c.Request().Context()); err != nil { if err := user.ClearUserAvatar(c.Request().Context()); err != nil {
c.Logger().Error(err) c.Logger().Error(err)

Wyświetl plik

@ -30,7 +30,8 @@ type (
RoomMetadata struct { RoomMetadata struct {
*Room *Room
Speakers []*AudonUser `json:"speakers"` Speakers []*AudonUser `json:"speakers"`
MastodonAccounts map[string]*MastodonAccount `json:"accounts"`
} }
Room struct { Room struct {

Wyświetl plik

@ -169,7 +169,8 @@ func main() {
e.POST("/app/webhook", livekitWebhookHandler) e.POST("/app/webhook", livekitWebhookHandler)
api := e.Group("/api", authMiddleware) api := e.Group("/api", authMiddleware)
api.GET("/token", getOAuthTokenHandler) api.GET("/token", getUserTokenHandler)
// api.GET("/room", getStatusHandler)
api.POST("/room", createRoomHandler) api.POST("/room", createRoomHandler)
api.DELETE("/room", leaveRoomHandler) api.DELETE("/room", leaveRoomHandler)
api.POST("/room/:id", joinRoomHandler) api.POST("/room/:id", joinRoomHandler)

54
user.go
Wyświetl plik

@ -3,11 +3,37 @@ package main
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
mastodon "github.com/mattn/go-mastodon"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
) )
type MastodonAccount struct {
ID mastodon.ID `json:"id"`
Username string `json:"username"`
Acct string `json:"acct"`
DisplayName string `json:"displayName"`
Locked bool `json:"locked"`
CreatedAt time.Time `json:"createdAt"`
FollowersCount int64 `json:"followersCount"`
FollowingCount int64 `json:"followingCount"`
StatusesCount int64 `json:"statusesCount"`
Note string `json:"note"`
URL string `json:"url"`
Avatar string `json:"avatar"`
AvatarStatic string `json:"avatarStatic"`
Header string `json:"header"`
HeaderStatic string `json:"headerStatic"`
Emojis []mastodon.Emoji `json:"emojis"`
Moved *MastodonAccount `json:"moved"`
Fields []mastodon.Field `json:"fields"`
Bot bool `json:"bot"`
Discoverable bool `json:"discoverable"`
Source *mastodon.AccountSource `json:"source"`
}
func getUserHandler(c echo.Context) error { func getUserHandler(c echo.Context) error {
audonID := c.Param("id") audonID := c.Param("id")
if err := mainValidator.Var(&audonID, "required,printascii"); err != nil { if err := mainValidator.Var(&audonID, "required,printascii"); err != nil {
@ -22,6 +48,18 @@ func getUserHandler(c echo.Context) error {
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
func getStatusHandler(c echo.Context) error {
u := c.Get("user").(*AudonUser)
ids, err := u.GetCurrentRoomIDs(c.Request().Context())
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, ids)
}
func (a *AudonUser) Equal(u *AudonUser) bool { func (a *AudonUser) Equal(u *AudonUser) bool {
if a == nil { if a == nil {
return false return false
@ -40,12 +78,26 @@ func (a *AudonUser) InLivekit(ctx context.Context) (bool, error) {
} }
func (a *AudonUser) ClearUserAvatar(ctx context.Context) error { func (a *AudonUser) ClearUserAvatar(ctx context.Context) error {
// os.Remove(a.getAvatarImagePath(a.AvatarFile))
coll := mainDB.Collection(COLLECTION_USER) coll := mainDB.Collection(COLLECTION_USER)
_, err := coll.UpdateOne(ctx, _, err := coll.UpdateOne(ctx,
bson.D{{Key: "audon_id", Value: a.AudonID}}, bson.D{{Key: "audon_id", Value: a.AudonID}},
bson.D{ bson.D{
{Key: "$set", Value: bson.D{{Key: "avatar", Value: ""}}}, {Key: "$set", Value: bson.D{{Key: "avatar", Value: ""}}},
}) })
// if err == nil {
// os.Remove(a.getAvatarImagePath(a.AvatarFile))
// }
return err return err
} }
func (a *AudonUser) GetCurrentRoomIDs(ctx context.Context) ([]string, error) {
rooms, err := a.GetCurrentLivekitRooms(ctx)
if err != nil {
return nil, err
}
roomIDs := make([]string, len(rooms))
for i, r := range rooms {
roomIDs[i] = r.GetName()
}
return roomIDs, nil
}

Wyświetl plik

@ -25,7 +25,8 @@ func livekitWebhookHandler(c echo.Context) error {
} }
if event.GetEvent() == webhook.EventRoomFinished { if event.GetEvent() == webhook.EventRoomFinished {
room, err := findRoomByID(c.Request().Context(), event.GetRoom().GetName()) lkRoom := event.GetRoom()
room, err := findRoomByID(c.Request().Context(), lkRoom.GetName())
if err != nil { if err != nil {
c.Logger().Error(err) c.Logger().Error(err)
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
@ -66,34 +67,36 @@ func livekitWebhookHandler(c echo.Context) error {
countdown := time.NewTimer(10 * time.Second) countdown := time.NewTimer(10 * time.Second)
webhookTimerCache.Set(audonID, countdown, ttlcache.DefaultTTL) webhookTimerCache.Set(audonID, countdown, ttlcache.DefaultTTL)
<-countdown.C go func() {
webhookTimerCache.Delete(audonID) <-countdown.C
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) webhookTimerCache.Delete(audonID)
defer cancel() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
// ctx := context.TODO() defer cancel()
stillAgain, err := user.InLivekit(ctx) stillAgain, err := user.InLivekit(ctx)
if stillAgain || err != nil {
return c.NoContent(http.StatusOK)
}
user, err = findUserByID(ctx, audonID)
if err == nil && user.AvatarFile != "" {
log.Printf("restoring avatar: %s\n", audonID)
if err != nil { if err != nil {
c.Logger().Error(err) log.Println(err)
return echo.NewHTTPError(http.StatusInternalServerError)
} }
avatar := user.getAvatarImagePath(user.AvatarFile) if stillAgain {
_, err = updateAvatar(ctx, mastoClient, avatar) return
if err != nil {
c.Logger().Warn(err)
} }
user.ClearUserAvatar(ctx) nextUser, err := findUserByID(ctx, audonID)
// os.Remove(avatar) if err == nil && nextUser.AvatarFile != "" {
} else if err != nil { log.Printf("restoring avatar: %s\n", audonID)
c.Logger().Error(err) if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError) log.Println(err)
} return
}
avatar := nextUser.getAvatarImagePath(nextUser.AvatarFile)
_, err = updateAvatar(ctx, mastoClient, avatar)
if err != nil {
log.Println(err)
}
nextUser.ClearUserAvatar(ctx)
} else if err != nil {
log.Println(err)
}
}()
} }
return c.NoContent(http.StatusOK) return c.NoContent(http.StatusOK)
} else if event.GetEvent() == webhook.EventRoomStarted { } else if event.GetEvent() == webhook.EventRoomStarted {