mirror of
https://github.com/YunoHost-Apps/galene_ynh.git
synced 2024-09-03 18:36:31 +02:00
parent
a2ab22aaf3
commit
0fadb85249
38 changed files with 6577 additions and 42 deletions
|
@ -11,7 +11,7 @@ If you don't have YunoHost, please consult [the guide](https://yunohost.org/#/in
|
|||
## Overview
|
||||
Galène is a videoconferencing server that is easy to deploy (just copy a few files and run the binary) and that requires moderate server resources. It was originally designed for lectures and conferences (where a single speaker streams audio and video to hundreds or thousands of users), but later evolved to be useful for student practicals (where users are divided into many small groups), and meetings (where a few dozen users interact with each other).
|
||||
|
||||
**Shipped version:** 0.1
|
||||
**Shipped version:** 0.2
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
@ -33,8 +33,8 @@ Galène is a videoconferencing server that is easy to deploy (just copy a few fi
|
|||
|
||||
#### Multi-user support
|
||||
|
||||
* Are LDAP and HTTP auth supported?
|
||||
* Can the app be used by multiple users?
|
||||
* Are LDAP and HTTP auth supported? **No**
|
||||
* Can the app be used by multiple users? **Yes**
|
||||
|
||||
#### Supported architectures
|
||||
|
||||
|
@ -49,9 +49,6 @@ Galène is a videoconferencing server that is easy to deploy (just copy a few fi
|
|||
|
||||
* Other info you would like to add about this app.
|
||||
|
||||
**More info on the documentation page:**
|
||||
https://yunohost.org/packaging_apps
|
||||
|
||||
## Links
|
||||
|
||||
* Report a bug: https://github.com/YunoHost-Apps/galene_ynh/issues
|
||||
|
|
|
@ -11,7 +11,7 @@ Si vous n'avez pas YunoHost, consultez [le guide](https://yunohost.org/#/install
|
|||
## Vue d'ensemble
|
||||
Galène est un serveur de visioconférence facile à déployer (il suffit de copier quelques fichiers et d'exécuter le binaire) et qui nécessite des ressources serveur modérées. Il a été conçu à l'origine pour les conférences (où un seul orateur diffuse l'audio et la vidéo à des centaines ou des milliers d'utilisateurs), mais a ensuite évolué pour être utile pour les travaux pratiques des étudiants (où les utilisateurs sont divisés en plusieurs petits groupes) et les réunions (où un quelques dizaines d'utilisateurs interagissent les uns avec les autres).
|
||||
|
||||
**Version incluse :** 0.1
|
||||
**Version incluse :** 0.2
|
||||
|
||||
## Captures d'écran
|
||||
|
||||
|
@ -23,7 +23,6 @@ Galène est un serveur de visioconférence facile à déployer (il suffit de cop
|
|||
|
||||
## Configuration
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
* Documentation officielle : https://galene.org/
|
||||
|
@ -33,8 +32,8 @@ Galène est un serveur de visioconférence facile à déployer (il suffit de cop
|
|||
|
||||
#### Support multi-utilisateur
|
||||
|
||||
* L'authentification LDAP est-elle prise en charge ?
|
||||
* L'application peut-elle être utilisée par plusieurs utilisateurs ?
|
||||
* L'authentification LDAP est-elle prise en charge ? **Non**
|
||||
* L'application peut-elle être utilisée par plusieurs utilisateurs ? **Oui**
|
||||
|
||||
#### Supported architectures
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[Unit]
|
||||
Description=Galene
|
||||
Description=Galene: videoconferencing server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"en": "Videoconferencing server that is easy to deploy",
|
||||
"fr": "Serveur de visioconférence facile à déployer"
|
||||
},
|
||||
"version": "0.1~ynh1",
|
||||
"version": "0.2~ynh1",
|
||||
"url": "https://galene.org/",
|
||||
"license": "MIT",
|
||||
"maintainer": {
|
||||
|
|
|
@ -24,7 +24,7 @@ app=$YNH_APP_INSTANCE_NAME
|
|||
#=================================================
|
||||
# LOAD SETTINGS
|
||||
#=================================================
|
||||
ynh_script_progression --message="Loading installation settings..." --time --weight=1
|
||||
ynh_script_progression --message="Loading installation settings..." --weight=1
|
||||
|
||||
# Needed for helper "ynh_add_nginx_config"
|
||||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
|
@ -33,7 +33,7 @@ port=$(ynh_app_setting_get --app=$app --key=port)
|
|||
#=================================================
|
||||
# BACKUP BEFORE UPGRADE THEN ACTIVE TRAP
|
||||
#=================================================
|
||||
ynh_script_progression --message="Backing up the app before changing its URL (may take a while)..." --time --weight=1
|
||||
ynh_script_progression --message="Backing up the app before changing its URL (may take a while)..." --weight=1
|
||||
|
||||
# Backup the current version of the app
|
||||
ynh_backup_before_upgrade
|
||||
|
@ -68,14 +68,14 @@ fi
|
|||
#=================================================
|
||||
# STOP SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Stopping a systemd service..." --time --weight=1
|
||||
ynh_script_progression --message="Stopping a systemd service..." --weight=1
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="stop" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
#=================================================
|
||||
# MODIFY URL IN NGINX CONF
|
||||
#=================================================
|
||||
ynh_script_progression --message="Updating NGINX web server configuration..." --time --weight=1
|
||||
ynh_script_progression --message="Updating NGINX web server configuration..." --weight=2
|
||||
|
||||
nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf
|
||||
|
||||
|
@ -101,18 +101,12 @@ then
|
|||
ynh_store_file_checksum --file="/etc/nginx/conf.d/$new_domain.d/$app.conf"
|
||||
fi
|
||||
|
||||
#=================================================
|
||||
# SPECIFIC MODIFICATIONS
|
||||
#=================================================
|
||||
# ...
|
||||
#=================================================
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALISATION
|
||||
#=================================================
|
||||
# START SYSTEMD SERVICE
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting a systemd service..." --time --weight=1
|
||||
ynh_script_progression --message="Starting a systemd service..." --time --weight=3
|
||||
|
||||
ynh_systemd_action --service_name=$app --action="start" --log_path="/var/log/$app/$app.log"
|
||||
|
||||
|
@ -127,4 +121,4 @@ ynh_systemd_action --service_name=nginx --action=reload
|
|||
# END OF SCRIPT
|
||||
#=================================================
|
||||
|
||||
ynh_script_progression --message="Change of URL completed for $app" --time --last
|
||||
ynh_script_progression --message="Change of URL completed for $app" --last
|
||||
|
|
|
@ -85,7 +85,7 @@ ynh_app_setting_set --app=$app --key=final_path --value=$final_path
|
|||
# Download, check integrity, uncompress and patch the source from app.src
|
||||
#ynh_setup_source --dest_dir="$final_path"
|
||||
mkdir -p $final_path
|
||||
cp -R ../sources/* $final_path/
|
||||
cp -R ../sources_2/* $final_path/
|
||||
|
||||
#=================================================
|
||||
# CREATE A SERVER CERTIFICATE
|
||||
|
@ -130,7 +130,7 @@ cp ../conf/passwd $final_path/data/passwd
|
|||
ynh_replace_string --match_string="__ADMIN__" --replace_string="$admin" --target_file="$final_path/data/passwd"
|
||||
ynh_replace_string --match_string="__PASSWORD__" --replace_string="$password" --target_file="$final_path/data/passwd"
|
||||
|
||||
cp ../sources/groups/groupname.json $final_path/groups/$group_name.json
|
||||
cp ../sources_2/groups/groupname.json $final_path/groups/$group_name.json
|
||||
|
||||
ynh_replace_string --match_string="__ADMIN__" --replace_string="$admin" --target_file="$final_path/groups/$group_name.json"
|
||||
ynh_replace_string --match_string="__PASSWORD__" --replace_string="$password" --target_file="$final_path/groups/$group_name.json"
|
||||
|
@ -179,7 +179,7 @@ ynh_systemd_action --service_name=$app --action="start" --log_path="/var/log/$ap
|
|||
#=================================================
|
||||
# SETUP SSOWAT
|
||||
#=================================================
|
||||
ynh_script_progression --message="Configuring SSOwat..." --weight=2
|
||||
ynh_script_progression --message="Configuring permissions..." --weight=2
|
||||
|
||||
# Make app public if necessary
|
||||
if [ $is_public -eq 1 ]
|
||||
|
|
|
@ -82,7 +82,8 @@ then
|
|||
ynh_secure_remove --file="$final_path"
|
||||
|
||||
mkdir -p $final_path
|
||||
cp -R ../sources/* $final_path/
|
||||
|
||||
cp -R ../sources_2/* $final_path/
|
||||
|
||||
# Copy the admin saved settings from tmp directory to final path
|
||||
cp -ar "$tmpdir/groups" "$final_path/groups"
|
||||
|
@ -94,7 +95,7 @@ then
|
|||
pushd "$final_path"
|
||||
ynh_exec_warn_less openssl req -newkey rsa:2048 -nodes -keyout data/key.pem -x509 -days 365 -out data/cert.pem \
|
||||
-subj "/C=/ST=/L=/O=/OU=/CN=/emailAddress="
|
||||
chmod 640 data/{key.pem,cert.pem}
|
||||
chmod 640 data/{key.pem,cert.pem}
|
||||
popd
|
||||
fi
|
||||
|
||||
|
@ -129,19 +130,6 @@ ynh_script_progression --message="Upgrading systemd configuration..." --weight=1
|
|||
# Create a dedicated systemd config
|
||||
ynh_add_systemd_config
|
||||
|
||||
#=================================================
|
||||
# MODIFY A CONFIG FILE
|
||||
#=================================================
|
||||
|
||||
# ### Verify the checksum of a file, stored by `ynh_store_file_checksum` in the install script.
|
||||
# ### And create a backup of this file if the checksum is different. So the file will be backed up if the admin had modified it.
|
||||
# ynh_backup_if_checksum_is_different --file="$final_path/CONFIG_FILE"
|
||||
|
||||
# ynh_replace_string --match_string="match_string" --replace_string="replace_string" --target_file="$final_path/CONFIG_FILE"
|
||||
|
||||
# # Recalculate and store the checksum of the file for the next upgrade.
|
||||
# ynh_store_file_checksum --file="$final_path/CONFIG_FILE"
|
||||
|
||||
#=================================================
|
||||
# GENERIC FINALIZATION
|
||||
#=================================================
|
||||
|
|
1
sources_2/data/ice-servers.json
Executable file
1
sources_2/data/ice-servers.json
Executable file
|
@ -0,0 +1 @@
|
|||
[]
|
BIN
sources_2/galene
Executable file
BIN
sources_2/galene
Executable file
Binary file not shown.
107
sources_2/group/client.go
Executable file
107
sources_2/group/client.go
Executable file
|
@ -0,0 +1,107 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"github.com/jech/galene/conn"
|
||||
)
|
||||
|
||||
type RawPassword struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Salt string `json:"salt,omitempty"`
|
||||
Iterations int `json:"iterations,omitempty"`
|
||||
}
|
||||
|
||||
type Password RawPassword
|
||||
|
||||
func (p Password) Match(pw string) (bool, error) {
|
||||
switch p.Type {
|
||||
case "":
|
||||
return p.Key == pw, nil
|
||||
case "pbkdf2":
|
||||
key, err := hex.DecodeString(p.Key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
salt, err := hex.DecodeString(p.Salt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var h func() hash.Hash
|
||||
switch p.Hash {
|
||||
case "sha-256":
|
||||
h = sha256.New
|
||||
default:
|
||||
return false, errors.New("unknown hash type")
|
||||
}
|
||||
theirKey := pbkdf2.Key(
|
||||
[]byte(pw), salt, p.Iterations, len(key), h,
|
||||
)
|
||||
return bytes.Compare(key, theirKey) == 0, nil
|
||||
default:
|
||||
return false, errors.New("unknown password type")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Password) UnmarshalJSON(b []byte) error {
|
||||
var k string
|
||||
err := json.Unmarshal(b, &k)
|
||||
if err == nil {
|
||||
*p = Password{
|
||||
Key: k,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var r RawPassword
|
||||
err = json.Unmarshal(b, &r)
|
||||
if err == nil {
|
||||
*p = Password(r)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Password) MarshalJSON() ([]byte, error) {
|
||||
if p.Type == "" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 {
|
||||
return json.Marshal(p.Key)
|
||||
}
|
||||
return json.Marshal(RawPassword(p))
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password *Password `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type ClientPermissions struct {
|
||||
Op bool `json:"op,omitempty"`
|
||||
Present bool `json:"present,omitempty"`
|
||||
Record bool `json:"record,omitempty"`
|
||||
}
|
||||
|
||||
type Challengeable interface {
|
||||
Username() string
|
||||
Challenge(string, ClientCredentials) bool
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Group() *Group
|
||||
Id() string
|
||||
Challengeable
|
||||
SetPermissions(ClientPermissions)
|
||||
OverridePermissions(*Group) bool
|
||||
PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack) error
|
||||
PushClient(id, username string, add bool) error
|
||||
}
|
||||
|
||||
type Kickable interface {
|
||||
Kick(id, user, message string) error
|
||||
}
|
87
sources_2/group/client_test.go
Executable file
87
sources_2/group/client_test.go
Executable file
|
@ -0,0 +1,87 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var pw1 = Password{}
|
||||
var pw2 = Password{Key: "pass"}
|
||||
var pw3 = Password{
|
||||
Type: "pbkdf2",
|
||||
Hash: "sha-256",
|
||||
Key: "fe499504e8f144693fae828e8e371d50e019d0e4c84994fa03f7f445bd8a570a",
|
||||
Salt: "bcc1717851030776",
|
||||
Iterations: 4096,
|
||||
}
|
||||
var pw4 = Password{
|
||||
Type: "bad",
|
||||
}
|
||||
|
||||
func TestGood(t *testing.T) {
|
||||
if match, err := pw2.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw2 doesn't match (%v)", err)
|
||||
}
|
||||
if match, err := pw3.Match("pass"); err != nil || !match {
|
||||
t.Errorf("pw3 doesn't match (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBad(t *testing.T) {
|
||||
if match, err := pw1.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw1 matches")
|
||||
}
|
||||
if match, err := pw2.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw2 matches")
|
||||
}
|
||||
if match, err := pw3.Match("bad"); err != nil || match {
|
||||
t.Errorf("pw3 matches")
|
||||
}
|
||||
if match, err := pw4.Match("bad"); err == nil || match {
|
||||
t.Errorf("pw4 matches")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
plain, err := json.Marshal(pw2)
|
||||
if err != nil || string(plain) != `"pass"` {
|
||||
t.Errorf("Expected \"pass\", got %v", string(plain))
|
||||
}
|
||||
|
||||
for _, pw := range []Password{pw1, pw2, pw3, pw4} {
|
||||
j, err := json.Marshal(pw)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
if testing.Verbose() {
|
||||
log.Printf("%v", string(j))
|
||||
}
|
||||
var pw2 Password
|
||||
err = json.Unmarshal(j, &pw2)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
} else if !reflect.DeepEqual(pw, pw2) {
|
||||
t.Errorf("Expected %v, got %v", pw, pw2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPlain(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
match, err := pw2.Match("bad")
|
||||
if err != nil || match {
|
||||
b.Errorf("pw2 matched")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPBKDF2(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
match, err := pw3.Match("bad")
|
||||
if err != nil || match {
|
||||
b.Errorf("pw3 matched")
|
||||
}
|
||||
}
|
||||
}
|
852
sources_2/group/group.go
Executable file
852
sources_2/group/group.go
Executable file
|
@ -0,0 +1,852 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
var Directory string
|
||||
var UseMDNS bool
|
||||
|
||||
var ErrNotAuthorised = errors.New("not authorised")
|
||||
|
||||
type UserError string
|
||||
|
||||
func (err UserError) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
type KickError struct {
|
||||
Id string
|
||||
Username string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err KickError) Error() string {
|
||||
m := "kicked out"
|
||||
if err.Message != "" {
|
||||
m += "(" + err.Message + ")"
|
||||
}
|
||||
if err.Username != "" {
|
||||
m += " by " + err.Username
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type ProtocolError string
|
||||
|
||||
func (err ProtocolError) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
||||
type ChatHistoryEntry struct {
|
||||
Id string
|
||||
User string
|
||||
Time int64
|
||||
Kind string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
const (
|
||||
MinBitrate = 200000
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
name string
|
||||
api *webrtc.API
|
||||
|
||||
mu sync.Mutex
|
||||
description *description
|
||||
locked *string
|
||||
clients map[string]Client
|
||||
history []ChatHistoryEntry
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func (g *Group) Name() string {
|
||||
return g.name
|
||||
}
|
||||
|
||||
func (g *Group) Locked() (bool, string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if g.locked != nil {
|
||||
return true, *g.locked
|
||||
} else {
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) SetLocked(locked bool, message string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if locked {
|
||||
g.locked = &message
|
||||
} else {
|
||||
g.locked = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) Public() bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.description.Public
|
||||
}
|
||||
|
||||
func (g *Group) Redirect() string {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.description.Redirect
|
||||
}
|
||||
|
||||
func (g *Group) AllowRecording() bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.description.AllowRecording
|
||||
}
|
||||
|
||||
var groups struct {
|
||||
mu sync.Mutex
|
||||
groups map[string]*Group
|
||||
}
|
||||
|
||||
func (g *Group) API() *webrtc.API {
|
||||
return g.api
|
||||
}
|
||||
|
||||
func codecFromName(name string) (webrtc.RTPCodecCapability, error) {
|
||||
switch name {
|
||||
case "vp8":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"video/VP8", 90000, 0,
|
||||
"",
|
||||
nil,
|
||||
}, nil
|
||||
case "vp9":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"video/VP9", 90000, 0,
|
||||
"profile-id=2",
|
||||
nil,
|
||||
}, nil
|
||||
case "h264":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"video/H264", 90000, 0,
|
||||
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||
nil,
|
||||
}, nil
|
||||
case "opus":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"audio/opus", 48000, 2,
|
||||
"minptime=10;useinbandfec=1",
|
||||
nil,
|
||||
}, nil
|
||||
case "g722":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"audio/G722", 8000, 1,
|
||||
"",
|
||||
nil,
|
||||
}, nil
|
||||
case "pcmu":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"audio/PCMU", 8000, 1,
|
||||
"",
|
||||
nil,
|
||||
}, nil
|
||||
case "pcma":
|
||||
return webrtc.RTPCodecCapability{
|
||||
"audio/PCMA", 8000, 1,
|
||||
"",
|
||||
nil,
|
||||
}, nil
|
||||
default:
|
||||
return webrtc.RTPCodecCapability{}, errors.New("unknown codec")
|
||||
}
|
||||
}
|
||||
|
||||
func payloadType(codec webrtc.RTPCodecCapability) (webrtc.PayloadType, error) {
|
||||
switch strings.ToLower(codec.MimeType) {
|
||||
case "video/vp8":
|
||||
return 96, nil
|
||||
case "video/vp9":
|
||||
return 98, nil
|
||||
case "video/h264":
|
||||
return 102, nil
|
||||
case "audio/opus":
|
||||
return 111, nil
|
||||
case "audio/g722":
|
||||
return 9, nil
|
||||
case "audio/pcmu":
|
||||
return 0, nil
|
||||
case "audio/pcma":
|
||||
return 8, nil
|
||||
default:
|
||||
return 0, errors.New("unknown codec")
|
||||
}
|
||||
}
|
||||
|
||||
func APIFromCodecs(codecs []webrtc.RTPCodecCapability) *webrtc.API {
|
||||
s := webrtc.SettingEngine{}
|
||||
s.SetSRTPReplayProtectionWindow(512)
|
||||
if !UseMDNS {
|
||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
}
|
||||
m := webrtc.MediaEngine{}
|
||||
|
||||
for _, codec := range codecs {
|
||||
var tpe webrtc.RTPCodecType
|
||||
var fb []webrtc.RTCPFeedback
|
||||
if strings.HasPrefix(strings.ToLower(codec.MimeType), "video/") {
|
||||
tpe = webrtc.RTPCodecTypeVideo
|
||||
fb = []webrtc.RTCPFeedback{
|
||||
{"goog-remb", ""},
|
||||
{"nack", ""},
|
||||
{"nack", "pli"},
|
||||
{"ccm", "fir"},
|
||||
}
|
||||
} else if strings.HasPrefix(strings.ToLower(codec.MimeType), "audio/") {
|
||||
tpe = webrtc.RTPCodecTypeAudio
|
||||
fb = []webrtc.RTCPFeedback{}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
ptpe, err := payloadType(codec)
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
continue
|
||||
}
|
||||
m.RegisterCodec(
|
||||
webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: codec.MimeType,
|
||||
ClockRate: codec.ClockRate,
|
||||
Channels: codec.Channels,
|
||||
SDPFmtpLine: codec.SDPFmtpLine,
|
||||
RTCPFeedback: fb,
|
||||
},
|
||||
PayloadType: ptpe,
|
||||
},
|
||||
tpe,
|
||||
)
|
||||
}
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithSettingEngine(s),
|
||||
webrtc.WithMediaEngine(&m),
|
||||
)
|
||||
}
|
||||
|
||||
func APIFromNames(names []string) *webrtc.API {
|
||||
if len(names) == 0 {
|
||||
names = []string{"vp8", "opus"}
|
||||
}
|
||||
codecs := make([]webrtc.RTPCodecCapability, 0, len(names))
|
||||
for _, n := range names {
|
||||
codec, err := codecFromName(n)
|
||||
if err != nil {
|
||||
log.Printf("Codec %v: %v", n, err)
|
||||
continue
|
||||
}
|
||||
codecs = append(codecs, codec)
|
||||
}
|
||||
|
||||
return APIFromCodecs(codecs)
|
||||
}
|
||||
|
||||
func Add(name string, desc *description) (*Group, error) {
|
||||
if name == "" || strings.HasSuffix(name, "/") {
|
||||
return nil, UserError("illegal group name")
|
||||
}
|
||||
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
if groups.groups == nil {
|
||||
groups.groups = make(map[string]*Group)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
g := groups.groups[name]
|
||||
if g == nil {
|
||||
if desc == nil {
|
||||
desc, err = GetDescription(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
g = &Group{
|
||||
name: name,
|
||||
description: desc,
|
||||
clients: make(map[string]Client),
|
||||
timestamp: time.Now(),
|
||||
api: APIFromNames(desc.Codecs),
|
||||
}
|
||||
groups.groups[name] = g
|
||||
return g, nil
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if desc != nil {
|
||||
g.description = desc
|
||||
g.api = APIFromNames(desc.Codecs)
|
||||
return g, nil
|
||||
}
|
||||
|
||||
if time.Since(g.description.loadTime) > 5*time.Second {
|
||||
if descriptionChanged(name, g.description) {
|
||||
desc, err := GetDescription(name)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Reading group %v: %v",
|
||||
name, err)
|
||||
}
|
||||
deleteUnlocked(g)
|
||||
return nil, err
|
||||
}
|
||||
g.description = desc
|
||||
g.api = APIFromNames(desc.Codecs)
|
||||
} else {
|
||||
g.description.loadTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func Range(f func(g *Group) bool) {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
for _, g := range groups.groups {
|
||||
ok := f(g)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetNames() []string {
|
||||
names := make([]string, 0)
|
||||
|
||||
Range(func(g *Group) bool {
|
||||
names = append(names, g.name)
|
||||
return true
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
type SubGroup struct {
|
||||
Name string
|
||||
Clients int
|
||||
}
|
||||
|
||||
func GetSubGroups(parent string) []SubGroup {
|
||||
prefix := parent + "/"
|
||||
subgroups := make([]SubGroup, 0)
|
||||
|
||||
Range(func(g *Group) bool {
|
||||
if strings.HasPrefix(g.name, prefix) {
|
||||
g.mu.Lock()
|
||||
count := len(g.clients)
|
||||
g.mu.Unlock()
|
||||
if count > 0 {
|
||||
subgroups = append(subgroups,
|
||||
SubGroup{g.name, count})
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return subgroups
|
||||
}
|
||||
|
||||
func Get(name string) *Group {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
|
||||
return groups.groups[name]
|
||||
}
|
||||
|
||||
func Delete(name string) bool {
|
||||
groups.mu.Lock()
|
||||
defer groups.mu.Unlock()
|
||||
g := groups.groups[name]
|
||||
if g == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return deleteUnlocked(g)
|
||||
}
|
||||
|
||||
// Called with both groups.mu and g.mu taken.
|
||||
func deleteUnlocked(g *Group) bool {
|
||||
if len(g.clients) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(groups.groups, g.name)
|
||||
return true
|
||||
}
|
||||
|
||||
func Expire() {
|
||||
names := GetNames()
|
||||
now := time.Now()
|
||||
|
||||
for _, name := range names {
|
||||
g := Get(name)
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
old := false
|
||||
|
||||
g.mu.Lock()
|
||||
empty := len(g.clients) == 0
|
||||
if empty && !g.description.Public {
|
||||
age := now.Sub(g.timestamp)
|
||||
old = age > maxHistoryAge(g.description)
|
||||
}
|
||||
// We cannot take groups.mu at this point without a deadlock.
|
||||
g.mu.Unlock()
|
||||
|
||||
if empty && old {
|
||||
// Delete will check if the group is still empty
|
||||
Delete(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddClient(group string, c Client) (*Group, error) {
|
||||
g, err := Add(group, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if !c.OverridePermissions(g) {
|
||||
perms, err := g.description.GetPermission(group, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.SetPermissions(perms)
|
||||
|
||||
if !perms.Op && g.locked != nil {
|
||||
m := *g.locked
|
||||
if m == "" {
|
||||
m = "group is locked"
|
||||
}
|
||||
return nil, UserError(m)
|
||||
}
|
||||
|
||||
if !perms.Op && g.description.MaxClients > 0 {
|
||||
if len(g.clients) >= g.description.MaxClients {
|
||||
return nil, UserError("too many users")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.clients[c.Id()] != nil {
|
||||
return nil, ProtocolError("duplicate client id")
|
||||
}
|
||||
|
||||
g.clients[c.Id()] = c
|
||||
g.timestamp = time.Now()
|
||||
|
||||
go func(clients []Client) {
|
||||
u := c.Username()
|
||||
c.PushClient(c.Id(), u, true)
|
||||
for _, cc := range clients {
|
||||
uu := cc.Username()
|
||||
c.PushClient(cc.Id(), uu, true)
|
||||
cc.PushClient(c.Id(), u, true)
|
||||
}
|
||||
}(g.getClientsUnlocked(c))
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func DelClient(c Client) {
|
||||
g := c.Group()
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if g.clients[c.Id()] != c {
|
||||
log.Printf("Deleting unknown client")
|
||||
return
|
||||
}
|
||||
delete(g.clients, c.Id())
|
||||
g.timestamp = time.Now()
|
||||
|
||||
go func(clients []Client) {
|
||||
for _, cc := range clients {
|
||||
cc.PushClient(c.Id(), c.Username(), false)
|
||||
}
|
||||
}(g.getClientsUnlocked(nil))
|
||||
}
|
||||
|
||||
func (g *Group) GetClients(except Client) []Client {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.getClientsUnlocked(except)
|
||||
}
|
||||
|
||||
func (g *Group) getClientsUnlocked(except Client) []Client {
|
||||
clients := make([]Client, 0, len(g.clients))
|
||||
for _, c := range g.clients {
|
||||
if c != except {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func (g *Group) GetClient(id string) Client {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.getClientUnlocked(id)
|
||||
}
|
||||
|
||||
func (g *Group) getClientUnlocked(id string) Client {
|
||||
for idd, c := range g.clients {
|
||||
if idd == id {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) Range(f func(c Client) bool) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
for _, c := range g.clients {
|
||||
ok := f(c)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) Shutdown(message string) {
|
||||
g.Range(func(c Client) bool {
|
||||
cc, ok := c.(Kickable)
|
||||
if ok {
|
||||
cc.Kick("", "", message)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
type warner interface {
|
||||
Warn(oponly bool, message string) error
|
||||
}
|
||||
|
||||
func (g *Group) WallOps(message string) {
|
||||
clients := g.GetClients(nil)
|
||||
for _, c := range clients {
|
||||
w, ok := c.(warner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := w.Warn(true, message)
|
||||
if err != nil {
|
||||
log.Printf("WallOps: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FromJSTime(tm int64) time.Time {
|
||||
if tm == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(int64(tm)/1000, (int64(tm)%1000)*1000000)
|
||||
}
|
||||
|
||||
func ToJSTime(tm time.Time) int64 {
|
||||
return int64((tm.Sub(time.Unix(0, 0)) + time.Millisecond/2) /
|
||||
time.Millisecond)
|
||||
}
|
||||
|
||||
const maxChatHistory = 50
|
||||
|
||||
func (g *Group) ClearChatHistory() {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.history = nil
|
||||
}
|
||||
|
||||
func (g *Group) AddToChatHistory(id, user string, time int64, kind string, value interface{}) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if len(g.history) >= maxChatHistory {
|
||||
copy(g.history, g.history[1:])
|
||||
g.history = g.history[:len(g.history)-1]
|
||||
}
|
||||
g.history = append(g.history,
|
||||
ChatHistoryEntry{Id: id, User: user, Time: time, Kind: kind, Value: value},
|
||||
)
|
||||
}
|
||||
|
||||
func discardObsoleteHistory(h []ChatHistoryEntry, duration time.Duration) []ChatHistoryEntry {
|
||||
i := 0
|
||||
for i < len(h) {
|
||||
if time.Since(FromJSTime(h[i].Time)) <= duration {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i > 0 {
|
||||
copy(h, h[i:])
|
||||
h = h[:len(h)-i]
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (g *Group) GetChatHistory() []ChatHistoryEntry {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
g.history = discardObsoleteHistory(
|
||||
g.history, maxHistoryAge(g.description),
|
||||
)
|
||||
|
||||
h := make([]ChatHistoryEntry, len(g.history))
|
||||
copy(h, g.history)
|
||||
return h
|
||||
}
|
||||
|
||||
func matchClient(group string, c Challengeable, users []ClientCredentials) (bool, bool) {
|
||||
for _, u := range users {
|
||||
if u.Username == "" {
|
||||
if c.Challenge(group, u) {
|
||||
return true, true
|
||||
}
|
||||
} else if u.Username == c.Username() {
|
||||
if c.Challenge(group, u) {
|
||||
return true, true
|
||||
} else {
|
||||
return true, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
type description struct {
|
||||
fileName string `json:"-"`
|
||||
loadTime time.Time `json:"-"`
|
||||
modTime time.Time `json:"-"`
|
||||
fileSize int64 `json:"-"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Redirect string `json:"redirect,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
MaxClients int `json:"max-clients,omitempty"`
|
||||
MaxHistoryAge int `json:"max-history-age,omitempty"`
|
||||
AllowAnonymous bool `json:"allow-anonymous,omitempty"`
|
||||
AllowRecording bool `json:"allow-recording,omitempty"`
|
||||
AllowSubgroups bool `json:"allow-subgroups,omitempty"`
|
||||
Op []ClientCredentials `json:"op,omitempty"`
|
||||
Presenter []ClientCredentials `json:"presenter,omitempty"`
|
||||
Other []ClientCredentials `json:"other,omitempty"`
|
||||
Codecs []string `json:"codecs,omitempty"`
|
||||
}
|
||||
|
||||
const DefaultMaxHistoryAge = 4 * time.Hour
|
||||
|
||||
func maxHistoryAge(desc *description) time.Duration {
|
||||
if desc.MaxHistoryAge != 0 {
|
||||
return time.Duration(desc.MaxHistoryAge) * time.Second
|
||||
}
|
||||
return DefaultMaxHistoryAge
|
||||
}
|
||||
|
||||
func openDescriptionFile(name string) (*os.File, string, bool, error) {
|
||||
isParent := false
|
||||
for name != "" {
|
||||
fileName := filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
r, err := os.Open(fileName)
|
||||
if !os.IsNotExist(err) {
|
||||
return r, fileName, isParent, err
|
||||
}
|
||||
isParent = true
|
||||
name, _ = path.Split(name)
|
||||
name = strings.TrimRight(name, "/")
|
||||
}
|
||||
return nil, "", false, os.ErrNotExist
|
||||
}
|
||||
|
||||
func statDescriptionFile(name string) (os.FileInfo, string, bool, error) {
|
||||
isParent := false
|
||||
for name != "" {
|
||||
fileName := filepath.Join(
|
||||
Directory, path.Clean("/"+name)+".json",
|
||||
)
|
||||
fi, err := os.Stat(fileName)
|
||||
if !os.IsNotExist(err) {
|
||||
return fi, fileName, isParent, err
|
||||
}
|
||||
isParent = true
|
||||
name, _ = path.Split(name)
|
||||
name = strings.TrimRight(name, "/")
|
||||
}
|
||||
return nil, "", false, os.ErrNotExist
|
||||
}
|
||||
|
||||
// descriptionChanged returns true if a group's description may have
|
||||
// changed since it was last read.
|
||||
func descriptionChanged(name string, desc *description) bool {
|
||||
fi, fileName, _, err := statDescriptionFile(name)
|
||||
if err != nil || fileName != desc.fileName {
|
||||
return true
|
||||
}
|
||||
|
||||
if fi.Size() != desc.fileSize || fi.ModTime() != desc.modTime {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetDescription(name string) (*description, error) {
|
||||
r, fileName, isParent, err := openDescriptionFile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var desc description
|
||||
|
||||
fi, err := r.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := json.NewDecoder(r)
|
||||
err = d.Decode(&desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isParent {
|
||||
if !desc.AllowSubgroups {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
desc.Public = false
|
||||
desc.Description = ""
|
||||
}
|
||||
|
||||
desc.fileName = fileName
|
||||
desc.fileSize = fi.Size()
|
||||
desc.modTime = fi.ModTime()
|
||||
desc.loadTime = time.Now()
|
||||
|
||||
return &desc, nil
|
||||
}
|
||||
|
||||
func (desc *description) GetPermission(group string, c Challengeable) (ClientPermissions, error) {
|
||||
var p ClientPermissions
|
||||
if !desc.AllowAnonymous && c.Username() == "" {
|
||||
return p, UserError("anonymous users not allowed in this group, please choose a username")
|
||||
}
|
||||
if found, good := matchClient(group, c, desc.Op); found {
|
||||
if good {
|
||||
p.Op = true
|
||||
p.Present = true
|
||||
if desc.AllowRecording {
|
||||
p.Record = true
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
if found, good := matchClient(group, c, desc.Presenter); found {
|
||||
if good {
|
||||
p.Present = true
|
||||
return p, nil
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
if found, good := matchClient(group, c, desc.Other); found {
|
||||
if good {
|
||||
return p, nil
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
return p, ErrNotAuthorised
|
||||
}
|
||||
|
||||
type Public struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ClientCount int `json:"clientCount"`
|
||||
}
|
||||
|
||||
func GetPublic() []Public {
|
||||
gs := make([]Public, 0)
|
||||
Range(func(g *Group) bool {
|
||||
if g.Public() {
|
||||
gs = append(gs, Public{
|
||||
Name: g.name,
|
||||
Description: g.description.Description,
|
||||
ClientCount: len(g.clients),
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
sort.Slice(gs, func(i, j int) bool {
|
||||
return gs[i].Name < gs[j].Name
|
||||
})
|
||||
return gs
|
||||
}
|
||||
|
||||
func ReadPublicGroups() {
|
||||
dir, err := os.Open(Directory)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
fis, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
log.Printf("readPublicGroups: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, fi := range fis {
|
||||
if !strings.HasSuffix(fi.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
name := fi.Name()[:len(fi.Name())-5]
|
||||
desc, err := GetDescription(name)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Reading group %v: %v", name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if desc.Public {
|
||||
Add(name, desc)
|
||||
}
|
||||
}
|
||||
}
|
58
sources_2/group/group_test.go
Executable file
58
sources_2/group/group_test.go
Executable file
|
@ -0,0 +1,58 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJSTime(t *testing.T) {
|
||||
tm := time.Now()
|
||||
js := ToJSTime(tm)
|
||||
tm2 := FromJSTime(js)
|
||||
js2 := ToJSTime(tm2)
|
||||
|
||||
if js != js2 {
|
||||
t.Errorf("%v != %v", js, js2)
|
||||
}
|
||||
|
||||
delta := tm.Sub(tm2)
|
||||
if delta < -time.Millisecond/2 || delta > time.Millisecond/2 {
|
||||
t.Errorf("Delta %v, %v, %v", delta, tm, tm2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionJSON(t *testing.T) {
|
||||
d := `
|
||||
{
|
||||
"op":[{"username": "jch","password": "topsecret"}],
|
||||
"max-history-age": 10,
|
||||
"allow-subgroups": true,
|
||||
"presenter":[
|
||||
{"user": "john", "password": "secret"},
|
||||
{}
|
||||
]
|
||||
}`
|
||||
|
||||
var dd description
|
||||
err := json.Unmarshal([]byte(d), &dd)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
ddd, err := json.Marshal(dd)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var dddd description
|
||||
err = json.Unmarshal([]byte(ddd), &dddd)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(dd, dddd) {
|
||||
t.Errorf("Got %v, expected %v", dddd, dd)
|
||||
}
|
||||
}
|
4
sources_2/groups/groupname.json
Normal file
4
sources_2/groups/groupname.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"op": [{"username": "__ADMIN__", "password": "__PASSWORD__"}],
|
||||
"presenter": [{}]
|
||||
}
|
69
sources_2/static/404.css
Executable file
69
sources_2/static/404.css
Executable file
|
@ -0,0 +1,69 @@
|
|||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
padding: 0px 30px;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.landing-page {
|
||||
max-width: 960px;
|
||||
height: 475px;
|
||||
margin: 0;
|
||||
box-shadow: 0px 0px 8px 1px #ccc;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #7e7e7e;
|
||||
font-size: 8em;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.logo .fa {
|
||||
color: #c39999;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin: 0;
|
||||
color: #7e7e7e;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
width: 35%;
|
||||
margin: 16px auto 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
background: #610a86;
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 4px 0 #ccc;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
31
sources_2/static/404.html
Executable file
31
sources_2/static/404.html
Executable file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Page not Found</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="/404.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
<!-- Font Awesome File -->
|
||||
<link rel="stylesheet" type="text/css" href="/css/fontawesome.min.css">
|
||||
<link href="/css/solid.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/regular.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="landing-page">
|
||||
<div class="logo">
|
||||
<i class="fas fa-frown" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<h1> Page not found!</h1>
|
||||
<p> We can't find the page you're looking for.</p>
|
||||
<a href="/" class="home-link">Back to home</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
44
sources_2/static/common.css
Executable file
44
sources_2/static/common.css
Executable file
|
@ -0,0 +1,44 @@
|
|||
h1 {
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.signature {
|
||||
border-top: solid;
|
||||
padding-top: 0;
|
||||
border-width: thin;
|
||||
clear: both;
|
||||
height: 3.125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
body, html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Metropolis,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #687281;
|
||||
text-align: left;
|
||||
background-color: #eff3f9;
|
||||
}
|
||||
|
||||
*, :after, :before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: Metropolis,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
5
sources_2/static/css/fontawesome.min.css
vendored
Executable file
5
sources_2/static/css/fontawesome.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
15
sources_2/static/css/regular.css
Executable file
15
sources_2/static/css/regular.css
Executable file
|
@ -0,0 +1,15 @@
|
|||
/*!
|
||||
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-regular-400.eot");
|
||||
src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.far {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400; }
|
16
sources_2/static/css/solid.css
Executable file
16
sources_2/static/css/solid.css
Executable file
|
@ -0,0 +1,16 @@
|
|||
/*!
|
||||
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-solid-900.eot");
|
||||
src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
|
||||
|
||||
.fa,
|
||||
.fas {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900; }
|
15
sources_2/static/css/toastify.min.css
vendored
Executable file
15
sources_2/static/css/toastify.min.css
vendored
Executable file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Minified by jsDelivr using clean-css v4.2.3.
|
||||
* Original file: /npm/toastify-js@1.9.1/src/toastify.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
/*!
|
||||
* Toastify js 1.9.1
|
||||
* https://github.com/apvarun/toastify-js
|
||||
* @license MIT licensed
|
||||
*
|
||||
* Copyright (C) 2018 Varun A P
|
||||
*/
|
||||
.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215,.61,.355,1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px);z-index:2147483647}.toastify.on{opacity:1}.toast-close{opacity:.4;padding:0 5px}.toastify-right{right:15px}.toastify-left{left:15px}.toastify-top{top:-150px}.toastify-bottom{bottom:-150px}.toastify-rounded{border-radius:25px}.toastify-avatar{width:1.5em;height:1.5em;margin:-7px 5px;border-radius:2px}.toastify-center{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}@media only screen and (max-width:360px){.toastify-left,.toastify-right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
|
||||
/*# sourceMappingURL=/sm/9c0bbf2acc17f6468f9dd75307f4d772b55e466d0ddceef6dc95ee31ca309918.map */
|
1307
sources_2/static/galene.css
Executable file
1307
sources_2/static/galene.css
Executable file
File diff suppressed because it is too large
Load diff
255
sources_2/static/galene.html
Executable file
255
sources_2/static/galene.html
Executable file
|
@ -0,0 +1,255 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Galène</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="ScreenOrientation" content="autoRotate:disabled">
|
||||
<link rel="stylesheet" type="text/css" href="/common.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/galene.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
<!-- Font Awesome File -->
|
||||
<link href="/css/fontawesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/solid.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/regular.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/toastify.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main" class="app">
|
||||
<div class="row full-height">
|
||||
<nav id="left-sidebar">
|
||||
<div class="users-header">
|
||||
<div class="galene-header">Galène</div>
|
||||
</div>
|
||||
<div class="header-sep"></div>
|
||||
<div id="users"></div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<header>
|
||||
<nav class="topnav navbar navbar-expand navbar-light fixed-top">
|
||||
<div id="header">
|
||||
<div class="collapse" title="Collapse left panel" id="sidebarCollapse">
|
||||
<svg class="svg-inline--fa" aria-hidden="true" data-icon="align-left" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<path fill="currentColor" d="M288 44v40c0 8.837-7.163 16-16 16H16c-8.837 0-16-7.163-16-16V44c0-8.837 7.163-16 16-16h256c8.837 0 16 7.163 16 16zM0 172v40c0 8.837 7.163 16 16 16h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16zm16 312h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm256-200H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16h256c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 id="title" class="header-title"></h1>
|
||||
</div>
|
||||
|
||||
<ul class="nav-menu">
|
||||
<li>
|
||||
<button id="presentbutton" class="invisible btn btn-success">
|
||||
<i class="fas fa-play" aria-hidden="true"></i><span class="nav-text"> Ready</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="unpresentbutton" class="invisible btn btn-cancel">
|
||||
<i class="fas fa-stop" aria-hidden="true"></i><span class="nav-text"> Panic</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<div id="mutebutton" class="nav-link nav-button">
|
||||
<span><i class="fas fa-microphone-slash" aria-hidden="true"></i></span>
|
||||
<label>Mute</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div id="sharebutton" class="invisible nav-link nav-button">
|
||||
<span><i class="fas fa-share-square" aria-hidden="true"></i></span>
|
||||
<label>Share Screen</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div id="unsharebutton" class="invisible nav-link nav-button nav-cancel">
|
||||
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
|
||||
<label>Unshare Screen</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div id="stopvideobutton" class="invisible nav-link nav-button nav-cancel">
|
||||
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
|
||||
<label>Stop Video</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="nav-button nav-link nav-more" id="openside">
|
||||
<span><i class="fas fa-ellipsis-v" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="row full-width" id="mainrow">
|
||||
<div class="coln-left" id="left">
|
||||
<div id="chat">
|
||||
<div id="chatbox">
|
||||
<div class="close-chat" id="close-chat" title="Hide chat">
|
||||
<span class="close-icon"></span>
|
||||
</div>
|
||||
<div id="box"></div>
|
||||
<div class="reply">
|
||||
<form id="inputform">
|
||||
<textarea id="input" class="form-reply"></textarea>
|
||||
<input id="inputbutton" type="submit" value="➤" class="btn btn-default"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resizer"></div>
|
||||
<div class="coln-right" id="right">
|
||||
<span class="show-video blink" id="switch-video"><i class="fas fa-exchange" aria-hidden="true"></i></span>
|
||||
<div class="collapse-video" id="collapse-video">
|
||||
<i class="far fa-comment-alt open-chat" title="Open chat"></i>
|
||||
</div>
|
||||
<div class="video-container no-video" id="video-container">
|
||||
<div id="expand-video" class="expand-video">
|
||||
<div id="peers"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-container invisible" id="login-container">
|
||||
<div class="login-box">
|
||||
<form id="userform" class="userform">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" type="text" name="username"
|
||||
autocomplete="username" class="form-control"/>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" name="password"
|
||||
autocomplete="current-password" class="form-control"/>
|
||||
<label>Auto ready</label>
|
||||
<div class="present-switch">
|
||||
<p class="switch-radio">
|
||||
<input id="presentoff" type="radio" name="presentradio" value="" checked/>
|
||||
<label for="presentoff">Disabled</label>
|
||||
</p>
|
||||
<p class="switch-radio">
|
||||
<input id="presentmike" type="radio" name="presentradio" value="mike"/>
|
||||
<label for="presentmike">Enable microphone</label>
|
||||
</p>
|
||||
<p class="switch-radio">
|
||||
<input id="presentboth" type="radio" name="presentradio" value="both"/>
|
||||
<label for="presentboth">Enable camera and microphone</label>
|
||||
</p>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
<div class="connect">
|
||||
<input id="connectbutton" type="submit" class="btn btn-blue" value="Connect"/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sidebarnav" class="sidenav">
|
||||
<div class="sidenav-header">
|
||||
<h2>Settings</h2>
|
||||
<a class="closebtn" id="clodeside"><i class="fas fa-times" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
<div class="sidenav-content" id="optionsdiv">
|
||||
<div id="profile" class="profile invisible">
|
||||
<div class="profile-user">
|
||||
<div class="profile-logo">
|
||||
<span><i class="fas fa-user" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<span id="userspan"></span>
|
||||
<span id="permspan"></span>
|
||||
</div>
|
||||
<div class="user-logout">
|
||||
<a id="disconnectbutton">
|
||||
<span class="logout-icon"><i class="fas fa-sign-out-alt"></i></span>
|
||||
<span class="logout-text">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mediaoptions" class="invisible">
|
||||
<fieldset>
|
||||
<legend>Media Options</legend>
|
||||
<label for="videoselect" class="sidenav-label-first">Camera:</label>
|
||||
<select id="videoselect" class="select select-inline">
|
||||
<option value="">off</option>
|
||||
</select>
|
||||
|
||||
<label for="audioselect" class="sidenav-label">Microphone:</label>
|
||||
<select id="audioselect" class="select select-inline">
|
||||
<option value="">off</option>
|
||||
</select>
|
||||
|
||||
<form>
|
||||
<input id="blackboardbox" type="checkbox"/>
|
||||
<label for="blackboardbox">Blackboard mode</label>
|
||||
</form>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Other Settings</legend>
|
||||
|
||||
<form id="sendform">
|
||||
<label for="sendselect" class="sidenav-label-first">Send:</label>
|
||||
<select id="sendselect" class="select select-inline">
|
||||
<option value="lowest">lowest</option>
|
||||
<option value="low">low</option>
|
||||
<option value="normal" selected>normal</option>
|
||||
<option value="unlimited">unlimited</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<form id="requestform">
|
||||
<label for="requestselect" class="sidenav-label">Receive:</label>
|
||||
<select id="requestselect" class="select select-inline">
|
||||
<option value="">nothing</option>
|
||||
<option value="audio">audio only</option>
|
||||
<option value="screenshare">screen share</option>
|
||||
<option value="everything" selected>everything</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<form>
|
||||
<input id="activitybox" type="checkbox"/>
|
||||
<label for="activitybox">Activity detection</label>
|
||||
</form>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<form id="fileform">
|
||||
<label for="fileinput" class=".sidenav-label-first">Play local file:</label>
|
||||
<input type="file" id="fileinput" accept="audio/*,video/*" multiple/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="videocontrols-template" class="invisible">
|
||||
<div class="video-controls vc-overlay">
|
||||
<div class="controls-button controls-left">
|
||||
<span class="video-play" title="Play video">
|
||||
<i class="fas fa-play"></i>
|
||||
</span>
|
||||
<span class="volume" title="Volume">
|
||||
<i class="fas fa-volume-up volume-mute" aria-hidden="true"></i>
|
||||
<input class="volume-slider" type="range" max="100" value="100" min="0" step="5" >
|
||||
</span>
|
||||
</div>
|
||||
<div class="controls-button controls-right">
|
||||
<span class="pip" title="Picture In Picture">
|
||||
<i class="far fa-clone" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="fullscreen" title="Fullscreen">
|
||||
<i class="fas fa-expand" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/protocol.js" defer></script>
|
||||
<script src="/scripts/toastify.js" defer></script>
|
||||
<script src="/galene.js" defer></script>
|
||||
</body>
|
||||
</html>
|
2353
sources_2/static/galene.js
Executable file
2353
sources_2/static/galene.js
Executable file
File diff suppressed because it is too large
Load diff
42
sources_2/static/index.html
Executable file
42
sources_2/static/index.html
Executable file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Galène</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/common.css">
|
||||
<link rel="stylesheet" href="/mainpage.css">
|
||||
<link rel="stylesheet" type="text/css" href="/galene.css"/>
|
||||
<link rel="author" href="https://www.irif.fr/~jch/"/>
|
||||
<!-- Font Awesome File -->
|
||||
<link href="/css/fontawesome.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/css/solid.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="home">
|
||||
<h1 id="title" class="navbar-brand">Galène</h1>
|
||||
|
||||
<form id="groupform">
|
||||
<label for="group">Group:</label>
|
||||
<input id="group" type="text" name="group" class="form-control form-control-inline"/>
|
||||
<input type="submit" value="Join" class="btn btn-default btn-large"/><br/>
|
||||
</form>
|
||||
|
||||
<div id="public-groups" class="groups">
|
||||
<h2>Public groups</h2>
|
||||
|
||||
<table id="public-groups-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="signature">
|
||||
<p><a href="https://galene.org/">Galène</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a>
|
||||
</footer>
|
||||
|
||||
<script src="/mainpage.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
40
sources_2/static/mainpage.css
Executable file
40
sources_2/static/mainpage.css
Executable file
|
@ -0,0 +1,40 @@
|
|||
.groups {
|
||||
}
|
||||
|
||||
.nogroups {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
.home {
|
||||
height: calc(100vh - 50px);
|
||||
padding: 1.875rem;
|
||||
}
|
||||
|
||||
#public-groups-table tr a{
|
||||
margin-left: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0058e4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0a429c;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 768px) {
|
||||
.home {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
}
|
73
sources_2/static/mainpage.js
Executable file
73
sources_2/static/mainpage.js
Executable file
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2020 by Juliusz Chroboczek.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
'use strict';
|
||||
|
||||
document.getElementById('groupform').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
let group = document.getElementById('group').value.trim();
|
||||
if(group !== '')
|
||||
location.href = '/group/' + group;
|
||||
};
|
||||
|
||||
async function listPublicGroups() {
|
||||
let div = document.getElementById('public-groups');
|
||||
let table = document.getElementById('public-groups-table');
|
||||
|
||||
let l;
|
||||
try {
|
||||
l = await (await fetch('/public-groups.json')).json();
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
l = [];
|
||||
}
|
||||
|
||||
if (l.length === 0) {
|
||||
table.textContent = '(No groups found.)';
|
||||
div.classList.remove('groups');
|
||||
div.classList.add('nogroups');
|
||||
return;
|
||||
}
|
||||
|
||||
div.classList.remove('nogroups');
|
||||
div.classList.add('groups');
|
||||
|
||||
for(let i = 0; i < l.length; i++) {
|
||||
let group = l[i];
|
||||
let tr = document.createElement('tr');
|
||||
let td = document.createElement('td');
|
||||
let a = document.createElement('a');
|
||||
a.textContent = group.name;
|
||||
a.href = '/group/' + encodeURIComponent(group.name);
|
||||
td.appendChild(a);
|
||||
tr.appendChild(td);
|
||||
let td2 = document.createElement('td');
|
||||
if(group.description)
|
||||
td2.textContent = group.description;
|
||||
tr.appendChild(td2);
|
||||
let td3 = document.createElement('td');
|
||||
td3.textContent = `(${group.clientCount} clients)`;
|
||||
tr.appendChild(td3);
|
||||
table.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
listPublicGroups();
|
1156
sources_2/static/protocol.js
Executable file
1156
sources_2/static/protocol.js
Executable file
File diff suppressed because it is too large
Load diff
8
sources_2/static/scripts/toastify.js
Executable file
8
sources_2/static/scripts/toastify.js
Executable file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Minified by jsDelivr using Terser v3.14.1.
|
||||
* Original file: /npm/toastify-js@1.9.1/src/toastify.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(t,o){"object"==typeof module&&module.exports?module.exports=o():t.Toastify=o()}(this,function(t){var o=function(t){return new o.lib.init(t)};function i(t,o){return o.offset[t]?isNaN(o.offset[t])?o.offset[t]:o.offset[t]+"px":"0px"}function s(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&t.className.trim().split(/\s+/gi).indexOf(o)>-1)}return o.lib=o.prototype={toastify:"1.9.1",constructor:o,init:function(t){t||(t={}),this.options={},this.toastElement=null,this.options.text=t.text||"Hi there!",this.options.node=t.node,this.options.duration=0===t.duration?0:t.duration||3e3,this.options.selector=t.selector,this.options.callback=t.callback||function(){},this.options.destination=t.destination,this.options.newWindow=t.newWindow||!1,this.options.close=t.close||!1,this.options.gravity="bottom"===t.gravity?"toastify-bottom":"toastify-top",this.options.positionLeft=t.positionLeft||!1,this.options.position=t.position||"",this.options.backgroundColor=t.backgroundColor,this.options.avatar=t.avatar||"",this.options.className=t.className||"",this.options.stopOnFocus=void 0===t.stopOnFocus||t.stopOnFocus,this.options.onClick=t.onClick;return this.options.offset=t.offset||{x:0,y:0},this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");if(t.className="toastify on "+this.options.className,this.options.position?t.className+=" toastify-"+this.options.position:!0===this.options.positionLeft?(t.className+=" toastify-left",console.warn("Property `positionLeft` will be depreciated in further versions. Please use `position` instead.")):t.className+=" toastify-right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&(t.style.background=this.options.backgroundColor),this.options.node&&this.options.node.nodeType===Node.ELEMENT_NODE)t.appendChild(this.options.node);else if(t.innerHTML=this.options.text,""!==this.options.avatar){var o=document.createElement("img");o.src=this.options.avatar,o.className="toastify-avatar","left"==this.options.position||!0===this.options.positionLeft?t.appendChild(o):t.insertAdjacentElement("afterbegin",o)}if(!0===this.options.close){var s=document.createElement("span");s.innerHTML="✖",s.className="toast-close",s.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(this.toastElement),window.clearTimeout(this.toastElement.timeOutValue)}.bind(this));var n=window.innerWidth>0?window.innerWidth:screen.width;("left"==this.options.position||!0===this.options.positionLeft)&&n>360?t.insertAdjacentElement("afterbegin",s):t.appendChild(s)}if(this.options.stopOnFocus&&this.options.duration>0){const o=this;t.addEventListener("mouseover",function(o){window.clearTimeout(t.timeOutValue)}),t.addEventListener("mouseleave",function(){t.timeOutValue=window.setTimeout(function(){o.removeElement(t)},o.options.duration)})}if(void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),"function"==typeof this.options.onClick&&void 0===this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),this.options.onClick()}.bind(this)),"object"==typeof this.options.offset){var e=i("x",this.options),a=i("y",this.options);const o="left"==this.options.position?e:`-${e}`,s="toastify-top"==this.options.gravity?a:`-${a}`;t.style.transform=`translate(${o}, ${s})`}return t},showToast:function(){var t;if(this.toastElement=this.buildToast(),!(t=void 0===this.options.selector?document.body:document.getElementById(this.options.selector)))throw"Root element is not defined";return t.insertBefore(this.toastElement,t.firstChild),o.reposition(),this.options.duration>0&&(this.toastElement.timeOutValue=window.setTimeout(function(){this.removeElement(this.toastElement)}.bind(this),this.options.duration)),this},hideToast:function(){this.toastElement.timeOutValue&&clearTimeout(this.toastElement.timeOutValue),this.removeElement(this.toastElement)},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){this.options.node&&this.options.node.parentNode&&this.options.node.parentNode.removeChild(this.options.node),t.parentNode&&t.parentNode.removeChild(t),this.options.callback.call(t),o.reposition()}.bind(this),400)}},o.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},n={top:15,bottom:15},e=document.getElementsByClassName("toastify"),a=0;a<e.length;a++){t=!0===s(e[a],"toastify-top")?"toastify-top":"toastify-bottom";var p=e[a].offsetHeight;t=t.substr(9,t.length-1);(window.innerWidth>0?window.innerWidth:screen.width)<=360?(e[a].style[t]=n[t]+"px",n[t]+=p+15):!0===s(e[a],"toastify-left")?(e[a].style[t]=o[t]+"px",o[t]+=p+15):(e[a].style[t]=i[t]+"px",i[t]+=p+15)}return this},o.lib.init.prototype=o.lib,o});
|
||||
//# sourceMappingURL=/sm/1df7b098cd6209fd67b5cc8f6f6518b79e5214ec3802d91f56f825883253df69.map
|
19
sources_2/static/tsconfig.json
Executable file
19
sources_2/static/tsconfig.json
Executable file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"noImplicitThis": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"files": [
|
||||
"protocol.js",
|
||||
"galene.js"
|
||||
]
|
||||
}
|
BIN
sources_2/static/webfonts/fa-regular-400.eot
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.eot
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-regular-400.ttf
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.ttf
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-regular-400.woff
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.woff
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-regular-400.woff2
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.woff2
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.eot
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.eot
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.ttf
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.ttf
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.woff
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.woff
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.woff2
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.woff2
Executable file
Binary file not shown.
Loading…
Add table
Reference in a new issue