Porównaj commity

...

90 Commity

Autor SHA1 Wiadomość Data
Namekuji 7adba191e6
Add link to Hamabē 2024-02-15 21:57:46 -05:00
Namekuji 3cc4ee4f84
use native 2023-07-02 12:04:34 -04:00
Namekuji a606f7f4fb
remove png 2023-07-02 11:51:52 -04:00
Namekuji 1bb384a10c
fix bot url 2023-07-02 02:43:02 -04:00
Namekuji 0fc4e5fbec
update introduction 2023-05-28 08:59:39 -04:00
Namekuji 30cc7930fc
disable avatar conversion 2023-05-28 08:36:24 -04:00
Namekuji 6c4ef49085
reduce sound effect volumes 2023-05-28 08:08:20 -04:00
Namekuji 85979a7ebf
remove avatar restore 2023-05-26 10:32:52 -04:00
Namekuji 6f7b58f2e2 fix mic status label 2023-04-28 21:16:23 -04:00
Namekuji 03e5cb4f02 bump version 2023-04-28 14:16:52 -04:00
Namekuji 87300ea112 remove docker 2023-04-28 14:15:43 -04:00
Namekuji 7b837651dd add aria-label to buttons missing content 2023-04-28 14:15:24 -04:00
Namekuji c40fb71147 use rootless 2023-04-28 13:32:50 -04:00
Namekuji d52ae18c44 disable avatar update 2023-04-28 09:31:40 -04:00
Namekuji 3138e2a596 fix returning string 2023-03-22 10:45:00 -04:00
Namekuji 80963c96e6 fix oauth redirect_uri 2023-03-22 10:14:00 -04:00
Namekuji 32dbfbc205 use png twemoji 2023-02-20 17:39:27 -05:00
Namekuji e2a5fb6fd6 fix import error 2023-02-20 15:12:47 -05:00
Namekuji f11d4f016a use twemoji 2023-02-20 15:08:35 -05:00
Namekuji fec8e75a74 remove error from url 2023-01-29 23:12:07 -05:00
Namekuji 150ad6df05 increase session cacche ttl 2023-01-29 21:30:10 -05:00
Namekuji db42f13ee4 fix cohost redirect 2023-01-28 23:48:25 -05:00
Namekuji 8dc11e7f84 fix host avatar restore 2023-01-28 23:33:16 -05:00
Namekuji 3ad0378bd2 bump version 2023-01-28 22:20:57 -05:00
Namekuji cff4f9eaab add role update 2023-01-28 22:16:21 -05:00
Namekuji 55ca35c3c8 allow cohosts to close the room 2023-01-28 15:08:15 -05:00
Namekuji d4d1ab6792 remove shceduling completely 2023-01-28 11:24:17 -05:00
Namekuji e1bbda00c8 remove scheduling 2023-01-28 11:20:00 -05:00
Namekuji 06ed20cd98 fix timezone 2023-01-28 09:41:59 -05:00
Namekuji 2b70a85bca add noise supression 2023-01-28 09:37:11 -05:00
Namekuji 6088af7101 avoid gif conversion 2023-01-28 08:21:01 -05:00
Namekuji a7f80e2ef0 revert audio capture options to default 2023-01-27 18:20:37 -05:00
Namekuji e28f2edb2e always convert original avatar to png to avoid recompression 2023-01-27 12:45:52 -05:00
Namekuji 6adbf5de60 change log message 2023-01-27 08:28:16 -05:00
Namekuji b571b4df30 revert index count 2023-01-27 01:52:04 -05:00
Namekuji 9543928276 use webfinger instead of acct 2023-01-27 01:22:40 -05:00
Namekuji abfb2f62f4 fix an issue that remote accounts cannot be added as cohosts 2023-01-27 01:11:37 -05:00
Namekuji 7028f5834c slightly change one sentence 2023-01-26 22:06:25 -05:00
Namekuji 8bfd860c24 fix unclear en sentence 2023-01-26 18:47:02 -05:00
Namekuji 63e2be37e2 fix unclear sentence 2023-01-26 18:45:14 -05:00
Namekuji ca77984520 bump version 2023-01-26 18:12:39 -05:00
Namekuji 2047c3b691 change url style 2023-01-26 17:43:04 -05:00
Namekuji 1a6b838c9c change timing to create room 2023-01-26 17:31:53 -05:00
Namekuji b21aa42aaa add static link 2023-01-26 14:39:19 -05:00
Namekuji 17e07d28a1 add static url 2023-01-26 13:28:25 -05:00
Namekuji 073510d4b5 fix ios regex error 2023-01-26 13:28:12 -05:00
Namekuji 9f6238e688 fix validation failure for specific instance #28 2023-01-25 16:17:39 -05:00
Namekuji 5b7c6735ed add listener online indicator 2023-01-25 15:58:15 -05:00
Namekuji 906170c110 bump version 2023-01-25 15:21:01 -05:00
Namekuji 7ce3aad1f8 remove mute indicator in preview mode #23 2023-01-25 15:20:26 -05:00
Namekuji fac090e68f change title to the room name #26 2023-01-25 15:13:22 -05:00
Namekuji 0888bf758e fix fqdn validation error #28 2023-01-25 14:58:17 -05:00
Namekuji ea2a771349 fix avatar issue again 2023-01-25 14:52:05 -05:00
Namekuji 26ec5e7086 bump version 2023-01-25 01:45:15 -05:00
Namekuji cc4337ebcb fix annoying avatar change 2023-01-25 01:37:31 -05:00
Namekuji 44e0b2b3d7 don't delete old avatars just in case 2023-01-24 15:52:43 -05:00
Namekuji 3d24066d2d fix preview avatar issue 2023-01-24 01:03:15 -05:00
Namekuji 693ea59845 fix preview mode 2023-01-23 11:13:50 -05:00
Namekuji 29dc87b382 change message emoji 2023-01-23 09:57:32 -05:00
Namekuji bd3dc5434b bump version 2023-01-23 09:55:13 -05:00
Namekuji f9c35e0170 fix avatar filename 2023-01-23 08:29:31 -05:00
Namekuji a91d7d340a add gif indicator 2023-01-23 07:10:21 -05:00
Namekuji 3469d62210 change advertise timing 2023-01-20 10:39:51 -05:00
Namekuji 7ceb656bdf fix message 2023-01-19 14:07:25 -05:00
Namekuji 9c5e5f2d7c change description 2023-01-19 13:57:32 -05:00
Namekuji 82cd78fef8 change bot message 2023-01-19 12:58:46 -05:00
Namekuji 465cc05cf0 change descriptions 2023-01-19 12:57:09 -05:00
Namekuji ea77efb601 change ja about 2023-01-19 12:25:34 -05:00
Namekuji 0b3f350da5 change bot message 2023-01-19 09:06:43 -05:00
Namekuji 66f7d89cf7 keep certs directory 2023-01-18 22:06:42 -05:00
Namekuji c98d6fad84 fix livekit room service in devcontainer 2023-01-18 22:03:24 -05:00
Namekuji 15ede80387 fix missing ja locale 2023-01-18 21:27:25 -05:00
Namekuji 60de3630eb add devcontainer 2023-01-18 21:27:06 -05:00
Namekuji 5ca7d22b94 small changes 2023-01-18 21:23:07 -05:00
Namekuji 0755f91e61 fix unable to signout 2023-01-18 21:22:20 -05:00
Namekuji 609fd256b3 fix ad condition 2023-01-15 17:01:37 -05:00
Namekuji ba6edbbe55 fix locale 2023-01-15 13:42:39 -05:00
Namekuji 393b03ffd9 fix copy 2023-01-15 13:34:44 -05:00
Namekuji b050c7c7c9 bump version 2023-01-15 13:31:07 -05:00
Namekuji f323b7459e change bot url 2023-01-15 12:45:15 -05:00
Namekuji c4b260f989 change emoji 2023-01-14 19:46:39 -05:00
Namekuji aa93feca49 add bot support 2023-01-14 18:02:15 -05:00
Namekuji ee9c1eda41 add notification sound of speaker request 2023-01-13 20:30:37 -05:00
Namekuji 11c24d095c bump version 2023-01-13 20:21:54 -05:00
Namekuji 3d67718b2c implement #19 2023-01-13 20:20:08 -05:00
Namekuji d1f02d9296 fix small bugs 2023-01-13 11:36:35 -05:00
Namekuji 7cd0f324b1 disable preview 2023-01-13 10:26:20 -05:00
Namekuji 1f084e12bc support emoji reactions #12 2023-01-13 10:01:02 -05:00
Namekuji 7545855b00 fix twitter card type 2023-01-12 21:41:04 -05:00
Namekuji 48350175f0 remove query in preview mode if the room is already closed 2023-01-12 21:13:10 -05:00
61 zmienionych plików z 4589 dodań i 4237 usunięć

1
.devcontainer/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1 @@
certs/*.pem

Wyświetl plik

@ -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
}

Wyświetl plik

Wyświetl plik

@ -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"
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

1
.dockerignore 100644
Wyświetl plik

@ -0,0 +1 @@
public/storage/

Wyświetl plik

@ -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=

4
.gitignore vendored
Wyświetl plik

@ -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

2
.vscode/launch.json vendored
Wyświetl plik

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"name": "Launch Go",
"type": "go",
"request": "launch",
"mode": "auto",

11
CREDITS 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

3567
audon-fe/package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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

Wyświetl plik

@ -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.

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -0,0 +1,173 @@
<script>
import { Room } from "livekit-client";
import { useMastodonStore } from "../stores/mastodon";
import { mdiArrowRightBold } from "@mdi/js";
import { pushNotFound } from "../assets/utils";
import axios from "axios";
export default {
setup() {
return {
mdiArrowRightBold,
donStore: useMastodonStore(),
};
},
data() {
return {
roomID: this.$route.params.id,
isLoading: true,
roomToken: null,
dialogEnabled: true,
uploading: false,
};
},
emits: ["connect"],
props: {
roomId: String,
roomClient: Room,
},
computed: {
uploadEnabled() {
// return this.roomToken?.original && this.roomToken?.indicator;
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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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."

Wyświetl plik

@ -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: "リスナー"

Wyświetl plik

@ -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 } : {};

Wyświetl plik

@ -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: "/",

Wyświetl plik

@ -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 };
});

Wyświetl plik

@ -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");
}
},
},

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -1,5 +0,0 @@
<script></script>
<template>
<v-alert type="error">Page not found</v-alert>
</template>

Wyświetl plik

@ -15,6 +15,7 @@ export default defineConfig({
include: "./src/locales/*.yaml",
}),
],
assetsInclude: ["**/*.oga"],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),

Wyświetl plik

@ -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)

171
avatar.go 100644
Wyświetl plik

@ -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
Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
Wyświetl plik

@ -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
Wyświetl plik

@ -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=

16
locale.go 100644
Wyświetl plik

@ -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
}

Wyświetl plik

@ -0,0 +1 @@
Advertise: '@{{.Host}} is streaming now!'

Wyświetl plik

@ -0,0 +1,3 @@
Advertise:
hash: sha1-bac4955e5b2655d6226dfb6e190f591f0e9f64cf
other: "@{{.Host}} est en streaming maintenant !"

Wyświetl plik

@ -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
Wyświetl plik

@ -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(&params); 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 {

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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())
}

194
user.go 100644
Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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)
}