1
0
Fork 0
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:
Éric Gaspar 2021-01-10 14:40:27 +01:00 committed by GitHub
parent a2ab22aaf3
commit 4587801b86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 76 additions and 6527 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
[Unit]
Description=Galene
Description=Galène: videoconferencing server
After=network.target
[Service]

7
conf/x86-64.src Normal file
View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
[]

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
{
"op": [{"username": "__ADMIN__", "password": "__PASSWORD__"}],
"presenter": [{}]
}

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

@ -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="&#10148;" 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

View file

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

View file

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

View file

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

View file

@ -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="&#10006;",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

View file

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