kopia lustrzana https://codeberg.org/nmkj/audon
Porównaj commity
90 Commity
v0.1.0-alp
...
main
Autor | SHA1 | Data |
---|---|---|
Namekuji | 7adba191e6 | |
Namekuji | 3cc4ee4f84 | |
Namekuji | a606f7f4fb | |
Namekuji | 1bb384a10c | |
Namekuji | 0fc4e5fbec | |
Namekuji | 30cc7930fc | |
Namekuji | 6c4ef49085 | |
Namekuji | 85979a7ebf | |
Namekuji | 6f7b58f2e2 | |
Namekuji | 03e5cb4f02 | |
Namekuji | 87300ea112 | |
Namekuji | 7b837651dd | |
Namekuji | c40fb71147 | |
Namekuji | d52ae18c44 | |
Namekuji | 3138e2a596 | |
Namekuji | 80963c96e6 | |
Namekuji | 32dbfbc205 | |
Namekuji | e2a5fb6fd6 | |
Namekuji | f11d4f016a | |
Namekuji | fec8e75a74 | |
Namekuji | 150ad6df05 | |
Namekuji | db42f13ee4 | |
Namekuji | 8dc11e7f84 | |
Namekuji | 3ad0378bd2 | |
Namekuji | cff4f9eaab | |
Namekuji | 55ca35c3c8 | |
Namekuji | d4d1ab6792 | |
Namekuji | e1bbda00c8 | |
Namekuji | 06ed20cd98 | |
Namekuji | 2b70a85bca | |
Namekuji | 6088af7101 | |
Namekuji | a7f80e2ef0 | |
Namekuji | e28f2edb2e | |
Namekuji | 6adbf5de60 | |
Namekuji | b571b4df30 | |
Namekuji | 9543928276 | |
Namekuji | abfb2f62f4 | |
Namekuji | 7028f5834c | |
Namekuji | 8bfd860c24 | |
Namekuji | 63e2be37e2 | |
Namekuji | ca77984520 | |
Namekuji | 2047c3b691 | |
Namekuji | 1a6b838c9c | |
Namekuji | b21aa42aaa | |
Namekuji | 17e07d28a1 | |
Namekuji | 073510d4b5 | |
Namekuji | 9f6238e688 | |
Namekuji | 5b7c6735ed | |
Namekuji | 906170c110 | |
Namekuji | 7ce3aad1f8 | |
Namekuji | fac090e68f | |
Namekuji | 0888bf758e | |
Namekuji | ea2a771349 | |
Namekuji | 26ec5e7086 | |
Namekuji | cc4337ebcb | |
Namekuji | 44e0b2b3d7 | |
Namekuji | 3d24066d2d | |
Namekuji | 693ea59845 | |
Namekuji | 29dc87b382 | |
Namekuji | bd3dc5434b | |
Namekuji | f9c35e0170 | |
Namekuji | a91d7d340a | |
Namekuji | 3469d62210 | |
Namekuji | 7ceb656bdf | |
Namekuji | 9c5e5f2d7c | |
Namekuji | 82cd78fef8 | |
Namekuji | 465cc05cf0 | |
Namekuji | ea77efb601 | |
Namekuji | 0b3f350da5 | |
Namekuji | 66f7d89cf7 | |
Namekuji | c98d6fad84 | |
Namekuji | 15ede80387 | |
Namekuji | 60de3630eb | |
Namekuji | 5ca7d22b94 | |
Namekuji | 0755f91e61 | |
Namekuji | 609fd256b3 | |
Namekuji | ba6edbbe55 | |
Namekuji | 393b03ffd9 | |
Namekuji | b050c7c7c9 | |
Namekuji | f323b7459e | |
Namekuji | c4b260f989 | |
Namekuji | aa93feca49 | |
Namekuji | ee9c1eda41 | |
Namekuji | 11c24d095c | |
Namekuji | 3d67718b2c | |
Namekuji | d1f02d9296 | |
Namekuji | 7cd0f324b1 | |
Namekuji | 1f084e12bc | |
Namekuji | 7545855b00 | |
Namekuji | 48350175f0 |
|
@ -0,0 +1 @@
|
|||
certs/*.pem
|
|
@ -0,0 +1,23 @@
|
|||
audon.localhost {
|
||||
tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem
|
||||
encode zstd gzip
|
||||
@backend {
|
||||
path /app/* /api/* /storage/* /u/*
|
||||
}
|
||||
handle @backend {
|
||||
reverse_proxy devcontainer:8100 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
handle {
|
||||
reverse_proxy devcontainer:5173 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
livekit.localhost {
|
||||
tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem
|
||||
encode zstd gzip
|
||||
reverse_proxy livekit:7880
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/universal
|
||||
{
|
||||
"name": "Audon Dev Container",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
// "image": "mcr.microsoft.com/devcontainers/universal:2-linux"
|
||||
"dockerComposeFile": "docker-compose.dev.yaml",
|
||||
"service": "devcontainer",
|
||||
"workspaceFolder": "/audon-go",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers/features/go:1": {},
|
||||
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
|
||||
"packages": "libmagick++-dev,libwebp-dev"
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": []
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "onCreateCommand": "",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"Vue.volar",
|
||||
"esbenp.prettier-vscode",
|
||||
"EditorConfig.EditorConfig"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
"remoteUser": "root"
|
||||
}
|
|
@ -1,18 +1,22 @@
|
|||
# Use root/example as user/password credentials
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
devcontainer:
|
||||
image: "mcr.microsoft.com/devcontainers/base:debian"
|
||||
volumes:
|
||||
- ..:/audon-go:cached
|
||||
command: sleep infinity
|
||||
environment:
|
||||
- "DOCKER_HOST=unix:///run/user/1000/docker.sock"
|
||||
|
||||
db:
|
||||
image: mongo:6
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "27017:27017"
|
||||
# ports:
|
||||
# - "27017:27017"
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: mongo
|
||||
MONGO_INITDB_ROOT_PASSWORD: mongo
|
||||
# volumes:
|
||||
# - ./mongo:/data/db
|
||||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
|
@ -20,34 +24,40 @@ services:
|
|||
ports:
|
||||
- 8081:8081
|
||||
environment:
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME:
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: example
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: mongo
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: mongo
|
||||
ME_CONFIG_MONGODB_URL: mongodb://mongo:mongo@db:27017/
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server
|
||||
ports:
|
||||
- "6379:6379"
|
||||
# volumes:
|
||||
# - ./redis:/data
|
||||
# - ./config/redis.conf:/etc/redis.conf
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
|
||||
redisinsight:
|
||||
image: redislabs/redisinsight:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8001:8001
|
||||
# volumes:
|
||||
# - redisinsight:/db
|
||||
- 8082:8001
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.3
|
||||
command: --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
depends_on:
|
||||
- redis
|
||||
ports:
|
||||
- "7881:7881"
|
||||
- "7882:7882/udp"
|
||||
volumes:
|
||||
- ./config/livekit.yaml:/etc/livekit.yaml
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
|
||||
caddy:
|
||||
image: caddy:2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./certs:/etc/caddy/certs:ro
|
|
@ -0,0 +1,30 @@
|
|||
# main TCP port for RoomService and RTC endpoint
|
||||
# for production setups, this port should be placed behind a load balancer with TLS
|
||||
port: 7880
|
||||
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
port_range_start: 7882
|
||||
port_range_end: 7882
|
||||
|
||||
turn:
|
||||
enabled: false
|
||||
|
||||
keys:
|
||||
devkey: secret
|
||||
|
||||
webhook:
|
||||
api_key: devkey
|
||||
urls:
|
||||
- http://devcontainer:8100/app/webhook
|
||||
|
||||
room:
|
||||
auto_create: false
|
||||
empty_timeout: 30
|
||||
max_participants: 0
|
||||
max_metadata_size: 0
|
||||
|
||||
audio:
|
||||
active_level: 50
|
||||
min_percentile: 20
|
||||
update_interval: 200
|
|
@ -0,0 +1 @@
|
|||
public/storage/
|
|
@ -28,3 +28,12 @@ LIVEKIT_API_SECRET=secret
|
|||
LIVEKIT_HOST=livekit.example.com
|
||||
# This value will be returned by Audon backend to browsers. Set the same domain as LIVEKIT_HOST if you are not sure.
|
||||
LIVEKIT_LOCAL_DOMAIN=livekit.example.com
|
||||
# If this period (seconds) passes, the new room will be automatically closed.
|
||||
LIVEKIT_EMPTY_ROOM_TIMEOUT=300
|
||||
|
||||
### Bot Settings ###
|
||||
# Leave the following fields empty to disable the notification bot.
|
||||
BOT_SERVER=
|
||||
BOT_CLIENT_ID=
|
||||
BOT_CLIENT_SECRET=
|
||||
BOT_ACCESS_TOKEN=
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
# Go workspace file
|
||||
go.work
|
||||
__debug_bin
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
|
@ -65,6 +66,7 @@ build/Release
|
|||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
.pnpm-store/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
@ -188,3 +190,5 @@ config/*
|
|||
# Ignore data directories
|
||||
mongo/
|
||||
redis/
|
||||
cache/
|
||||
public/storage
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"name": "Launch Go",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
audon-fe/src/assets/boop.oga
|
||||
|
||||
Copyright: Lucas McCallister
|
||||
URL: http://www.freesound.org/samplesViewSingle.php?id=67091
|
||||
License: CC-BY-SA (relicensed with permission from author)
|
||||
|
||||
audon-fe/src/assets/message.oga
|
||||
|
||||
Copyright: Ivica Bukvic
|
||||
URL: http://gnome-look.org/content/show.php/%22Borealis%22+sound+theme?content=12584
|
||||
License: CC-BY-SA
|
18
Dockerfile
18
Dockerfile
|
@ -4,8 +4,9 @@ WORKDIR /workspace
|
|||
|
||||
COPY audon-fe/ /workspace/
|
||||
|
||||
RUN npm install && \
|
||||
npm run build
|
||||
RUN npm install -g pnpm && \
|
||||
pnpm install && \
|
||||
pnpm run build
|
||||
|
||||
FROM golang:1.19-bullseye
|
||||
|
||||
|
@ -13,22 +14,27 @@ WORKDIR /workspace
|
|||
|
||||
COPY go.mod /workspace/go.mod
|
||||
COPY go.sum /workspace/go.sum
|
||||
RUN go mod download
|
||||
RUN go mod download -x
|
||||
|
||||
COPY *.go /workspace/
|
||||
|
||||
RUN go build -v -o audon-bin .
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install libmagick++-dev libwebp-dev && \
|
||||
go build -v -o audon-bin .
|
||||
|
||||
FROM debian:bullseye
|
||||
FROM ubuntu:jammy
|
||||
|
||||
WORKDIR /audon
|
||||
|
||||
COPY --from=0 /workspace/dist /audon/audon-fe/dist
|
||||
COPY --from=1 /workspace/audon-bin /audon/
|
||||
COPY locales /audon/locales
|
||||
COPY public /audon/public
|
||||
|
||||
RUN echo "Etc/UTC" > /etc/localtime && \
|
||||
RUN echo "UTC" > /etc/localtime && \
|
||||
apt-get update && apt-get upgrade -y && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
imagemagick webp \
|
||||
tini \
|
||||
tzdata \
|
||||
ca-certificates
|
||||
|
|
|
@ -4,13 +4,19 @@
|
|||
|
||||
----
|
||||
|
||||
This repository is archived and will no longer be updated. Successor: [Hamabē](https://codeberg.org/hamabe/hamabe)
|
||||
|
||||
----
|
||||
|
||||
<div align="right">
|
||||
<img src="audon-fe/src/assets/img/mascot.webp" alt="Mascot" width="150" align="right" title="Mascot designed by Taiyo Fujii" />
|
||||
</div>
|
||||
|
||||
Audio + Mastodon = Audon
|
||||
|
||||
Audon is a service for Mastodon (and Pleroma) users to create and join rooms of live audio conversations.
|
||||
Audon is a service of realtime audio chat for Mastodon, Akkoma, GoToSocial, and Calckey.
|
||||
|
||||
Other Fediverse platforms supporting Mastodon API may work, but not tested (yet).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Audon</title>
|
||||
<meta name="description" content="Audon: audio conversation, radio talk room for Mastodon (and Pleroma)">
|
||||
<meta name="description" content="Audon: audio spaces for Mastodon.">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta property="og:site_name" content='{% printf "Audon hosted on %s" .Config.LocalDomain %}'>
|
||||
<meta property="og:image" content='{% printf "https://%s/static/opengraph.80fbb63d.png" .Config.LocalDomain %}'>
|
||||
<meta property="og:image:width" content='1200'>
|
||||
|
@ -20,15 +21,15 @@
|
|||
{% if .Room.Description %}
|
||||
<meta property="og:description" content='{% printf "%.150s" .Room.Description %}'>
|
||||
{% else %}
|
||||
<meta property="og:description" content='Audon: audio conversation, radio talk room for Mastodon (and Pleroma)'>
|
||||
<meta property="og:description" content='Audon: audio spaces for Mastodon'>
|
||||
{% end %}
|
||||
{% else %}
|
||||
<meta property="og:title" content='Audon'>
|
||||
<meta property="og:description" content='Audio conversation, radio talk room for Mastodon (and Pleroma)'>
|
||||
<meta property="og:description" content='Audio spaces for Mastodon'>
|
||||
{% end %}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-version='0.1.0-alpha'></div>
|
||||
<div id="app" data-version='0.3.2'></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audon-fe",
|
||||
"version": "0.1.0-alpha",
|
||||
"version": "0.3.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cp -v index.dev.html index.html && vite",
|
||||
|
@ -9,31 +9,35 @@
|
|||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.8.2",
|
||||
"@picmo/popup-picker": "^5.7.6",
|
||||
"@picmo/renderer-twemoji": "^5.7.6",
|
||||
"@uriopass/nosleep.js": "^0.12.2",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"@vueuse/core": "^9.6.0",
|
||||
"axios": "^1.2.0",
|
||||
"livekit-client": "^1.5.0",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"axios": "^1.3.3",
|
||||
"howler": "^2.2.3",
|
||||
"livekit-client": "^1.6.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.1.1",
|
||||
"masto": "^4.9.0",
|
||||
"pinia": "^2.0.26",
|
||||
"vue": "^3.2.45",
|
||||
"luxon": "^3.2.1",
|
||||
"masto": "^5.10.0",
|
||||
"picmo": "^5.7.6",
|
||||
"pinia": "^2.0.31",
|
||||
"vue": "^3.2.47",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuetify": "^3.0.3"
|
||||
"vuetify": "^3.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/js": "^7.0.96",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@mdi/js": "^7.1.96",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"prettier": "^2.7.1",
|
||||
"vite": "^3.2.4",
|
||||
"vite-plugin-vuetify": "^1.0.0"
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"prettier": "^2.8.4",
|
||||
"vite": "^3.2.5",
|
||||
"vite-plugin-vuetify": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -1,11 +1,10 @@
|
|||
<script>
|
||||
import { RouterView, RouterLink } from "vue-router";
|
||||
import { RouterView } from "vue-router";
|
||||
import locales from "./locales";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RouterView,
|
||||
RouterLink,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
|
@ -33,13 +32,11 @@ export default {
|
|||
</div>
|
||||
<v-system-bar window>
|
||||
<div class="d-flex justify-center align-center w-100">
|
||||
<RouterLink :to="{ name: 'home' }" class="d-flex align-center">
|
||||
<img
|
||||
height="20"
|
||||
src="./assets/img/audon-logo-orange.svg"
|
||||
alt="Branding Logo"
|
||||
/>
|
||||
</RouterLink>
|
||||
<img
|
||||
height="20"
|
||||
src="./assets/img/audon-logo-orange.svg"
|
||||
alt="Branding Logo"
|
||||
/>
|
||||
</div>
|
||||
</v-system-bar>
|
||||
<v-main>
|
||||
|
@ -51,7 +48,7 @@ export default {
|
|||
id="mainArea"
|
||||
>
|
||||
<v-col>
|
||||
<v-responsive class="mx-auto" max-width="600px">
|
||||
<v-responsive id="mainContainer" class="mx-auto" max-width="600px">
|
||||
<RouterView />
|
||||
</v-responsive>
|
||||
</v-col>
|
||||
|
@ -91,7 +88,7 @@ export default {
|
|||
<style>
|
||||
#mascot {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
@ -109,4 +106,8 @@ export default {
|
|||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
background-color: rgba(18, 18, 18, 0.8);
|
||||
}
|
||||
</style>
|
||||
|
|
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 45 KiB |
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 19 KiB Po Szerokość: | Wysokość: | Rozmiar: 24 KiB |
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
|
@ -29,7 +29,10 @@ body,
|
|||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Hiragino Sans", "Noto Sans CJK JP", "Original Yu Gothic", "Yu Gothic", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Sans Emoji";
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Hiragino Sans", "Noto Sans CJK JP", "Original Yu Gothic", "Yu Gothic",
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Sans Emoji";
|
||||
}
|
||||
|
||||
a.plain {
|
||||
|
|
|
@ -3,7 +3,6 @@ import router from "../router";
|
|||
|
||||
export const validators = {
|
||||
fqdn: helpers.regex(
|
||||
/^[a-zA-Z]([a-zA-Z0-9\-]+[\.]?)*[a-zA-Z0-9]$/,
|
||||
/^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z]{1}[a-zA-Z0-9]{0,62})\.?$/
|
||||
),
|
||||
};
|
||||
|
@ -11,7 +10,8 @@ export const validators = {
|
|||
export function webfinger(user) {
|
||||
if (!user) return "";
|
||||
const url = new URL(user.url);
|
||||
return `${user.acct}@${url.host}`;
|
||||
const finger = user.username.split("@");
|
||||
return `${finger[0]}@${url.host}`;
|
||||
}
|
||||
|
||||
export function pushNotFound(route) {
|
||||
|
|
|
@ -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;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
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 {
|
||||
this.donStore.avatar = this.roomToken.original;
|
||||
if (indicator && this.uploadEnabled) {
|
||||
this.uploading = true;
|
||||
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>
|
|
@ -1,20 +1,29 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { mdiMicrophone, mdiMicrophoneOff } from "@mdi/js";
|
||||
import { webfinger } from "../assets/utils";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
mdiMicrophone,
|
||||
mdiMicrophoneOff,
|
||||
webfinger,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
talking: Boolean,
|
||||
type: String,
|
||||
data: Object,
|
||||
muted: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mdiMicrophone,
|
||||
mdiMicrophoneOff,
|
||||
};
|
||||
emoji: String,
|
||||
preview: Boolean,
|
||||
enableMenu: Boolean,
|
||||
},
|
||||
computed: {
|
||||
showEmoji() {
|
||||
return this.emoji !== undefined;
|
||||
},
|
||||
canSpeak() {
|
||||
return (
|
||||
this.type === "host" ||
|
||||
|
@ -26,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:
|
||||
|
@ -47,9 +56,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
webfinger,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -62,7 +68,24 @@ export default {
|
|||
:color="badgeProps.colour"
|
||||
>
|
||||
<v-avatar :class="{ rounded: true, talk: talking }" size="70">
|
||||
<v-img :src="data?.avatar"></v-img>
|
||||
<v-overlay
|
||||
v-model="showEmoji"
|
||||
contained
|
||||
persistent
|
||||
scroll-strategy="none"
|
||||
no-click-animation
|
||||
scrim="#000000"
|
||||
class="align-center justify-center reaction"
|
||||
>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<img class="emoji" :src="emoji" />
|
||||
</div>
|
||||
</v-overlay>
|
||||
<v-img
|
||||
:class="{ cursorPointer: enableMenu }"
|
||||
:id="`mod-${data?.identity}`"
|
||||
:src="data?.avatar"
|
||||
></v-img>
|
||||
</v-avatar>
|
||||
</v-badge>
|
||||
<v-avatar
|
||||
|
@ -70,15 +93,54 @@ export default {
|
|||
:class="{ rounded: true, talk: talking, 'mt-2': true }"
|
||||
size="70"
|
||||
>
|
||||
<v-img :src="data?.avatar"></v-img>
|
||||
<v-overlay
|
||||
v-model="showEmoji"
|
||||
contained
|
||||
persistent
|
||||
scroll-strategy="none"
|
||||
no-click-animation
|
||||
scrim="#000000"
|
||||
class="align-center justify-center reaction"
|
||||
>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<img class="emoji" :src="emoji" />
|
||||
</div>
|
||||
</v-overlay>
|
||||
<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"
|
||||
v-if="canSpeak && !preview"
|
||||
:icon="muted ? mdiMicrophoneOff : mdiMicrophone"
|
||||
></v-icon>
|
||||
<a :href="data?.url" class="plain" target="_blank">{{
|
||||
data?.displayName ?? webfinger(data)
|
||||
!data?.displayName ? webfinger(data) : data?.displayName
|
||||
}}</a>
|
||||
</h4>
|
||||
</v-col>
|
||||
|
@ -88,4 +150,12 @@ export default {
|
|||
.talk {
|
||||
outline: 3px solid cornflowerblue;
|
||||
}
|
||||
|
||||
.reaction img {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.cursorPointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,41 +6,67 @@ 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"
|
||||
copy: "Copy"
|
||||
copied: "Copied"
|
||||
enterRoom: "Enter"
|
||||
leaveRoom: "Leave"
|
||||
closeRoom: "Close"
|
||||
leaveRoom: "Leave but keep this room open"
|
||||
closeRoom: "Close this room"
|
||||
close: "Close"
|
||||
emojiReaction: "Open emoji reaction picker"
|
||||
micStatus:
|
||||
mute: "Mute microphone"
|
||||
unmute: "Unmute microphone"
|
||||
retry: "Retry enabling microphone"
|
||||
request: "Send speaker request"
|
||||
roomOperation:
|
||||
operation: "Leave or close"
|
||||
leave: "Leave this room"
|
||||
close: "Close this room"
|
||||
openRequests: "Open list of speaker requests"
|
||||
edit: "Edit room information"
|
||||
requestOperation:
|
||||
decline: "Decline this speaker request"
|
||||
accept: "Accept this speaker request"
|
||||
connecting: "Connecting"
|
||||
server: "Your Mastodon instance"
|
||||
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?"
|
||||
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."
|
||||
form:
|
||||
title: "Title"
|
||||
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:
|
||||
everyone: "Everyone"
|
||||
following: "Followed accounts only"
|
||||
follower: "Followers-only"
|
||||
knowing: "Followed accounts and/or followers"
|
||||
following: "Followees only (Accounts you're following)"
|
||||
follower: "Followers only (Accounts following you)"
|
||||
knowing: "Followees 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!"
|
||||
message: "Your room \"{title}\" is now ready. Share the following URL with other participants."
|
||||
timeout: "The room will be closed automatically if you don't enter within {minutes} minutes."
|
||||
errors:
|
||||
offline: "This user is not hosting now."
|
||||
invalidAddress: "Invalid address"
|
||||
serverNotFound: "Instance not found"
|
||||
notFound: "{value} not found"
|
||||
|
@ -56,15 +82,31 @@ 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 you leave."
|
||||
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?"
|
||||
norequest: "No request"
|
||||
sent: "Request sent!"
|
||||
receive: "New speaker request received!"
|
||||
microphoneBlocked: "Your browser has blocked access to the microphone."
|
||||
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"
|
||||
|
|
|
@ -29,6 +29,7 @@ form:
|
|||
cohosts: "Cohôtes"
|
||||
cohostCanAlwaysJoin: "Cohôtes peuvent s'y joindre, quel que soit le paramètre de confidentialité."
|
||||
schedule: "Programmée à"
|
||||
advertise: "Autorisez le robot ({bot}) à faire de la publicité pour votre salle"
|
||||
relationships:
|
||||
everyone: "Tout le monde"
|
||||
following: "Vos abonnements"
|
||||
|
@ -63,7 +64,7 @@ speakRequest:
|
|||
norequest: "Pas de demande"
|
||||
sent: "Demande envoyée!"
|
||||
receive: "Nouvelle demande de parole reçue !"
|
||||
microphoneBlocked: "Votre navigateur a bloqué l'accès au microphone."
|
||||
microphoneBlocked: "Votre navigateur a bloqué l'accès au microphone. Vérifiez les paramètres d'autorisation de votre appareil et de votre navigateur."
|
||||
closeRoomConfirm: "Vous êtes sûr de vouloir fermer cette salle ?"
|
||||
roomEvent:
|
||||
closedByHost: "L'hôte a fermé cette salle."
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
about: "Audonについて"
|
||||
about: "Audon って何?"
|
||||
back: "戻る"
|
||||
login: "ログイン"
|
||||
logout: "ログアウト"
|
||||
|
@ -6,20 +6,41 @@ logoutConfirm: "Audon からログアウトしますか?"
|
|||
loginRequired: "続行するにはログインする必要があります。"
|
||||
create: "作成"
|
||||
cancel: "キャンセル"
|
||||
sure: "はい"
|
||||
nope: "いいえ"
|
||||
edit: "編集"
|
||||
save: "保存"
|
||||
share: "シェア"
|
||||
copy: "コピー"
|
||||
copied: "コピーしました"
|
||||
enterRoom: "入室"
|
||||
leaveRoom: "退室"
|
||||
closeRoom: "閉室"
|
||||
leaveRoom: "部屋を閉じずに退室"
|
||||
closeRoom: "部屋を閉じる"
|
||||
close: "閉じる"
|
||||
emojiReaction: "絵文字ピッカーを開く"
|
||||
micStatus:
|
||||
mute: "マイクをミュート"
|
||||
unmute: "マイクのミュートを解除"
|
||||
retry: "マイク有効化を再試行"
|
||||
request: "発言リクエストを送る"
|
||||
roomOperation:
|
||||
operation: "退室または閉室"
|
||||
leave: "部屋から退室する"
|
||||
close: "部屋を閉室する"
|
||||
openRequests: "発言リクエストの一覧を開く"
|
||||
edit: "部屋の情報を編集する"
|
||||
connecting: "接続中"
|
||||
server: "Mastodon サーバー"
|
||||
addressRequired: "アドレスを入力してください"
|
||||
createNewRoom: "部屋を作成"
|
||||
editRoom: "部屋の編集"
|
||||
comingFuture: "今後のアップデートで追加予定"
|
||||
processing: "処理中です。<br />画面を閉じないでください。"
|
||||
lostWarning: "この画面を閉じると保存前の内容が失われます。構いませんか?"
|
||||
cannotUndone: "この操作は取り消せません。続行しますか?"
|
||||
staticLink:
|
||||
title: "Audon リンク"
|
||||
hint: "あなたが部屋をホストしたとき、他の人はこの固定 URL からでも参加できます。"
|
||||
form:
|
||||
title: "タイトル"
|
||||
titleRequired: "部屋の名前を入力してください"
|
||||
|
@ -28,6 +49,7 @@ form:
|
|||
cohosts: "共同ホスト"
|
||||
cohostCanAlwaysJoin: "共同ホストは制限に関わらず入室できます。"
|
||||
schedule: "開始予約"
|
||||
advertise: "Bot({bot})による部屋の宣伝を許可する"
|
||||
relationships:
|
||||
everyone: "制限なし"
|
||||
following: "あなたのフォロー限定"
|
||||
|
@ -39,7 +61,9 @@ shareRoomMessage: "Audon で部屋を作りました!\n参加用リンク: {
|
|||
roomReady:
|
||||
header: "お部屋の用意ができました!"
|
||||
message: "{title} を作りました。参加者に以下の URL を共有してください。"
|
||||
timeout: "{minutes} 分以内に入室しないと部屋が閉じますのでご注意ください。"
|
||||
errors:
|
||||
offline: "このユーザーは現在ホスト中ではありません。"
|
||||
invalidAddress: "アドレスが有効ではありません"
|
||||
serverNotFound: "サーバーが見つかりません"
|
||||
notFound: "{value} が見つかりません"
|
||||
|
@ -55,15 +79,31 @@ errors:
|
|||
private: "この部屋は共同ホスト限定です。"
|
||||
default: "入室が許可されていません。"
|
||||
startListening: "視聴を始める"
|
||||
browserMuted: "ブラウザの設定により無音になっています。続行するには @:startListening ボタンを押してください。"
|
||||
browserMuted: "大きな音であなたが驚かないよう、無音になっています。続行するには @:startListening ボタンを押してください。"
|
||||
onlineIndicator:
|
||||
message: "あなたが部屋に参加中であることを表示しますか?「表示する」を選ぶとアカウントのアバターがこのようになります。"
|
||||
hint: "部屋から退出後、アバターは自動で元に戻ります。"
|
||||
warning: "サーバーが変更を反映するまで時間がかかることがあります。"
|
||||
sure: "表示する"
|
||||
nope: "表示しない"
|
||||
speakRequest:
|
||||
label: "発言リクエスト"
|
||||
dialog: "発言をリクエストしますか?"
|
||||
norequest: "リクエストはありません"
|
||||
sent: "発言リクエストを送信しました"
|
||||
receive: "新しい発言リクエストがあります"
|
||||
microphoneBlocked: "ブラウザがマイクの使用を許可していません。"
|
||||
microphoneBlocked: "マイクが禁止されています。ブラウザやデバイスの設定からマイクの使用を許可してください。"
|
||||
closeRoomConfirm: "この部屋を閉じますか?"
|
||||
roomEvent:
|
||||
closedByHost: "ホストにより部屋が閉じられました。"
|
||||
removed: "部屋から退去しました。"
|
||||
closedByHost: "部屋が閉じられました。"
|
||||
removed: "リクエストにより部屋から退去しました。"
|
||||
disconneced: "切断されました。"
|
||||
moderation:
|
||||
promote: "{role} にする"
|
||||
demote: "リスナー に戻す"
|
||||
kick: "追い出す"
|
||||
role:
|
||||
host: "ホスト"
|
||||
cohost: "共同ホスト"
|
||||
speaker: "スピーカー"
|
||||
listener: "リスナー"
|
||||
|
|
|
@ -53,6 +53,7 @@ axios.interceptors.response.use(undefined, (error) => {
|
|||
return Promise.reject(error);
|
||||
});
|
||||
router.beforeEach(async (to) => {
|
||||
document.title = to.meta.title ?? "Audon";
|
||||
const donStore = useMastodonStore();
|
||||
if ((!to.meta.noauth || to.name === "login") && !donStore.authorized) {
|
||||
try {
|
||||
|
@ -64,7 +65,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 } : {};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import LoginView from "../views/LoginView.vue";
|
||||
import RoomView from "../views/RoomView.vue";
|
||||
import NotFoundView from "../views/NotFoundView.vue";
|
||||
import ErrorView from "../views/ErrorView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -12,7 +12,15 @@ const router = createRouter({
|
|||
meta: {
|
||||
noauth: true,
|
||||
},
|
||||
component: NotFoundView,
|
||||
component: ErrorView,
|
||||
},
|
||||
{
|
||||
path: "/offline",
|
||||
name: "offline",
|
||||
meta: {
|
||||
noauth: true,
|
||||
},
|
||||
component: ErrorView,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { ref, computed } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCounterStore = defineStore("counter", () => {
|
||||
const count = ref(0);
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++;
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment };
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import axios from "axios";
|
||||
import { login } from "masto";
|
||||
import router from "../router";
|
||||
import { createClient } from "masto";
|
||||
import { webfinger } from "../assets/utils";
|
||||
|
||||
export const useMastodonStore = defineStore("mastodon", {
|
||||
|
@ -11,10 +10,11 @@ export const useMastodonStore = defineStore("mastodon", {
|
|||
oauth: {
|
||||
url: "",
|
||||
token: "",
|
||||
audon_id: "",
|
||||
audon: null,
|
||||
},
|
||||
client: null,
|
||||
userinfo: null,
|
||||
avatar: "",
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
|
@ -24,29 +24,50 @@ export const useMastodonStore = defineStore("mastodon", {
|
|||
}
|
||||
return "";
|
||||
},
|
||||
myStaticLink() {
|
||||
if (this.oauth.audon?.webfinger) {
|
||||
const url = new URL(location.href);
|
||||
return `${url.origin}/u/@${this.oauth.audon.webfinger}`;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async fetchToken() {
|
||||
const resp = await axios.get("/api/token");
|
||||
this.oauth = resp.data;
|
||||
const client = await login({
|
||||
const client = createClient({
|
||||
url: this.oauth.url,
|
||||
accessToken: this.oauth.token,
|
||||
disableVersionCheck: true,
|
||||
});
|
||||
this.client = client;
|
||||
this.userinfo = await client.accounts.verifyCredentials();
|
||||
const user = await client.v1.accounts.verifyCredentials();
|
||||
this.userinfo = user;
|
||||
this.authorized = true;
|
||||
},
|
||||
async callMastodonAPI(caller, ...args) {
|
||||
try {
|
||||
return await caller(...args);
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
this.$reset();
|
||||
router.push({ name: "login" });
|
||||
async updateAvatar(img, filename) {
|
||||
return;
|
||||
/*
|
||||
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()}_${filename}`),
|
||||
});
|
||||
*/
|
||||
},
|
||||
async revertAvatar() {
|
||||
const token = await axios.get("/api/token");
|
||||
const rooms = await axios.get("/api/room");
|
||||
if (
|
||||
token.data.audon.avatar &&
|
||||
(rooms.data.length === 0 ||
|
||||
(rooms.data.length === 1 && rooms.data[0].role === "host"))
|
||||
) {
|
||||
if (this.avatar) {
|
||||
await this.updateAvatar(this.avatar, token.data.audon.avatar);
|
||||
}
|
||||
throw error;
|
||||
await axios.delete("/api/room");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -13,7 +13,6 @@ import { useClipboard } from "@vueuse/core";
|
|||
import { useMastodonStore } from "../stores/mastodon";
|
||||
import { helpers, maxLength, required } from "@vuelidate/validators";
|
||||
import { debounce, some, map, truncate, trim } from "lodash-es";
|
||||
import { login } from "masto";
|
||||
import { webfinger } from "../assets/utils";
|
||||
import axios from "axios";
|
||||
|
||||
|
@ -33,7 +32,15 @@ export default {
|
|||
clipboard: useClipboard(),
|
||||
};
|
||||
},
|
||||
created() {
|
||||
async created() {
|
||||
const resp = await axios.get("/api/room");
|
||||
if (resp.data.length > 0) {
|
||||
const canCreate = !some(resp.data, { role: "host" });
|
||||
if (!canCreate) {
|
||||
alert(this.$t("errors.alreadyAdded"));
|
||||
this.$router.replace({ name: "home" });
|
||||
}
|
||||
}
|
||||
this.cohostSearch = debounce(this.search, 1000);
|
||||
},
|
||||
unmounted() {
|
||||
|
@ -53,7 +60,6 @@ export default {
|
|||
{ title: this.$t("form.relationships.mutual"), value: "mutual" },
|
||||
{ title: this.$t("form.relationships.private"), value: "private" },
|
||||
],
|
||||
scheduledAt: null,
|
||||
searchResult: null,
|
||||
searchQuery: "",
|
||||
isCandiadateLoading: false,
|
||||
|
@ -65,6 +71,7 @@ export default {
|
|||
},
|
||||
isSubmissionLoading: false,
|
||||
createdRoomID: "",
|
||||
advertise: true,
|
||||
};
|
||||
},
|
||||
validations() {
|
||||
|
@ -96,7 +103,10 @@ export default {
|
|||
if (!donURL) return "";
|
||||
const url = new URL(donURL);
|
||||
const texts = [
|
||||
this.$t("shareRoomMessage", { link: this.roomURL, title: this.title }),
|
||||
this.$t("shareRoomMessage", {
|
||||
link: this.donStore.myStaticLink,
|
||||
title: this.title,
|
||||
}),
|
||||
];
|
||||
if (this.description)
|
||||
texts.push(truncate("\n" + this.description, { length: 200 }));
|
||||
|
@ -106,11 +116,11 @@ export default {
|
|||
watch: {
|
||||
searchQuery(val) {
|
||||
this.isCandiadateLoading = false;
|
||||
this.searchError.enabled = false;
|
||||
this.cohostSearch.cancel();
|
||||
if (!val) return;
|
||||
if (some(this.cohosts, { finger: val })) {
|
||||
this.searchError.message = this.$t("errors.alreadyAdded");
|
||||
this.searchError.colour = "warning";
|
||||
this.searchError.enabled = true;
|
||||
return;
|
||||
}
|
||||
|
@ -120,30 +130,32 @@ export default {
|
|||
this.isCandiadateLoading = true;
|
||||
this.cohostSearch(val);
|
||||
},
|
||||
relationship(to) {
|
||||
this.advertise = to === "everyone";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async search(val) {
|
||||
const finger = val.split("@");
|
||||
if (finger.length < 2) return;
|
||||
else if (finger.length === 3) {
|
||||
finger.splice(0, 1);
|
||||
this.searchQuery = finger.join("@");
|
||||
if (finger.length < 2 || finger.length > 3) {
|
||||
this.searchError.message = this.$t("errors.invalidAddress");
|
||||
this.searchError.enabled = true;
|
||||
this.isCandiadateLoading = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(`https://${finger[1]}`);
|
||||
const client = await login({
|
||||
url: url.toString(),
|
||||
disableVersionCheck: true,
|
||||
const resp = await this.donStore.client.v1.accounts.search({
|
||||
q: val,
|
||||
resolve: true,
|
||||
});
|
||||
const user = await client.accounts.lookup({ acct: finger[0] });
|
||||
if (resp.length != 1) throw "";
|
||||
const user = resp[0];
|
||||
user.finger = webfinger(user);
|
||||
this.searchResult = user;
|
||||
this.searchError.enabled = false;
|
||||
} catch (error) {
|
||||
if (error.isMastoError && error.statusCode === 404) {
|
||||
this.searchError.message = this.$t("errors.notFound", { value: val });
|
||||
this.searchError.colour = "error";
|
||||
this.searchError.enabled = true;
|
||||
}
|
||||
this.searchError.message = this.$t("errors.notFound", { value: val });
|
||||
this.searchError.enabled = true;
|
||||
} finally {
|
||||
this.isCandiadateLoading = false;
|
||||
}
|
||||
|
@ -167,17 +179,19 @@ export default {
|
|||
title: this.title,
|
||||
description: this.description,
|
||||
cohosts: map(this.cohosts, (u) => ({
|
||||
remote_id: u.id,
|
||||
remote_url: u.url,
|
||||
webfinger: webfinger(u),
|
||||
})),
|
||||
restriction: this.relationship,
|
||||
advertise:
|
||||
this.advertise && this.relationship === "everyone"
|
||||
? this.$i18n.locale
|
||||
: "",
|
||||
};
|
||||
this.isSubmissionLoading = false;
|
||||
try {
|
||||
const resp = await axios.post("/api/room", payload);
|
||||
if (resp.status === 201) {
|
||||
this.createdRoomID = resp.data;
|
||||
// this.$router.push({ name: "room", params: { id: resp.data } });
|
||||
}
|
||||
} catch (error) {
|
||||
this.searchError.message = `Error: ${error}`;
|
||||
|
@ -198,7 +212,7 @@ export default {
|
|||
{{ $t("roomReady.message", { title }) }}
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<h3 style="word-break: break-all">{{ roomURL }}</h3>
|
||||
<h3 style="word-break: break-all">{{ donStore.myStaticLink }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<v-btn
|
||||
|
@ -210,7 +224,7 @@ export default {
|
|||
>{{ $t("share") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
@click="clipboard.copy(roomURL)"
|
||||
@click="clipboard.copy(donStore.myStaticLink)"
|
||||
color="lime"
|
||||
size="small"
|
||||
:prepend-icon="
|
||||
|
@ -219,7 +233,10 @@ export default {
|
|||
>{{ clipboard.copied.value ? $t("copied") : $t("copy") }}</v-btn
|
||||
>
|
||||
</div>
|
||||
<div class="text-center mt-10 mb-1">
|
||||
<v-alert class="mt-5" density="compact" type="warning" variant="tonal">{{
|
||||
$t("roomReady.timeout", { minutes: 5 })
|
||||
}}</v-alert>
|
||||
<div class="text-center mt-5 mb-1">
|
||||
<v-btn
|
||||
color="indigo"
|
||||
:to="{ name: 'room', params: { id: createdRoomID } }"
|
||||
|
@ -317,7 +334,7 @@ export default {
|
|||
<v-list lines="two">
|
||||
<v-list-item
|
||||
:key="0"
|
||||
:value="searchResult.acct"
|
||||
:value="webfinger(searchResult)"
|
||||
:title="searchResult.displayName"
|
||||
@click="onResultClick"
|
||||
>
|
||||
|
@ -356,17 +373,31 @@ export default {
|
|||
</v-text-field>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-text-field
|
||||
type="datetime-local"
|
||||
v-model="scheduledAt"
|
||||
:label="$t('form.schedule')"
|
||||
disabled
|
||||
:messages="[$t('comingFuture')]"
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
v-model="advertise"
|
||||
:disabled="relationship !== 'everyone'"
|
||||
density="compact"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<i18n-t keypath="form.advertise" tag="div">
|
||||
<template v-slot:bot>
|
||||
<a href="https://i.audon.space/@now" target="_blank"
|
||||
>now@i.audon.space</a
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn block color="indigo" @click="onSubmit" variant="flat">
|
||||
<v-btn
|
||||
block
|
||||
:disabled="isSubmissionLoading"
|
||||
color="indigo"
|
||||
@click="onSubmit"
|
||||
variant="flat"
|
||||
>
|
||||
{{ $t("create") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: this.$route.name,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.name === "offline") {
|
||||
alert(this.$t("errors.offline"));
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-alert v-if="this.name === 'notfound'" type="error">Page not found</v-alert>
|
||||
</template>
|
|
@ -1,18 +1,28 @@
|
|||
<script>
|
||||
import { useMastodonStore } from "../stores/mastodon";
|
||||
import axios from "axios";
|
||||
import { some } from "lodash-es";
|
||||
import { mdiLinkVariant } from "@mdi/js";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
mdiLinkVariant,
|
||||
donStore: useMastodonStore(),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canCreate: true,
|
||||
query: "",
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
const resp = await axios.get("/api/room");
|
||||
if (resp.data.length > 0) {
|
||||
this.canCreate = !some(resp.data, { role: "host" });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onLogout() {
|
||||
// if (!confirm(this.$t("logoutConfirm"))) return;
|
||||
|
@ -41,7 +51,7 @@ export default {
|
|||
{{ $t("logout") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="text-center my-10">
|
||||
<div class="text-center my-8">
|
||||
<v-avatar class="rounded" size="100">
|
||||
<v-img
|
||||
:src="donStore.userinfo?.avatar"
|
||||
|
@ -63,10 +73,33 @@ export default {
|
|||
<v-text-field v-mode="query"></v-text-field>
|
||||
</v-col> -->
|
||||
<v-col cols="12">
|
||||
<v-btn block :to="{ name: 'create' }" color="indigo">{{
|
||||
$t("createNewRoom")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
:disabled="!canCreate"
|
||||
block
|
||||
:to="{ name: 'create' }"
|
||||
color="indigo"
|
||||
>{{ $t("createNewRoom") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex justify-center mt-6">
|
||||
<v-alert
|
||||
:icon="mdiLinkVariant"
|
||||
:title="$t('staticLink.title')"
|
||||
variant="tonal"
|
||||
>
|
||||
<div class="my-1">
|
||||
<h4 style="word-break: break-all">
|
||||
<a
|
||||
:href="donStore.myStaticLink"
|
||||
@click.prevent=""
|
||||
class="text-white"
|
||||
>{{ donStore.myStaticLink }}</a
|
||||
>
|
||||
</h4>
|
||||
</div>
|
||||
<p>{{ $t("staticLink.hint") }}</p>
|
||||
</v-alert>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
|
|
@ -81,13 +81,14 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center mb-8">
|
||||
<div class="text-center mb-7">
|
||||
<img
|
||||
src="../assets/img/audon-wordmark-white-text.svg"
|
||||
:draggable="false"
|
||||
alt="Branding Wordmark"
|
||||
style="width: 100%; max-width: 200px"
|
||||
/>
|
||||
<p class="mt-2">Audio space for Mastodon</p>
|
||||
</div>
|
||||
<v-alert v-if="$route.query.l" type="warning" variant="text">
|
||||
<div>{{ $t("loginRequired") }}</div>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<script></script>
|
||||
|
||||
<template>
|
||||
<v-alert type="error">Page not found</v-alert>
|
||||
</template>
|
Plik diff jest za duży
Load Diff
|
@ -15,6 +15,7 @@ export default defineConfig({
|
|||
include: "./src/locales/*.yaml",
|
||||
}),
|
||||
],
|
||||
assetsInclude: ["**/*.oga"],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/labstack/echo/v4"
|
||||
mastodon "github.com/mattn/go-mastodon"
|
||||
"github.com/oklog/ulid/v2"
|
||||
|
@ -20,18 +21,18 @@ func verifyTokenInSession(c echo.Context) (bool, *mastodon.Account, error) {
|
|||
return false, nil, err
|
||||
}
|
||||
|
||||
mastoClient, err := getMastodonClient(c)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
mastoClient := getMastodonClient(data)
|
||||
if mastoClient == nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
acc, err := mastoClient.GetAccountCurrentUser(c.Request().Context())
|
||||
acctUrl, _ := url.Parse(acc.URL)
|
||||
finger := strings.Split(acc.Username, "@")
|
||||
webfinger := fmt.Sprintf("%s@%s", finger[0], acctUrl.Host)
|
||||
user, dbErr := findUserByID(c.Request().Context(), data.AudonID)
|
||||
|
||||
if err != nil || dbErr != nil || string(acc.ID) != user.RemoteID {
|
||||
if err != nil || dbErr != nil || webfinger != user.Webfinger {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,7 @@ func verifyTokenInSession(c echo.Context) (bool, *mastodon.Account, error) {
|
|||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
ServerHost string `validate:"required,hostname,fqdn" form:"server"`
|
||||
ServerHost string `validate:"required,fqdn" form:"server"`
|
||||
Redirect string `validate:"url_encoded" form:"redir"`
|
||||
}
|
||||
|
||||
|
@ -65,14 +66,14 @@ func loginHandler(c echo.Context) (err error) {
|
|||
req.Redirect = "/"
|
||||
}
|
||||
|
||||
appConfig, err := getAppConfig(serverURL.String(), req.Redirect)
|
||||
appConfig, err := getAppConfig(serverURL.String())
|
||||
if err != nil {
|
||||
return ErrInvalidRequestFormat
|
||||
}
|
||||
// mastApp, err := mastodon.RegisterApp(c.Request().Context(), appConfig)
|
||||
mastApp, err := registerApp(c.Request().Context(), appConfig)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
c.Logger().Warn(err)
|
||||
return echo.NewHTTPError(http.StatusNotFound, "server_not_found")
|
||||
}
|
||||
|
||||
|
@ -88,15 +89,24 @@ func loginHandler(c echo.Context) (err error) {
|
|||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.String(http.StatusCreated, mastApp.AuthURI)
|
||||
redirURL, err := url.Parse(mastApp.AuthURI)
|
||||
if err != nil {
|
||||
c.Logger().Warn(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "invalid_auth_uri")
|
||||
}
|
||||
q := redirURL.Query()
|
||||
q.Add("state", req.Redirect)
|
||||
redirURL.RawQuery = q.Encode()
|
||||
|
||||
return c.String(http.StatusCreated, redirURL.String())
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type OAuthRequest struct {
|
||||
Code string `query:"code"`
|
||||
Redirect string `query:"redir"`
|
||||
Code string `query:"code"`
|
||||
State string `query:"state"`
|
||||
}
|
||||
|
||||
// handler for GET to /app/oauth?code=****
|
||||
|
@ -121,7 +131,7 @@ func oauthHandler(c echo.Context) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appConf, err := getAppConfig(data.MastodonConfig.Server, req.Redirect)
|
||||
appConf, err := getAppConfig(data.MastodonConfig.Server)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
@ -141,7 +151,10 @@ func oauthHandler(c echo.Context) (err error) {
|
|||
}
|
||||
|
||||
coll := mainDB.Collection(COLLECTION_USER)
|
||||
if result, dbErr := findUserByRemote(c.Request().Context(), string(acc.ID), acc.URL); dbErr == mongo.ErrNoDocuments {
|
||||
acctUrl, _ := url.Parse(acc.URL)
|
||||
finger := strings.Split(acc.Username, "@")
|
||||
webfinger := fmt.Sprintf("%s@%s", finger[0], acctUrl.Host)
|
||||
if result, dbErr := findUserByWebfinger(c.Request().Context(), webfinger); dbErr == mongo.ErrNoDocuments {
|
||||
entropy := ulid.Monotonic(rand.Reader, 0)
|
||||
id, err := ulid.New(ulid.Timestamp(time.Now().UTC()), entropy)
|
||||
if err != nil {
|
||||
|
@ -149,12 +162,11 @@ func oauthHandler(c echo.Context) (err error) {
|
|||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
data.AudonID = id.String()
|
||||
acctUrl, _ := url.Parse(acc.URL)
|
||||
newUser := AudonUser{
|
||||
AudonID: data.AudonID,
|
||||
RemoteID: string(acc.ID),
|
||||
RemoteURL: acc.URL,
|
||||
Webfinger: fmt.Sprintf("%s@%s", acc.Username, acctUrl.Host),
|
||||
Webfinger: webfinger,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
if _, insertErr := coll.InsertOne(c.Request().Context(), newUser); insertErr != nil {
|
||||
|
@ -175,20 +187,23 @@ func oauthHandler(c echo.Context) (err error) {
|
|||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, req.Redirect)
|
||||
// return c.Redirect(http.StatusFound, "http://localhost:5173")
|
||||
return c.Redirect(http.StatusFound, req.State)
|
||||
}
|
||||
|
||||
func getOAuthTokenHandler(c echo.Context) (err error) {
|
||||
func getUserTokenHandler(c echo.Context) (err error) {
|
||||
data, ok := c.Get("data").(*SessionData)
|
||||
if !ok {
|
||||
return ErrInvalidSession
|
||||
}
|
||||
user, ok := c.Get("user").(*AudonUser)
|
||||
if !ok {
|
||||
return ErrInvalidSession
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, &TokenResponse{
|
||||
Url: data.MastodonConfig.Server,
|
||||
Token: data.MastodonConfig.AccessToken,
|
||||
AudonID: data.AudonID,
|
||||
Url: data.MastodonConfig.Server,
|
||||
Token: data.MastodonConfig.AccessToken,
|
||||
Audon: user,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -210,12 +225,9 @@ func logoutHandler(c echo.Context) (err error) {
|
|||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
request.Header.Add("User-Agent", USER_AGENT)
|
||||
resp, err := http.DefaultClient.Do(request)
|
||||
if err == nil && resp.StatusCode == http.StatusOK {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest)
|
||||
http.DefaultClient.Do(request) // don't care even if revoking failed
|
||||
writeSessionData(c, nil) // to reset, write nil to user's session
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "login_required")
|
||||
|
@ -226,6 +238,7 @@ func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
data, err := getSessionData(c)
|
||||
if err == nil && data.AudonID != "" {
|
||||
if user, err := findUserByID(c.Request().Context(), data.AudonID); err == nil {
|
||||
userSessionCache.Set(data.AudonID, data, ttlcache.DefaultTTL)
|
||||
c.Set("user", user)
|
||||
c.Set("data", data)
|
||||
return next(c)
|
|
@ -0,0 +1,171 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/sizeofint/webpanimation"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/webp"
|
||||
"gopkg.in/gographics/imagick.v2/imagick"
|
||||
)
|
||||
|
||||
func (u *AudonUser) GetIndicator(ctx context.Context, fnew []byte, room *Room) (indicator []byte, original []byte, isGIF bool, err error) {
|
||||
isGIF = false
|
||||
|
||||
if u == nil {
|
||||
err = errors.New("nil user")
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mimetype.Detect(fnew)
|
||||
if !mimetype.EqualsAny(mtype.String(), "image/png", "image/jpeg", "image/webp", "image/gif") {
|
||||
err = errors.New("file type not supported")
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewReader(fnew)
|
||||
|
||||
var newImg image.Image
|
||||
if mtype.Is("image/png") {
|
||||
newImg, err = png.Decode(buf)
|
||||
} else if mtype.Is("image/jpeg") {
|
||||
newImg, err = jpeg.Decode(buf)
|
||||
} else if mtype.Is("image/webp") {
|
||||
newImg, err = webp.Decode(buf)
|
||||
} else if mtype.Is("image/gif") {
|
||||
newImg, err = gif.Decode(buf)
|
||||
isGIF = true
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// encode to png to avoid recompression, except GIF
|
||||
var origImg []byte
|
||||
if !isGIF {
|
||||
origBuf := new(bytes.Buffer)
|
||||
if err = png.Encode(origBuf, newImg); err != nil {
|
||||
return
|
||||
}
|
||||
origImg = origBuf.Bytes()
|
||||
} else {
|
||||
origImg = fnew
|
||||
}
|
||||
hash := sha256.Sum256(origImg)
|
||||
|
||||
// Check if user's original avatar exists
|
||||
var filename string
|
||||
if isGIF {
|
||||
filename = fmt.Sprintf("%x.gif", hash)
|
||||
} else {
|
||||
filename = fmt.Sprintf("%x.png", hash)
|
||||
}
|
||||
saved := u.getAvatarImagePath(filename)
|
||||
if _, err = os.Stat(saved); err != nil {
|
||||
if err = os.MkdirAll(filepath.Dir(saved), 0775); err != nil {
|
||||
return
|
||||
}
|
||||
// Write user's avatar if the original version doesn't exist
|
||||
if err = os.WriteFile(saved, origImg, 0664); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
coll := mainDB.Collection(COLLECTION_USER)
|
||||
if _, err = coll.UpdateOne(ctx,
|
||||
bson.D{{Key: "audon_id", Value: u.AudonID}},
|
||||
bson.D{
|
||||
{Key: "$set", Value: bson.D{{Key: "avatar", Value: filename}}},
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// indicator, err = u.createGIF(newImg, room.IsHost(u) || room.IsCoHost(u))
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
return indicator, origImg, isGIF, nil
|
||||
}
|
||||
|
||||
func (u *AudonUser) createGIF(avatar image.Image, blue bool) ([]byte, error) {
|
||||
avatarPNG := image.NewRGBA(image.Rect(0, 0, 150, 150))
|
||||
draw.BiLinear.Scale(avatarPNG, avatarPNG.Rect, avatar, avatar.Bounds(), draw.Src, nil)
|
||||
|
||||
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)
|
||||
logoImageBack := mainConfig.LogoImageWhiteBack
|
||||
if blue {
|
||||
logoImageBack = mainConfig.LogoImageBlueBack
|
||||
}
|
||||
draw.Draw(baseFrame, baseFrame.Bounds(), logoImageBack, image.Point{-55, -105}, draw.Over)
|
||||
|
||||
anim := webpanimation.NewWebpAnimation(150, 150, 0)
|
||||
defer anim.ReleaseMemory()
|
||||
webpConf := webpanimation.NewWebpConfig()
|
||||
webpConf.SetLossless(1)
|
||||
|
||||
count := 20
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
frame := image.NewRGBA(baseFrame.Bounds())
|
||||
draw.Copy(frame, image.Point{}, baseFrame, baseFrame.Bounds(), draw.Src, nil)
|
||||
|
||||
var alpha uint8
|
||||
if i < count/2 {
|
||||
alpha = uint8(255. * (1. - float32(2*i)/float32(count)))
|
||||
} else {
|
||||
alpha = uint8(255. * (float32(2*i)/float32(count) - 1.))
|
||||
}
|
||||
|
||||
mask := image.NewUniform(color.Alpha{alpha})
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
outBuf, _ := os.Create(u.getWebPAvatarPath())
|
||||
defer outBuf.Close()
|
||||
anim.Encode(outBuf)
|
||||
|
||||
imagick.Initialize()
|
||||
defer imagick.Terminate()
|
||||
|
||||
if _, err := imagick.ConvertImageCommand([]string{"convert", u.getWebPAvatarPath(), u.getGIFAvatarPath()}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.ReadFile(u.getGIFAvatarPath())
|
||||
}
|
||||
|
||||
func (u *AudonUser) getGIFAvatarPath() string {
|
||||
return u.getAvatarImagePath("indicator.gif")
|
||||
}
|
||||
|
||||
func (u *AudonUser) getWebPAvatarPath() string {
|
||||
return u.getAvatarImagePath("indicator.webp")
|
||||
}
|
||||
|
||||
func (u *AudonUser) getAvatarImagePath(name string) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(mainConfig.StorageDir, u.AudonID, "avatar", name)
|
||||
}
|
107
config.go
107
config.go
|
@ -1,8 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/png"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
@ -14,19 +19,25 @@ type (
|
|||
MongoURL *url.URL
|
||||
Database *DBConfig
|
||||
Redis *RedisConfig
|
||||
Bot *BotConfig
|
||||
}
|
||||
|
||||
AppConfigBase struct {
|
||||
LocalDomain string `validate:"required,hostname|hostname_port"`
|
||||
Environment string `validate:"printascii"`
|
||||
LocalDomain string `validate:"required,fqdn"`
|
||||
Environment string `validate:"printascii"`
|
||||
StorageDir string
|
||||
LogoImageBlueBack image.Image
|
||||
LogoImageWhiteBack image.Image
|
||||
LogoImageFront image.Image
|
||||
}
|
||||
|
||||
LivekitConfig struct {
|
||||
APIKey string `validate:"required,ascii"`
|
||||
APISecret string `validate:"required,ascii"`
|
||||
Host string `validate:"required,hostname|hostname_port"`
|
||||
LocalDomain string `validate:"required,hostname|hostname_port"`
|
||||
URL *url.URL
|
||||
APIKey string `validate:"required,ascii"`
|
||||
APISecret string `validate:"required,ascii"`
|
||||
Host string `validate:"required,hostname|hostname_port"`
|
||||
LocalDomain string `validate:"required,hostname|hostname_port"`
|
||||
URL *url.URL
|
||||
EmptyRoomTimeout time.Duration `validate:"required"`
|
||||
}
|
||||
|
||||
DBConfig struct {
|
||||
|
@ -41,6 +52,14 @@ type (
|
|||
User string `validate:"printascii"`
|
||||
Password string `validate:"printascii"`
|
||||
}
|
||||
|
||||
BotConfig struct {
|
||||
Enable bool
|
||||
Server *url.URL
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
AccessToken string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -69,9 +88,49 @@ func loadConfig(envname string) (*AppConfig, error) {
|
|||
var appConf AppConfig
|
||||
|
||||
// Setup base config
|
||||
storageDir, err := filepath.Abs("public/storage")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(storageDir, 0775); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicDir, _ := filepath.Abs("public")
|
||||
logoBlueBack, err := os.Open(filepath.Join(publicDir, "logo_back_blue.png"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer logoBlueBack.Close()
|
||||
logoBlueBackPng, err := png.Decode(logoBlueBack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logoWhiteBack, err := os.Open(filepath.Join(publicDir, "logo_back_white.png"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer logoWhiteBack.Close()
|
||||
logoWhiteBackPng, err := png.Decode(logoWhiteBack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logoFront, err := os.Open(filepath.Join(publicDir, "logo_front.png"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer logoFront.Close()
|
||||
logoFrontPng, err := png.Decode(logoFront)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basicConf := AppConfigBase{
|
||||
LocalDomain: os.Getenv("LOCAL_DOMAIN"),
|
||||
Environment: envname,
|
||||
LocalDomain: os.Getenv("LOCAL_DOMAIN"),
|
||||
Environment: envname,
|
||||
StorageDir: storageDir,
|
||||
LogoImageBlueBack: logoBlueBackPng,
|
||||
LogoImageWhiteBack: logoWhiteBackPng,
|
||||
LogoImageFront: logoFrontPng,
|
||||
}
|
||||
if err := mainValidator.Struct(&basicConf); err != nil {
|
||||
return nil, err
|
||||
|
@ -108,11 +167,16 @@ func loadConfig(envname string) (*AppConfig, error) {
|
|||
appConf.Redis = redisConf
|
||||
|
||||
// Setup LiveKit config
|
||||
timeout, err := strconv.Atoi(os.Getenv("LIVEKIT_EMPTY_ROOM_TIMEOUT"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lkConf := &LivekitConfig{
|
||||
APIKey: os.Getenv("LIVEKIT_API_KEY"),
|
||||
APISecret: os.Getenv("LIVEKIT_API_SECRET"),
|
||||
Host: os.Getenv("LIVEKIT_HOST"),
|
||||
LocalDomain: os.Getenv("LIVEKIT_LOCAL_DOMAIN"),
|
||||
APIKey: os.Getenv("LIVEKIT_API_KEY"),
|
||||
APISecret: os.Getenv("LIVEKIT_API_SECRET"),
|
||||
Host: os.Getenv("LIVEKIT_HOST"),
|
||||
LocalDomain: os.Getenv("LIVEKIT_LOCAL_DOMAIN"),
|
||||
EmptyRoomTimeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
if err := mainValidator.Struct(lkConf); err != nil {
|
||||
return nil, err
|
||||
|
@ -124,5 +188,22 @@ func loadConfig(envname string) (*AppConfig, error) {
|
|||
lkConf.URL = lkURL
|
||||
appConf.Livekit = lkConf
|
||||
|
||||
// Setup Notification Bot config
|
||||
botHost := os.Getenv("BOT_SERVER")
|
||||
botConf := &BotConfig{
|
||||
Enable: botHost != "",
|
||||
ClientID: os.Getenv("BOT_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("BOT_CLIENT_SECRET"),
|
||||
AccessToken: os.Getenv("BOT_ACCESS_TOKEN"),
|
||||
}
|
||||
if botConf.Enable {
|
||||
botConf.Server = &url.URL{
|
||||
Host: botHost,
|
||||
Scheme: "https",
|
||||
Path: "/",
|
||||
}
|
||||
}
|
||||
appConf.Bot = botConf
|
||||
|
||||
return &appConf, nil
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# Use root/example as user/password credentials
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
|
@ -13,14 +12,14 @@ services:
|
|||
- ./mongo:/data/db
|
||||
|
||||
# mongo-express:
|
||||
# image: mongo-express
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - 8081:8081
|
||||
# environment:
|
||||
# ME_CONFIG_MONGODB_ADMINUSERNAME:
|
||||
# ME_CONFIG_MONGODB_ADMINPASSWORD: example
|
||||
# ME_CONFIG_MONGODB_URL: mongodb://mongo:mongo@db:27017/
|
||||
# image: mongo-express
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - 8081:8081
|
||||
# environment:
|
||||
# ME_CONFIG_MONGODB_ADMINUSERNAME: mongo
|
||||
# ME_CONFIG_MONGODB_ADMINPASSWORD: mongo
|
||||
# ME_CONFIG_MONGODB_URL: mongodb://mongo:mongo@db:27017/
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
|
@ -30,7 +29,7 @@ services:
|
|||
- "127.0.0.1:6379:6379"
|
||||
volumes:
|
||||
- ./redis:/data
|
||||
- ./config/redis.conf:/etc/redis.conf
|
||||
- ./config/redis.conf:/etc/redis.conf:ro
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.3
|
||||
|
@ -40,7 +39,7 @@ services:
|
|||
depends_on:
|
||||
- redis
|
||||
volumes:
|
||||
- ./config/livekit.yaml:/etc/livekit.yaml
|
||||
- ./config/livekit.yaml:/etc/livekit.yaml:ro
|
||||
|
||||
audon:
|
||||
build: .
|
||||
|
@ -54,11 +53,5 @@ services:
|
|||
- db
|
||||
- redis
|
||||
- livekit
|
||||
|
||||
# redisinsight:
|
||||
# image: redislabs/redisinsight:latest
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - 8001:8001
|
||||
# volumes:
|
||||
# - redisinsight:/db
|
||||
volumes:
|
||||
- ./public/storage:/audon/public/storage
|
||||
|
|
12
go.mod
12
go.mod
|
@ -3,20 +3,28 @@ module audon
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/go-playground/validator/v10 v10.11.1
|
||||
github.com/go-redis/redis/v9 v9.0.0-rc.2
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/jaevor/go-nanoid v1.3.0
|
||||
github.com/jellydator/ttlcache/v3 v3.0.1
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/labstack/echo-contrib v0.13.0
|
||||
github.com/labstack/echo/v4 v4.9.1
|
||||
github.com/livekit/protocol v1.2.3
|
||||
github.com/livekit/server-sdk-go v1.0.5
|
||||
github.com/mattn/go-mastodon v0.0.6
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rbcervilla/redisstore/v9 v9.0.0-rc1
|
||||
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882
|
||||
go.mongodb.org/mongo-driver v1.11.0
|
||||
golang.org/x/image v0.3.0
|
||||
golang.org/x/text v0.6.0
|
||||
gopkg.in/gographics/imagick.v2 v2.6.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -87,13 +95,11 @@ require (
|
|||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
|
||||
google.golang.org/grpc v1.50.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
30
go.sum
30
go.sum
|
@ -32,6 +32,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
|||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
@ -74,6 +76,8 @@ github.com/frostbyte73/go-throttle v0.0.0-20210621200530-8018c891361d/go.mod h1:
|
|||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
|
@ -182,6 +186,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
|||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
|
||||
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
|
||||
github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU=
|
||||
github.com/jellydator/ttlcache/v3 v3.0.1/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
|
@ -247,6 +253,8 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6f
|
|||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
|
@ -345,6 +353,8 @@ github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZ
|
|||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 h1:A7o8tOERTtpD/poS+2VoassCjXpjHn916luXbf5QKD0=
|
||||
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882/go.mod h1:5IwJoz9Pw7JsrCN4/skkxUtSWT7myuUPLhCgv6Q5vvQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
|
@ -379,6 +389,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE=
|
||||
go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
|
@ -402,6 +413,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
|
@ -419,6 +431,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
|
|||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
|
||||
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -437,6 +451,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -470,6 +485,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
|
|||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
@ -478,6 +494,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
|||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
|
@ -500,8 +518,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -553,6 +571,7 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
|
@ -566,8 +585,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -615,6 +635,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
|
|||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -707,6 +728,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gographics/imagick.v2 v2.6.2 h1:8ILTJzDKQKSYSfav+9GZs9H8zOOR2UtZVTWkUdFoiZ8=
|
||||
gopkg.in/gographics/imagick.v2 v2.6.2/go.mod h1:/QVPLV/iKdNttRKthmDkeeGg+vdHurVEPc8zkU0XgBk=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
@ -717,6 +740,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func initLocaleBundle() *i18n.Bundle {
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
||||
bundle.LoadMessageFile("locales/active.ja.yaml")
|
||||
bundle.LoadMessageFile("locales/active.fr.yaml")
|
||||
|
||||
return bundle
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Advertise: '@{{.Host}} is streaming now!'
|
|
@ -0,0 +1,3 @@
|
|||
Advertise:
|
||||
hash: sha1-bac4955e5b2655d6226dfb6e190f591f0e9f64cf
|
||||
other: "@{{.Host}} est en streaming maintenant !"
|
|
@ -0,0 +1,3 @@
|
|||
Advertise:
|
||||
hash: sha1-bac4955e5b2655d6226dfb6e190f591f0e9f64cf
|
||||
other: "@{{.Host}} がライブ配信中!"
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 5.6 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 5.6 KiB |
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
361
room.go
361
room.go
|
@ -2,12 +2,18 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jaevor/go-nanoid"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
@ -28,42 +34,26 @@ func createRoomHandler(c echo.Context) error {
|
|||
host := c.Get("user").(*AudonUser)
|
||||
room.Host = host
|
||||
|
||||
// check if user is already hosting or cohosting
|
||||
lkRooms, err := host.GetCurrentLivekitRooms(c.Request().Context())
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
for _, r := range lkRooms {
|
||||
meta, err := getRoomMetadataFromLivekitRoom(r)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
if meta.IsHost(host) || meta.IsCoHost(host) {
|
||||
return ErrOperationNotPermitted
|
||||
}
|
||||
}
|
||||
|
||||
coll := mainDB.Collection(COLLECTION_ROOM)
|
||||
|
||||
now := time.Now().UTC()
|
||||
if now.After(room.ScheduledAt) {
|
||||
// host is trying to create an instant room even though there is another instant room that wasn't used, assumed that host won't use such rooms
|
||||
if cur, err := coll.Find(c.Request().Context(),
|
||||
bson.D{
|
||||
{Key: "host.audon_id", Value: host.AudonID},
|
||||
{Key: "ended_at", Value: time.Time{}}, // host didn't close
|
||||
{Key: "$expr", Value: bson.D{ // instant room
|
||||
{Key: "$eq", Value: bson.A{"$created_at", "$scheduled_at"}},
|
||||
}},
|
||||
}); err == nil {
|
||||
defer cur.Close(c.Request().Context())
|
||||
|
||||
roomIDsToBeDeleted := []string{}
|
||||
for cur.Next(c.Request().Context()) {
|
||||
emptyRoom := new(Room)
|
||||
if err := cur.Decode(emptyRoom); err == nil {
|
||||
if !emptyRoom.IsAnyomeInLivekitRoom(c.Request().Context()) {
|
||||
roomIDsToBeDeleted = append(roomIDsToBeDeleted, emptyRoom.RoomID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(roomIDsToBeDeleted) > 0 {
|
||||
coll.DeleteMany(c.Request().Context(), bson.D{{
|
||||
Key: "room_id",
|
||||
Value: bson.D{{Key: "$in", Value: roomIDsToBeDeleted}}},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
room.ScheduledAt = now
|
||||
} else {
|
||||
// TODO: limit the number of rooms one can schedule?
|
||||
}
|
||||
|
||||
// TODO: use a job scheduler to manage rooms?
|
||||
|
||||
|
@ -75,9 +65,11 @@ func createRoomHandler(c echo.Context) error {
|
|||
}
|
||||
room.RoomID = canonic()
|
||||
|
||||
room.CreatedAt = now
|
||||
|
||||
// if cohosts are already registered, retrieve their data from DB
|
||||
for i, cohost := range room.CoHosts {
|
||||
cohostUser, err := findUserByRemote(c.Request().Context(), cohost.RemoteID, cohost.RemoteURL)
|
||||
cohostUser, err := findUserByWebfinger(c.Request().Context(), cohost.Webfinger)
|
||||
if err == nil {
|
||||
room.CoHosts[i] = cohostUser
|
||||
}
|
||||
|
@ -88,6 +80,37 @@ func createRoomHandler(c echo.Context) error {
|
|||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Create livekit room
|
||||
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,
|
||||
Metadata: string(metadata),
|
||||
})
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusConflict)
|
||||
}
|
||||
countdown := time.NewTimer(mainConfig.Livekit.EmptyRoomTimeout)
|
||||
orphanRooms.Set(room.RoomID, true, ttlcache.DefaultTTL)
|
||||
|
||||
go func(r *Room, logger echo.Logger) {
|
||||
<-countdown.C
|
||||
|
||||
if orphaned := orphanRooms.Get(r.RoomID); orphaned == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if !r.IsAnyomeInLivekitRoom(ctx) {
|
||||
if err := endRoom(ctx, r); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}(room, c.Logger())
|
||||
|
||||
return c.String(http.StatusCreated, room.RoomID)
|
||||
}
|
||||
|
||||
|
@ -227,34 +250,13 @@ func joinRoomHandler(c echo.Context) (err error) {
|
|||
return ErrRoomNotFound
|
||||
}
|
||||
|
||||
// remove old connection if user is already in the room
|
||||
if room.IsUserInLivekitRoom(c.Request().Context(), user.AudonID) {
|
||||
lkRoomServiceClient.RemoveParticipant(c.Request().Context(), &livekit.RoomParticipantIdentity{
|
||||
Room: room.RoomID,
|
||||
Identity: user.AudonID,
|
||||
})
|
||||
// return echo.NewHTTPError(http.StatusNotAcceptable, "already_in_room")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
// check if room is not yet started
|
||||
if room.ScheduledAt.After(now) {
|
||||
return echo.NewHTTPError(http.StatusConflict, "not_yet_started")
|
||||
}
|
||||
|
||||
// check if room has already ended
|
||||
if !room.EndedAt.IsZero() && room.EndedAt.Before(now) {
|
||||
return ErrAlreadyEnded
|
||||
}
|
||||
|
||||
// return 403 if one has been kicked
|
||||
for _, kicked := range room.Kicked {
|
||||
if kicked.Equal(user) {
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
canTalk := room.IsHost(user) || room.IsCoHost(user) // host and cohost can talk from the beginning
|
||||
|
||||
// check room restriction
|
||||
|
@ -262,7 +264,8 @@ func joinRoomHandler(c echo.Context) (err error) {
|
|||
return c.String(http.StatusForbidden, string(room.Restriction))
|
||||
}
|
||||
if !canTalk && (room.IsFollowingOnly() || room.IsFollowerOnly() || room.IsFollowingOrFollowerOnly() || room.IsMutualOnly()) {
|
||||
mastoClient, _ := getMastodonClient(c)
|
||||
data, _ := getSessionData(c)
|
||||
mastoClient := getMastodonClient(data)
|
||||
if mastoClient == nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
|
@ -291,19 +294,24 @@ func joinRoomHandler(c echo.Context) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
roomMetadata := &RoomMetadata{Room: room}
|
||||
lkRoom, _ := getRoomInLivekit(c.Request().Context(), room.RoomID) // lkRoom will be nil if it doesn't exist
|
||||
if lkRoom == nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
roomMetadata, _ := getRoomMetadataFromLivekitRoom(lkRoom)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,36 +322,106 @@ func joinRoomHandler(c echo.Context) (err error) {
|
|||
}
|
||||
|
||||
resp := &TokenResponse{
|
||||
Url: mainConfig.Livekit.URL.String(),
|
||||
Token: token,
|
||||
AudonID: user.AudonID,
|
||||
Url: mainConfig.Livekit.URL.String(),
|
||||
Token: token,
|
||||
Audon: user,
|
||||
}
|
||||
|
||||
// Create room in LiveKit if it doesn't exist
|
||||
if lkRoom == nil {
|
||||
room.CreatedAt = now
|
||||
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: "created_at", Value: now}}}}); err != nil {
|
||||
mastoAccount := new(MastodonAccount)
|
||||
if err := c.Bind(&mastoAccount); err != nil {
|
||||
c.Logger().Error(err)
|
||||
return ErrInvalidRequestFormat
|
||||
}
|
||||
|
||||
roomMetadata.MastodonAccounts[user.AudonID] = mastoAccount
|
||||
|
||||
// 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:image/png;base64,%s", 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c.Logger().Error(err)
|
||||
return ErrInvalidRequestFormat
|
||||
}
|
||||
req.Header.Set("User-Agent", USER_AGENT)
|
||||
|
||||
avatarResp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return ErrInvalidRequestFormat
|
||||
}
|
||||
defer avatarResp.Body.Close()
|
||||
|
||||
fnew, err := io.ReadAll(avatarResp.Body)
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
metadata, _ := json.Marshal(roomMetadata)
|
||||
_, err = lkRoomServiceClient.CreateRoom(c.Request().Context(), &livekit.CreateRoomRequest{
|
||||
Name: room.RoomID,
|
||||
Metadata: string(metadata),
|
||||
})
|
||||
|
||||
// Generate indicator GIF
|
||||
// indicator, original, isGIF, err := user.GetIndicator(c.Request().Context(), fnew, room)
|
||||
_, original, isGIF, err := user.GetIndicator(c.Request().Context(), fnew, room)
|
||||
origMime := "image/png"
|
||||
if isGIF {
|
||||
origMime = "image/gif"
|
||||
}
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusConflict)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
resp.Original = fmt.Sprintf("data:%s;base64,%s", origMime, base64.StdEncoding.EncodeToString(original))
|
||||
// resp.Indicator = fmt.Sprintf("data:image/gif;base64,%s", base64.StdEncoding.EncodeToString(indicator))
|
||||
} else if err != nil {
|
||||
c.Logger().Error(err)
|
||||
}
|
||||
|
||||
// Update room metadata
|
||||
roomMetadata.MastodonAccounts[user.AudonID] = mastoAccount
|
||||
newMetadata, err := json.Marshal(roomMetadata)
|
||||
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
|
||||
orphanRooms.Delete(roomID)
|
||||
|
||||
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 {
|
||||
|
@ -358,15 +436,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
|
||||
}
|
||||
|
||||
// only host can close the room
|
||||
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) {
|
||||
if !meta.IsHost(user) && !meta.IsCoHost(user) {
|
||||
return ErrOperationNotPermitted
|
||||
}
|
||||
|
||||
|
@ -378,8 +460,25 @@ func closeRoomHandler(c echo.Context) error {
|
|||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func updatePermissionHandler(c echo.Context) error {
|
||||
roomID := c.Param("room")
|
||||
// Client notifies server that user left room
|
||||
func leaveRoomHandler(c echo.Context) error {
|
||||
user := c.Get("user").(*AudonUser)
|
||||
still, err := user.InLivekit(c.Request().Context())
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
} else if still {
|
||||
return c.NoContent(http.StatusConflict)
|
||||
}
|
||||
if err := user.ClearUserAvatar(c.Request().Context()); err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func updateRoleHandler(c echo.Context) error {
|
||||
roomID := c.Param("id")
|
||||
|
||||
// look up lkRoom in livekit
|
||||
lkRoom, exists := getRoomInLivekit(c.Request().Context(), roomID)
|
||||
|
@ -399,11 +498,16 @@ func updatePermissionHandler(c echo.Context) error {
|
|||
return ErrOperationNotPermitted
|
||||
}
|
||||
|
||||
tgtAudonID := c.Param("user")
|
||||
if !lkRoomMetadata.IsUserInLivekitRoom(c.Request().Context(), tgtAudonID) {
|
||||
params := make(map[string]string)
|
||||
if err := c.Bind(¶ms); err != nil {
|
||||
return ErrInvalidRequestFormat
|
||||
}
|
||||
audonID := params["identity"]
|
||||
operation := params["op"]
|
||||
if !lkRoomMetadata.IsUserInLivekitRoom(c.Request().Context(), audonID) {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
tgtUser, err := findUserByID(c.Request().Context(), tgtAudonID)
|
||||
tgtUser, err := findUserByID(c.Request().Context(), audonID)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
@ -414,17 +518,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)
|
||||
|
@ -432,26 +567,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: tgtAudonID,
|
||||
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) {
|
||||
|
@ -481,6 +618,10 @@ func getRoomInLivekit(ctx context.Context, roomID string) (*livekit.Room, bool)
|
|||
}
|
||||
|
||||
func findRoomByID(ctx context.Context, roomID string) (*Room, error) {
|
||||
if err := mainValidator.Var(&roomID, "required,printascii"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var room Room
|
||||
collRoom := mainDB.Collection(COLLECTION_ROOM)
|
||||
if err := collRoom.FindOne(ctx, bson.D{{Key: "room_id", Value: roomID}}).Decode(&room); err != nil {
|
||||
|
|
88
schema.go
88
schema.go
|
@ -20,16 +20,19 @@ type (
|
|||
}
|
||||
|
||||
AudonUser struct {
|
||||
AudonID string `bson:"audon_id" json:"audon_id" validate:"alphanum"`
|
||||
RemoteID string `bson:"remote_id" json:"remote_id" validate:"printascii"`
|
||||
RemoteURL string `bson:"remote_url" json:"remote_url" validate:"url"`
|
||||
Webfinger string `bson:"webfinger" json:"webfinger" validate:"email"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
AudonID string `bson:"audon_id" json:"audon_id" validate:"alphanum"`
|
||||
RemoteID string `bson:"remote_id" json:"remote_id" validate:"printascii"`
|
||||
RemoteURL string `bson:"remote_url" json:"remote_url" validate:"url"`
|
||||
Webfinger string `bson:"webfinger" json:"webfinger" validate:"email"`
|
||||
AvatarFile string `bson:"avatar" json:"avatar"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
RoomMetadata struct {
|
||||
*Room
|
||||
Speakers []*AudonUser `json:"speakers"`
|
||||
Speakers []*AudonUser `json:"speakers"`
|
||||
Kicked []*AudonUser `json:"kicked"`
|
||||
MastodonAccounts map[string]*MastodonAccount `json:"accounts"`
|
||||
}
|
||||
|
||||
Room struct {
|
||||
|
@ -39,16 +42,17 @@ 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"`
|
||||
ScheduledAt time.Time `bson:"scheduled_at" json:"scheduled_at"`
|
||||
EndedAt time.Time `bson:"ended_at" json:"ended_at"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
Advertise string `bson:"advertise" json:"advertise"`
|
||||
}
|
||||
|
||||
TokenResponse struct {
|
||||
Url string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
AudonID string `json:"audon_id"`
|
||||
Url string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Audon *AudonUser `json:"audon"`
|
||||
Indicator string `json:"indicator"`
|
||||
Original string `json:"original"`
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -66,12 +70,28 @@ const (
|
|||
PRIVATE JoinRestriction = "private"
|
||||
)
|
||||
|
||||
func (a *AudonUser) Equal(u *AudonUser) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
func (a *AudonUser) GetCurrentLivekitRooms(ctx context.Context) ([]*livekit.Room, error) {
|
||||
resp, err := lkRoomServiceClient.ListRooms(ctx, &livekit.ListRoomsRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.AudonID == u.AudonID || (a.RemoteID == u.RemoteID && a.RemoteURL == u.RemoteURL)
|
||||
rooms := resp.GetRooms()
|
||||
current := []*livekit.Room{}
|
||||
for _, r := range rooms {
|
||||
partResp, err := lkRoomServiceClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{
|
||||
Room: r.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range partResp.GetParticipants() {
|
||||
if p.Identity == a.AudonID {
|
||||
current = append(current, r)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (r *Room) IsFollowingOnly() bool {
|
||||
|
@ -112,6 +132,15 @@ func (r *Room) IsHost(u *AudonUser) bool {
|
|||
return r != nil && r.Host.Equal(u)
|
||||
}
|
||||
|
||||
func (r *RoomMetadata) IsSpeaker(u *AudonUser) bool {
|
||||
for _, s := range r.Speakers {
|
||||
if s.Equal(u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getRoomMetadataFromLivekitRoom(lkRoom *livekit.Room) (*RoomMetadata, error) {
|
||||
metadata := new(RoomMetadata)
|
||||
if err := json.Unmarshal([]byte(lkRoom.GetMetadata()), metadata); err != nil {
|
||||
|
@ -121,6 +150,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}})
|
||||
|
||||
|
@ -164,10 +205,8 @@ func createIndexes(ctx context.Context) error {
|
|||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "remote_url", Value: 1},
|
||||
{Key: "remote_id", Value: 1},
|
||||
},
|
||||
Keys: bson.D{{Key: "webfinger", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -216,3 +255,12 @@ func findUserByID(ctx context.Context, audonID string) (*AudonUser, error) {
|
|||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func findUserByWebfinger(ctx context.Context, webfinger string) (*AudonUser, error) {
|
||||
var result AudonUser
|
||||
coll := mainDB.Collection(COLLECTION_USER)
|
||||
if err := coll.FindOne(ctx, bson.D{{Key: "webfinger", Value: webfinger}}).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
|
61
server.go
61
server.go
|
@ -18,10 +18,12 @@ import (
|
|||
"github.com/go-playground/validator/v10"
|
||||
"github.com/go-redis/redis/v9"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
lksdk "github.com/livekit/server-sdk-go"
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/rbcervilla/redisstore/v9"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
@ -50,6 +52,10 @@ var (
|
|||
mainValidator = validator.New()
|
||||
mainConfig *AppConfig
|
||||
lkRoomServiceClient *lksdk.RoomServiceClient
|
||||
localeBundle *i18n.Bundle
|
||||
userSessionCache *ttlcache.Cache[string, *SessionData]
|
||||
webhookTimerCache *ttlcache.Cache[string, *time.Timer]
|
||||
orphanRooms *ttlcache.Cache[string, bool]
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -69,11 +75,17 @@ func main() {
|
|||
log.Fatalf("Failed loading config values: %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Load locales
|
||||
localeBundle = initLocaleBundle()
|
||||
|
||||
// Setup Livekit RoomService Client
|
||||
lkURL := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: mainConfig.Livekit.Host,
|
||||
}
|
||||
if mainConfig.Environment == "development" {
|
||||
lkURL.Scheme = "http"
|
||||
}
|
||||
lkRoomServiceClient = lksdk.NewRoomServiceClient(lkURL.String(), mainConfig.Livekit.APIKey, mainConfig.Livekit.APISecret)
|
||||
|
||||
backContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
@ -142,25 +154,38 @@ func main() {
|
|||
redisStore.Options(sessionOptions)
|
||||
e.Use(session.Middleware(redisStore))
|
||||
|
||||
// Setup caches
|
||||
userSessionCache = ttlcache.New(ttlcache.WithTTL[string, *SessionData](168 * time.Hour))
|
||||
webhookTimerCache = ttlcache.New(ttlcache.WithTTL[string, *time.Timer](5 * time.Minute))
|
||||
orphanRooms = ttlcache.New(ttlcache.WithTTL[string, bool](24 * time.Hour))
|
||||
go userSessionCache.Start()
|
||||
go webhookTimerCache.Start()
|
||||
go orphanRooms.Start()
|
||||
|
||||
e.POST("/app/login", loginHandler)
|
||||
e.GET("/app/oauth", oauthHandler)
|
||||
e.GET("/app/verify", verifyHandler)
|
||||
e.POST("/app/logout", logoutHandler)
|
||||
e.GET("/app/preview/:id", previewRoomHandler)
|
||||
e.GET("/app/user/:id", getUserHandler)
|
||||
|
||||
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.GET("/room/:id", joinRoomHandler)
|
||||
api.DELETE("/room", leaveRoomHandler)
|
||||
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")
|
||||
e.Static("/storage", mainConfig.StorageDir)
|
||||
e.GET("/r/:id", renderRoomHandler)
|
||||
e.GET("/u/:webfinger", redirectUserHandler)
|
||||
e.GET("/*", func(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "tmpl", &TemplateData{Config: &mainConfig.AppConfigBase})
|
||||
})
|
||||
|
@ -181,6 +206,9 @@ func main() {
|
|||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
e.Logger.Print("Attempting graceful shutdown")
|
||||
defer shutdownCancel()
|
||||
userSessionCache.DeleteAll()
|
||||
webhookTimerCache.DeleteAll()
|
||||
orphanRooms.DeleteAll()
|
||||
if err := e.Shutdown(shutdownCtx); err != nil {
|
||||
e.Logger.Fatalf("Failed shutting down gracefully: %s\n", err.Error())
|
||||
}
|
||||
|
@ -197,34 +225,19 @@ func (cv *CustomValidator) Validate(i interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getAppConfig(server string, redirPath string) (*mastodon.AppConfig, error) {
|
||||
// if mastAppConfigBase != nil {
|
||||
// return &mastodon.AppConfig{
|
||||
// Server: server,
|
||||
// ClientName: mastAppConfigBase.ClientName,
|
||||
// Scopes: mastAppConfigBase.Scopes,
|
||||
// Website: mastAppConfigBase.Website,
|
||||
// RedirectURIs: mastAppConfigBase.RedirectURIs,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
if redirPath == "" {
|
||||
redirPath = "/"
|
||||
}
|
||||
func getAppConfig(server string) (*mastodon.AppConfig, error) {
|
||||
redirectURI := "urn:ietf:wg:oauth:2.0:oob"
|
||||
u := &url.URL{
|
||||
Host: mainConfig.LocalDomain,
|
||||
Scheme: "https",
|
||||
Path: "/",
|
||||
}
|
||||
q := u.Query()
|
||||
q.Add("redir", redirPath)
|
||||
u.RawQuery = q.Encode()
|
||||
u = u.JoinPath("app", "oauth")
|
||||
redirectURI = u.String()
|
||||
|
||||
conf := &mastodon.AppConfig{
|
||||
ClientName: "Audon",
|
||||
ClientName: "Audon",
|
||||
// Scopes: "read:accounts read:follows write:accounts",
|
||||
Scopes: "read:accounts read:follows",
|
||||
Website: "https://codeberg.org/nmkj/audon",
|
||||
RedirectURIs: redirectURI,
|
||||
|
@ -275,7 +288,11 @@ func writeSessionData(c echo.Context, data *SessionData) error {
|
|||
return err
|
||||
}
|
||||
|
||||
sess.Values[SESSION_DATASTORE_NAME] = data
|
||||
if data == nil {
|
||||
sess.Values[SESSION_DATASTORE_NAME] = ""
|
||||
} else {
|
||||
sess.Values[SESSION_DATASTORE_NAME] = data
|
||||
}
|
||||
|
||||
return sess.Save(c.Request(), c.Response())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
mastodon "github.com/mattn/go-mastodon"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
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 {
|
||||
return wrapValidationError(err)
|
||||
}
|
||||
|
||||
user, err := findUserByID(c.Request().Context(), audonID)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func getStatusHandler(c echo.Context) error {
|
||||
u := c.Get("user").(*AudonUser)
|
||||
|
||||
status, err := u.GetCurrentRoomStatus(c.Request().Context())
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
func redirectUserHandler(c echo.Context) error {
|
||||
input := c.Param("webfinger")
|
||||
if err := mainValidator.Var(&input, "required,startswith=@,min=4"); err != nil {
|
||||
return wrapValidationError(err)
|
||||
}
|
||||
webfinger := input[1:]
|
||||
if err := mainValidator.Var(&webfinger, "email"); err != nil {
|
||||
return wrapValidationError(err)
|
||||
}
|
||||
|
||||
user, err := findUserByWebfinger(c.Request().Context(), webfinger)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
coll := mainDB.Collection(COLLECTION_ROOM)
|
||||
opts := options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}})
|
||||
var room Room
|
||||
|
||||
searchCohost := false
|
||||
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 {
|
||||
searchCohost = true
|
||||
}
|
||||
} else {
|
||||
searchCohost = true
|
||||
}
|
||||
if searchCohost {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/offline")
|
||||
}
|
||||
|
||||
func (a *AudonUser) Equal(u *AudonUser) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.AudonID == u.AudonID || a.Webfinger == u.Webfinger
|
||||
}
|
||||
|
||||
func (a *AudonUser) InLivekit(ctx context.Context) (bool, error) {
|
||||
rooms, err := a.GetCurrentLivekitRooms(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(rooms) > 0, nil
|
||||
}
|
||||
|
||||
func (a *AudonUser) ClearUserAvatar(ctx context.Context) error {
|
||||
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
|
||||
}
|
||||
|
||||
type UserStatus struct {
|
||||
RoomID string `json:"roomID"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (a *AudonUser) GetCurrentRoomStatus(ctx context.Context) ([]UserStatus, error) {
|
||||
rooms, err := a.GetCurrentLivekitRooms(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roomList := make([]UserStatus, len(rooms))
|
||||
for _, r := range rooms {
|
||||
meta, _ := getRoomMetadataFromLivekitRoom(r)
|
||||
role := "listener"
|
||||
if meta.Room.IsHost(a) {
|
||||
role = "host"
|
||||
} else if meta.Room.IsCoHost(a) {
|
||||
role = "cohost"
|
||||
} else if meta.IsSpeaker(a) {
|
||||
role = "speaker"
|
||||
}
|
||||
roomList = append(roomList, UserStatus{
|
||||
RoomID: r.GetName(),
|
||||
Role: role,
|
||||
})
|
||||
}
|
||||
|
||||
for _, s := range roomList {
|
||||
if s.Role == "host" {
|
||||
return roomList, nil
|
||||
}
|
||||
}
|
||||
|
||||
allRooms, err := lkRoomServiceClient.ListRooms(ctx, &livekit.ListRoomsRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range allRooms.GetRooms() {
|
||||
meta, _ := getRoomMetadataFromLivekitRoom(r)
|
||||
if meta.IsHost(a) {
|
||||
roomList = append(roomList, UserStatus{
|
||||
RoomID: r.GetName(),
|
||||
Role: "host",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return roomList, nil
|
||||
}
|
67
utils.go
67
utils.go
|
@ -1,15 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
mastodon "github.com/mattn/go-mastodon"
|
||||
)
|
||||
|
||||
|
@ -73,13 +78,65 @@ func registerApp(ctx context.Context, appConfig *mastodon.AppConfig) (*mastodon.
|
|||
return &app, nil
|
||||
}
|
||||
|
||||
func getMastodonClient(c echo.Context) (*mastodon.Client, error) {
|
||||
data, err := getSessionData(c)
|
||||
if err != nil || data.MastodonConfig.AccessToken == "" {
|
||||
// Updates the avatar of the current user.
|
||||
func updateAvatar(ctx context.Context, c *mastodon.Client, filename string) (*mastodon.Account, error) {
|
||||
u, err := url.Parse(c.Config.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Path = path.Join(u.Path, "/api/v1/accounts/update_credentials")
|
||||
|
||||
avatar, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
mw := multipart.NewWriter(buf)
|
||||
|
||||
// h := make(textproto.MIMEHeader)
|
||||
// h.Set("Content-Disposition", "form-data; name=\"avatar\"; filename=\"blob\"")
|
||||
// h.Set("Content-Type", mimetype.Detect(avatar).String())
|
||||
// part, err := mw.CreatePart(h)
|
||||
part, err := mw.CreateFormFile("avatar", filepath.Base(filename))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
io.Copy(part, avatar)
|
||||
mw.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
c.UserAgent = USER_AGENT
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
account := new(mastodon.Account)
|
||||
err = json.NewDecoder(resp.Body).Decode(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func getMastodonClient(data *SessionData) *mastodon.Client {
|
||||
if data == nil || data.MastodonConfig.AccessToken == "" {
|
||||
return nil
|
||||
}
|
||||
mastoClient := mastodon.NewClient(data.MastodonConfig)
|
||||
mastoClient.UserAgent = USER_AGENT
|
||||
|
||||
return mastoClient, nil
|
||||
return mastoClient
|
||||
}
|
||||
|
|
93
webhooks.go
93
webhooks.go
|
@ -1,11 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/webhook"
|
||||
mastodon "github.com/mattn/go-mastodon"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
)
|
||||
|
||||
func livekitWebhookHandler(c echo.Context) error {
|
||||
|
@ -17,19 +24,85 @@ func livekitWebhookHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
if event.GetEvent() == webhook.EventRoomFinished {
|
||||
roomID := event.GetRoom().GetName()
|
||||
if err := mainValidator.Var(&roomID, "required,printascii"); err == nil {
|
||||
room, err := findRoomByID(c.Request().Context(), roomID)
|
||||
if err == nil && room.EndedAt.IsZero() {
|
||||
if err := endRoom(c.Request().Context(), room); err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
lkRoom := event.GetRoom()
|
||||
room, err := findRoomByID(c.Request().Context(), lkRoom.GetName())
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
if room.EndedAt.IsZero() {
|
||||
if err := endRoom(c.Request().Context(), room); err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
} else if event.GetEvent() == webhook.EventParticipantLeft {
|
||||
audonID := event.GetParticipant().GetIdentity()
|
||||
user, err := findUserByID(c.Request().Context(), audonID)
|
||||
if user == nil || err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
still, err := user.InLivekit(c.Request().Context())
|
||||
if !still && err == nil {
|
||||
data := userSessionCache.Get(audonID)
|
||||
if data == nil {
|
||||
return echo.NewHTTPError(http.StatusGone)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
nextUser, err := findUserByID(ctx, audonID)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
nextUser.ClearUserAvatar(ctx)
|
||||
}
|
||||
} else if event.GetEvent() == webhook.EventRoomStarted {
|
||||
// Have the bot advertise the room
|
||||
room, err := findRoomByID(c.Request().Context(), event.GetRoom().GetName())
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
if err == nil && mainConfig.Bot.Enable && room.Advertise != "" && room.Restriction == EVERYONE {
|
||||
botClient := mastodon.NewClient(&mastodon.Config{
|
||||
Server: mainConfig.Bot.Server.String(),
|
||||
ClientID: mainConfig.Bot.ClientID,
|
||||
ClientSecret: mainConfig.Bot.ClientSecret,
|
||||
AccessToken: mainConfig.Bot.AccessToken,
|
||||
})
|
||||
botClient.UserAgent = USER_AGENT
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
localizer := i18n.NewLocalizer(localeBundle, room.Advertise)
|
||||
header := localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "Advertise",
|
||||
Other: "@{{.Host}} is streaming now!",
|
||||
},
|
||||
TemplateData: map[string]string{
|
||||
"Host": room.Host.Webfinger,
|
||||
},
|
||||
})
|
||||
|
||||
messages := []string{
|
||||
header,
|
||||
fmt.Sprintf(":udon: %s\n🎙️ https://%s/u/@%s", room.Title, mainConfig.LocalDomain, room.Host.Webfinger),
|
||||
}
|
||||
if room.Description != "" {
|
||||
messages = append(messages, room.Description)
|
||||
}
|
||||
messages = append(messages, "#Audon")
|
||||
message := strings.Join(messages, "\n\n")
|
||||
|
||||
if _, err := botClient.PostStatus(c.Request().Context(), &mastodon.Toot{
|
||||
Status: message,
|
||||
Language: room.Advertise,
|
||||
Visibility: "public",
|
||||
}); err != nil {
|
||||
c.Logger().Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue