diff --git a/check_process b/check_process index 86287d5..f2ce840 100644 --- a/check_process +++ b/check_process @@ -21,7 +21,7 @@ upgrade=1 backup_restore=1 multi_instance=0 - port_already_use=0 + port_already_use=1 change_url=1 ;;; Options Email= diff --git a/sources_2/data/ice-servers.json b/sources_2/data/ice-servers.json deleted file mode 100755 index fe51488..0000000 --- a/sources_2/data/ice-servers.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/sources_2/galene b/sources_2/galene deleted file mode 100755 index a727ebb..0000000 Binary files a/sources_2/galene and /dev/null differ diff --git a/sources_2/group/client.go b/sources_2/group/client.go deleted file mode 100755 index 67212a3..0000000 --- a/sources_2/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) error - PushClient(id, username string, add bool) error -} - -type Kickable interface { - Kick(id, user, message string) error -} diff --git a/sources_2/group/client_test.go b/sources_2/group/client_test.go deleted file mode 100755 index 9352fc6..0000000 --- a/sources_2/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_2/group/group.go b/sources_2/group/group.go deleted file mode 100755 index 60f6026..0000000 --- a/sources_2/group/group.go +++ /dev/null @@ -1,852 +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) -} - -type ChatHistoryEntry struct { - Id string - User string - Time int64 - Kind string - Value interface{} -} - -const ( - MinBitrate = 200000 -) - -type Group struct { - name string - api *webrtc.API - - mu sync.Mutex - description *description - locked *string - clients map[string]Client - history []ChatHistoryEntry - timestamp time.Time -} - -func (g *Group) Name() string { - return g.name -} - -func (g *Group) Locked() (bool, string) { - g.mu.Lock() - defer g.mu.Unlock() - if g.locked != nil { - return true, *g.locked - } else { - return false, "" - } -} - -func (g *Group) SetLocked(locked bool, message string) { - g.mu.Lock() - defer g.mu.Unlock() - if locked { - g.locked = &message - } else { - g.locked = nil - } -} - -func (g *Group) Public() bool { - g.mu.Lock() - defer g.mu.Unlock() - return g.description.Public -} - -func (g *Group) Redirect() string { - g.mu.Lock() - defer g.mu.Unlock() - return g.description.Redirect -} - -func (g *Group) AllowRecording() bool { - g.mu.Lock() - defer g.mu.Unlock() - return g.description.AllowRecording -} - -var groups struct { - mu sync.Mutex - groups map[string]*Group -} - -func (g *Group) API() *webrtc.API { - return g.api -} - -func codecFromName(name string) (webrtc.RTPCodecCapability, error) { - switch name { - case "vp8": - return webrtc.RTPCodecCapability{ - "video/VP8", 90000, 0, - "", - nil, - }, nil - case "vp9": - return webrtc.RTPCodecCapability{ - "video/VP9", 90000, 0, - "profile-id=2", - nil, - }, nil - case "h264": - return webrtc.RTPCodecCapability{ - "video/H264", 90000, 0, - "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", - nil, - }, nil - case "opus": - return webrtc.RTPCodecCapability{ - "audio/opus", 48000, 2, - "minptime=10;useinbandfec=1", - nil, - }, nil - case "g722": - return webrtc.RTPCodecCapability{ - "audio/G722", 8000, 1, - "", - nil, - }, nil - case "pcmu": - return webrtc.RTPCodecCapability{ - "audio/PCMU", 8000, 1, - "", - nil, - }, nil - case "pcma": - return webrtc.RTPCodecCapability{ - "audio/PCMA", 8000, 1, - "", - nil, - }, nil - default: - return webrtc.RTPCodecCapability{}, errors.New("unknown codec") - } -} - -func payloadType(codec webrtc.RTPCodecCapability) (webrtc.PayloadType, error) { - switch strings.ToLower(codec.MimeType) { - case "video/vp8": - return 96, nil - case "video/vp9": - return 98, nil - case "video/h264": - return 102, nil - case "audio/opus": - return 111, nil - case "audio/g722": - return 9, nil - case "audio/pcmu": - return 0, nil - case "audio/pcma": - return 8, nil - default: - return 0, errors.New("unknown codec") - } -} - -func APIFromCodecs(codecs []webrtc.RTPCodecCapability) *webrtc.API { - s := webrtc.SettingEngine{} - s.SetSRTPReplayProtectionWindow(512) - if !UseMDNS { - s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled) - } - m := webrtc.MediaEngine{} - - for _, codec := range codecs { - var tpe webrtc.RTPCodecType - var fb []webrtc.RTCPFeedback - if strings.HasPrefix(strings.ToLower(codec.MimeType), "video/") { - tpe = webrtc.RTPCodecTypeVideo - fb = []webrtc.RTCPFeedback{ - {"goog-remb", ""}, - {"nack", ""}, - {"nack", "pli"}, - {"ccm", "fir"}, - } - } else if strings.HasPrefix(strings.ToLower(codec.MimeType), "audio/") { - tpe = webrtc.RTPCodecTypeAudio - fb = []webrtc.RTCPFeedback{} - } else { - continue - } - - ptpe, err := payloadType(codec) - if err != nil { - log.Printf("%v", err) - continue - } - m.RegisterCodec( - webrtc.RTPCodecParameters{ - RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: codec.MimeType, - ClockRate: codec.ClockRate, - Channels: codec.Channels, - SDPFmtpLine: codec.SDPFmtpLine, - RTCPFeedback: fb, - }, - PayloadType: ptpe, - }, - tpe, - ) - } - return webrtc.NewAPI( - webrtc.WithSettingEngine(s), - webrtc.WithMediaEngine(&m), - ) -} - -func APIFromNames(names []string) *webrtc.API { - if len(names) == 0 { - names = []string{"vp8", "opus"} - } - codecs := make([]webrtc.RTPCodecCapability, 0, len(names)) - for _, n := range names { - codec, err := codecFromName(n) - if err != nil { - log.Printf("Codec %v: %v", n, err) - continue - } - codecs = append(codecs, codec) - } - - return APIFromCodecs(codecs) -} - -func Add(name string, desc *description) (*Group, error) { - if name == "" || strings.HasSuffix(name, "/") { - return nil, UserError("illegal group name") - } - - groups.mu.Lock() - defer groups.mu.Unlock() - - if groups.groups == nil { - groups.groups = make(map[string]*Group) - } - - var err error - - g := groups.groups[name] - if g == nil { - if desc == nil { - desc, err = GetDescription(name) - if err != nil { - return nil, err - } - } - - g = &Group{ - name: name, - description: desc, - clients: make(map[string]Client), - timestamp: time.Now(), - api: APIFromNames(desc.Codecs), - } - groups.groups[name] = g - return g, nil - } - - g.mu.Lock() - defer g.mu.Unlock() - - if desc != nil { - g.description = desc - g.api = APIFromNames(desc.Codecs) - return g, nil - } - - if time.Since(g.description.loadTime) > 5*time.Second { - if descriptionChanged(name, g.description) { - desc, err := GetDescription(name) - if err != nil { - if !os.IsNotExist(err) { - log.Printf("Reading group %v: %v", - name, err) - } - deleteUnlocked(g) - return nil, err - } - g.description = desc - g.api = APIFromNames(desc.Codecs) - } else { - g.description.loadTime = time.Now() - } - } - - return g, nil -} - -func Range(f func(g *Group) bool) { - groups.mu.Lock() - defer groups.mu.Unlock() - - for _, g := range groups.groups { - ok := f(g) - if !ok { - break - } - } -} - -func GetNames() []string { - names := make([]string, 0) - - Range(func(g *Group) bool { - names = append(names, g.name) - return true - }) - return names -} - -type SubGroup struct { - Name string - Clients int -} - -func GetSubGroups(parent string) []SubGroup { - prefix := parent + "/" - subgroups := make([]SubGroup, 0) - - Range(func(g *Group) bool { - if strings.HasPrefix(g.name, prefix) { - g.mu.Lock() - count := len(g.clients) - g.mu.Unlock() - if count > 0 { - subgroups = append(subgroups, - SubGroup{g.name, count}) - } - } - return true - }) - return subgroups -} - -func Get(name string) *Group { - groups.mu.Lock() - defer groups.mu.Unlock() - - return groups.groups[name] -} - -func Delete(name string) bool { - groups.mu.Lock() - defer groups.mu.Unlock() - g := groups.groups[name] - if g == nil { - return false - } - - g.mu.Lock() - defer g.mu.Unlock() - return deleteUnlocked(g) -} - -// Called with both groups.mu and g.mu taken. -func deleteUnlocked(g *Group) bool { - if len(g.clients) != 0 { - return false - } - - delete(groups.groups, g.name) - return true -} - -func Expire() { - names := GetNames() - now := time.Now() - - for _, name := range names { - g := Get(name) - if g == nil { - continue - } - - old := false - - g.mu.Lock() - empty := len(g.clients) == 0 - if empty && !g.description.Public { - age := now.Sub(g.timestamp) - old = age > maxHistoryAge(g.description) - } - // We cannot take groups.mu at this point without a deadlock. - g.mu.Unlock() - - if empty && old { - // Delete will check if the group is still empty - Delete(name) - } - } -} - -func AddClient(group string, c Client) (*Group, error) { - g, err := Add(group, nil) - if err != nil { - return nil, err - } - - g.mu.Lock() - defer g.mu.Unlock() - - if !c.OverridePermissions(g) { - perms, err := g.description.GetPermission(group, c) - if err != nil { - return nil, err - } - - c.SetPermissions(perms) - - if !perms.Op && g.locked != nil { - m := *g.locked - if m == "" { - m = "group is locked" - } - return nil, UserError(m) - } - - if !perms.Op && g.description.MaxClients > 0 { - if len(g.clients) >= g.description.MaxClients { - return nil, UserError("too many users") - } - } - } - - if g.clients[c.Id()] != nil { - return nil, ProtocolError("duplicate client id") - } - - g.clients[c.Id()] = c - g.timestamp = time.Now() - - go func(clients []Client) { - u := c.Username() - c.PushClient(c.Id(), u, true) - for _, cc := range clients { - uu := cc.Username() - c.PushClient(cc.Id(), uu, true) - cc.PushClient(c.Id(), u, true) - } - }(g.getClientsUnlocked(c)) - - return g, nil -} - -func DelClient(c Client) { - g := c.Group() - if g == nil { - return - } - g.mu.Lock() - defer g.mu.Unlock() - - if g.clients[c.Id()] != c { - log.Printf("Deleting unknown client") - return - } - delete(g.clients, c.Id()) - g.timestamp = time.Now() - - go func(clients []Client) { - for _, cc := range clients { - cc.PushClient(c.Id(), c.Username(), false) - } - }(g.getClientsUnlocked(nil)) -} - -func (g *Group) GetClients(except Client) []Client { - g.mu.Lock() - defer g.mu.Unlock() - return g.getClientsUnlocked(except) -} - -func (g *Group) getClientsUnlocked(except Client) []Client { - clients := make([]Client, 0, len(g.clients)) - for _, c := range g.clients { - if c != except { - clients = append(clients, c) - } - } - return clients -} - -func (g *Group) GetClient(id string) Client { - g.mu.Lock() - defer g.mu.Unlock() - return g.getClientUnlocked(id) -} - -func (g *Group) getClientUnlocked(id string) Client { - for idd, c := range g.clients { - if idd == id { - return c - } - } - return nil -} - -func (g *Group) Range(f func(c Client) bool) { - g.mu.Lock() - defer g.mu.Unlock() - for _, c := range g.clients { - ok := f(c) - if !ok { - break - } - } -} - -func (g *Group) Shutdown(message string) { - g.Range(func(c Client) bool { - cc, ok := c.(Kickable) - if ok { - cc.Kick("", "", message) - } - return true - }) -} - -type warner interface { - Warn(oponly bool, message string) error -} - -func (g *Group) WallOps(message string) { - clients := g.GetClients(nil) - for _, c := range clients { - w, ok := c.(warner) - if !ok { - continue - } - err := w.Warn(true, message) - if err != nil { - log.Printf("WallOps: %v", err) - } - } -} - -func FromJSTime(tm int64) time.Time { - if tm == 0 { - return time.Time{} - } - return time.Unix(int64(tm)/1000, (int64(tm)%1000)*1000000) -} - -func ToJSTime(tm time.Time) int64 { - return int64((tm.Sub(time.Unix(0, 0)) + time.Millisecond/2) / - time.Millisecond) -} - -const maxChatHistory = 50 - -func (g *Group) ClearChatHistory() { - g.mu.Lock() - defer g.mu.Unlock() - g.history = nil -} - -func (g *Group) AddToChatHistory(id, user string, time int64, kind string, value interface{}) { - g.mu.Lock() - defer g.mu.Unlock() - - if len(g.history) >= maxChatHistory { - copy(g.history, g.history[1:]) - g.history = g.history[:len(g.history)-1] - } - g.history = append(g.history, - ChatHistoryEntry{Id: id, User: user, Time: time, Kind: kind, Value: value}, - ) -} - -func discardObsoleteHistory(h []ChatHistoryEntry, duration time.Duration) []ChatHistoryEntry { - i := 0 - for i < len(h) { - if time.Since(FromJSTime(h[i].Time)) <= duration { - break - } - i++ - } - if i > 0 { - copy(h, h[i:]) - h = h[:len(h)-i] - } - return h -} - -func (g *Group) GetChatHistory() []ChatHistoryEntry { - g.mu.Lock() - defer g.mu.Unlock() - - g.history = discardObsoleteHistory( - g.history, maxHistoryAge(g.description), - ) - - h := make([]ChatHistoryEntry, len(g.history)) - copy(h, g.history) - return h -} - -func matchClient(group string, c Challengeable, users []ClientCredentials) (bool, bool) { - for _, u := range users { - if u.Username == "" { - if c.Challenge(group, u) { - return true, true - } - } else if u.Username == c.Username() { - if c.Challenge(group, u) { - return true, true - } else { - return true, false - } - } - } - return false, false -} - -type description struct { - fileName string `json:"-"` - loadTime time.Time `json:"-"` - modTime time.Time `json:"-"` - fileSize int64 `json:"-"` - Description string `json:"description,omitempty"` - Redirect string `json:"redirect,omitempty"` - Public bool `json:"public,omitempty"` - MaxClients int `json:"max-clients,omitempty"` - MaxHistoryAge int `json:"max-history-age,omitempty"` - AllowAnonymous bool `json:"allow-anonymous,omitempty"` - AllowRecording bool `json:"allow-recording,omitempty"` - AllowSubgroups bool `json:"allow-subgroups,omitempty"` - Op []ClientCredentials `json:"op,omitempty"` - Presenter []ClientCredentials `json:"presenter,omitempty"` - Other []ClientCredentials `json:"other,omitempty"` - Codecs []string `json:"codecs,omitempty"` -} - -const DefaultMaxHistoryAge = 4 * time.Hour - -func maxHistoryAge(desc *description) time.Duration { - if desc.MaxHistoryAge != 0 { - return time.Duration(desc.MaxHistoryAge) * time.Second - } - return DefaultMaxHistoryAge -} - -func openDescriptionFile(name string) (*os.File, string, bool, error) { - isParent := false - for name != "" { - fileName := filepath.Join( - Directory, path.Clean("/"+name)+".json", - ) - r, err := os.Open(fileName) - if !os.IsNotExist(err) { - return r, fileName, isParent, err - } - isParent = true - name, _ = path.Split(name) - name = strings.TrimRight(name, "/") - } - return nil, "", false, os.ErrNotExist -} - -func statDescriptionFile(name string) (os.FileInfo, string, bool, error) { - isParent := false - for name != "" { - fileName := filepath.Join( - Directory, path.Clean("/"+name)+".json", - ) - fi, err := os.Stat(fileName) - if !os.IsNotExist(err) { - return fi, fileName, isParent, err - } - isParent = true - name, _ = path.Split(name) - name = strings.TrimRight(name, "/") - } - return nil, "", false, os.ErrNotExist -} - -// descriptionChanged returns true if a group's description may have -// changed since it was last read. -func descriptionChanged(name string, desc *description) bool { - fi, fileName, _, err := statDescriptionFile(name) - if err != nil || fileName != desc.fileName { - return true - } - - if fi.Size() != desc.fileSize || fi.ModTime() != desc.modTime { - return true - } - return false -} - -func GetDescription(name string) (*description, error) { - r, fileName, isParent, err := openDescriptionFile(name) - if err != nil { - return nil, err - } - defer r.Close() - - var desc description - - fi, err := r.Stat() - if err != nil { - return nil, err - } - - d := json.NewDecoder(r) - err = d.Decode(&desc) - if err != nil { - return nil, err - } - if isParent { - if !desc.AllowSubgroups { - return nil, os.ErrNotExist - } - desc.Public = false - desc.Description = "" - } - - desc.fileName = fileName - desc.fileSize = fi.Size() - desc.modTime = fi.ModTime() - desc.loadTime = time.Now() - - return &desc, nil -} - -func (desc *description) GetPermission(group string, c Challengeable) (ClientPermissions, error) { - var p ClientPermissions - if !desc.AllowAnonymous && c.Username() == "" { - return p, UserError("anonymous users not allowed in this group, please choose a username") - } - if found, good := matchClient(group, c, desc.Op); found { - if good { - p.Op = true - p.Present = true - if desc.AllowRecording { - p.Record = true - } - return p, nil - } - return p, ErrNotAuthorised - } - if found, good := matchClient(group, c, desc.Presenter); found { - if good { - p.Present = true - return p, nil - } - return p, ErrNotAuthorised - } - if found, good := matchClient(group, c, desc.Other); found { - if good { - return p, nil - } - return p, ErrNotAuthorised - } - return p, ErrNotAuthorised -} - -type Public struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - ClientCount int `json:"clientCount"` -} - -func GetPublic() []Public { - gs := make([]Public, 0) - Range(func(g *Group) bool { - if g.Public() { - gs = append(gs, Public{ - Name: g.name, - Description: g.description.Description, - ClientCount: len(g.clients), - }) - } - return true - }) - sort.Slice(gs, func(i, j int) bool { - return gs[i].Name < gs[j].Name - }) - return gs -} - -func ReadPublicGroups() { - dir, err := os.Open(Directory) - if err != nil { - return - } - defer dir.Close() - - fis, err := dir.Readdir(-1) - if err != nil { - log.Printf("readPublicGroups: %v", err) - return - } - - for _, fi := range fis { - if !strings.HasSuffix(fi.Name(), ".json") { - continue - } - name := fi.Name()[:len(fi.Name())-5] - desc, err := GetDescription(name) - if err != nil { - if !os.IsNotExist(err) { - log.Printf("Reading group %v: %v", name, err) - } - continue - } - if desc.Public { - Add(name, desc) - } - } -} diff --git a/sources_2/group/group_test.go b/sources_2/group/group_test.go deleted file mode 100755 index fddd982..0000000 --- a/sources_2/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_2/groups/groupname.json b/sources_2/groups/groupname.json deleted file mode 100644 index 9751310..0000000 --- a/sources_2/groups/groupname.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "op": [{"username": "__ADMIN__", "password": "__PASSWORD__"}], - "presenter": [{}] -} diff --git a/sources_2/static/404.css b/sources_2/static/404.css deleted file mode 100755 index 0dcc899..0000000 --- a/sources_2/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_2/static/404.html b/sources_2/static/404.html deleted file mode 100755 index fbf187a..0000000 --- a/sources_2/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_2/static/common.css b/sources_2/static/common.css deleted file mode 100755 index 9553cbf..0000000 --- a/sources_2/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_2/static/css/fontawesome.min.css b/sources_2/static/css/fontawesome.min.css deleted file mode 100755 index 8e36e25..0000000 --- a/sources_2/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_2/static/css/regular.css b/sources_2/static/css/regular.css deleted file mode 100755 index 8db06c6..0000000 --- a/sources_2/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_2/static/css/solid.css b/sources_2/static/css/solid.css deleted file mode 100755 index 62922cb..0000000 --- a/sources_2/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_2/static/css/toastify.min.css b/sources_2/static/css/toastify.min.css deleted file mode 100755 index 8041580..0000000 --- a/sources_2/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_2/static/galene.css b/sources_2/static/galene.css deleted file mode 100755 index 814b186..0000000 --- a/sources_2/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_2/static/galene.html b/sources_2/static/galene.html deleted file mode 100755 index 73f2d0b..0000000 --- a/sources_2/static/galene.html +++ /dev/null @@ -1,255 +0,0 @@ - - - - Galène - - - - - - - - - - - - - -
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
- - -
-
-
-
-
-
- -
-
-
-
- -
-
-

Settings

- -
-
- - - -
- Other Settings - -
- - -
- -
- - -
- -
- - -
- -
- -
- - -
-
-
- - - - - - - - diff --git a/sources_2/static/galene.js b/sources_2/static/galene.js deleted file mode 100755 index 4ca1b3a..0000000 --- a/sources_2/static/galene.js +++ /dev/null @@ -1,2353 +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; -} - -/** - * @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.onstatus = function(status) { - setMediaStatus(c); - }; - c.onstats = gotDownStats; - if(getSettings().activityDetection) - c.setStatsInterval(activityDetectionInterval); - - setMedia(c, false); -} - -// 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'); - /** @ts-ignore */ - let canFile = !!HTMLVideoElement.prototype.captureStream; - - // 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', canFile && 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]).catch(e => { - console.error(e); - displayError(e); - }); - } - 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) { - /** @ts-ignore */ - if(!HTMLVideoElement.prototype.captureStream) { - displayError("This browser doesn't support file playback"); - return; - } - - 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.username; - 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" - ); - } - } -} - -/** - * @param {string} id - * @param {string} dest - * @param {string} username - * @param {number} time - * @param {boolean} privileged - * @param {string} kind - * @param {unknown} message - */ -function gotUserMessage(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': - 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; - case 'clearchat': - if(privileged) { - clearChat(); - } else { - console.error(`Got unprivileged message of kind ${kind}`); - } - break; - default: - console.warn(`Got unknown user message ${kind}`); - break; - } -}; - - -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 {unknown} 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.toString().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); - } 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.toString()).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 = {}; - } - container.appendChild(footer); - - 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('clearchat'); - } -}; - -commands.lock = { - predicate: operatorPredicate, - description: 'lock this group', - parameters: '[message]', - f: (c, r) => { - serverConnection.groupAction('lock', r); - } -}; - -commands.unlock = { - predicate: operatorPredicate, - description: 'unlock this group, revert the effect of /lock', - f: (c, r) => { - serverConnection.groupAction('unlock'); - } -}; - -commands.record = { - predicate: recordingPredicate, - description: 'start recording', - f: (c, r) => { - serverConnection.groupAction('record'); - } -}; - -commands.unrecord = { - predicate: recordingPredicate, - description: 'stop recording', - f: (c, r) => { - serverConnection.groupAction('unrecord'); - } -}; - -commands.subgroups = { - predicate: operatorPredicate, - description: 'list subgroups', - f: (c, r) => { - serverConnection.groupAction('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]}`); - serverConnection.chat('', id, p[1]); - addToChatbox(serverConnection.id, id, serverConnection.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(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(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('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; - } - - try { - serverConnection.chat(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.onusermessage = gotUserMessage; - - 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_2/static/index.html b/sources_2/static/index.html deleted file mode 100755 index 83e32ee..0000000 --- a/sources_2/static/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Galène - - - - - - - - - - - - - -
-

Galène

- -
- - -
-
- -
-

Public groups

- -
-
-
- - - - - - - - diff --git a/sources_2/static/mainpage.css b/sources_2/static/mainpage.css deleted file mode 100755 index 14b891c..0000000 --- a/sources_2/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_2/static/mainpage.js b/sources_2/static/mainpage.js deleted file mode 100755 index 1f80c40..0000000 --- a/sources_2/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_2/static/protocol.js b/sources_2/static/protocol.js deleted file mode 100755 index 611d0da..0000000 --- a/sources_2/static/protocol.js +++ /dev/null @@ -1,1156 +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 null if we haven't joined yet. - * - * @type {string} - */ - this.group = null; - /** - * The username we joined as. - */ - this.username = 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 {RTCConfiguration} - */ - this.rtcConfiguration = 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: unknown) => 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: unknown) => void} - */ - this.onusermessage = null; -} - -/** - * @typedef {Object} message - * @property {string} type - * @property {string} [kind] - * @property {string} [id] - * @property {string} [source] - * @property {string} [dest] - * @property {string} [username] - * @property {string} [password] - * @property {boolean} [privileged] - * @property {Object} [permissions] - * @property {string} [group] - * @property {unknown} [value] - * @property {string} [sdp] - * @property {RTCIceCandidate} [candidate] - * @property {Object} [labels] - * @property {Object} [request] - * @property {Object} [rtcConfiguration] - */ - -/** - * 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)); -}; - -/** - * 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; - } - - 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; - sc.username = 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 'handshake': - break; - case 'offer': - sc.gotOffer(m.id, m.labels, m.source, m.username, - m.sdp, m.kind === 'renegotiate'); - break; - case 'answer': - sc.gotAnswer(m.id, m.sdp); - 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 'joined': - if(sc.group) { - if(m.group !== sc.group) { - throw new Error('Joined multiple groups'); - } - } else { - sc.group = m.group; - } - sc.username = m.username; - sc.permissions = m.permissions || []; - sc.rtcConfiguration = m.rtcConfiguration || null; - 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.source, m.dest, m.username, m.time, - m.privileged, m.kind, m.value, - ); - break; - case 'usermessage': - if(sc.onusermessage) - sc.onusermessage.call( - sc, m.source, m.dest, m.username, m.time, - m.privileged, m.kind, m.value, - ); - 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(sc.rtcConfiguration); - 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} 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(kind, dest, value) { - this.send({ - type: 'chat', - source: this.id, - dest: dest, - username: this.username, - kind: kind, - value: value, - }); -}; - -/** - * userAction sends a request to act on a user. - * - * @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(kind, dest, value) { - this.send({ - type: 'useraction', - source: this.id, - dest: dest, - username: this.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} 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(kind, dest, value) { - this.send({ - type: 'usermessage', - source: this.id, - dest: dest, - username: this.username, - kind: kind, - value: value, - }); -}; - -/** - * groupAction sends a request to act on the current group. - * - * @param {string} kind - * - One of 'clearchat', 'lock', 'unlock', 'record' or 'unrecord'. - * @param {string} [message] - An optional user-readable message. - */ -ServerConnection.prototype.groupAction = function(kind, message) { - this.send({ - type: 'groupaction', - source: this.id, - kind: kind, - username: this.username, - value: message, - }); -}; - -/** - * Called when we receive an offer from the server. Don't call this. - * - * @param {string} id - * @param {Object} labels - * @param {string} source - * @param {string} username - * @param {string} sdp - * @param {boolean} renegotiate - * @function - */ -ServerConnection.prototype.gotOffer = async function(id, labels, source, username, sdp, 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(sc.rtcConfiguration); - 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"); - } - c.stream = e.streams[0]; - if(c.ondowntrack) { - c.ondowntrack.call( - c, e.track, e.transceiver, label, e.streams[0], - ); - } - }; - } - - c.labelsByMid = labels; - c.source = source; - c.username = username; - - if(sc.ondownstream) - sc.ondownstream.call(sc, c); - - try { - await c.pc.setRemoteDescription({ - type: 'offer', - sdp: sdp, - }); - - 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, - sdp: answer.sdp, - }); - } catch(e) { - try { - if(c.onerror) - c.onerror.call(c, e); - } finally { - c.abort(); - } - return; - } - - c.localDescriptionSent = true; - c.flushLocalIceCandidates(); - if(c.onnegotiationcompleted) - c.onnegotiationcompleted.call(c); -}; - -/** - * Called when we receive an answer from the server. Don't call this. - * - * @param {string} id - * @param {string} sdp - * @function - */ -ServerConnection.prototype.gotAnswer = async function(id, sdp) { - let c = this.up[id]; - if(!c) - throw new Error('unknown up stream'); - try { - await c.pc.setRemoteDescription({ - type: 'answer', - sdp: sdp, - }); - } catch(e) { - try { - if(c.onerror) - c.onerror.call(c, e); - } finally { - c.close(); - } - 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, the id of the client that created the stream. - * - * @type {string} - */ - this.source = null; - /** - * For down streams, the username of the client who created the stream. - * - * @type {string} - */ - this.username = null; - /** - * The associated RTCPeerConnection. 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; - /** - * 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. - * - * For streams in the up direction, this may be called at any time. For - * streams in the down direction, this will be called automatically when - * the server signals that it is closing 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; -}; - -/** - * abort requests that the server close a down stream. - */ -Stream.prototype.abort = function() { - let c = this; - if(c.up) - throw new Error("Abort called on an up stream"); - c.sc.send({ - type: 'abort', - id: c.id, - }); -}; - -/** - * 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', - source: c.sc.id, - username: c.sc.username, - kind: this.localDescriptionSent ? 'renegotiate' : '', - id: c.id, - labels: c.labelsByMid, - sdp: offer.sdp, - }); - 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_2/static/scripts/toastify.js b/sources_2/static/scripts/toastify.js deleted file mode 100755 index 2e3bdb5..0000000 --- a/sources_2/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_2/static/tsconfig.json b/sources_2/static/tsconfig.json deleted file mode 100755 index 6081949..0000000 --- a/sources_2/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_2/static/webfonts/fa-regular-400.eot b/sources_2/static/webfonts/fa-regular-400.eot deleted file mode 100755 index bef9f72..0000000 Binary files a/sources_2/static/webfonts/fa-regular-400.eot and /dev/null differ diff --git a/sources_2/static/webfonts/fa-regular-400.ttf b/sources_2/static/webfonts/fa-regular-400.ttf deleted file mode 100755 index 659527a..0000000 Binary files a/sources_2/static/webfonts/fa-regular-400.ttf and /dev/null differ diff --git a/sources_2/static/webfonts/fa-regular-400.woff b/sources_2/static/webfonts/fa-regular-400.woff deleted file mode 100755 index 31f44b2..0000000 Binary files a/sources_2/static/webfonts/fa-regular-400.woff and /dev/null differ diff --git a/sources_2/static/webfonts/fa-regular-400.woff2 b/sources_2/static/webfonts/fa-regular-400.woff2 deleted file mode 100755 index 0332a9b..0000000 Binary files a/sources_2/static/webfonts/fa-regular-400.woff2 and /dev/null differ diff --git a/sources_2/static/webfonts/fa-solid-900.eot b/sources_2/static/webfonts/fa-solid-900.eot deleted file mode 100755 index 5da4fa0..0000000 Binary files a/sources_2/static/webfonts/fa-solid-900.eot and /dev/null differ diff --git a/sources_2/static/webfonts/fa-solid-900.ttf b/sources_2/static/webfonts/fa-solid-900.ttf deleted file mode 100755 index e074608..0000000 Binary files a/sources_2/static/webfonts/fa-solid-900.ttf and /dev/null differ diff --git a/sources_2/static/webfonts/fa-solid-900.woff b/sources_2/static/webfonts/fa-solid-900.woff deleted file mode 100755 index ef6b447..0000000 Binary files a/sources_2/static/webfonts/fa-solid-900.woff and /dev/null differ diff --git a/sources_2/static/webfonts/fa-solid-900.woff2 b/sources_2/static/webfonts/fa-solid-900.woff2 deleted file mode 100755 index 120b300..0000000 Binary files a/sources_2/static/webfonts/fa-solid-900.woff2 and /dev/null differ