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:
auto_create: false
empty_timeout: 30
empty_timeout: 3600
max_participants: 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>
import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js";
import { webfinger } from "../assets/utils";
@ -103,7 +104,7 @@ export default {
:icon="muted ? mdiMicrophoneOff : mdiMicrophone"
></v-icon>
<a :href="data?.url" class="plain" target="_blank">{{
!data?.displayName ? data?.acct : data?.displayName
!data?.displayName ? webfinger(data) : data?.displayName
}}</a>
</h4>
</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."
create: "Create"
cancel: "Cancel"
sure: "Yes"
nope: "No"
edit: "Edit"
save: "Save"
share: "Share"
@ -21,6 +23,8 @@ addressRequired: "Enter your instance address"
createNewRoom: "Create a New Room"
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?"
form:
title: "Title"
titleRequired: "Room title required"
@ -57,7 +61,13 @@ errors:
private: "Only cohosts can join."
default: "You are not allowed to join."
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:
label: "Speaker Requests"
dialog: "Are you sure you want to send a request to be a speaker?"

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -7,6 +7,7 @@ import { darkTheme } from "picmo";
import { createPopup } from "@picmo/popup-picker";
import { Howl } from "howler";
import Participant from "../components/Participant.vue";
import JoinDialog from "../components/JoinDialog.vue";
import {
mdiMicrophone,
mdiMicrophoneOff,
@ -28,7 +29,6 @@ import {
DataPacket_Kind,
AudioPresets,
} from "livekit-client";
import { createClient } from "masto";
import { useVuelidate } from "@vuelidate/core";
import { helpers, maxLength, required } from "@vuelidate/validators";
import NoSleep from "@uriopass/nosleep.js";
@ -96,6 +96,7 @@ export default {
},
components: {
Participant,
JoinDialog,
},
validations() {
return {
@ -116,7 +117,7 @@ export default {
data() {
return {
roomID: this.$route.params.id,
loading: true,
loading: false,
mainHeight: 700,
roomInfo: {
title: this.$t("connecting"),
@ -126,6 +127,7 @@ export default {
cohosts: [],
speakers: [],
created_at: null,
accounts: {},
},
editingRoomInfo: {
title: "",
@ -146,12 +148,12 @@ export default {
activeSpeakerIDs: new Set(),
mutedSpeakerIDs: new Set(),
micGranted: false,
autoplayDisabled: false,
speakRequests: new Set(),
showRequestNotification: false,
showRequestDialog: false,
showRequestedNotification: false,
isEditLoading: false,
closeLoading: false,
showEditDialog: false,
timeElapsed: "",
preview: false,
@ -172,7 +174,7 @@ export default {
this.mutedSpeakerIDs = new Set(Object.keys(this.participants));
for (const [key, value] of Object.entries(this.participants)) {
if (value !== null) {
this.fetchMastoData(key, value);
this.fetchMastoData(key);
}
}
} 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.refreshTimeElapsed, 1000);
},
@ -258,219 +253,175 @@ export default {
const delta = now.diff(createdAt);
this.timeElapsed = delta.toFormat("hh:mm:ss");
},
async joinRoom() {
if (!this.donStore.authorized) return;
async joinRoom(token) {
if (!this.donStore.authorized) {
this.$router.replace({ name: "home" });
}
try {
const timeout = sessionStorage.getItem("avatar_timeout");
if (timeout) {
const timeoutID = parseInt(timeout);
clearTimeout(timeoutID);
sessionStorage.removeItem("avatar_timeout");
}
const token = await axios.get("/api/token");
this.donStore.oauth = token.data;
let avatarURL = this.donStore.userinfo.avatar;
if (this.donStore.oauth.audon?.avatar) {
avatarURL = "";
}
const resp = await axios.postForm(`/api/room/${this.roomID}`, {
avatar: avatarURL,
});
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);
this.loading = true;
await this.connectLivekit(token);
} catch (error) {
alert(this.$t("errors.connectionFailed"));
} finally {
this.loading = false;
}
},
async connectLivekit(payload) {
const self = this;
this.roomClient
.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === Track.Kind.Audio) {
const element = track.attach();
self.$refs.audioDOM.appendChild(element);
}
}
const room = new Room();
const self = this;
room
.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === Track.Kind.Audio) {
const element = track.attach();
self.$refs.audioDOM.appendChild(element);
}
})
.on(RoomEvent.TrackUnsubscribed, (track) => {
track.detach();
})
.on(RoomEvent.LocalTrackPublished, () => {
self.micGranted = true;
})
.on(RoomEvent.LocalTrackUnpublished, (publication) => {
publication.track?.detach();
})
.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
self.activeSpeakerIDs = new Set(map(speakers, (p) => p.identity));
})
.on(RoomEvent.ParticipantConnected, (participant) => {
if (self.iamHost || self.iamCohost) self.sounds.boop.play();
const metadata = self.addParticipant(participant);
if (metadata !== null) {
self.fetchMastoData(participant.identity, metadata);
}
})
.on(RoomEvent.ParticipantDisconnected, (participant) => {
self.participants = omit(self.participants, participant.identity);
})
.on(RoomEvent.AudioPlaybackStatusChanged, () => {
if (!room.canPlaybackAudio) {
self.autoplayDisabled = true;
}
})
.on(RoomEvent.Disconnected, (reason) => {
// TODO: change this from alert to a vuetify thing
self.noSleep.disable();
if (reason === DisconnectReason.PARTICIPANT_REMOVED) {
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";
})
.on(RoomEvent.TrackUnsubscribed, (track) => {
track.detach();
})
.on(RoomEvent.LocalTrackPublished, () => {
self.micGranted = true;
})
.on(RoomEvent.LocalTrackUnpublished, (publication) => {
publication.track?.detach();
})
.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
self.activeSpeakerIDs = new Set(map(speakers, (p) => p.identity));
})
.on(RoomEvent.ParticipantConnected, (participant) => {
if (self.iamHost || self.iamCohost) self.sounds.boop.play();
const metadata = self.addParticipant(participant);
if (metadata !== null) {
self.fetchMastoData(participant.identity);
}
})
.on(RoomEvent.ParticipantDisconnected, (participant) => {
self.participants = omit(self.participants, participant.identity);
})
.on(RoomEvent.Disconnected, async (reason) => {
// TODO: change this from alert to a vuetify thing
self.noSleep.disable();
if (reason === DisconnectReason.PARTICIPANT_REMOVED) {
alert(self.$t("roomEvent.removed"));
self.$router.push({ name: "home" });
} else {
let message = "";
switch (reason) {
case DisconnectReason.ROOM_DELETED:
if (self.iamHost || self.iamCohost) {
self.closeLoading = true;
try {
await self.donStore.revertAvatar();
} catch (error) {
console.log(error);
} finally {
self.closeLoading = false;
}
}
if (message !== "") {
alert(message);
message = self.$t("roomEvent.closedByHost");
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";
}
})
.on(RoomEvent.DataReceived, (payload, participant) => {
try {
/* data should be like
if (message !== "") {
alert(message);
}
self.$router.push({ name: "home" });
}
})
.on(RoomEvent.DataReceived, (payload, participant) => {
try {
/* data should be like
{ "kind": "speak_request" }
{ "kind": "chat", "data": "..." }
{ "kind": "request_declined", "audon_id": "..."}
{ "kind": "emoji", "emoji": "..." }
*/
const strData = self.decoder.decode(payload);
const jsonData = JSON.parse(strData);
const metadata = JSON.parse(participant.metadata);
switch (jsonData?.kind) {
case "emoji":
self.addEmojiReaction(participant.identity, jsonData.emoji);
break;
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);
if (self.speakRequests.size < 1)
self.showRequestNotification = false;
}
break;
}
} catch (error) {
console.log(
"invalida data received from: ",
participant.identity
);
const strData = self.decoder.decode(payload);
const jsonData = JSON.parse(strData);
const metadata = JSON.parse(participant.metadata);
switch (jsonData?.kind) {
case "emoji":
self.addEmojiReaction(participant.identity, jsonData.emoji);
break;
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);
if (self.speakRequests.size < 1)
self.showRequestNotification = false;
}
break;
}
})
.on(RoomEvent.RoomMetadataChanged, (metadata) => {
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);
} catch (error) {
console.log("invalida data received from: ", participant.identity);
}
}
if (this.iamHost || this.iamCohost || this.iamSpeaker) {
try {
await room.localParticipant.setMicrophoneEnabled(
true,
captureOpts,
publishOpts
);
} catch {
alert(this.$t("microphoneBlocked"));
} finally {
await room.localParticipant.setMicrophoneEnabled(false);
})
.on(RoomEvent.RoomMetadataChanged, (metadata) => {
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 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 = "";
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);
}
if (this.iamHost || this.iamCohost || this.iamSpeaker) {
try {
await this.roomClient.localParticipant.setMicrophoneEnabled(
true,
captureOpts,
publishOpts
);
} catch {
alert(this.$t("microphoneBlocked"));
} finally {
await this.roomClient.localParticipant.setMicrophoneEnabled(false);
}
this.noSleep.disable();
this.$router.push({ name: "home" });
}
},
refreshRemoteMuteStatus() {
@ -612,17 +563,24 @@ export default {
}
return metadata;
},
async fetchMastoData(identity, { remote_id, remote_url }) {
if (this.cachedMastoData[identity] !== undefined) return;
async fetchMastoData(identity) {
if (
this.cachedMastoData[identity] !== undefined ||
this.roomInfo.accounts[identity] === undefined
)
return;
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}`);
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;
} catch (error) {
// FIXME: display error snackbar
@ -680,15 +638,6 @@ export default {
async onLeave() {
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() {
this.editingRoomInfo.title = trim(this.editingRoomInfo.title);
this.editingRoomInfo.description = trim(this.editingRoomInfo.description);
@ -718,6 +667,20 @@ export default {
</script>
<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-card :loading="isEditLoading">
<v-card-title>{{ $t("editRoom") }}</v-card-title>
@ -760,23 +723,12 @@ export default {
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="autoplayDisabled" max-width="500" persistent>
<v-alert color="indigo">
<div class="mb-5">
{{ $t("browserMuted") }}
</div>
<div class="text-center mb-3">
<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>
<JoinDialog
v-if="!preview"
:room-id="roomID"
:room-client="roomClient"
@connect.once="joinRoom"
></JoinDialog>
<v-dialog v-model="showRequestDialog" max-width="500">
<v-card max-height="600" class="d-flex flex-column">
<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")
}
func getOAuthTokenHandler(c echo.Context) (err error) {
func getUserTokenHandler(c echo.Context) (err error) {
data, ok := c.Get("data").(*SessionData)
if !ok {
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 {
return nil, err
}
if u.AvatarFile != "" {
// os.Remove(u.getAvatarImagePath(u.AvatarFile))
}
isAvatarNew = true
}
@ -94,7 +91,7 @@ func (u *AudonUser) createGIF(avatar image.Image) ([]byte, error) {
baseFrame := image.NewRGBA(avatarPNG.Bounds())
draw.Draw(baseFrame, baseFrame.Bounds(), image.Black, image.Point{}, draw.Src)
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)
defer anim.ReleaseMemory()
@ -115,7 +112,7 @@ func (u *AudonUser) createGIF(avatar image.Image) ([]byte, error) {
}
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 {
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
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,
Audon: user,
}
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))
}
mastoAccount := new(MastodonAccount)
if err := c.Bind(&mastoAccount); err != nil {
c.Logger().Error(err)
return ErrInvalidRequestFormat
}
avatarLink := c.FormValue("avatar")
if avatarLink != "" {
roomMetadata.MastodonAccounts[user.AudonID] = mastoAccount
// 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)
if err != nil {
c.Logger().Error(err)
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
req, err := http.NewRequest(http.MethodGet, avatarURL.String(), nil)
if err != nil {
@ -362,12 +384,14 @@ func joinRoomHandler(c echo.Context) (err error) {
return echo.NewHTTPError(http.StatusInternalServerError)
}
// Generate indicator GIF
indicator, err := user.GetIndicator(c.Request().Context(), fnew)
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.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(indicator))
} else if err != nil {
c.Logger().Error(err)
}
@ -392,6 +416,26 @@ func joinRoomHandler(c echo.Context) (err error) {
c.Logger().Error(err)
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
@ -444,7 +488,7 @@ func leaveRoomHandler(c echo.Context) error {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
} else if still {
return c.NoContent(http.StatusAccepted)
return c.NoContent(http.StatusConflict)
}
if err := user.ClearUserAvatar(c.Request().Context()); err != nil {
c.Logger().Error(err)

Wyświetl plik

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

Wyświetl plik

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

54
user.go
Wyświetl plik

@ -3,11 +3,37 @@ package main
import (
"context"
"net/http"
"time"
"github.com/labstack/echo/v4"
mastodon "github.com/mattn/go-mastodon"
"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 {
audonID := c.Param("id")
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)
}
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 {
if a == nil {
return false
@ -40,12 +78,26 @@ func (a *AudonUser) InLivekit(ctx context.Context) (bool, error) {
}
func (a *AudonUser) ClearUserAvatar(ctx context.Context) error {
// os.Remove(a.getAvatarImagePath(a.AvatarFile))
coll := mainDB.Collection(COLLECTION_USER)
_, err := coll.UpdateOne(ctx,
bson.D{{Key: "audon_id", Value: a.AudonID}},
bson.D{
{Key: "$set", Value: bson.D{{Key: "avatar", Value: ""}}},
})
// if err == nil {
// os.Remove(a.getAvatarImagePath(a.AvatarFile))
// }
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 {
room, err := findRoomByID(c.Request().Context(), event.GetRoom().GetName())
lkRoom := event.GetRoom()
room, err := findRoomByID(c.Request().Context(), lkRoom.GetName())
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusNotFound)
@ -66,34 +67,36 @@ func livekitWebhookHandler(c echo.Context) error {
countdown := time.NewTimer(10 * time.Second)
webhookTimerCache.Set(audonID, countdown, ttlcache.DefaultTTL)
<-countdown.C
webhookTimerCache.Delete(audonID)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// ctx := context.TODO()
go func() {
<-countdown.C
webhookTimerCache.Delete(audonID)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
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)
stillAgain, err := user.InLivekit(ctx)
if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
log.Println(err)
}
avatar := user.getAvatarImagePath(user.AvatarFile)
_, err = updateAvatar(ctx, mastoClient, avatar)
if err != nil {
c.Logger().Warn(err)
if stillAgain {
return
}
user.ClearUserAvatar(ctx)
// os.Remove(avatar)
} else if err != nil {
c.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError)
}
nextUser, err := findUserByID(ctx, audonID)
if err == nil && nextUser.AvatarFile != "" {
log.Printf("restoring avatar: %s\n", audonID)
if err != nil {
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)
} else if event.GetEvent() == webhook.EventRoomStarted {