diff --git a/France_in_XXI_Century._School.jpg b/France_in_XXI_Century._School.jpg
new file mode 100644
index 0000000..fa6a037
Binary files /dev/null and b/France_in_XXI_Century._School.jpg differ
diff --git a/README.md b/README.md
index b3f40ea..287320d 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Galène is a videoconferencing server that is easy to deploy (just copy a few fi
## Screenshots
-
+
## Demo
diff --git a/README_fr.md b/README_fr.md
index 0df2488..22f3ab1 100644
--- a/README_fr.md
+++ b/README_fr.md
@@ -15,7 +15,7 @@ Galène est un serveur de visioconférence facile à déployer (il suffit de cop
## Captures d'écran
-
+
## Démo
diff --git a/check_process b/check_process
index a2ca1a6..86287d5 100644
--- a/check_process
+++ b/check_process
@@ -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
diff --git a/conf/arm64.src b/conf/arm64.src
new file mode 100644
index 0000000..5d728d4
--- /dev/null
+++ b/conf/arm64.src
@@ -0,0 +1,7 @@
+SOURCE_URL=https://github.com/ericgaspar/galene_ynh/releases/download/v0.2/galene_0.2_Linux_arm64.tar.gz
+SOURCE_SUM=71233692bed5b11a040aa9ba2e3fb819d6eede84182b9f1da1b8c506bde14594
+SOURCE_SUM_PRG=sha256sum
+SOURCE_FORMAT=tar.gz
+SOURCE_IN_SUBDIR=true
+SOURCE_FILENAME=
+SOURCE_EXTRACT=true
\ No newline at end of file
diff --git a/conf/systemd.service b/conf/systemd.service
index cc2d126..ddc546d 100644
--- a/conf/systemd.service
+++ b/conf/systemd.service
@@ -1,5 +1,5 @@
[Unit]
-Description=Galene: videoconferencing server
+Description=Galène: videoconferencing server
After=network.target
[Service]
@@ -11,4 +11,4 @@ ExecStart=__FINALPATH__/galene
LimitNOFILE=65536
[Install]
-WantedBy=multi-user.target
\ No newline at end of file
+WantedBy=multi-user.target
diff --git a/conf/x86-64.src b/conf/x86-64.src
new file mode 100644
index 0000000..742d028
--- /dev/null
+++ b/conf/x86-64.src
@@ -0,0 +1,7 @@
+SOURCE_URL=https://github.com/ericgaspar/galene_ynh/releases/download/v0.2/galene-0.2_Linux_x86_64.tar.gz
+SOURCE_SUM=438ba8fbb2d3c8a02b5baa2c11940ee2d957b64c830062dae8456b290130e8bd
+SOURCE_SUM_PRG=sha256sum
+SOURCE_FORMAT=tar.gz
+SOURCE_IN_SUBDIR=true
+SOURCE_FILENAME=
+SOURCE_EXTRACT=true
\ No newline at end of file
diff --git a/scripts/_common.sh b/scripts/_common.sh
index ecd263f..52781d8 100755
--- a/scripts/_common.sh
+++ b/scripts/_common.sh
@@ -18,3 +18,22 @@ 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"
+ else
+ architecture="unknown"
+ fi
+ echo $architecture
+}
\ No newline at end of file
diff --git a/scripts/backup b/scripts/backup
index 95950f9..94a5224 100755
--- a/scripts/backup
+++ b/scripts/backup
@@ -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
diff --git a/scripts/install b/scripts/install
index fcf5a1b..763b6b6 100755
--- a/scripts/install
+++ b/scripts/install
@@ -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_2/* $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_2/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"
diff --git a/scripts/restore b/scripts/restore
index f46aaba..43e1834 100755
--- a/scripts/restore
+++ b/scripts/restore
@@ -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
diff --git a/scripts/upgrade b/scripts/upgrade
index 129d5d3..e9093c1 100755
--- a/scripts/upgrade
+++ b/scripts/upgrade
@@ -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,9 +82,7 @@ then
# Remove the app directory securely
ynh_secure_remove --file="$final_path"
- mkdir -p $final_path
-
- cp -R ../sources_2/* $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"
diff --git a/sources/data/ice-servers.json b/sources/data/ice-servers.json
deleted file mode 100755
index fe51488..0000000
--- a/sources/data/ice-servers.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/sources/galene b/sources/galene
deleted file mode 100755
index 747b172..0000000
Binary files a/sources/galene and /dev/null differ
diff --git a/sources/group/client.go b/sources/group/client.go
deleted file mode 100755
index 0f17377..0000000
--- a/sources/group/client.go
+++ /dev/null
@@ -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
-}
diff --git a/sources/group/client_test.go b/sources/group/client_test.go
deleted file mode 100755
index 9352fc6..0000000
--- a/sources/group/client_test.go
+++ /dev/null
@@ -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")
- }
- }
-}
diff --git a/sources/group/group.go b/sources/group/group.go
deleted file mode 100755
index c52c1fb..0000000
--- a/sources/group/group.go
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/sources/group/group_test.go b/sources/group/group_test.go
deleted file mode 100755
index fddd982..0000000
--- a/sources/group/group_test.go
+++ /dev/null
@@ -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)
- }
-}
diff --git a/sources/groups/groupname.json b/sources/groups/groupname.json
deleted file mode 100644
index 9751310..0000000
--- a/sources/groups/groupname.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "op": [{"username": "__ADMIN__", "password": "__PASSWORD__"}],
- "presenter": [{}]
-}
diff --git a/sources/static/404.css b/sources/static/404.css
deleted file mode 100755
index 0dcc899..0000000
--- a/sources/static/404.css
+++ /dev/null
@@ -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;
-}
diff --git a/sources/static/404.html b/sources/static/404.html
deleted file mode 100755
index fbf187a..0000000
--- a/sources/static/404.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
- Page not Found
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Page not found!
-
We can't find the page you're looking for.
-
Back to home
-
-
-
-
-
diff --git a/sources/static/common.css b/sources/static/common.css
deleted file mode 100755
index 9553cbf..0000000
--- a/sources/static/common.css
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/sources/static/css/fontawesome.min.css b/sources/static/css/fontawesome.min.css
deleted file mode 100755
index 8e36e25..0000000
--- a/sources/static/css/fontawesome.min.css
+++ /dev/null
@@ -1,5 +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)
- */
-.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\e05e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hive:before{content:"\e07f"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\e065"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\e013"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-innosoft:before{content:"\e080"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\e066"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\e01a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\e068"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-perbyte:before{content:"\e083"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\e07a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\e057"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\e06d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\e070"}.fa-store-slash:before{content:"\e071"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\e07b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-uncharted:before{content:"\e084"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\e074"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-watchman-monitoring:before{content:"\e087"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}
\ No newline at end of file
diff --git a/sources/static/css/regular.css b/sources/static/css/regular.css
deleted file mode 100755
index 8db06c6..0000000
--- a/sources/static/css/regular.css
+++ /dev/null
@@ -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; }
diff --git a/sources/static/css/solid.css b/sources/static/css/solid.css
deleted file mode 100755
index 62922cb..0000000
--- a/sources/static/css/solid.css
+++ /dev/null
@@ -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; }
diff --git a/sources/static/css/toastify.min.css b/sources/static/css/toastify.min.css
deleted file mode 100755
index 8041580..0000000
--- a/sources/static/css/toastify.min.css
+++ /dev/null
@@ -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 */
\ No newline at end of file
diff --git a/sources/static/galene.css b/sources/static/galene.css
deleted file mode 100755
index 814b186..0000000
--- a/sources/static/galene.css
+++ /dev/null
@@ -1,1307 +0,0 @@
-.nav-fixed .topnav {
- z-index: 1039;
-}
-
-.fixed-top{
- position: fixed;
- top: 0;
- right: 0;
- left: 0;
- z-index: 1030;
-}
-
-.topnav {
- padding-left: 0;
- height: 3.5rem;
- z-index: 1039;
-}
-
-.navbar .form-control, .topnav {
- font-size: 1rem;
-}
-
-.form-control {
- display: block;
- padding: .375rem .75rem;
- font-size: 1rem;
- line-height: 1.5;
- color: #495057;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #ced4da;
- border-radius: .25rem;
- transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
-}
-
-.form-control-inline {
- display: inline-block;
-}
-
-.shadow {
- box-shadow: 0 .15rem 1.75rem 0 rgba(31,45,65,.15);
-}
-.bg-white {
- background-color: #fff;
-}
-
-.bg-gray {
- background-color: #eee;
-}
-
-.profile {
- width: 230px;
-}
-
-.profile-logo {
- float: left;
- width: 50px;
- height: 50px;
- background: #b681c3;
- border-radius: 25px;
- text-align: center;
- vertical-align: middle;
- font-size: 1.4em;
- padding: 7px;
- color: #f9f9f9;
-}
-
-.profile-info {
- float: left;
- margin-left: 10px;
- margin-top: 8px;
- color: #616263;
- width: 120px;
-}
-
-.user-logout {
- float: right;
- text-align: center;
-}
-
-.logout-icon {
- display: block;
- font-size: 1.5em;
-}
-
-.logout-text {
- font-size: .7em;
-}
-
-.profile-info span {
- display: block;
- line-height: 1.2;
- text-transform: capitalize;
-}
-
-#permspan {
- font-size: .9em;
- color: #108e07;
- font-style: italic;
-}
-
-.sidenav .user-logout a {
- font-size: 1em;
- padding: 7px 0 0;
- color: #e4157e;
- cursor: pointer;
- line-height: .7;
-}
-
-.sidenav .user-logout a:hover {
- color: #ab0659;
-}
-
-.navbar, .navbar .container, .navbar .container-fluid, .navbar .container-lg, .navbar .container-md, .navbar .container-sm, .navbar .container-xl {
- display: -webkit-box;
- display: flex;
- flex-wrap: wrap;
- -webkit-box-align: center;
- align-items: center;
- -webkit-box-pack: justify;
- justify-content: space-between;
- background: #610a86;
-}
-.navbar {
- position: relative;
- padding: .1rem;
-}
-
-.topnav .navbar-brand {
- width: 15rem;
- padding-left: 1rem;
- padding-right: 1rem;
- margin: 0;
- font-size: 1rem;
- font-weight: 700;
-}
-
-.btn {
- display: inline-block;
- font-weight: 400;
- font-size: 1em;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- border: 1px solid transparent;
- padding: 0.255rem .75rem;
- line-height: 1.5;
- border-radius: .25rem;
- transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
-}
-
-.btn {
- transition-duration: 0.4s;
-}
-
-.btn-default:hover {
- color: #fff;
- background-color: #545b62;
- border-color: #4e555b;
-}
-
-.btn-default {
- color: #fff;
- background-color: #6c757d;
- border-color: #6c757d;
-}
-
-.btn:not(:disabled):not(.disabled) {
- cursor: pointer;
-}
-
-.btn-success {
- color: #fff;
- background-color: #28a745;
- border-color: #28a745;
-}
-
-.btn-success:hover {
- color: #fff;
- background-color: #218838;
- border-color: #1e7e34;
-}
-
-.btn-cancel {
- color: #fff;
- background-color: #dc3545;
- border-color: #dc3545;
-}
-
-.btn-cancel:hover {
- color: #fff;
- background-color: #c82333;
- border-color: #bd2130;
-}
-
-.btn-blue {
- color: #fff;
- background-color: #007bff;
- border-color: #007bff;
-}
-
-.btn-blue:hover {
- color: #fff;
- background-color: #0069d9;
- border-color: #0062cc;
-}
-
-.btn-warn {
- color: #ffc107;
- background-color: transparent;
- background-image: none;
- border-color: #ffc107;
-}
-
-.btn-warn:hover {
- color: #212529;
- background-color: #ffc107;
- border-color: #ffc107;
-}
-
-.btn-large {
- font-size: 110%;
-}
-
-.app {
- background-color: #f4f4f4;
- overflow: hidden;
- margin: 0;
- padding: 0;
- box-shadow: 0 1px 1px 0 rgba(0, 0, 0, .06), 0 2px 5px 0 rgba(0, 0, 0, .2);
-}
-
-.coln-left {
- flex: 30%;
- padding: 0;
- margin: 0;
-}
-
-.coln-left-hide {
- flex: 0;
-}
-
-.coln-right {
- flex: 70%;
- position: relative;
-}
-
-/* Clear floats after the columns */
-.row {
- display: flex;
-}
-
-.full-height {
- height: calc(var(--vh, 1vh) * 100);
-}
-
-.full-width {
- width: calc(100vw - 200px);
- height: calc(var(--vh, 1vh) * 100 - 56px);
-}
-
-.full-width-active {
- width: 100vw;
-}
-
-.container {
- width: 100%;
-}
-
-.users-header {
- height: 3.5rem;
- padding: 10px;
- background: #610a86;
- font-size: .95rem;
- font-weight: 500;
-}
-
-.users-header:after, .profile-user:after, .users-header:before {
- display: table;
- content: " ";
-}
-
-.users-header:after, .profile-user:after {
- clear: both;
-}
-
-.reply {
- height: 53px;
- width: 100%;
- background-color: #eae7e5;
- padding: 10px 5px 10px 5px;
- margin: 0;
- z-index: 1000;
-}
-
-.reply textarea {
- width: 100%;
- resize: none;
- overflow: hidden;
- padding: 5px;
- outline: none;
- border: none;
- text-indent: 5px;
- box-shadow: none;
- height: 100%;
-}
-
-textarea.form-reply {
- height: 2.1em;
- margin-right: .5em;
-}
-
-.form-reply {
- display: block;
- width: 100%;
- height: 34px;
- padding: 6px 12px;
- font-size: 1rem;
- color: #555;
- background-color: #fff;
- background-image: none;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
- transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
-}
-
-.form-reply::placeholder {
- opacity: .7;
-}
-
-.select {
- display: block;
- width: 100%;
- padding: .275rem .75rem;
- font-size: 1rem;
- line-height: 1.5;
- color: #495057;
- background-color: #fff;
- background-clip: padding-box;
- border: 1px solid #ced4da;
- border-radius: .25rem;
- transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
-}
-
-.select-inline {
- display: inline-block;
-}
-
-.message {
- width: auto !important;
- padding: 4px 10px 7px !important;
- background: #daf1c6;
- font-size: 12px;
- box-shadow: 0 1px 1px rgba(43, 43, 43, 0.16);
- border-radius: 5px;
- word-wrap: break-word;
- display: inline-block;
- margin: 1em 0 0;
- max-width: 90%;
- text-align: left;
-}
-
-.message-sender {
- background: #e6e6e6;
-}
-
-.message-private {
- background: white;
-}
-
-.message-private .message-header:after {
- content: "(private)";
- margin-left: 1em;
-}
-
-.message-system {
- font-size: 10px;
- background: #ececec;
-}
-
-.message-row:after, .message-row:before {
- display: table;
- content: " ";
-}
-
-.message-row:after {
- clear: both;
-}
-
-.message-content {
- white-space: pre-wrap;
- margin: 0;
- padding: 0;
- padding-left: 5px;
- word-wrap: break-word;
- word-break: break-word;
- font-weight: 400;
- font-size: 14px;
- color: #202035;
-}
-
-.message-header {
- margin: 0;
- font-style: italic;
- text-shadow: none;
-}
-
-.message-footer {
- margin: 0;
- padding: 0;
- margin-bottom: -5px;
- line-height: .9;
- text-align: right;
-}
-
-.message-time {
- margin-left: 1em;
-}
-
-.message-me-asterisk, .message-me-user {
- margin-right: 0.33em;
-}
-
-.video-container {
- height: calc(var(--vh, 1vh) * 100 - 56px);
- position: relative;
- background: rgba(0, 0, 0, 0.91);
- /* Display only when showing video */
- display: block;
-}
-
-.collapse-video {
- display: none;
- /*on top of video peers*/
- z-index: 1002;
- font-size: 1.8em;
- position: absolute;
- top: 10px;
- left: 10px;
- cursor: pointer;
-}
-
-.collapse-video .open-chat {
- color: #cac7c7;
- height: 50px;
- padding: 10px;
- text-shadow: 0px 0px 1px #b3adad;
-}
-
-.no-video {
- display: none;
-}
-
-.video-controls, .top-video-controls {
- position: absolute;
- width: 100%;
- left: 0;
- bottom: 25px;
- text-align: center;
- color: #eaeaea;
- font-size: 1.1em;
- opacity: 0;
- height: 32px;
-}
-
-.video-controls:after, .top-video-controls:after {
- clear: both;
- display: table;
- content: " ";
-}
-
-.top-video-controls {
- text-align: right;
- bottom: inherit;
- top: 5px;
-}
-
-.controls-button {
- padding: 3px 10px;
- vertical-align: middle;
- height: 100%;
-}
-
-.controls-left {
- float: left;
- text-align: left;
-}
-
-.controls-right {
- float: right;
- text-align: right;
-}
-
-.vc-overlay {
- background: linear-gradient(180deg, rgb(0 0 0 / 20%) 0%, rgb(0 0 0 / 50%) 0%, rgb(0 0 0 / 70%) 100%);
-}
-
-.peer:hover > .video-controls, .peer:hover > .top-video-controls {
- opacity: 1;
-}
-
-.video-controls span, .top-video-controls span {
- margin-right: 20px;
- transition: opacity .7s ease-out;
- opacity: 1;
- cursor: pointer;
-}
-
-.video-controls span:last-child {
- margin-right: 0;
-}
-
-.video-controls span:hover, .top-video-controls span:hover {
- opacity: .8;
- transition: opacity .5s ease-out;
-}
-
-.video-controls .volume {
- display: inline-block;
- text-align: center;
-}
-
-.video-controls .video-play {
- font-size: 0.85em;
-}
-
-.video-controls span.disabled, .video-controls span.disabled:hover, .top-video-controls span.disabled:hover{
- opacity: .2;
- color: #c8c8c8
-}
-
-.volume-mute {
- vertical-align: middle;
- width: 25px;
- display: var(--dv, inline);
-}
-
-.volume-slider {
- height: 4px;
- width: 60px;
- cursor: pointer;
- margin: 5px 5px;
- vertical-align: middle;
- opacity: var(--ov, 0);
- transition: opacity .5s ease-out;
-}
-
-.video-controls .volume:hover {
- --ov: 1;
- --dv: inline;
-}
-
-.mobile-container {
- display: block !important;
-}
-
-.login-container {
- height: calc(var(--vh, 1vh) * 100 - 56px);
- position: relative;
- display: flex;
- justify-content: center;
-}
-
-.login-box {
- width: 20em;
- padding: 1em;
- margin: 5em auto;
- height: 23em;
- background: #fcfcfc;
-}
-
-.login-box .connect {
- width: 100%;
- text-align: center;
-}
-
-.login-box h2 {
- text-align: center;
- margin-bottom: 40px;
-}
-
-.label-fallback {
- opacity: 0.5;
-}
-
-.label {
- left: 0;
- position: absolute;
- bottom: 0;
- width: 100%;
- z-index: 1;
- text-align: center;
- line-height: 24px;
- color: #ffffff;
-}
-
-.nav-link {
- padding: 0;
- color: #dbd9d9;
- min-width: 30px;
- display: block;
- text-align: center;
- margin: 0 10px;
- position: relative;
- line-height: 1.1;
-}
-
-.nav-link span {
- display: block;
-}
-
-.nav-link label {
- display: block;
- cursor: pointer;
- color: #fff;
- font-size: 55%;
-}
-
-.nav-link:hover {
- color: #c2a4e0;
-}
-.nav-link label:hover {
- color: #c2a4e0;
-}
-
-.nav-cancel, .muted, .nav-cancel label, .muted label {
- color: #d03e3e
-}
-
-.nav-cancel:hover, .muted:hover, .nav-cancel label:hover, .muted label:hover {
- color: #d03e3e
-}
-
-.nav-button {
- cursor: pointer;
- font-size: 25px;
-}
-
-.nav-more {
- padding-top: 5px;
- margin-left: 0;
-}
-
-.header-title {
- float: left;
- margin: 0;
- font-size: 1rem;
- font-weight: 700;
- color: #ebebeb;
- line-height: 2em;
-}
-
-#title {
- text-align: center;
-}
-
-h1 {
- white-space: nowrap;
-}
-
-#statdiv {
- white-space: nowrap;
- margin-bottom: 16px;
-}
-
-#errspan {
- margin-left: 1em;
-}
-
-.connected {
- color: green;
-}
-
-.disconnected {
- color: red;
- font-weight: bold;
-}
-
-.userform {
- display: inline
-}
-
-.userform label {
- min-width: 3em;
- display: inline-block;
- padding-top: 10px;
-}
-
-.userform input[type="text"], .userform input[type="password"] {
- width: 100%;
-}
-
-.switch-radio {
- margin: 0;
-}
-
-.invisible {
- display: none;
-}
-
-.error {
- color: red;
- font-weight: bold;
-}
-
-.noerror {
- display: none;
-}
-
-.clear {
- clear: both;
- content: "";
-}
-
-#optionsdiv {
- margin-bottom: 4px;
-}
-
-#optionsdiv input[type="checkbox"] {
- vertical-align: middle;
-}
-
-#presentbutton, #unpresentbutton {
- white-space: nowrap;
- margin-right: 0.4em;
- margin-top: .1em;
- font-size: 1.1em;
- text-align: left;
- width: 5.5em;
-}
-
-#videoselect {
- text-align-last: center;
- margin-right: 0.4em;
-}
-
-#audioselect {
- text-align-last: center;
-}
-
-#sharebutton, #unsharebutton {
- white-space: nowrap;
-}
-
-#unsharebutton {
- margin-right: 0.4em;
-}
-
-#sendselect {
- width: 8em;
- text-align-last: center;
- margin-right: 0.4em;
-}
-
-#requestselect {
- width: 8em;
- text-align-last: center;
-}
-
-#chatbox {
- height: 100%;
- position: relative;
-}
-
-#chat {
- padding: 0;
- margin: 0;
- background-color: #f8f8f8;
- background-size: cover;
- overflow-y: scroll;
- border: none;
- border-right: 4px solid #e6e6e6;
- /* force to fill height */
- height: 100% !important;
- width: 100%;
- min-width: 300px;
- overflow: hidden;
-}
-
-#inputform {
- display: flex;
-}
-
-#box {
- overflow: auto;
- height: calc(100% - 53px);
- padding: 10px;
-}
-
-.close-chat {
- position: absolute;
- top: 2px;
- right: 14px;
- width: 25px;
- font-size: 1em;
- text-align: center;
- font-weight: 700;
- color: #8f8f8f;
- cursor: pointer;
- border: 1px solid transparent;
-}
-
-.close-chat:hover, .close-chat:active {
- border: 1px solid #dfdfdf;
- border-radius: 4px;
-}
-
-#connectbutton {
- margin-top: 1em;
- padding: 0.37rem 1.5rem;
-}
-
-#input {
- width: 100%;
- border: none;
- resize: none;
- overflow-y: hidden;
-}
-
-#input:focus {
- outline: none;
-}
-
-#inputbutton:focus {
- outline: none;
-}
-
-#resizer {
- width: 4px;
- margin-left: -4px;
- z-index: 1000;
-}
-
-#resizer:hover {
- cursor: ew-resize;
-}
-
-#peers {
- padding: 10px;
- display: grid;
- grid-template-columns: repeat(1, 1fr);
- grid-template-rows: repeat(1, auto);
- row-gap: 5px;
- column-gap: 10px;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- min-width: 100%;
- min-height: 100%;
- width: auto;
- height: auto;
- z-index: 1000;
- background-size: cover;
- overflow: hidden;
- vertical-align: top!important;
-}
-
-.peer {
- margin-top: auto;
- margin-bottom: auto;
- position: relative;
- border: 2px solid rgba(0,0,0,0);
- background: #80808014;
-}
-
-.peer-active {
- border: 2px solid #610a86;
-}
-
-.media {
- width: 100%;
- max-height: calc(var(--vh, 1vh) * 100 - 76px);
- padding-bottom: 20px;
- object-fit: contain;
-}
-
-.media-failed {
- opacity: 0.7;
-}
-
-.mirror {
- transform: scaleX(-1);
-}
-
-#inputform {
- width: 100%;
-}
-
-.sidenav {
- background-color: #4d076b;
- box-shadow: 0 0 24px 0 rgba(71,77,86,.1), 0 1px 0 0 rgba(71,77,86,.08);
- display: block;
- position: fixed;
- -webkit-transition: all .2s ease-out;
- transition: all .2s ease-out;
- width: 0px;
- /* on top of everything */
- z-index: 2999;
- top: 0;
- right: 0;
- height: calc(var(--vh, 1vh) * 100);
- overflow-x: hidden;
- overflow-y: hidden;
-}
-
-.sidenav a {
- padding: 10px 20px;
- text-decoration: none;
- font-size: 30px;
- color: #dbd9d9;
- display: block;
- transition: 0.3s;
- line-height: 1.0;
-}
-
-.sidenav a:hover {
- color: #c2a4e0;
-}
-
-.sidenav .closebtn {
- cursor: pointer;
- position: absolute;
- top: 0;
- right: 0;
- height: 56px;
-}
-
-.sidenav-label {
- display: block;
- margin-top: 15px;
-}
-
-.sidenav-label-first {
- display: block;
- margin-top: 0;
-}
-
-.sidenav form{
- margin-top: 15px;
-}
-
-.sidenav-header {
- height: 56px;
-}
-
-.sidenav-header h2{
- color: #fff;
- padding: 10px;
- margin: 0;
- max-width: 70%;
- line-height: 36px;
-}
-
-.sidenav-content {
- padding: 10px;
- background: #fff;
- height: 100%;
-}
-
-.sidenav-content h2 {
- margin: 0;
-}
-
-fieldset {
- margin: 0;
- margin-top: 20px;
- border: 1px solid #e9e8e8;
- padding: 8px;
- border-radius: 4px;
-}
-legend {
- padding: 2px;
- color: #4d4f51;
-}
-
-.nav-menu {
- margin: 0;
- padding: 0;
-}
-
-.nav-menu li {
- float: left;
- max-height: 70px;
- list-style: none;
-}
-
-.show-video {
- position: absolute;
- display: none;
- right: 30px;
- bottom: 120px;
- color: white;
- width: 50px;
- height: 50px;
- text-align: center;
- line-height: 50px;
- font-size: 150%;
- border-radius: 30px;
- background: #600aa0;
- box-shadow: 4px 4px 7px 1px rgba(0,0,0,0.16);
-}
-
-.blink {
- -ms-animation: blink 1.0s linear infinite;
- -o-animation: blink 1.0s linear infinite;
- animation: blink 1.0s linear infinite;
-}
-
-@keyframes blink {
- 0% { box-shadow: 0 0 15px #600aa0; }
- 50% { box-shadow: none; }
- 100% { box-shadow: 0 0 15px #600aa0; }
-}
-
-@-webkit-keyframes blink {
- 0% { box-shadow: 0 0 15px #600aa0; }
- 50% { box-shadow: 0 0 0; }
- 100% { box-shadow: 0 0 15px #600aa0; }
-}
-
-/* Dropdown Menu */
-.dropbtn {
- cursor: pointer;
-}
-
-.dropdown {
- position: relative;
- display: inline-block;
-}
-
-.dropdown-content {
- display: none;
- position: absolute;
- background-color: #fff;
- max-width: 300px;
- min-width: 200px;
- margin-top: 7px;
- overflow: auto;
- right: 7px;
- box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
- z-index: 1;
- padding: 15px;
-}
-
-.dropdown-content a {
- color: black;
- padding: 12px 16px;
- text-decoration: none;
- display: block;
-}
-
-.dropdown a:hover {background-color: #ddd;}
-
-.show {display: block;}
-
-.dropdown-content label{
- display: block;
- margin-top: 15px;
-}
-
-/* END Dropdown Menu */
-
-/* Sidebar left */
-
-.svg-inline--fa {
- display: inline-block;
- font-size: inherit;
- height: 1.2em;
- overflow: visible;
- vertical-align: -.125em;
- color: #dbd9d9;
-}
-
-.svg-inline--fa:hover {
- color: #c2a4e0;
-}
-
-#left-sidebar {
- min-width: 200px;
- max-width: 200px;
- transition: all 0.3s;
- background: #ffffff;
- border-right: 1px solid #dcdcdc;
- z-index: 1039;
-}
-
-#left-sidebar .galene-header {
- display: inline-block;
-}
-
-header .collapse {
- float: left;
- text-align: center;
- cursor: pointer;
- padding-top: 5px;
- margin-right: 20px;
- margin-left: 5px;
-}
-
-.galene-header {
- font-size: 1.3rem;
- font-weight: 900;
- color: #dbd9d9;
- line-height: 34px;
-}
-
-.header-sep {
- height: 20px;
-}
-
-/* Shrinking the sidebar from 200px to 60px and center aligining its content*/
-#left-sidebar.active {
- min-width: 60px;
- max-width: 60px;
- text-align: center;
- margin-left: -60px !important;
-}
-
-#left-sidebar .sidebar-header strong {
- display: none;
-}
-#left-sidebar.active .sidebar-header h3 {
- display: none;
-}
-#left-sidebar.active .sidebar-header strong {
- display: block;
-}
-
-#users {
- padding: 0;
- margin: 0;
- height: calc(100% - 84px);
- width: 100%;
- z-index: 1;
- position: relative;
- display: block;
- background-color: #fff;
- overflow-y: auto;
- border: 1px solid #f7f7f7;
-}
-
-#users .user-p {
- position: relative;
- padding: 10px !important;
- border-bottom: 1px solid #f0f0f0;
- height: 40px;
- line-height: 18px;
- margin: 0 !important;
- cursor: pointer;
- overflow: hidden;
- white-space: pre;
-}
-
-#left-sidebar.active #users > div {
- padding: 10px 5px !important;
-}
-
-#users > div:hover {
- background-color: #f2f2f2;
-}
-
-#users > div::before {
- content: "\f111";
- font-family: 'Font Awesome 5 Free';
- color: #20b91e;
- margin-right: 5px;
- font-weight: 900;
-}
-
-.close-icon {
- font: normal 1em/1 Arial, sans-serif;
- display: inline-block;
-}
-
-.close-icon:before{ content: "\2715"; }
-
-/* END Sidebar Left */
-
-@media only screen and (max-device-width: 1024px) {
- #presentbutton, #unpresentbutton {
- width: auto;
- }
- .nav-link {
- margin: 0 4px;
- line-height: 1.5;
- }
-
- .nav-link label {
- display: none;
- }
-
- .nav-text {
- display: none;
- }
-
- .nav-more {
- padding-top: 0;
- margin-left: inherit;
- }
-
- .full-width {
- height: calc(var(--vh, 1vh) * 100 - 56px);
- }
-
- .collapse-video {
- left: inherit;
- right: 60px;
- }
-
- .close-chat {
- display: none;
- }
-
- .video-container {
- position: fixed;
- height: calc(var(--vh, 1vh) * 100 - 56px);
- top: 56px;
- right: 0;
- left: 0;
- margin-bottom: 60px;
- }
-
- .login-container {
- position: fixed;
- height: calc(var(--vh, 1vh) * 100 - 56px);
- top: 56px;
- right: 0;
- left: 0;
- background: #eff3f9;
- }
-
- .login-box {
- background: transparent;
- }
-
- .coln-left {
- flex: 100%;
- width: 100vw;
- }
-
- .coln-right {
- flex: none;
- position: relative;
- }
-
- .full-width {
- width: 100vw;
- }
-
- #left-sidebar.active {
- min-width: 200px;
- max-width: 200px;
- }
-
- #left-sidebar {
- min-width: 60px;
- max-width: 60px;
- text-align: center;
- margin-left: -60px !important;
- }
-
- /* Reappearing the sidebar on toggle button click */
- #left-sidebar {
- margin-left: 0;
- }
-
- #left-sidebar .sidebar-header strong {
- display: none;
- }
-
- #left-sidebar.active .sidebar-header h3 {
- display: none;
- }
-
- #left-sidebar.active .sidebar-header strong {
- display: block;
- }
-
- .sidenav a {padding: 10px 10px;}
-
- .sidenav-header h2 {
- line-height: 36px;
- }
-
- #peers {
- padding: 3px;
- }
-
- #resizer {
- display: none;
- }
-
- #chat {
- border-right: none;
- }
-
- .dropdown-content {
- margin-top: 10px;
- }
-
-}
diff --git a/sources/static/galene.html b/sources/static/galene.html
deleted file mode 100755
index 73f2d0b..0000000
--- a/sources/static/galene.html
+++ /dev/null
@@ -1,255 +0,0 @@
-
-
-
- Galène
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sources/static/galene.js b/sources/static/galene.js
deleted file mode 100755
index a4b595a..0000000
--- a/sources/static/galene.js
+++ /dev/null
@@ -1,2338 +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';
-
-/** @type {string} */
-let group;
-
-/** @type {ServerConnection} */
-let serverConnection;
-
-/**
- * @typedef {Object} userpass
- * @property {string} username
- * @property {string} password
- */
-
-/* Some browsers disable session storage when cookies are disabled,
- we fall back to a global variable. */
-/**
- * @type {userpass}
- */
-let fallbackUserPass = null;
-
-
-/**
- * @param {string} username
- * @param {string} password
- */
-function storeUserPass(username, password) {
- let userpass = {username: username, password: password};
- try {
- window.sessionStorage.setItem('userpass', JSON.stringify(userpass));
- fallbackUserPass = null;
- } catch(e) {
- console.warn("Couldn't store password:", e);
- fallbackUserPass = userpass;
- }
-}
-
-/**
- * Returns null if the user hasn't logged in yet.
- *
- * @returns {userpass}
- */
-function getUserPass() {
- /** @type{userpass} */
- let userpass;
- try {
- let json = window.sessionStorage.getItem('userpass');
- userpass = JSON.parse(json);
- } catch(e) {
- console.warn("Couldn't retrieve password:", e);
- userpass = fallbackUserPass;
- }
- return userpass || null;
-}
-
-/**
- * Return null if the user hasn't logged in yet.
- *
- * @returns {string}
- */
-function getUsername() {
- let userpass = getUserPass();
- if(!userpass)
- return null;
- return userpass.username;
-}
-
-/**
- * @typedef {Object} settings
- * @property {boolean} [localMute]
- * @property {string} [video]
- * @property {string} [audio]
- * @property {string} [send]
- * @property {string} [request]
- * @property {boolean} [activityDetection]
- * @property {Array.} [resolution]
- * @property {boolean} [blackboardMode]
- */
-
-/** @type{settings} */
-let fallbackSettings = null;
-
-/**
- * @param {settings} settings
- */
-function storeSettings(settings) {
- try {
- window.sessionStorage.setItem('settings', JSON.stringify(settings));
- fallbackSettings = null;
- } catch(e) {
- console.warn("Couldn't store password:", e);
- fallbackSettings = settings;
- }
-}
-
-/**
- * This always returns a dictionary.
- *
- * @returns {settings}
- */
-function getSettings() {
- /** @type {settings} */
- let settings;
- try {
- let json = window.sessionStorage.getItem('settings');
- settings = JSON.parse(json);
- } catch(e) {
- console.warn("Couldn't retrieve password:", e);
- settings = fallbackSettings;
- }
- return settings || {};
-}
-
-/**
- * @param {settings} settings
- */
-function updateSettings(settings) {
- let s = getSettings();
- for(let key in settings)
- s[key] = settings[key];
- storeSettings(s);
-}
-
-/**
- * @param {string} key
- * @param {any} value
- */
-function updateSetting(key, value) {
- let s = {};
- s[key] = value;
- updateSettings(s);
-}
-
-/**
- * @param {string} key
- */
-function delSetting(key) {
- let s = getSettings();
- if(!(key in s))
- return;
- delete(s[key]);
- storeSettings(s)
-}
-
-/**
- * @param {string} id
- */
-function getSelectElement(id) {
- let elt = document.getElementById(id);
- if(!elt || !(elt instanceof HTMLSelectElement))
- throw new Error(`Couldn't find ${id}`);
- return elt;
-}
-
-/**
- * @param {string} id
- */
-function getInputElement(id) {
- let elt = document.getElementById(id);
- if(!elt || !(elt instanceof HTMLInputElement))
- throw new Error(`Couldn't find ${id}`);
- return elt;
-}
-
-/**
- * @param {string} id
- */
-function getButtonElement(id) {
- let elt = document.getElementById(id);
- if(!elt || !(elt instanceof HTMLButtonElement))
- throw new Error(`Couldn't find ${id}`);
- return elt;
-}
-
-function reflectSettings() {
- let settings = getSettings();
- let store = false;
-
- setLocalMute(settings.localMute);
-
- let videoselect = getSelectElement('videoselect');
- if(!settings.hasOwnProperty('video') ||
- !selectOptionAvailable(videoselect, settings.video)) {
- settings.video = selectOptionDefault(videoselect);
- store = true;
- }
- videoselect.value = settings.video;
-
- let audioselect = getSelectElement('audioselect');
- if(!settings.hasOwnProperty('audio') ||
- !selectOptionAvailable(audioselect, settings.audio)) {
- settings.audio = selectOptionDefault(audioselect);
- store = true;
- }
- audioselect.value = settings.audio;
-
- if(settings.hasOwnProperty('request')) {
- getSelectElement('requestselect').value = settings.request;
- } else {
- settings.request = getSelectElement('requestselect').value;
- store = true;
- }
-
- if(settings.hasOwnProperty('send')) {
- getSelectElement('sendselect').value = settings.send;
- } else {
- settings.send = getSelectElement('sendselect').value;
- store = true;
- }
-
- getInputElement('activitybox').checked = settings.activityDetection;
-
- getInputElement('blackboardbox').checked = settings.blackboardMode;
-
- if(store)
- storeSettings(settings);
-}
-
-function showVideo() {
- let width = window.innerWidth;
- let video_container = document.getElementById('video-container');
- video_container.classList.remove('no-video');
- if (width <= 768)
- document.getElementById('collapse-video').style.display = "block";
-}
-
-/**
- * @param {boolean} [force]
- */
-function hideVideo(force) {
- let mediadiv = document.getElementById('peers');
- if(mediadiv.childElementCount > 0 && !force)
- return;
- let video_container = document.getElementById('video-container');
- video_container.classList.add('no-video');
- let left = document.getElementById("left");
- if (left.style.display !== "none") {
- // hide all video buttons used to switch video on mobile layout
- closeVideoControls();
- }
-}
-
-function closeVideoControls() {
- // hide all video buttons used to switch video on mobile layout
- document.getElementById('switch-video').style.display = "";
- document.getElementById('collapse-video').style.display = "";
-}
-
-function fillLogin() {
- let userpass = getUserPass();
- getInputElement('username').value =
- userpass ? userpass.username : '';
- getInputElement('password').value =
- userpass ? userpass.password : '';
-}
-
-/**
- * @param{boolean} connected
- */
-function setConnected(connected) {
- let userbox = document.getElementById('profile');
- let connectionbox = document.getElementById('login-container');
- if(connected) {
- resetUsers();
- clearChat();
- userbox.classList.remove('invisible');
- connectionbox.classList.add('invisible');
- displayUsername();
- } else {
- resetUsers();
- fillLogin();
- userbox.classList.add('invisible');
- connectionbox.classList.remove('invisible');
- displayError('Disconnected', 'error');
- hideVideo();
- closeVideoControls();
- }
-}
-
-/** @this {ServerConnection} */
-function gotConnected() {
- setConnected(true);
- let up = getUserPass();
- this.join(group, up.username, up.password);
-}
-
-/**
- * @this {ServerConnection}
- * @param {number} code
- * @param {string} reason
- */
-function gotClose(code, reason) {
- delUpMediaKind(null);
- setConnected(false);
- if(code != 1000) {
- console.warn('Socket close', code, reason);
- }
-}
-
-/**
- * @this {ServerConnection}
- * @param {Stream} c
- */
-function gotDownStream(c) {
- c.onclose = function() {
- delMedia(c.id);
- };
- c.onerror = function(e) {
- console.error(e);
- displayError(e);
- }
- c.ondowntrack = function(track, transceiver, label, stream) {
- setMedia(c, false);
- }
- c.onlabel = function(label) {
- setLabel(c);
- }
- c.onstatus = function(status) {
- setMediaStatus(c);
- }
- c.onstats = gotDownStats;
- if(getSettings().activityDetection)
- c.setStatsInterval(activityDetectionInterval);
-}
-
-// Store current browser viewport height in css variable
-function setViewportHeight() {
- document.documentElement.style.setProperty(
- '--vh', `${window.innerHeight/100}px`,
- );
- // Ajust video component size
- resizePeers();
-}
-setViewportHeight();
-
-// On resize and orientation change, we update viewport height
-addEventListener('resize', setViewportHeight);
-addEventListener('orientationchange', setViewportHeight);
-
-getButtonElement('presentbutton').onclick = async function(e) {
- e.preventDefault();
- let button = this;
- if(!(button instanceof HTMLButtonElement))
- throw new Error('Unexpected type for this.');
- // there's a potential race condition here: the user might click the
- // button a second time before the stream is set up and the button hidden.
- button.disabled = true;
- try {
- let id = findUpMedia('local');
- if(!id)
- await addLocalMedia();
- } finally {
- button.disabled = false;
- }
-};
-
-getButtonElement('unpresentbutton').onclick = function(e) {
- e.preventDefault();
- delUpMediaKind('local');
- resizePeers();
-};
-
-function changePresentation() {
- let id = findUpMedia('local');
- if(id) {
- addLocalMedia(id);
- }
-}
-
-/**
- * @param {string} id
- * @param {boolean} visible
- */
-function setVisibility(id, visible) {
- let elt = document.getElementById(id);
- if(visible)
- elt.classList.remove('invisible');
- else
- elt.classList.add('invisible');
-}
-
-function setButtonsVisibility() {
- let permissions = serverConnection.permissions;
- let local = !!findUpMedia('local');
- let share = !!findUpMedia('screenshare');
- let video = !!findUpMedia('video');
-
- // don't allow multiple presentations
- setVisibility('presentbutton', permissions.present && !local);
- setVisibility('unpresentbutton', local);
-
- setVisibility('mutebutton', permissions.present);
-
- // allow multiple shared documents
- setVisibility('sharebutton', permissions.present &&
- ('getDisplayMedia' in navigator.mediaDevices));
- setVisibility('unsharebutton', share);
-
- setVisibility('stopvideobutton', video);
-
- setVisibility('mediaoptions', permissions.present);
- setVisibility('sendform', permissions.present);
- setVisibility('fileform', permissions.present);
-}
-
-/**
- * @param {boolean} mute
- * @param {boolean} [reflect]
- */
-function setLocalMute(mute, reflect) {
- muteLocalTracks(mute);
- let button = document.getElementById('mutebutton');
- let icon = button.querySelector("span .fas");
- if(mute){
- icon.classList.add('fa-microphone-slash');
- icon.classList.remove('fa-microphone');
- button.classList.add('muted');
- } else {
- icon.classList.remove('fa-microphone-slash');
- icon.classList.add('fa-microphone');
- button.classList.remove('muted');
- }
- if(reflect)
- updateSettings({localMute: mute});
-}
-
-getSelectElement('videoselect').onchange = function(e) {
- e.preventDefault();
- if(!(this instanceof HTMLSelectElement))
- throw new Error('Unexpected type for this');
- updateSettings({video: this.value});
- changePresentation();
-};
-
-getSelectElement('audioselect').onchange = function(e) {
- e.preventDefault();
- if(!(this instanceof HTMLSelectElement))
- throw new Error('Unexpected type for this');
- updateSettings({audio: this.value});
- changePresentation();
-};
-
-getInputElement('blackboardbox').onchange = function(e) {
- e.preventDefault();
- if(!(this instanceof HTMLInputElement))
- throw new Error('Unexpected type for this');
- updateSettings({blackboardMode: this.checked});
- changePresentation();
-}
-
-document.getElementById('mutebutton').onclick = function(e) {
- e.preventDefault();
- let localMute = getSettings().localMute;
- localMute = !localMute;
- setLocalMute(localMute, true);
-}
-
-document.getElementById('sharebutton').onclick = function(e) {
- e.preventDefault();
- addShareMedia();
-};
-
-document.getElementById('unsharebutton').onclick = function(e) {
- e.preventDefault();
- delUpMediaKind('screenshare');
- resizePeers();
-}
-
-document.getElementById('stopvideobutton').onclick = function(e) {
- e.preventDefault();
- delUpMediaKind('video');
- resizePeers();
-}
-
-/** @returns {number} */
-function getMaxVideoThroughput() {
- let v = getSettings().send;
- switch(v) {
- case 'lowest':
- return 150000;
- case 'low':
- return 300000;
- case 'normal':
- return 700000;
- case 'unlimited':
- return null;
- default:
- console.error('Unknown video quality', v);
- return 700000;
- }
-}
-
-getSelectElement('sendselect').onchange = async function(e) {
- if(!(this instanceof HTMLSelectElement))
- throw new Error('Unexpected type for this');
- updateSettings({send: this.value});
- let t = getMaxVideoThroughput();
- for(let id in serverConnection.up) {
- let c = serverConnection.up[id];
- await setMaxVideoThroughput(c, t);
- }
-}
-
-getSelectElement('requestselect').onchange = function(e) {
- e.preventDefault();
- if(!(this instanceof HTMLSelectElement))
- throw new Error('Unexpected type for this');
- updateSettings({request: this.value});
- serverConnection.request(this.value);
-};
-
-const activityDetectionInterval = 200;
-const activityDetectionPeriod = 700;
-const activityDetectionThreshold = 0.2;
-
-getInputElement('activitybox').onchange = function(e) {
- if(!(this instanceof HTMLInputElement))
- throw new Error('Unexpected type for this');
- updateSettings({activityDetection: this.checked});
- for(let id in serverConnection.down) {
- let c = serverConnection.down[id];
- if(this.checked)
- c.setStatsInterval(activityDetectionInterval);
- else {
- c.setStatsInterval(0);
- setActive(c, false);
- }
- }
-}
-
-getInputElement('fileinput').onchange = function(e) {
- if(!(this instanceof HTMLInputElement))
- throw new Error('Unexpected type for this');
- let input = this;
- let files = input.files;
- for(let i = 0; i < files.length; i++)
- addFileMedia(files[i]);
- input.value = '';
- closeNav();
-}
-
-/**
- * @this {Stream}
- * @param {Object} stats
- */
-function gotUpStats(stats) {
- let c = this;
-
- let text = '';
-
- c.pc.getSenders().forEach(s => {
- let tid = s.track && s.track.id;
- let stats = tid && c.stats[tid];
- let rate = stats && stats['outbound-rtp'] && stats['outbound-rtp'].rate;
- if(typeof rate === 'number') {
- if(text)
- text = text + ' + ';
- text = text + Math.round(rate / 1000) + 'kbps';
- }
- });
-
- setLabel(c, text);
-}
-
-/**
- * @param {Stream} c
- * @param {boolean} value
- */
-function setActive(c, value) {
- let peer = document.getElementById('peer-' + c.id);
- if(value)
- peer.classList.add('peer-active');
- else
- peer.classList.remove('peer-active');
-}
-
-/**
- * @this {Stream}
- * @param {Object} stats
- */
-function gotDownStats(stats) {
- if(!getInputElement('activitybox').checked)
- return;
-
- let c = this;
-
- let maxEnergy = 0;
-
- c.pc.getReceivers().forEach(r => {
- let tid = r.track && r.track.id;
- let s = tid && stats[tid];
- let energy = s && s['track'] && s['track'].audioEnergy;
- if(typeof energy === 'number')
- maxEnergy = Math.max(maxEnergy, energy);
- });
-
- // totalAudioEnergy is defined as the integral of the square of the
- // volume, so square the threshold.
- if(maxEnergy > activityDetectionThreshold * activityDetectionThreshold) {
- c.userdata.lastVoiceActivity = Date.now();
- setActive(c, true);
- } else {
- let last = c.userdata.lastVoiceActivity;
- if(!last || Date.now() - last > activityDetectionPeriod)
- setActive(c, false);
- }
-}
-
-/**
- * @param {HTMLSelectElement} select
- * @param {string} label
- * @param {string} [value]
- */
-function addSelectOption(select, label, value) {
- if(!value)
- value = label;
- for(let i = 0; i < select.children.length; i++) {
- let child = select.children[i];
- if(!(child instanceof HTMLOptionElement)) {
- console.warn('Unexpected select child');
- continue;
- }
- if(child.value === value) {
- if(child.label !== label) {
- child.label = label;
- }
- return;
- }
- }
-
- let option = document.createElement('option');
- option.value = value;
- option.textContent = label;
- select.appendChild(option);
-}
-
-/**
- * @param {HTMLSelectElement} select
- * @param {string} value
- */
-function selectOptionAvailable(select, value) {
- let children = select.children;
- for(let i = 0; i < children.length; i++) {
- let child = select.children[i];
- if(!(child instanceof HTMLOptionElement)) {
- console.warn('Unexpected select child');
- continue;
- }
- if(child.value === value)
- return true;
- }
- return false;
-}
-
-/**
- * @param {HTMLSelectElement} select
- * @returns {string}
- */
-function selectOptionDefault(select) {
- /* First non-empty option. */
- for(let i = 0; i < select.children.length; i++) {
- let child = select.children[i];
- if(!(child instanceof HTMLOptionElement)) {
- console.warn('Unexpected select child');
- continue;
- }
- if(child.value)
- return child.value;
- }
- /* The empty option is always available. */
- return '';
-}
-
-/* media names might not be available before we call getDisplayMedia. So
- we call this twice, the second time to update the menu with user-readable
- labels. */
-/** @type {boolean} */
-let mediaChoicesDone = false;
-
-/**
- * @param{boolean} done
- */
-async function setMediaChoices(done) {
- if(mediaChoicesDone)
- return;
-
- let devices = [];
- try {
- devices = await navigator.mediaDevices.enumerateDevices();
- } catch(e) {
- console.error(e);
- return;
- }
-
- let cn = 1, mn = 1;
-
- devices.forEach(d => {
- let label = d.label;
- if(d.kind === 'videoinput') {
- if(!label)
- label = `Camera ${cn}`;
- addSelectOption(getSelectElement('videoselect'),
- label, d.deviceId);
- cn++;
- } else if(d.kind === 'audioinput') {
- if(!label)
- label = `Microphone ${mn}`;
- addSelectOption(getSelectElement('audioselect'),
- label, d.deviceId);
- mn++;
- }
- });
-
- mediaChoicesDone = done;
-}
-
-
-/**
- * @param {string} [id]
- */
-function newUpStream(id) {
- let c = serverConnection.newUpStream(id);
- c.onstatus = function(status) {
- setMediaStatus(c);
- }
- c.onerror = function(e) {
- console.error(e);
- displayError(e);
- delUpMedia(c);
- }
- c.onabort = function() {
- delUpMedia(c);
- }
- c.onnegotiationcompleted = function() {
- setMaxVideoThroughput(c, getMaxVideoThroughput())
- }
- return c;
-}
-
-/**
- * @param {Stream} c
- * @param {number} [bps]
- */
-async function setMaxVideoThroughput(c, bps) {
- let senders = c.pc.getSenders();
- for(let i = 0; i < senders.length; i++) {
- let s = senders[i];
- if(!s.track || s.track.kind !== 'video')
- continue;
- let p = s.getParameters();
- if(!p.encodings)
- p.encodings = [{}];
- p.encodings.forEach(e => {
- if(bps > 0)
- e.maxBitrate = bps;
- else
- delete e.maxBitrate;
- });
- try {
- await s.setParameters(p);
- } catch(e) {
- console.error(e);
- }
- }
-}
-
-function isSafari() {
- let ua = navigator.userAgent.toLowerCase();
- return ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0;
-}
-
-/**
- * @param {string} [id]
- */
-async function addLocalMedia(id) {
- let settings = getSettings();
-
- let audio = settings.audio ? {deviceId: settings.audio} : false;
- let video = settings.video ? {deviceId: settings.video} : false;
-
- if(video) {
- let resolution = settings.resolution;
- if(resolution) {
- video.width = { ideal: resolution[0] };
- video.height = { ideal: resolution[1] };
- } else if(settings.blackboardMode) {
- video.width = { min: 640, ideal: 1920 };
- video.height = { min: 400, ideal: 1080 };
- }
- }
-
- let old = id && serverConnection.up[id];
-
- if(!audio && !video) {
- if(old)
- delUpMedia(old);
- return;
- }
-
- if(old)
- stopUpMedia(old);
-
- let constraints = {audio: audio, video: video};
- /** @type {MediaStream} */
- let stream = null;
- try {
- stream = await navigator.mediaDevices.getUserMedia(constraints);
- } catch(e) {
- displayError(e);
- if(old)
- delUpMedia(old);
- return;
- }
-
- setMediaChoices(true);
-
- let c = newUpStream(id);
-
- c.kind = 'local';
- c.stream = stream;
- let mute = getSettings().localMute;
- stream.getTracks().forEach(t => {
- c.labels[t.id] = t.kind
- if(t.kind == 'audio') {
- if(mute)
- t.enabled = false;
- } else if(t.kind == 'video') {
- if(settings.blackboardMode) {
- /** @ts-ignore */
- t.contentHint = 'detail';
- }
- }
- c.pc.addTrack(t, stream);
- });
-
- c.onstats = gotUpStats;
- c.setStatsInterval(2000);
- await setMedia(c, true, true);
- setButtonsVisibility();
-}
-
-let safariScreenshareDone = false;
-
-async function addShareMedia() {
- /** @type {MediaStream} */
- let stream = null;
- try {
- if(!('getDisplayMedia' in navigator.mediaDevices))
- throw new Error('Your browser does not support screen sharing');
- /** @ts-ignore */
- stream = await navigator.mediaDevices.getDisplayMedia({video: true});
- } catch(e) {
- console.error(e);
- displayError(e);
- return;
- }
-
- if(!safariScreenshareDone) {
- if(isSafari())
- displayWarning('Screen sharing under Safari is experimental. ' +
- 'Please use a different browser if possible.');
- safariScreenshareDone = true;
- }
-
- let c = newUpStream();
- c.kind = 'screenshare';
- c.stream = stream;
- stream.getTracks().forEach(t => {
- c.pc.addTrack(t, stream);
- t.onended = e => {
- delUpMedia(c);
- };
- c.labels[t.id] = 'screenshare';
- });
- c.onstats = gotUpStats;
- c.setStatsInterval(2000);
- await setMedia(c, true);
- setButtonsVisibility()
-}
-
-/**
- * @param {File} file
- */
-async function addFileMedia(file) {
- let url = URL.createObjectURL(file);
- let video = document.createElement('video');
- video.src = url;
- video.controls = true;
- /** @ts-ignore */
- let stream = video.captureStream();
-
- let c = newUpStream();
- c.kind = 'video';
- c.stream = stream;
- stream.onaddtrack = function(e) {
- let t = e.track;
- if(t.kind === 'audio') {
- let presenting = !!findUpMedia('local');
- let muted = getSettings().localMute;
- if(presenting && !muted) {
- setLocalMute(true, true);
- displayWarning('You have been muted');
- }
- }
- c.pc.addTrack(t, stream);
- c.labels[t.id] = t.kind;
- c.onstats = gotUpStats;
- c.setStatsInterval(2000);
- };
- stream.onremovetrack = function(e) {
- let t = e.track;
- delete(c.labels[t.id]);
-
- /** @type {RTCRtpSender} */
- let sender;
- c.pc.getSenders().forEach(s => {
- if(s.track === t)
- sender = s;
- });
- if(sender) {
- c.pc.removeTrack(sender)
- } else {
- console.warn('Removing unknown track');
- }
-
- if(Object.keys(c.labels).length === 0) {
- stream.onaddtrack = null;
- stream.onremovetrack == null;
- delUpMedia(c);
- }
- };
- await setMedia(c, true, false, video);
- c.userdata.play = true;
- setButtonsVisibility()
-}
-
-/**
- * @param {Stream} c
- */
-function stopUpMedia(c) {
- if(!c.stream)
- return;
- c.stream.getTracks().forEach(t => {
- try {
- t.stop();
- } catch(e) {
- }
- });
-}
-
-/**
- * @param {Stream} c
- */
-function delUpMedia(c) {
- stopUpMedia(c);
- try {
- delMedia(c.id);
- } catch(e) {
- console.warn(e);
- }
- c.close();
- delete(serverConnection.up[c.id]);
- setButtonsVisibility()
-}
-
-/**
- * delUpMediaKind reoves all up media of the given kind. If kind is
- * falseish, it removes all up media.
- * @param {string} kind
-*/
-function delUpMediaKind(kind) {
- for(let id in serverConnection.up) {
- let c = serverConnection.up[id];
- if(kind && c.kind != kind)
- continue
- c.close();
- delMedia(id);
- delete(serverConnection.up[id]);
- }
-
- setButtonsVisibility();
- hideVideo();
-}
-
-/**
- * @param {string} kind
- */
-function findUpMedia(kind) {
- for(let id in serverConnection.up) {
- if(serverConnection.up[id].kind === kind)
- return id;
- }
- return null;
-}
-
-/**
- * @param {boolean} mute
- */
-function muteLocalTracks(mute) {
- if(!serverConnection)
- return;
- for(let id in serverConnection.up) {
- let c = serverConnection.up[id];
- if(c.kind === 'local') {
- let stream = c.stream;
- stream.getTracks().forEach(t => {
- if(t.kind === 'audio') {
- t.enabled = !mute;
- }
- });
- }
- }
-}
-
-/**
- * setMedia adds a new media element corresponding to stream c.
- *
- * @param {Stream} c
- * @param {boolean} isUp
- * - indicates whether the stream goes in the up direction
- * @param {boolean} [mirror]
- * - whether to mirror the video
- * @param {HTMLVideoElement} [video]
- * - the video element to add. If null, a new element with custom
- * controls will be created.
- */
-async function setMedia(c, isUp, mirror, video) {
- let peersdiv = document.getElementById('peers');
-
- let div = document.getElementById('peer-' + c.id);
- if(!div) {
- div = document.createElement('div');
- div.id = 'peer-' + c.id;
- div.classList.add('peer');
- peersdiv.appendChild(div);
- }
-
- let media = /** @type {HTMLVideoElement} */
- (document.getElementById('media-' + c.id));
- if(media) {
- if(video) {
- throw new Error("Duplicate video");
- }
- } else {
- if(video) {
- media = video;
- } else {
- media = document.createElement('video');
- if(isUp)
- media.muted = true;
- }
-
- media.classList.add('media');
- media.autoplay = true;
- /** @ts-ignore */
- media.playsinline = true;
- media.id = 'media-' + c.id;
- div.appendChild(media);
- if(!video)
- addCustomControls(media, div, c);
- if(mirror)
- media.classList.add('mirror');
- }
-
- if(!video)
- media.srcObject = c.stream;
-
- let label = document.getElementById('label-' + c.id);
- if(!label) {
- label = document.createElement('div');
- label.id = 'label-' + c.id;
- label.classList.add('label');
- div.appendChild(label);
- }
-
- setLabel(c);
- setMediaStatus(c);
-
- showVideo();
- resizePeers();
-
- if(!isUp && isSafari() && !findUpMedia('local')) {
- // Safari doesn't allow autoplay unless the user has granted media access
- try {
- let stream = await navigator.mediaDevices.getUserMedia({audio: true});
- stream.getTracks().forEach(t => t.stop());
- } catch(e) {
- }
- }
-}
-
-/**
- * @param {Element} elt
- */
-function cloneHTMLElement(elt) {
- if(!(elt instanceof HTMLElement))
- throw new Error('Unexpected element type');
- return /** @type{HTMLElement} */(elt.cloneNode(true));
-}
-
-/**
- * @param {HTMLVideoElement} media
- * @param {HTMLElement} container
- * @param {Stream} c
- */
-function addCustomControls(media, container, c) {
- media.controls = false;
- let controls = document.getElementById('controls-' + c.id);
- if(controls) {
- console.warn('Attempted to add duplicate controls');
- return;
- }
-
- let template =
- document.getElementById('videocontrols-template').firstElementChild;
- controls = cloneHTMLElement(template);
- controls.id = 'controls-' + c.id;
-
- let volume = getVideoButton(controls, 'volume');
- if(c.kind === 'local') {
- volume.remove();
- } else {
- setVolumeButton(media.muted,
- getVideoButton(controls, "volume-mute"),
- getVideoButton(controls, "volume-slider"));
- }
-
- container.appendChild(controls);
- registerControlHandlers(media, container);
-}
-
-/**
- * @param {HTMLElement} container
- * @param {string} name
- */
-function getVideoButton(container, name) {
- return /** @type {HTMLElement} */(container.getElementsByClassName(name)[0]);
-}
-
-/**
- * @param {boolean} muted
- * @param {HTMLElement} button
- * @param {HTMLElement} slider
- */
-function setVolumeButton(muted, button, slider) {
- if(!muted) {
- button.classList.remove("fa-volume-mute");
- button.classList.add("fa-volume-up");
- } else {
- button.classList.remove("fa-volume-up");
- button.classList.add("fa-volume-mute");
- }
-
- if(!(slider instanceof HTMLInputElement))
- throw new Error("Couldn't find volume slider");
- slider.disabled = muted;
-}
-
-/**
- * @param {HTMLVideoElement} media
- * @param {HTMLElement} container
- */
-function registerControlHandlers(media, container) {
- let play = getVideoButton(container, 'video-play');
- if(play) {
- play.onclick = function(event) {
- event.preventDefault();
- media.play();
- };
- }
-
- let volume = getVideoButton(container, 'volume');
- if (volume) {
- volume.onclick = function(event) {
- let target = /** @type{HTMLElement} */(event.target);
- if(!target.classList.contains('volume-mute'))
- // if click on volume slider, do nothing
- return;
- event.preventDefault();
- media.muted = !media.muted;
- setVolumeButton(media.muted, target,
- getVideoButton(volume, "volume-slider"));
- };
- volume.oninput = function() {
- let slider = /** @type{HTMLInputElement} */
- (getVideoButton(volume, "volume-slider"));
- media.volume = parseInt(slider.value, 10)/100;
- };
- }
-
- let pip = getVideoButton(container, 'pip');
- if(pip) {
- /** @ts-ignore */
- if(HTMLVideoElement.prototype.requestPictureInPicture) {
- pip.onclick = function(e) {
- e.preventDefault();
- /** @ts-ignore */
- if(media.requestPictureInPicture) {
- /** @ts-ignore */
- media.requestPictureInPicture();
- } else {
- displayWarning('Picture in Picture not supported.');
- }
- };
- } else {
- pip.style.display = 'none';
- }
- }
-
- let fs = getVideoButton(container, 'fullscreen');
- if(fs) {
- if(HTMLVideoElement.prototype.requestFullscreen ||
- /** @ts-ignore */
- HTMLVideoElement.prototype.webkitRequestFullscreen) {
- fs.onclick = function(e) {
- e.preventDefault();
- if(media.requestFullscreen) {
- media.requestFullscreen();
- /** @ts-ignore */
- } else if(media.webkitRequestFullscreen) {
- /** @ts-ignore */
- media.webkitRequestFullscreen();
- } else {
- displayWarning('Full screen not supported!');
- }
- };
- } else {
- fs.style.display = 'none';
- }
- }
-}
-
-/**
- * @param {string} id
- */
-function delMedia(id) {
- let mediadiv = document.getElementById('peers');
- let peer = document.getElementById('peer-' + id);
- if(!peer)
- throw new Error('Removing unknown media');
-
- let media = /** @type{HTMLVideoElement} */
- (document.getElementById('media-' + id));
-
- if(media.src) {
- URL.revokeObjectURL(media.src);
- media.src = null;
- }
-
- media.srcObject = null;
- mediadiv.removeChild(peer);
-
- resizePeers();
- hideVideo();
-}
-
-/**
- * @param {Stream} c
- */
-function setMediaStatus(c) {
- let state = c && c.pc && c.pc.iceConnectionState;
- let good = state === 'connected' || state === 'completed';
-
- let media = document.getElementById('media-' + c.id);
- if(!media) {
- console.warn('Setting status of unknown media.');
- return;
- }
- if(good) {
- media.classList.remove('media-failed');
- if(c.userdata.play) {
- if(media instanceof HTMLMediaElement)
- media.play().catch(e => {
- console.error(e);
- displayError(e);
- });
- delete(c.userdata.play);
- }
- } else {
- media.classList.add('media-failed');
- }
-}
-
-
-/**
- * @param {Stream} c
- * @param {string} [fallback]
- */
-function setLabel(c, fallback) {
- let label = document.getElementById('label-' + c.id);
- if(!label)
- return;
- let l = c.label;
- if(l) {
- label.textContent = l;
- label.classList.remove('label-fallback');
- } else if(fallback) {
- label.textContent = fallback;
- label.classList.add('label-fallback');
- } else {
- label.textContent = '';
- label.classList.remove('label-fallback');
- }
-}
-
-function resizePeers() {
- // Window resize can call this method too early
- if (!serverConnection)
- return;
- let count =
- Object.keys(serverConnection.up).length +
- Object.keys(serverConnection.down).length;
- let peers = document.getElementById('peers');
- let columns = Math.ceil(Math.sqrt(count));
- if (!count)
- // No video, nothing to resize.
- return;
- let container = document.getElementById("video-container");
- // Peers div has total padding of 40px, we remove 40 on offsetHeight
- // Grid has row-gap of 5px
- let rows = Math.ceil(count / columns);
- let margins = (rows - 1) * 5 + 40;
-
- if (count <= 2 && container.offsetHeight > container.offsetWidth) {
- peers.style['grid-template-columns'] = "repeat(1, 1fr)";
- rows = count;
- } else {
- peers.style['grid-template-columns'] = `repeat(${columns}, 1fr)`;
- }
- if (count === 1)
- return;
- let max_video_height = (peers.offsetHeight - margins) / rows;
- let media_list = peers.querySelectorAll(".media");
- for(let i = 0; i < media_list.length; i++) {
- let media = media_list[i];
- if(!(media instanceof HTMLMediaElement)) {
- console.warn('Unexpected media');
- continue;
- }
- media.style['max-height'] = max_video_height + "px";
- }
-}
-
-/** @type{Object} */
-let users = {};
-
-/**
- * Lexicographic order, with case differences secondary.
- * @param{string} a
- * @param{string} b
- */
-function stringCompare(a, b) {
- let la = a.toLowerCase()
- let lb = b.toLowerCase()
- if(la < lb)
- return -1;
- else if(la > lb)
- return +1;
- else if(a < b)
- return -1;
- else if(a > b)
- return +1;
- return 0
-}
-
-/**
- * @param {string} id
- * @param {string} name
- */
-function addUser(id, name) {
- if(!name)
- name = null;
- if(id in users)
- throw new Error('Duplicate user id');
- users[id] = name;
-
- let div = document.getElementById('users');
- let user = document.createElement('div');
- user.id = 'user-' + id;
- user.classList.add("user-p");
- user.textContent = name ? name : '(anon)';
-
- if(name) {
- let us = div.children;
- for(let i = 0; i < us.length; i++) {
- let child = us[i];
- let childname = users[child.id.slice('user-'.length)] || null;
- if(!childname || stringCompare(childname, name) > 0) {
- div.insertBefore(user, child);
- return;
- }
- }
- }
- div.appendChild(user);
-}
-
-/**
- * @param {string} id
- * @param {string} name
- */
-function delUser(id, name) {
- if(!name)
- name = null;
- if(!(id in users))
- throw new Error('Unknown user id');
- if(users[id] !== name)
- throw new Error('Inconsistent user name');
- delete(users[id]);
- let div = document.getElementById('users');
- let user = document.getElementById('user-' + id);
- div.removeChild(user);
-}
-
-function resetUsers() {
- for(let id in users)
- delUser(id, users[id]);
-}
-
-/**
- * @param {string} id
- * @param {string} kind
- * @param {string} name
- */
-function gotUser(id, kind, name) {
- switch(kind) {
- case 'add':
- addUser(id, name);
- break;
- case 'delete':
- delUser(id, name);
- break;
- default:
- console.warn('Unknown user kind', kind);
- break;
- }
-}
-
-function displayUsername() {
- let userpass = getUserPass();
- let text = '';
- if(userpass && userpass.username)
- document.getElementById('userspan').textContent = userpass.username;
- if(serverConnection.permissions.op && serverConnection.permissions.present)
- text = '(op, presenter)';
- else if(serverConnection.permissions.op)
- text = 'operator';
- else if(serverConnection.permissions.present)
- text = 'presenter';
- document.getElementById('permspan').textContent = text;
-}
-
-let presentRequested = null;
-
-/**
- * @this {ServerConnection}
- * @param {string} group
- * @param {Object} perms
- */
-async function gotJoined(kind, group, perms, message) {
- let present = presentRequested;
- presentRequested = null;
-
- switch(kind) {
- case 'fail':
- displayError('The server said: ' + message);
- this.close();
- return;
- case 'redirect':
- this.close();
- document.location = message;
- return;
- case 'leave':
- this.close();
- return;
- case 'join':
- case 'change':
- displayUsername();
- setButtonsVisibility();
- if(kind === 'change')
- return;
- break;
- default:
- displayError('Unknown join message');
- this.close();
- return;
- }
-
- let input = /** @type{HTMLTextAreaElement} */
- (document.getElementById('input'));
- input.placeholder = 'Type /help for help';
- setTimeout(() => {input.placeholder = '';}, 8000);
-
- this.request(getSettings().request);
-
- if(serverConnection.permissions.present && !findUpMedia('local')) {
- if(present) {
- if(present === 'mike')
- updateSettings({video: ''});
- else if(present === 'both')
- delSetting('video');
- reflectSettings();
-
- let button = getButtonElement('presentbutton');
- button.disabled = true;
- try {
- await addLocalMedia();
- } finally {
- button.disabled = false;
- }
- } else {
- displayMessage(
- "Press Ready to enable your camera or microphone"
- );
- }
- }
-}
-
-const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#&()=?]+[-a-zA-Z0-9@:%/_\\+~#&()=]/g;
-
-/**
- * @param {string} line
- * @returns {Array.}
- */
-function formatLine(line) {
- let r = new RegExp(urlRegexp);
- let result = [];
- let pos = 0;
- while(true) {
- let m = r.exec(line);
- if(!m)
- break;
- result.push(document.createTextNode(line.slice(pos, m.index)));
- let a = document.createElement('a');
- a.href = m[0];
- a.textContent = m[0];
- a.target = '_blank';
- a.rel = 'noreferrer noopener';
- result.push(a);
- pos = m.index + m[0].length;
- }
- result.push(document.createTextNode(line.slice(pos)));
- return result;
-}
-
-/**
- * @param {string[]} lines
- * @returns {HTMLElement}
- */
-function formatLines(lines) {
- let elts = [];
- if(lines.length > 0)
- elts = formatLine(lines[0]);
- for(let i = 1; i < lines.length; i++) {
- elts.push(document.createElement('br'));
- elts = elts.concat(formatLine(lines[i]));
- }
- let elt = document.createElement('p');
- elts.forEach(e => elt.appendChild(e));
- return elt;
-}
-
-/**
- * @param {number} time
- * @returns {string}
- */
-function formatTime(time) {
- let delta = Date.now() - time;
- let date = new Date(time);
- let m = date.getMinutes();
- if(delta > -30000)
- return date.getHours() + ':' + ((m < 10) ? '0' : '') + m;
- return date.toLocaleString();
-}
-
-/**
- * @typedef {Object} lastMessage
- * @property {string} [nick]
- * @property {string} [peerId]
- * @property {string} [dest]
- * @property {number} [time]
- */
-
-/** @type {lastMessage} */
-let lastMessage = {};
-
-/**
- * @param {string} peerId
- * @param {string} nick
- * @param {number} time
- * @param {string} kind
- * @param {string} message
- */
-function addToChatbox(peerId, dest, nick, time, privileged, kind, message) {
- let userpass = getUserPass();
- let row = document.createElement('div');
- row.classList.add('message-row');
- let container = document.createElement('div');
- container.classList.add('message');
- row.appendChild(container);
- let footer = document.createElement('p');
- footer.classList.add('message-footer');
- if(!peerId)
- container.classList.add('message-system');
- if(userpass.username === nick)
- container.classList.add('message-sender');
- if(dest)
- container.classList.add('message-private');
-
- if(kind !== 'me') {
- let p = formatLines(message.split('\n'));
- let doHeader = true;
- if(!peerId && !dest && !nick) {
- doHeader = false;
- } else if(lastMessage.nick !== (nick || null) ||
- lastMessage.peerId !== peerId ||
- lastMessage.dest !== (dest || null) ||
- !time || !lastMessage.time) {
- doHeader = true;
- } else {
- let delta = time - lastMessage.time;
- doHeader = delta < 0 || delta > 60000;
- }
-
- if(doHeader) {
- let header = document.createElement('p');
- if(peerId || nick || dest) {
- let user = document.createElement('span');
- user.textContent = dest ?
- `${nick||'(anon)'} \u2192 ${users[dest]||'(anon)'}` :
- (nick || '(anon)');
- user.classList.add('message-user');
- header.appendChild(user);
- }
- header.classList.add('message-header');
- container.appendChild(header);
- if(time) {
- let tm = document.createElement('span');
- tm.textContent = formatTime(time);
- tm.classList.add('message-time');
- header.appendChild(tm);
- }
- }
-
- p.classList.add('message-content');
- container.appendChild(p);
- lastMessage.nick = (nick || null);
- lastMessage.peerId = peerId;
- lastMessage.dest = (dest || null);
- lastMessage.time = (time || null);
- container.appendChild(footer);
- } else {
- let asterisk = document.createElement('span');
- asterisk.textContent = '*';
- asterisk.classList.add('message-me-asterisk');
- let user = document.createElement('span');
- user.textContent = nick || '(anon)';
- user.classList.add('message-me-user');
- let content = document.createElement('span');
- formatLine(message).forEach(elt => {
- content.appendChild(elt);
- });
- content.classList.add('message-me-content');
- container.appendChild(asterisk);
- container.appendChild(user);
- container.appendChild(content);
- container.classList.add('message-me');
- lastMessage = {};
- }
-
- let box = document.getElementById('box');
- box.appendChild(row);
- if(box.scrollHeight > box.clientHeight) {
- box.scrollTop = box.scrollHeight - box.clientHeight;
- }
-
- return message;
-}
-
-function clearChat() {
- lastMessage = {};
- document.getElementById('box').textContent = '';
-}
-
-/**
- * A command known to the command-line parser.
- *
- * @typedef {Object} command
- * @property {string} [parameters]
- * - A user-readable list of parameters.
- * @property {string} [description]
- * - A user-readable description, null if undocumented.
- * @property {() => string} [predicate]
- * - Returns null if the command is available.
- * @property {(c: string, r: string) => void} f
- */
-
-/**
- * The set of commands known to the command-line parser.
- *
- * @type {Object.}
- */
-let commands = {}
-
-function operatorPredicate() {
- if(serverConnection && serverConnection.permissions &&
- serverConnection.permissions.op)
- return null;
- return 'You are not an operator';
-}
-
-function recordingPredicate() {
- if(serverConnection && serverConnection.permissions &&
- serverConnection.permissions.record)
- return null;
- return 'You are not allowed to record';
-}
-
-commands.help = {
- description: 'display this help',
- f: (c, r) => {
- /** @type {string[]} */
- let cs = [];
- for(let cmd in commands) {
- let c = commands[cmd];
- if(!c.description)
- continue;
- if(c.predicate && c.predicate())
- continue;
- cs.push(`/${cmd}${c.parameters?' ' + c.parameters:''}: ${c.description}`);
- }
- cs.sort();
- let s = '';
- for(let i = 0; i < cs.length; i++)
- s = s + cs[i] + '\n';
- addToChatbox(null, null, null, Date.now(), false, null, s);
- }
-};
-
-commands.me = {
- f: (c, r) => {
- // handled as a special case
- throw new Error("this shouldn't happen");
- }
-};
-
-commands.set = {
- f: (c, r) => {
- if(!r) {
- let settings = getSettings();
- let s = "";
- for(let key in settings)
- s = s + `${key}: ${JSON.stringify(settings[key])}\n`;
- addToChatbox(null, null, null, Date.now(), false, null, s);
- return;
- }
- let p = parseCommand(r);
- let value;
- if(p[1]) {
- value = JSON.parse(p[1])
- } else {
- value = true;
- }
- updateSetting(p[0], value);
- reflectSettings();
- }
-};
-
-commands.unset = {
- f: (c, r) => {
- delSetting(r.trim());
- return;
- }
-};
-
-commands.leave = {
- description: "leave group",
- f: (c, r) => {
- if(!serverConnection)
- throw new Error('Not connected');
- serverConnection.close();
- }
-};
-
-commands.clear = {
- predicate: operatorPredicate,
- description: 'clear the chat history',
- f: (c, r) => {
- serverConnection.groupAction(getUsername(), 'clearchat');
- }
-};
-
-commands.lock = {
- predicate: operatorPredicate,
- description: 'lock this group',
- parameters: '[message]',
- f: (c, r) => {
- serverConnection.groupAction(getUsername(), 'lock', r);
- }
-};
-
-commands.unlock = {
- predicate: operatorPredicate,
- description: 'unlock this group, revert the effect of /lock',
- f: (c, r) => {
- serverConnection.groupAction(getUsername(), 'unlock');
- }
-};
-
-commands.record = {
- predicate: recordingPredicate,
- description: 'start recording',
- f: (c, r) => {
- serverConnection.groupAction(getUsername(), 'record');
- }
-};
-
-commands.unrecord = {
- predicate: recordingPredicate,
- description: 'stop recording',
- f: (c, r) => {
- serverConnection.groupAction(getUsername(), 'unrecord');
- }
-};
-
-commands.subgroups = {
- predicate: operatorPredicate,
- description: 'list subgroups',
- f: (c, r) => {
- serverConnection.groupAction(getUsername(), 'subgroups');
- }
-};
-
-commands.renegotiate = {
- description: 'renegotiate media streams',
- f: (c, r) => {
- for(let id in serverConnection.up)
- serverConnection.up[id].restartIce();
- for(let id in serverConnection.down)
- serverConnection.down[id].restartIce();
- }
-};
-
-/**
- * parseCommand splits a string into two space-separated parts. The first
- * part may be quoted and may include backslash escapes.
- *
- * @param {string} line
- * @returns {string[]}
- */
-function parseCommand(line) {
- let i = 0;
- while(i < line.length && line[i] === ' ')
- i++;
- let start = ' ';
- if(i < line.length && line[i] === '"' || line[i] === "'") {
- start = line[i];
- i++;
- }
- let first = "";
- while(i < line.length) {
- if(line[i] === start) {
- if(start !== ' ')
- i++;
- break;
- }
- if(line[i] === '\\' && i < line.length - 1)
- i++;
- first = first + line[i];
- i++;
- }
-
- while(i < line.length && line[i] === ' ')
- i++;
- return [first, line.slice(i)];
-}
-
-/**
- * @param {string} user
- */
-function findUserId(user) {
- if(user in users)
- return user;
-
- for(let id in users) {
- if(users[id] === user)
- return id;
- }
- return null;
-}
-
-commands.msg = {
- parameters: 'user message',
- description: 'send a private message',
- f: (c, r) => {
- let p = parseCommand(r);
- if(!p[0])
- throw new Error('/msg requires parameters');
- let id = findUserId(p[0]);
- if(!id)
- throw new Error(`Unknown user ${p[0]}`);
- let username = getUsername();
- serverConnection.chat(username, '', id, p[1]);
- addToChatbox(serverConnection.id, id, username,
- Date.now(), false, '', p[1]);
- }
-};
-
-/**
- @param {string} c
- @param {string} r
-*/
-function userCommand(c, r) {
- let p = parseCommand(r);
- if(!p[0])
- throw new Error(`/${c} requires parameters`);
- let id = findUserId(p[0]);
- if(!id)
- throw new Error(`Unknown user ${p[0]}`);
- serverConnection.userAction(getUsername(), c, id, p[1]);
-}
-
-function userMessage(c, r) {
- let p = parseCommand(r);
- if(!p[0])
- throw new Error(`/${c} requires parameters`);
- let id = findUserId(p[0]);
- if(!id)
- throw new Error(`Unknown user ${p[0]}`);
- serverConnection.userMessage(getUsername(), c, id, p[1]);
-}
-
-commands.kick = {
- parameters: 'user [message]',
- description: 'kick out a user',
- predicate: operatorPredicate,
- f: userCommand,
-};
-
-commands.op = {
- parameters: 'user',
- description: 'give operator status',
- predicate: operatorPredicate,
- f: userCommand,
-};
-
-commands.unop = {
- parameters: 'user',
- description: 'revoke operator status',
- predicate: operatorPredicate,
- f: userCommand,
-};
-
-commands.present = {
- parameters: 'user',
- description: 'give user the right to present',
- predicate: operatorPredicate,
- f: userCommand,
-};
-
-commands.unpresent = {
- parameters: 'user',
- description: 'revoke the right to present',
- predicate: operatorPredicate,
- f: userCommand,
-};
-
-commands.mute = {
- parameters: 'user',
- description: 'mute a remote user',
- predicate: operatorPredicate,
- f: userMessage,
-};
-
-commands.warn = {
- parameters: 'user message',
- description: 'send a warning to a user',
- predicate: operatorPredicate,
- f: (c, r) => {
- userMessage('warning', r);
- },
-};
-
-commands.wall = {
- parameters: 'message',
- description: 'send a warning to all users',
- predicate: operatorPredicate,
- f: (c, r) => {
- if(!r)
- throw new Error('empty message');
- serverConnection.userMessage(getUsername(), 'warning', '', r);
- },
-};
-
-function handleInput() {
- let input = /** @type {HTMLTextAreaElement} */
- (document.getElementById('input'));
- let data = input.value;
- input.value = '';
-
- let message, me;
-
- if(data === '')
- return;
-
- if(data[0] === '/') {
- if(data.length > 1 && data[1] === '/') {
- message = data.slice(1);
- me = false;
- } else {
- let cmd, rest;
- let space = data.indexOf(' ');
- if(space < 0) {
- cmd = data.slice(1);
- rest = '';
- } else {
- cmd = data.slice(1, space);
- rest = data.slice(space + 1);
- }
-
- if(cmd === 'me') {
- message = rest;
- me = true;
- } else {
- let c = commands[cmd];
- if(!c) {
- displayError(`Uknown command /${cmd}, type /help for help`);
- return;
- }
- if(c.predicate) {
- let s = c.predicate();
- if(s) {
- displayError(s);
- return;
- }
- }
- try {
- c.f(cmd, rest);
- } catch(e) {
- displayError(e);
- }
- return;
- }
- }
- } else {
- message = data;
- me = false;
- }
-
- if(!serverConnection || !serverConnection.socket) {
- displayError("Not connected.");
- return;
- }
-
- let username = getUsername();
- try {
- serverConnection.chat(username, me ? 'me' : '', '', message);
- } catch(e) {
- console.error(e);
- displayError(e);
- }
-}
-
-document.getElementById('inputform').onsubmit = function(e) {
- e.preventDefault();
- handleInput();
-};
-
-document.getElementById('input').onkeypress = function(e) {
- if(e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
- e.preventDefault();
- handleInput();
- }
-};
-
-function chatResizer(e) {
- e.preventDefault();
- let full_width = document.getElementById("mainrow").offsetWidth;
- let left = document.getElementById("left");
- let right = document.getElementById("right");
-
- let start_x = e.clientX;
- let start_width = left.offsetWidth;
-
- function start_drag(e) {
- let left_width = (start_width + e.clientX - start_x) * 100 / full_width;
- // set min chat width to 300px
- let min_left_width = 300 * 100 / full_width;
- if (left_width < min_left_width) {
- return;
- }
- left.style.flex = left_width.toString();
- right.style.flex = (100 - left_width).toString();
- }
- function stop_drag(e) {
- document.documentElement.removeEventListener(
- 'mousemove', start_drag, false,
- );
- document.documentElement.removeEventListener(
- 'mouseup', stop_drag, false,
- );
- }
-
- document.documentElement.addEventListener(
- 'mousemove', start_drag, false,
- );
- document.documentElement.addEventListener(
- 'mouseup', stop_drag, false,
- );
-}
-
-document.getElementById('resizer').addEventListener('mousedown', chatResizer, false);
-
-/**
- * @param {unknown} message
- * @param {string} [level]
- */
-function displayError(message, level) {
- if(!level)
- level = "error";
-
- var background = 'linear-gradient(to right, #e20a0a, #df2d2d)';
- var position = 'center';
- var gravity = 'top';
-
- switch(level) {
- case "info":
- background = 'linear-gradient(to right, #529518, #96c93d)';
- position = 'right';
- gravity = 'bottom';
- break;
- case "warning":
- background = "linear-gradient(to right, #bdc511, #c2cf01)";
- break;
- }
-
- /** @ts-ignore */
- Toastify({
- text: message,
- duration: 4000,
- close: true,
- position: position,
- gravity: gravity,
- backgroundColor: background,
- className: level,
- }).showToast();
-}
-
-/**
- * @param {unknown} message
- */
-function displayWarning(message) {
- return displayError(message, "warning");
-}
-
-/**
- * @param {unknown} message
- */
-function displayMessage(message) {
- return displayError(message, "info");
-}
-
-let connecting = false;
-
-document.getElementById('userform').onsubmit = async function(e) {
- e.preventDefault();
- if(connecting)
- return;
- connecting = true;
- try {
- let username = getInputElement('username').value.trim();
- let password = getInputElement('password').value;
- storeUserPass(username, password);
- serverConnect();
- } finally {
- connecting = false;
- }
-
- if(getInputElement('presentboth').checked)
- presentRequested = 'both';
- else if(getInputElement('presentmike').checked)
- presentRequested = 'mike';
- else
- presentRequested = null;
-
- getInputElement('presentoff').checked = true;
-};
-
-document.getElementById('disconnectbutton').onclick = function(e) {
- serverConnection.close();
- closeNav();
-};
-
-function openNav() {
- document.getElementById("sidebarnav").style.width = "250px";
-}
-
-function closeNav() {
- document.getElementById("sidebarnav").style.width = "0";
-}
-
-document.getElementById('sidebarCollapse').onclick = function(e) {
- document.getElementById("left-sidebar").classList.toggle("active");
- document.getElementById("mainrow").classList.toggle("full-width-active");
-};
-
-document.getElementById('openside').onclick = function(e) {
- e.preventDefault();
- let sidewidth = document.getElementById("sidebarnav").style.width;
- if (sidewidth !== "0px" && sidewidth !== "") {
- closeNav();
- return;
- } else {
- openNav();
- }
-};
-
-
-document.getElementById('clodeside').onclick = function(e) {
- e.preventDefault();
- closeNav();
-};
-
-document.getElementById('collapse-video').onclick = function(e) {
- e.preventDefault();
- if(!(this instanceof HTMLElement))
- throw new Error('Unexpected type for this');
- let width = window.innerWidth;
- let left = document.getElementById("left");
- if (left.style.display === "" || left.style.display === "none") {
- //left chat is hidden, we show the chat and hide collapse button
- left.style.display = "block";
- this.style.display = "";
- }
- if (width <= 768) {
- // fixed div for small screen
- this.style.display = "";
- hideVideo(true);
- document.getElementById('switch-video').style.display = "block";
- }
-};
-
-document.getElementById('switch-video').onclick = function(e) {
- e.preventDefault();
- if(!(this instanceof HTMLElement))
- throw new Error('Unexpected type for this');
- showVideo();
- this.style.display = "";
- document.getElementById('collapse-video').style.display = "block";
-};
-
-document.getElementById('close-chat').onclick = function(e) {
- e.preventDefault();
- let left = document.getElementById("left");
- left.style.display = "none";
- document.getElementById('collapse-video').style.display = "block";
-};
-
-async function serverConnect() {
- if(serverConnection && serverConnection.socket)
- serverConnection.close();
- serverConnection = new ServerConnection();
- serverConnection.onconnected = gotConnected;
- serverConnection.onclose = gotClose;
- serverConnection.ondownstream = gotDownStream;
- serverConnection.onuser = gotUser;
- serverConnection.onjoined = gotJoined;
- serverConnection.onchat = addToChatbox;
- serverConnection.onclearchat = clearChat;
- serverConnection.onusermessage = function(id, dest, username, time, privileged, kind, message) {
- switch(kind) {
- case 'error':
- case 'warning':
- case 'info':
- let from = id ? (username || 'Anonymous') : 'The Server';
- if(privileged)
- displayError(`${from} said: ${message}`, kind);
- else
- console.error(`Got unprivileged message of kind ${kind}`);
- break;
- case 'mute':
- console.log(id, dest, username);
- if(privileged) {
- setLocalMute(true, true);
- let by = username ? ' by ' + username : '';
- displayWarning(`You have been muted${by}`);
- } else {
- console.error(`Got unprivileged message of kind ${kind}`);
- }
- break;
- default:
- console.warn(`Got unknown user message ${kind}`);
- break;
- }
- };
- let url = `ws${location.protocol === 'https:' ? 's' : ''}://${location.host}/ws`;
- try {
- await serverConnection.connect(url);
- } catch(e) {
- console.error(e);
- displayError(e.message ? e.message : "Couldn't connect to " + url);
- }
-}
-
-function start() {
- group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, ''));
- let title = group.charAt(0).toUpperCase() + group.slice(1);
- if(group !== '') {
- document.title = title;
- document.getElementById('title').textContent = title;
- }
-
- setMediaChoices(false).then(e => reflectSettings());
-
- fillLogin();
- document.getElementById("login-container").classList.remove('invisible');
-}
-
-start();
diff --git a/sources/static/index.html b/sources/static/index.html
deleted file mode 100755
index 83e32ee..0000000
--- a/sources/static/index.html
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- Galène
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/sources/static/mainpage.css b/sources/static/mainpage.css
deleted file mode 100755
index 14b891c..0000000
--- a/sources/static/mainpage.css
+++ /dev/null
@@ -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;
- }
-
-}
diff --git a/sources/static/mainpage.js b/sources/static/mainpage.js
deleted file mode 100755
index 1f80c40..0000000
--- a/sources/static/mainpage.js
+++ /dev/null
@@ -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();
diff --git a/sources/static/protocol.js b/sources/static/protocol.js
deleted file mode 100755
index b93667e..0000000
--- a/sources/static/protocol.js
+++ /dev/null
@@ -1,1173 +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';
-
-/**
- * toHex formats an array as a hexadecimal string.
- * @param {number[]|Uint8Array} array - the array to format
- * @returns {string} - the hexadecimal representation of array
- */
-function toHex(array) {
- let a = new Uint8Array(array);
- function hex(x) {
- let h = x.toString(16);
- if(h.length < 2)
- h = '0' + h;
- return h;
- }
- return a.reduce((x, y) => x + hex(y), '');
-}
-
-/** randomid returns a random string of 32 hex digits (16 bytes).
- * @returns {string}
- */
-function randomid() {
- let a = new Uint8Array(16);
- crypto.getRandomValues(a);
- return toHex(a);
-}
-
-/**
- * ServerConnection encapsulates a websocket connection to the server and
- * all the associated streams.
- * @constructor
- */
-function ServerConnection() {
- /**
- * The id of this connection.
- *
- * @type {string}
- * @const
- */
- this.id = randomid();
- /**
- * The group that we have joined, or nil if we haven't joined yet.
- *
- * @type {string}
- */
- this.group = null;
- /**
- * The underlying websocket.
- *
- * @type {WebSocket}
- */
- this.socket = null;
- /**
- * The set of all up streams, indexed by their id.
- *
- * @type {Object}
- */
- this.up = {};
- /**
- * The set of all down streams, indexed by their id.
- *
- * @type {Object}
- */
- this.down = {};
- /**
- * The ICE configuration used by all associated streams.
- *
- * @type {RTCIceServer[]}
- */
- this.iceServers = null;
- /**
- * The permissions granted to this connection.
- *
- * @type {Object}
- */
- this.permissions = {};
- /**
- * userdata is a convenient place to attach data to a ServerConnection.
- * It is not used by the library.
- *
- * @type{Object}
- */
- this.userdata = {};
-
- /* Callbacks */
-
- /**
- * onconnected is called when the connection has been established
- *
- * @type{(this: ServerConnection) => void}
- */
- this.onconnected = null;
- /**
- * onclose is called when the connection is closed
- *
- * @type{(this: ServerConnection, code: number, reason: string) => void}
- */
- this.onclose = null;
- /**
- * onuser is called whenever a user is added or removed from the group
- *
- * @type{(this: ServerConnection, id: string, kind: string, username: string) => void}
- */
- this.onuser = null;
- /**
- * onjoined is called whenever we join or leave a group or whenever the
- * permissions we have in a group change.
- *
- * kind is one of 'join', 'fail', 'change' or 'leave'.
- *
- * @type{(this: ServerConnection, kind: string, group: string, permissions: Object, message: string) => void}
- */
- this.onjoined = null;
- /**
- * ondownstream is called whenever a new down stream is added. It
- * should set up the stream's callbacks; actually setting up the UI
- * should be done in the stream's ondowntrack callback.
- *
- * @type{(this: ServerConnection, stream: Stream) => void}
- */
- this.ondownstream = null;
- /**
- * onchat is called whenever a new chat message is received.
- *
- * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, kind: string, message: string) => void}
- */
- this.onchat = null;
- /**
- * onusermessage is called when an application-specific message is
- * received. Id is null when the message originated at the server,
- * a user-id otherwise.
- *
- * 'kind' is typically one of 'error', 'warning', 'info' or 'mute'. If
- * 'id' is non-null, 'privileged' indicates whether the message was
- * sent by an operator.
- *
- * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, kind: string, message: string) => void}
- */
- this.onusermessage = null;
- /**
- * onclearchat is called whenever the server requests that the chat
- * be cleared.
- *
- * @type{(this: ServerConnection) => void}
- */
- this.onclearchat = null;
-}
-
-/**
- * @typedef {Object} message
- * @property {string} type
- * @property {string} [kind]
- * @property {string} [id]
- * @property {string} [dest]
- * @property {string} [username]
- * @property {string} [password]
- * @property {boolean} [privileged]
- * @property {Object} [permissions]
- * @property {string} [group]
- * @property {string} [value]
- * @property {RTCSessionDescriptionInit} [offer]
- * @property {RTCSessionDescriptionInit} [answer]
- * @property {RTCIceCandidate} [candidate]
- * @property {Object} [labels]
- * @property {Object} [request]
- */
-
-/**
- * close forcibly closes a server connection. The onclose callback will
- * be called when the connection is effectively closed.
- */
-ServerConnection.prototype.close = function() {
- this.socket && this.socket.close(1000, 'Close requested by client');
- this.socket = null;
-}
-
-/**
- * send sends a message to the server.
- * @param {message} m - the message to send.
- */
-ServerConnection.prototype.send = function(m) {
- if(!this.socket || this.socket.readyState !== this.socket.OPEN) {
- // send on a closed socket doesn't throw
- throw(new Error('Connection is not open'));
- }
- return this.socket.send(JSON.stringify(m));
-}
-
-/**
- * getIceServers fetches an ICE configuration from the server and
- * populates the iceServers field of a ServerConnection. It is called
- * lazily by connect.
- *
- * @returns {Promise}
- * @function
- */
-ServerConnection.prototype.getIceServers = async function() {
- let r = await fetch('/ice-servers.json');
- if(!r.ok)
- throw new Error("Couldn't fetch ICE servers: " +
- r.status + ' ' + r.statusText);
- let servers = await r.json();
- if(!(servers instanceof Array))
- throw new Error("couldn't parse ICE servers");
- this.iceServers = servers;
- return servers;
-}
-
-/**
- * connect connects to the server.
- *
- * @param {string} url - The URL to connect to.
- * @returns {Promise}
- * @function
- */
-ServerConnection.prototype.connect = async function(url) {
- let sc = this;
- if(sc.socket) {
- sc.socket.close(1000, 'Reconnecting');
- sc.socket = null;
- }
-
- if(!sc.iceServers) {
- try {
- await sc.getIceServers();
- } catch(e) {
- console.warn(e);
- }
- }
-
- sc.socket = new WebSocket(url);
-
- return await new Promise((resolve, reject) => {
- this.socket.onerror = function(e) {
- reject(e);
- };
- this.socket.onopen = function(e) {
- sc.send({
- type: 'handshake',
- id: sc.id,
- });
- if(sc.onconnected)
- sc.onconnected.call(sc);
- resolve(sc);
- };
- this.socket.onclose = function(e) {
- sc.permissions = {};
- for(let id in sc.down) {
- let c = sc.down[id];
- delete(sc.down[id]);
- c.close();
- if(c.onclose)
- c.onclose.call(c);
- }
- if(sc.group && sc.onjoined)
- sc.onjoined.call(sc, 'leave', sc.group, {}, '');
- sc.group = null;
- if(sc.onclose)
- sc.onclose.call(sc, e.code, e.reason);
- reject(new Error('websocket close ' + e.code + ' ' + e.reason));
- };
- this.socket.onmessage = function(e) {
- let m = JSON.parse(e.data);
- switch(m.type) {
- case 'offer':
- sc.gotOffer(m.id, m.labels, m.offer, m.kind === 'renegotiate');
- break;
- case 'answer':
- sc.gotAnswer(m.id, m.answer);
- break;
- case 'renegotiate':
- sc.gotRenegotiate(m.id)
- break;
- case 'close':
- sc.gotClose(m.id);
- break;
- case 'abort':
- sc.gotAbort(m.id);
- break;
- case 'ice':
- sc.gotRemoteIce(m.id, m.candidate);
- break;
- case 'label':
- sc.gotLabel(m.id, m.value);
- break;
- case 'joined':
- if(sc.group) {
- if(m.group !== sc.group) {
- throw new Error('Joined multiple groups');
- }
- } else {
- sc.group = m.group;
- }
- sc.permissions = m.permissions;
- if(sc.onjoined)
- sc.onjoined.call(sc, m.kind, m.group,
- m.permissions || {},
- m.value || null);
- break;
- case 'user':
- if(sc.onuser)
- sc.onuser.call(sc, m.id, m.kind, m.username);
- break;
- case 'chat':
- if(sc.onchat)
- sc.onchat.call(
- sc, m.id, m.dest, m.username, m.time,
- m.privileged, m.kind, m.value,
- );
- break;
- case 'usermessage':
- if(sc.onusermessage)
- sc.onusermessage.call(
- sc, m.id, m.dest, m.username, m.time,
- m.privileged, m.kind, m.value,
- );
- break;
- case 'clearchat':
- if(sc.onclearchat)
- sc.onclearchat.call(sc);
- break;
- case 'ping':
- sc.send({
- type: 'pong',
- });
- break;
- case 'pong':
- /* nothing */
- break;
- default:
- console.warn('Unexpected server message', m.type);
- return;
- }
- };
- });
-}
-
-/**
- * join requests to join a group. The onjoined callback will be called
- * when we've effectively joined.
- *
- * @param {string} group - The name of the group to join.
- * @param {string} username - the username to join as.
- * @param {string} password - the password.
- */
-ServerConnection.prototype.join = function(group, username, password) {
- this.send({
- type: 'join',
- kind: 'join',
- group: group,
- username: username,
- password: password,
- });
-}
-
-/**
- * leave leaves a group. The onjoined callback will be called when we've
- * effectively left.
- *
- * @param {string} group - The name of the group to join.
- */
-ServerConnection.prototype.leave = function(group) {
- this.send({
- type: 'join',
- kind: 'leave',
- group: group,
- });
-}
-
-/**
- * request sets the list of requested media types.
- *
- * @param {string} what - One of '', 'audio', 'screenshare' or 'everything'.
- */
-ServerConnection.prototype.request = function(what) {
- /** @type {Object} */
- let request = {};
- switch(what) {
- case '':
- request = {};
- break;
- case 'audio':
- request = {audio: true};
- break;
- case 'screenshare':
- request = {audio: true, screenshare: true};
- break;
- case 'everything':
- request = {audio: true, screenshare: true, video: true};
- break;
- default:
- console.error(`Unknown value ${what} in request`);
- break;
- }
-
- this.send({
- type: 'request',
- request: request,
- });
-};
-
-/**
- * newUpStream requests the creation of a new up stream.
- *
- * @param {string} [id] - The id of the stream to create.
- * @returns {Stream}
- */
-ServerConnection.prototype.newUpStream = function(id) {
- let sc = this;
- if(!id) {
- id = randomid();
- if(sc.up[id])
- throw new Error('Eek!');
- }
- let pc = new RTCPeerConnection({
- iceServers: sc.iceServers || [],
- });
- if(!pc)
- throw new Error("Couldn't create peer connection");
- if(sc.up[id]) {
- sc.up[id].close();
- }
- let c = new Stream(this, id, pc, true);
- sc.up[id] = c;
-
- pc.onnegotiationneeded = async e => {
- await c.negotiate();
- };
-
- pc.onicecandidate = e => {
- if(!e.candidate)
- return;
- c.gotLocalIce(e.candidate);
- };
-
- pc.oniceconnectionstatechange = e => {
- if(c.onstatus)
- c.onstatus.call(c, pc.iceConnectionState);
- if(pc.iceConnectionState === 'failed')
- c.restartIce();
- };
-
- pc.ontrack = console.error;
-
- return c;
-}
-
-/**
- * chat sends a chat message to the server. The server will normally echo
- * the message back to the client.
- *
- * @param {string} username - The sender's username.
- * @param {string} kind
- * - The kind of message, either '', 'me' or an application-specific type.
- * @param {string} dest - The id to send the message to, empty for broadcast.
- * @param {string} value - The text of the message.
- */
-ServerConnection.prototype.chat = function(username, kind, dest, value) {
- this.send({
- type: 'chat',
- id: this.id,
- dest: dest,
- username: username,
- kind: kind,
- value: value,
- });
-};
-
-/**
- * userAction sends a request to act on a user.
- *
- * @param {string} username - The sender's username.
- * @param {string} kind - One of "op", "unop", "kick", "present", "unpresent".
- * @param {string} dest - The id of the user to act upon.
- * @param {string} [value] - An optional user-readable message.
- */
-ServerConnection.prototype.userAction = function(username, kind, dest, value) {
- this.send({
- type: 'useraction',
- id: this.id,
- dest: dest,
- username: username,
- kind: kind,
- value: value,
- });
-};
-
-/**
- * userMessage sends an application-specific message to a user.
- * This is similar to a chat message, but is not saved in the chat history.
- *
- * @param {string} username - The sender's username.
- * @param {string} kind - The kind of application-specific message.
- * @param {string} dest - The id to send the message to, empty for broadcast.
- * @param {string} [value] - An optional parameter.
- */
-ServerConnection.prototype.userMessage = function(username, kind, dest, value) {
- this.send({
- type: 'usermessage',
- id: this.id,
- dest: dest,
- username: username,
- kind: kind,
- value: value,
- });
-};
-
-/**
- * groupAction sends a request to act on the current group.
- *
- * @param {string} username - The sender's username.
- * @param {string} kind
- * - One of 'clearchat', 'lock', 'unlock', 'record' or 'unrecord'.
- * @param {string} [message] - An optional user-readable message.
- */
-ServerConnection.prototype.groupAction = function(username, kind, message) {
- this.send({
- type: 'groupaction',
- id: this.id,
- kind: kind,
- username: username,
- value: message,
- });
-};
-
-/**
- * Called when we receive an offer from the server. Don't call this.
- *
- * @param {string} id
- * @param {Object} labels
- * @param {RTCSessionDescriptionInit} offer
- * @param {boolean} renegotiate
- * @function
- */
-ServerConnection.prototype.gotOffer = async function(id, labels, offer, renegotiate) {
- let sc = this;
- let c = sc.down[id];
- if(c && !renegotiate) {
- // SDP is rather inflexible as to what can be renegotiated.
- // Unless the server indicates that this is a renegotiation with
- // all parameters unchanged, tear down the existing connection.
- delete(sc.down[id]);
- c.close();
- c = null;
- }
-
- if(sc.up[id])
- throw new Error('Duplicate connection id');
-
- if(!c) {
- let pc = new RTCPeerConnection({
- iceServers: this.iceServers,
- });
- c = new Stream(this, id, pc, false);
- sc.down[id] = c;
-
- c.pc.onicecandidate = function(e) {
- if(!e.candidate)
- return;
- c.gotLocalIce(e.candidate);
- };
-
- pc.oniceconnectionstatechange = e => {
- if(c.onstatus)
- c.onstatus.call(c, pc.iceConnectionState);
- if(pc.iceConnectionState === 'failed') {
- sc.send({
- type: 'renegotiate',
- id: id,
- });
- }
- };
-
- c.pc.ontrack = function(e) {
- let label = e.transceiver && c.labelsByMid[e.transceiver.mid];
- if(label) {
- c.labels[e.track.id] = label;
- } else {
- console.warn("Couldn't find label for track");
- }
- if(c.stream !== e.streams[0]) {
- c.stream = e.streams[0];
- let label =
- e.transceiver && c.labelsByMid[e.transceiver.mid];
- c.labels[e.track.id] = label;
- if(c.ondowntrack) {
- c.ondowntrack.call(
- c, e.track, e.transceiver, label, e.streams[0],
- );
- }
- if(c.onlabel) {
- c.onlabel.call(c, label);
- }
- }
- };
- }
-
- c.labelsByMid = labels;
-
- if(sc.ondownstream)
- sc.ondownstream.call(sc, c);
-
- await c.pc.setRemoteDescription(offer);
- await c.flushRemoteIceCandidates()
- let answer = await c.pc.createAnswer();
- if(!answer)
- throw new Error("Didn't create answer");
- await c.pc.setLocalDescription(answer);
- this.send({
- type: 'answer',
- id: id,
- answer: answer,
- });
- c.localDescriptionSent = true;
- c.flushLocalIceCandidates();
- if(c.onnegotiationcompleted)
- c.onnegotiationcompleted.call(c);
-};
-
-/**
- * Called when we receive a stream label from the server. Don't call this.
- *
- * @param {string} id
- * @param {string} label
- */
-ServerConnection.prototype.gotLabel = function(id, label) {
- let c = this.down[id];
- if(!c)
- throw new Error('Got label for unknown id');
-
- c.label = label;
- if(c.onlabel)
- c.onlabel.call(c, label);
-};
-
-/**
- * Called when we receive an answer from the server. Don't call this.
- *
- * @param {string} id
- * @param {RTCSessionDescriptionInit} answer
- * @function
- */
-ServerConnection.prototype.gotAnswer = async function(id, answer) {
- let c = this.up[id];
- if(!c)
- throw new Error('unknown up stream');
- try {
- await c.pc.setRemoteDescription(answer);
- } catch(e) {
- if(c.onerror)
- c.onerror.call(c, e);
- return;
- }
- await c.flushRemoteIceCandidates();
- if(c.onnegotiationcompleted)
- c.onnegotiationcompleted.call(c);
-};
-
-/**
- * Called when we receive a renegotiation request from the server. Don't
- * call this.
- *
- * @param {string} id
- * @function
- */
-ServerConnection.prototype.gotRenegotiate = async function(id) {
- let c = this.up[id];
- if(!c)
- throw new Error('unknown up stream');
- c.restartIce();
-};
-
-/**
- * Called when we receive a close request from the server. Don't call this.
- *
- * @param {string} id
- */
-ServerConnection.prototype.gotClose = function(id) {
- let c = this.down[id];
- if(!c)
- throw new Error('unknown down stream');
- delete(this.down[id]);
- c.close();
- if(c.onclose)
- c.onclose.call(c);
-};
-
-/**
- * Called when we receive an abort message from the server. Don't call this.
- *
- * @param {string} id
- */
-ServerConnection.prototype.gotAbort = function(id) {
- let c = this.up[id];
- if(!c)
- throw new Error('unknown up stream');
- if(c.onabort)
- c.onabort.call(c);
-};
-
-/**
- * Called when we receive an ICE candidate from the server. Don't call this.
- *
- * @param {string} id
- * @param {RTCIceCandidate} candidate
- * @function
- */
-ServerConnection.prototype.gotRemoteIce = async function(id, candidate) {
- let c = this.up[id];
- if(!c)
- c = this.down[id];
- if(!c)
- throw new Error('unknown stream');
- if(c.pc.remoteDescription)
- await c.pc.addIceCandidate(candidate).catch(console.warn);
- else
- c.remoteIceCandidates.push(candidate);
-};
-
-/**
- * Stream encapsulates a MediaStream, a set of tracks.
- *
- * A stream is said to go "up" if it is from the client to the server, and
- * "down" otherwise.
- *
- * @param {ServerConnection} sc
- * @param {string} id
- * @param {RTCPeerConnection} pc
- *
- * @constructor
- */
-function Stream(sc, id, pc, up) {
- /**
- * The associated ServerConnection.
- *
- * @type {ServerConnection}
- * @const
- */
- this.sc = sc;
- /**
- * The id of this stream.
- *
- * @type {string}
- * @const
- */
- this.id = id;
- /**
- * Indicates whether the stream is in the client->server direction.
- *
- * @type {boolean}
- * @const
- */
- this.up = up
- /**
- * For up streams, one of "local" or "screenshare".
- *
- * @type {string}
- */
- this.kind = null;
- /**
- * For down streams, a user-readable label.
- *
- * @type {string}
- */
- this.label = null;
- /**
- * The associated RTCPeerConnectoin. This is null before the stream
- * is connected, and may change over time.
- *
- * @type {RTCPeerConnection}
- */
- this.pc = pc;
- /**
- * The associated MediaStream. This is null before the stream is
- * connected, and may change over time.
- *
- * @type {MediaStream}
- */
- this.stream = null;
- /**
- * Track labels, indexed by track id.
- *
- * @type {Object}
- */
- this.labels = {};
- /**
- * Track labels, indexed by mid.
- *
- * @type {Object}
- */
- this.labelsByMid = {};
- /**
- * Indicates whether we have already sent a local description.
- *
- * @type {boolean}
- */
- this.localDescriptionSent = false;
- /**
- * Buffered local ICE candidates. This will be flushed by
- * flushLocalIceCandidates after we send a local description.
- *
- * @type {RTCIceCandidate[]}
- */
- this.localIceCandidates = [];
- /**
- * Buffered remote ICE candidates. This will be flushed by
- * flushRemoteIceCandidates when we get a remote SDP description.
- *
- * @type {RTCIceCandidate[]}
- */
- this.remoteIceCandidates = [];
- /**
- * The statistics last computed by the stats handler. This is
- * a dictionary indexed by track id, with each value a dictionary of
- * statistics.
- *
- * @type {Object}
- */
- this.stats = {};
- /**
- * The id of the periodic handler that computes statistics, as
- * returned by setInterval.
- *
- * @type {number}
- */
- this.statsHandler = null;
- /**
- * userdata is a convenient place to attach data to a Stream.
- * It is not used by the library.
- *
- * @type{Object}
- */
- this.userdata = {};
-
- /* Callbacks */
-
- /**
- * onclose is called when the stream is closed.
- *
- * @type{(this: Stream) => void}
- */
- this.onclose = null;
- /**
- * onerror is called whenever an error occurs. If the error is
- * fatal, then onclose will be called afterwards.
- *
- * @type{(this: Stream, error: unknown) => void}
- */
- this.onerror = null;
- /**
- * onnegotiationcompleted is called whenever negotiation or
- * renegotiation has completed.
- *
- * @type{(this: Stream) => void}
- */
- this.onnegotiationcompleted = null;
- /**
- * ondowntrack is called whenever a new track is added to a stream.
- * If the stream parameter differs from its previous value, then it
- * indicates that the old stream has been discarded.
- *
- * @type{(this: Stream, track: MediaStreamTrack, transceiver: RTCRtpTransceiver, label: string, stream: MediaStream) => void}
- */
- this.ondowntrack = null;
- /**
- * onlabel is called whenever the server sets a new label for the stream.
- *
- * @type{(this: Stream, label: string) => void}
- */
- this.onlabel = null;
- /**
- * onstatus is called whenever the status of the stream changes.
- *
- * @type{(this: Stream, status: string) => void}
- */
- this.onstatus = null;
- /**
- * onabort is called when the server requested that an up stream be
- * closed. It is the resposibility of the client to close the stream.
- *
- * @type{(this: Stream) => void}
- */
- this.onabort = null;
- /**
- * onstats is called when we have new statistics about the connection
- *
- * @type{(this: Stream, stats: Object) => void}
- */
- this.onstats = null;
-}
-
-/**
- * close closes a stream.
- */
-Stream.prototype.close = function() {
- let c = this;
- if(c.statsHandler) {
- clearInterval(c.statsHandler);
- c.statsHandler = null;
- }
-
- if(c.stream) {
- c.stream.getTracks().forEach(t => {
- try {
- t.stop();
- } catch(e) {
- }
- });
- }
- c.pc.close();
-
- if(c.up && c.localDescriptionSent) {
- try {
- c.sc.send({
- type: 'close',
- id: c.id,
- });
- } catch(e) {
- }
- }
- c.sc = null;
-};
-
-/**
- * Called when we get a local ICE candidate. Don't call this.
- *
- * @param {RTCIceCandidate} candidate
- * @function
- */
-Stream.prototype.gotLocalIce = function(candidate) {
- let c = this;
- if(c.localDescriptionSent)
- c.sc.send({type: 'ice',
- id: c.id,
- candidate: candidate,
- });
- else
- c.localIceCandidates.push(candidate);
-}
-
-/**
- * flushLocalIceCandidates flushes any buffered local ICE candidates.
- * It is called when we send an offer.
- * @function
- */
-Stream.prototype.flushLocalIceCandidates = function () {
- let c = this;
- let candidates = c.localIceCandidates;
- c.localIceCandidates = [];
- candidates.forEach(candidate => {
- try {
- c.sc.send({type: 'ice',
- id: c.id,
- candidate: candidate,
- });
- } catch(e) {
- console.warn(e);
- }
- });
- c.localIceCandidates = [];
-}
-
-/**
- * flushRemoteIceCandidates flushes any buffered remote ICE candidates. It is
- * called automatically when we get a remote description.
- * @function
- */
-Stream.prototype.flushRemoteIceCandidates = async function () {
- let c = this;
- let candidates = c.remoteIceCandidates;
- c.remoteIceCandidates = [];
- /** @type {Array.>} */
- let promises = [];
- candidates.forEach(candidate => {
- promises.push(c.pc.addIceCandidate(candidate).catch(console.warn));
- });
- return await Promise.all(promises);
-};
-
-/**
- * negotiate negotiates or renegotiates an up stream. It is called
- * automatically when required. If the client requires renegotiation, it
- * is probably better to call restartIce which will cause negotiate to be
- * called asynchronously.
- *
- * @function
- * @param {boolean} [restartIce] - Whether to restart ICE.
- */
-Stream.prototype.negotiate = async function (restartIce) {
- let c = this;
- if(!c.up)
- throw new Error('not an up stream');
-
- let options = {};
- if(restartIce)
- options = {iceRestart: true};
- let offer = await c.pc.createOffer(options);
- if(!offer)
- throw(new Error("Didn't create offer"));
- await c.pc.setLocalDescription(offer);
-
- // mids are not known until this point
- c.pc.getTransceivers().forEach(t => {
- if(t.sender && t.sender.track) {
- let label = c.labels[t.sender.track.id];
- if(label)
- c.labelsByMid[t.mid] = label;
- else
- console.warn("Couldn't find label for track");
- }
- });
-
- c.sc.send({
- type: 'offer',
- kind: this.localDescriptionSent ? 'renegotiate' : '',
- id: c.id,
- labels: c.labelsByMid,
- offer: offer,
- });
- this.localDescriptionSent = true;
- c.flushLocalIceCandidates();
-};
-
-/**
- * restartIce causes an ICE restart on a stream. For up streams, it is
- * called automatically when ICE signals that the connection has failed,
- * but may also be called by the application. For down streams, it
- * requests that the server perform an ICE restart. In either case,
- * it returns immediately, negotiation will happen asynchronously.
- */
-
-Stream.prototype.restartIce = function () {
- let c = this;
- if(!c.up) {
- c.sc.send({
- type: 'renegotiate',
- id: c.id,
- });
- return;
- }
-
- if('restartIce' in c.pc) {
- try {
- /** @ts-ignore */
- c.pc.restartIce();
- return;
- } catch(e) {
- console.warn(e);
- }
- }
-
- // negotiate is async, but this returns immediately.
- c.negotiate(true);
-};
-
-/**
- * updateStats is called periodically, if requested by setStatsInterval,
- * in order to recompute stream statistics and invoke the onstats handler.
- *
- * @function
- */
-Stream.prototype.updateStats = async function() {
- let c = this;
- let old = c.stats;
- /** @type{Object} */
- let stats = {};
-
- let transceivers = c.pc.getTransceivers();
- for(let i = 0; i < transceivers.length; i++) {
- let t = transceivers[i];
- let stid = t.sender.track && t.sender.track.id;
- let rtid = t.receiver.track && t.receiver.track.id;
-
- let report = null;
- if(stid) {
- try {
- report = await t.sender.getStats();
- } catch(e) {
- }
- }
-
- if(report) {
- for(let r of report.values()) {
- if(stid && r.type === 'outbound-rtp') {
- if(!('bytesSent' in r))
- continue;
- if(!stats[stid])
- stats[stid] = {};
- stats[stid][r.type] = {};
- stats[stid][r.type].timestamp = r.timestamp;
- stats[stid][r.type].bytesSent = r.bytesSent;
- if(old[stid] && old[stid][r.type])
- stats[stid][r.type].rate =
- ((r.bytesSent - old[stid][r.type].bytesSent) * 1000 /
- (r.timestamp - old[stid][r.type].timestamp)) * 8;
- }
- }
- }
-
- report = null;
- if(rtid) {
- try {
- report = await t.receiver.getStats();
- } catch(e) {
- console.error(e);
- }
- }
-
- if(report) {
- for(let r of report.values()) {
- if(rtid && r.type === 'track') {
- if(!('totalAudioEnergy' in r))
- continue;
- if(!stats[rtid])
- stats[rtid] = {};
- stats[rtid][r.type] = {};
- stats[rtid][r.type].timestamp = r.timestamp;
- stats[rtid][r.type].totalAudioEnergy = r.totalAudioEnergy;
- if(old[rtid] && old[rtid][r.type])
- stats[rtid][r.type].audioEnergy =
- (r.totalAudioEnergy - old[rtid][r.type].totalAudioEnergy) * 1000 /
- (r.timestamp - old[rtid][r.type].timestamp);
- }
- }
- }
- }
-
- c.stats = stats;
-
- if(c.onstats)
- c.onstats.call(c, c.stats);
-};
-
-/**
- * setStatsInterval sets the interval in milliseconds at which the onstats
- * handler will be called. This is only useful for up streams.
- *
- * @param {number} ms - The interval in milliseconds.
- */
-Stream.prototype.setStatsInterval = function(ms) {
- let c = this;
- if(c.statsHandler) {
- clearInterval(c.statsHandler);
- c.statsHandler = null;
- }
-
- if(ms <= 0)
- return;
-
- c.statsHandler = setInterval(() => {
- c.updateStats();
- }, ms);
-};
diff --git a/sources/static/scripts/toastify.js b/sources/static/scripts/toastify.js
deleted file mode 100755
index 2e3bdb5..0000000
--- a/sources/static/scripts/toastify.js
+++ /dev/null
@@ -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;a0?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
\ No newline at end of file
diff --git a/sources/static/tsconfig.json b/sources/static/tsconfig.json
deleted file mode 100755
index 6081949..0000000
--- a/sources/static/tsconfig.json
+++ /dev/null
@@ -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"
- ]
-}
diff --git a/sources/static/webfonts/fa-regular-400.eot b/sources/static/webfonts/fa-regular-400.eot
deleted file mode 100755
index bef9f72..0000000
Binary files a/sources/static/webfonts/fa-regular-400.eot and /dev/null differ
diff --git a/sources/static/webfonts/fa-regular-400.ttf b/sources/static/webfonts/fa-regular-400.ttf
deleted file mode 100755
index 659527a..0000000
Binary files a/sources/static/webfonts/fa-regular-400.ttf and /dev/null differ
diff --git a/sources/static/webfonts/fa-regular-400.woff b/sources/static/webfonts/fa-regular-400.woff
deleted file mode 100755
index 31f44b2..0000000
Binary files a/sources/static/webfonts/fa-regular-400.woff and /dev/null differ
diff --git a/sources/static/webfonts/fa-regular-400.woff2 b/sources/static/webfonts/fa-regular-400.woff2
deleted file mode 100755
index 0332a9b..0000000
Binary files a/sources/static/webfonts/fa-regular-400.woff2 and /dev/null differ
diff --git a/sources/static/webfonts/fa-solid-900.eot b/sources/static/webfonts/fa-solid-900.eot
deleted file mode 100755
index 5da4fa0..0000000
Binary files a/sources/static/webfonts/fa-solid-900.eot and /dev/null differ
diff --git a/sources/static/webfonts/fa-solid-900.ttf b/sources/static/webfonts/fa-solid-900.ttf
deleted file mode 100755
index e074608..0000000
Binary files a/sources/static/webfonts/fa-solid-900.ttf and /dev/null differ
diff --git a/sources/static/webfonts/fa-solid-900.woff b/sources/static/webfonts/fa-solid-900.woff
deleted file mode 100755
index ef6b447..0000000
Binary files a/sources/static/webfonts/fa-solid-900.woff and /dev/null differ
diff --git a/sources/static/webfonts/fa-solid-900.woff2 b/sources/static/webfonts/fa-solid-900.woff2
deleted file mode 100755
index 120b300..0000000
Binary files a/sources/static/webfonts/fa-solid-900.woff2 and /dev/null differ