mirror of
https://github.com/YunoHost-Apps/galene_ynh.git
synced 2024-09-03 18:36:31 +02:00
Upgrade to version 0.2 (#8)
- Upgrade to version 0.2 - Add prebuid binaries for x86-64, arm64, arm architectures
This commit is contained in:
parent
a2ab22aaf3
commit
4587801b86
47 changed files with 76 additions and 6527 deletions
BIN
France_in_XXI_Century._School.jpg
Normal file
BIN
France_in_XXI_Century._School.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 183 KiB |
11
README.md
11
README.md
|
@ -11,11 +11,11 @@ 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
|
||||
|
||||
![](Link to a screenshot of this app.)
|
||||
![](France_in_XXI_Century._School.jpg)
|
||||
|
||||
## Demo
|
||||
|
||||
|
@ -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,11 +11,11 @@ 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
|
||||
|
||||
![](Link to a screenshot of this app.)
|
||||
![](France_in_XXI_Century._School.jpg)
|
||||
|
||||
## Démo
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
admin="john" (USER)
|
||||
is_public=1 (PUBLIC|public=1|private=0)
|
||||
password="pass"
|
||||
group_name="groupname"
|
||||
group_name="public"
|
||||
; Checks
|
||||
pkg_linter=1
|
||||
setup_sub_dir=0
|
||||
|
@ -21,7 +21,7 @@
|
|||
upgrade=1
|
||||
backup_restore=1
|
||||
multi_instance=0
|
||||
port_already_use=0
|
||||
port_already_use=1
|
||||
change_url=1
|
||||
;;; Options
|
||||
Email=
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
SOURCE_URL=https://github.com/jech/galene/archive/galene-0.1.zip
|
||||
SOURCE_SUM=738a6ba9649185112a53c22ba2d7afa7f5ac00bb35ee8b71192194d123c861c2
|
||||
SOURCE_SUM_PRG=sha256sum
|
||||
SOURCE_FORMAT=zip
|
||||
SOURCE_IN_SUBDIR=true
|
||||
SOURCE_FILENAME=
|
||||
SOURCE_EXTRACT=true
|
7
conf/arm.src
Normal file
7
conf/arm.src
Normal file
|
@ -0,0 +1,7 @@
|
|||
SOURCE_URL=https://github.com/YunoHost-Apps/galene_ynh/releases/download/v0.2/galene_0.2_Linux_arm.tar.gz
|
||||
SOURCE_SUM=a7da5ff9a34422732fea1bbe9fb591c42813875ff7fcd4c30590a54c786bdf19
|
||||
SOURCE_SUM_PRG=sha256sum
|
||||
SOURCE_FORMAT=tar.gz
|
||||
SOURCE_IN_SUBDIR=true
|
||||
SOURCE_FILENAME=
|
||||
SOURCE_EXTRACT=true
|
7
conf/arm64.src
Normal file
7
conf/arm64.src
Normal file
|
@ -0,0 +1,7 @@
|
|||
SOURCE_URL=https://github.com/YunoHost-Apps/galene_ynh/releases/download/v0.2/galene_0.2_Linux_arm64.tar.gz
|
||||
SOURCE_SUM=8e755dc9779c5301d9f63e8120e2bd307118fd2ebc1bdc003e2c2c0ce905f9c7
|
||||
SOURCE_SUM_PRG=sha256sum
|
||||
SOURCE_FORMAT=tar.gz
|
||||
SOURCE_IN_SUBDIR=true
|
||||
SOURCE_FILENAME=
|
||||
SOURCE_EXTRACT=true
|
|
@ -1,5 +1,5 @@
|
|||
[Unit]
|
||||
Description=Galene
|
||||
Description=Galène: videoconferencing server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
|
@ -11,4 +11,4 @@ ExecStart=__FINALPATH__/galene
|
|||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
|
7
conf/x86-64.src
Normal file
7
conf/x86-64.src
Normal file
|
@ -0,0 +1,7 @@
|
|||
SOURCE_URL=https://github.com/YunoHost-Apps/galene_ynh/releases/download/v0.2/galene_0.2_Linux_x86_64.tar.gz
|
||||
SOURCE_SUM=4878741a204a35e900cf75399093f121a56f9e32b6a08a60fff254d561c18444
|
||||
SOURCE_SUM_PRG=sha256sum
|
||||
SOURCE_FORMAT=tar.gz
|
||||
SOURCE_IN_SUBDIR=true
|
||||
SOURCE_FILENAME=
|
||||
SOURCE_EXTRACT=true
|
|
@ -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": {
|
||||
|
|
|
@ -18,3 +18,29 @@ pkg_dependencies=""
|
|||
#=================================================
|
||||
# FUTURE OFFICIAL HELPERS
|
||||
#=================================================
|
||||
|
||||
# Check the architecture
|
||||
#
|
||||
# example: architecture=$(ynh_detect_arch)
|
||||
#
|
||||
# usage: ynh_detect_arch
|
||||
#
|
||||
# Requires YunoHost version 2.2.4 or higher.
|
||||
|
||||
ynh_detect_arch(){
|
||||
local architecture
|
||||
if [ -n "$(uname -m | grep arm64)" ] || [ -n "$(uname -m | grep aarch64)" ]; then
|
||||
architecture="arm64"
|
||||
elif [ -n "$(uname -m | grep 64)" ]; then
|
||||
architecture="x86-64"
|
||||
elif [ -n "$(uname -m | grep armv7)" ]; then
|
||||
architecture="arm"
|
||||
elif [ -n "$(uname -m | grep armv6)" ]; then
|
||||
architecture="arm"
|
||||
elif [ -n "$(uname -m | grep armv5)" ]; then
|
||||
architecture="arm"
|
||||
else
|
||||
architecture="unknown"
|
||||
fi
|
||||
echo $architecture
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
# IMPORT GENERIC HELPERS
|
||||
#=================================================
|
||||
|
||||
# Keep this path for calling _common.sh inside the execution's context of backup and restore scripts
|
||||
source ../settings/scripts/_common.sh
|
||||
source /usr/share/yunohost/helpers
|
||||
|
||||
|
@ -15,8 +14,7 @@ source /usr/share/yunohost/helpers
|
|||
#=================================================
|
||||
|
||||
ynh_clean_setup () {
|
||||
### Remove this function if there's nothing to clean before calling the remove script.
|
||||
true
|
||||
ynh_clean_check_starting
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,8 +14,7 @@ source /usr/share/yunohost/helpers
|
|||
#=================================================
|
||||
|
||||
ynh_clean_setup () {
|
||||
### Remove this function if there's nothing to clean before calling the remove script.
|
||||
true
|
||||
ynh_clean_check_starting
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
@ -30,6 +29,7 @@ admin=$YNH_APP_ARG_ADMIN
|
|||
is_public=$YNH_APP_ARG_IS_PUBLIC
|
||||
password=$YNH_APP_ARG_PASSWORD
|
||||
group_name=$YNH_APP_ARG_GROUP_NAME
|
||||
architecture=$(ynh_detect_arch)
|
||||
|
||||
app=$YNH_APP_INSTANCE_NAME
|
||||
|
||||
|
@ -83,9 +83,7 @@ ynh_script_progression --message="Setting up source files..." --weight=1
|
|||
|
||||
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/
|
||||
ynh_setup_source --dest_dir="$final_path" --source_id="$architecture"
|
||||
|
||||
#=================================================
|
||||
# CREATE A SERVER CERTIFICATE
|
||||
|
@ -130,7 +128,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
|
||||
mv -f $final_path/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 +177,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 ]
|
||||
|
|
|
@ -15,8 +15,7 @@ source /usr/share/yunohost/helpers
|
|||
#=================================================
|
||||
|
||||
ynh_clean_setup () {
|
||||
#### Remove this function if there's nothing to clean before calling the remove script.
|
||||
true
|
||||
ynh_clean_check_starting
|
||||
}
|
||||
# Exit if an error occurs during the execution of the script
|
||||
ynh_abort_if_errors
|
||||
|
|
|
@ -23,6 +23,7 @@ is_public=$(ynh_app_setting_get --app=$app --key=is_public)
|
|||
final_path=$(ynh_app_setting_get --app=$app --key=final_path)
|
||||
group_name=$(ynh_app_setting_get --app=$app --key=group_name)
|
||||
port=$(ynh_app_setting_get --app=$app --key=port)
|
||||
architecture=$(ynh_detect_arch)
|
||||
|
||||
#=================================================
|
||||
# CHECK VERSION
|
||||
|
@ -81,8 +82,7 @@ then
|
|||
# Remove the app directory securely
|
||||
ynh_secure_remove --file="$final_path"
|
||||
|
||||
mkdir -p $final_path
|
||||
cp -R ../sources/* $final_path/
|
||||
ynh_setup_source --dest_dir="$final_path" --source_id="$architecture"
|
||||
|
||||
# Copy the admin saved settings from tmp directory to final path
|
||||
cp -ar "$tmpdir/groups" "$final_path/groups"
|
||||
|
@ -94,7 +94,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 +129,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 +0,0 @@
|
|||
[]
|
BIN
sources/galene
BIN
sources/galene
Binary file not shown.
|
@ -1,107 +0,0 @@
|
|||
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, label string) error
|
||||
PushClient(id, username string, add bool) error
|
||||
}
|
||||
|
||||
type Kickable interface {
|
||||
Kick(id, user, message string) error
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,756 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
||||
var IceFilename string
|
||||
|
||||
var iceConf webrtc.Configuration
|
||||
var iceOnce sync.Once
|
||||
|
||||
func IceConfiguration() webrtc.Configuration {
|
||||
iceOnce.Do(func() {
|
||||
var iceServers []webrtc.ICEServer
|
||||
file, err := os.Open(IceFilename)
|
||||
if err != nil {
|
||||
log.Printf("Open %v: %v", IceFilename, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
d := json.NewDecoder(file)
|
||||
err = d.Decode(&iceServers)
|
||||
if err != nil {
|
||||
log.Printf("Get ICE configuration: %v", err)
|
||||
return
|
||||
}
|
||||
iceConf = webrtc.Configuration{
|
||||
ICEServers: iceServers,
|
||||
}
|
||||
})
|
||||
|
||||
return iceConf
|
||||
}
|
||||
|
||||
type ChatHistoryEntry struct {
|
||||
Id string
|
||||
User string
|
||||
Time int64
|
||||
Kind string
|
||||
Value string
|
||||
}
|
||||
|
||||
const (
|
||||
MinBitrate = 200000
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
name string
|
||||
|
||||
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
|
||||
api *webrtc.API
|
||||
}
|
||||
|
||||
func (g *Group) API() *webrtc.API {
|
||||
return groups.api
|
||||
}
|
||||
|
||||
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)
|
||||
s := webrtc.SettingEngine{}
|
||||
s.SetSRTPReplayProtectionWindow(512)
|
||||
if !UseMDNS {
|
||||
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
|
||||
}
|
||||
m := webrtc.MediaEngine{}
|
||||
m.RegisterCodec(
|
||||
webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
"video/VP8", 90000, 0,
|
||||
"",
|
||||
[]webrtc.RTCPFeedback{
|
||||
{"goog-remb", ""},
|
||||
{"nack", ""},
|
||||
{"nack", "pli"},
|
||||
{"ccm", "fir"},
|
||||
},
|
||||
},
|
||||
PayloadType: 96,
|
||||
},
|
||||
webrtc.RTPCodecTypeVideo,
|
||||
)
|
||||
m.RegisterCodec(
|
||||
webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
"audio/opus", 48000, 2,
|
||||
"minptime=10;useinbandfec=1",
|
||||
nil,
|
||||
},
|
||||
PayloadType: 111,
|
||||
},
|
||||
webrtc.RTPCodecTypeAudio,
|
||||
)
|
||||
groups.api = webrtc.NewAPI(
|
||||
webrtc.WithSettingEngine(s),
|
||||
webrtc.WithMediaEngine(&m),
|
||||
)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
groups.groups[name] = g
|
||||
return g, nil
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if desc != nil {
|
||||
g.description = desc
|
||||
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
|
||||
} 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
|
||||
})
|
||||
}
|
||||
|
||||
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, value string) {
|
||||
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"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"op": [{"username": "__ADMIN__", "password": "__PASSWORD__"}],
|
||||
"presenter": [{}]
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<!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>
|
|
@ -1,44 +0,0 @@
|
|||
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/static/css/fontawesome.min.css
vendored
5
sources/static/css/fontawesome.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -1,15 +0,0 @@
|
|||
/*!
|
||||
* 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; }
|
|
@ -1,16 +0,0 @@
|
|||
/*!
|
||||
* 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/static/css/toastify.min.css
vendored
15
sources/static/css/toastify.min.css
vendored
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* 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 */
|
File diff suppressed because it is too large
Load diff
|
@ -1,255 +0,0 @@
|
|||
<!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>
|
File diff suppressed because it is too large
Load diff
|
@ -1,42 +0,0 @@
|
|||
<!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>
|
||||
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
// 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();
|
File diff suppressed because it is too large
Load diff
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* 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
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue