1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/galene_ynh.git synced 2024-09-03 18:36:31 +02:00

Upgrade to version 0.2

This commit is contained in:
ericgaspar 2021-01-10 09:39:29 +01:00
parent f3c8e800c4
commit 67965ba464
No known key found for this signature in database
GPG key ID: 574F281483054D44
31 changed files with 6557 additions and 0 deletions

View file

@ -0,0 +1 @@
[]

BIN
sources_2/galene Executable file

Binary file not shown.

107
sources_2/group/client.go Executable file
View file

@ -0,0 +1,107 @@
package group
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"hash"
"golang.org/x/crypto/pbkdf2"
"github.com/jech/galene/conn"
)
type RawPassword struct {
Type string `json:"type,omitempty"`
Hash string `json:"hash,omitempty"`
Key string `json:"key"`
Salt string `json:"salt,omitempty"`
Iterations int `json:"iterations,omitempty"`
}
type Password RawPassword
func (p Password) Match(pw string) (bool, error) {
switch p.Type {
case "":
return p.Key == pw, nil
case "pbkdf2":
key, err := hex.DecodeString(p.Key)
if err != nil {
return false, err
}
salt, err := hex.DecodeString(p.Salt)
if err != nil {
return false, err
}
var h func() hash.Hash
switch p.Hash {
case "sha-256":
h = sha256.New
default:
return false, errors.New("unknown hash type")
}
theirKey := pbkdf2.Key(
[]byte(pw), salt, p.Iterations, len(key), h,
)
return bytes.Compare(key, theirKey) == 0, nil
default:
return false, errors.New("unknown password type")
}
}
func (p *Password) UnmarshalJSON(b []byte) error {
var k string
err := json.Unmarshal(b, &k)
if err == nil {
*p = Password{
Key: k,
}
return nil
}
var r RawPassword
err = json.Unmarshal(b, &r)
if err == nil {
*p = Password(r)
}
return err
}
func (p Password) MarshalJSON() ([]byte, error) {
if p.Type == "" && p.Hash == "" && p.Salt == "" && p.Iterations == 0 {
return json.Marshal(p.Key)
}
return json.Marshal(RawPassword(p))
}
type ClientCredentials struct {
Username string `json:"username,omitempty"`
Password *Password `json:"password,omitempty"`
}
type ClientPermissions struct {
Op bool `json:"op,omitempty"`
Present bool `json:"present,omitempty"`
Record bool `json:"record,omitempty"`
}
type Challengeable interface {
Username() string
Challenge(string, ClientCredentials) bool
}
type Client interface {
Group() *Group
Id() string
Challengeable
SetPermissions(ClientPermissions)
OverridePermissions(*Group) bool
PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack) error
PushClient(id, username string, add bool) error
}
type Kickable interface {
Kick(id, user, message string) error
}

87
sources_2/group/client_test.go Executable file
View file

@ -0,0 +1,87 @@
package group
import (
"encoding/json"
"log"
"reflect"
"testing"
)
var pw1 = Password{}
var pw2 = Password{Key: "pass"}
var pw3 = Password{
Type: "pbkdf2",
Hash: "sha-256",
Key: "fe499504e8f144693fae828e8e371d50e019d0e4c84994fa03f7f445bd8a570a",
Salt: "bcc1717851030776",
Iterations: 4096,
}
var pw4 = Password{
Type: "bad",
}
func TestGood(t *testing.T) {
if match, err := pw2.Match("pass"); err != nil || !match {
t.Errorf("pw2 doesn't match (%v)", err)
}
if match, err := pw3.Match("pass"); err != nil || !match {
t.Errorf("pw3 doesn't match (%v)", err)
}
}
func TestBad(t *testing.T) {
if match, err := pw1.Match("bad"); err != nil || match {
t.Errorf("pw1 matches")
}
if match, err := pw2.Match("bad"); err != nil || match {
t.Errorf("pw2 matches")
}
if match, err := pw3.Match("bad"); err != nil || match {
t.Errorf("pw3 matches")
}
if match, err := pw4.Match("bad"); err == nil || match {
t.Errorf("pw4 matches")
}
}
func TestJSON(t *testing.T) {
plain, err := json.Marshal(pw2)
if err != nil || string(plain) != `"pass"` {
t.Errorf("Expected \"pass\", got %v", string(plain))
}
for _, pw := range []Password{pw1, pw2, pw3, pw4} {
j, err := json.Marshal(pw)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if testing.Verbose() {
log.Printf("%v", string(j))
}
var pw2 Password
err = json.Unmarshal(j, &pw2)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
} else if !reflect.DeepEqual(pw, pw2) {
t.Errorf("Expected %v, got %v", pw, pw2)
}
}
}
func BenchmarkPlain(b *testing.B) {
for i := 0; i < b.N; i++ {
match, err := pw2.Match("bad")
if err != nil || match {
b.Errorf("pw2 matched")
}
}
}
func BenchmarkPBKDF2(b *testing.B) {
for i := 0; i < b.N; i++ {
match, err := pw3.Match("bad")
if err != nil || match {
b.Errorf("pw3 matched")
}
}
}

852
sources_2/group/group.go Executable file
View file

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

58
sources_2/group/group_test.go Executable file
View file

@ -0,0 +1,58 @@
package group
import (
"encoding/json"
"reflect"
"testing"
"time"
)
func TestJSTime(t *testing.T) {
tm := time.Now()
js := ToJSTime(tm)
tm2 := FromJSTime(js)
js2 := ToJSTime(tm2)
if js != js2 {
t.Errorf("%v != %v", js, js2)
}
delta := tm.Sub(tm2)
if delta < -time.Millisecond/2 || delta > time.Millisecond/2 {
t.Errorf("Delta %v, %v, %v", delta, tm, tm2)
}
}
func TestDescriptionJSON(t *testing.T) {
d := `
{
"op":[{"username": "jch","password": "topsecret"}],
"max-history-age": 10,
"allow-subgroups": true,
"presenter":[
{"user": "john", "password": "secret"},
{}
]
}`
var dd description
err := json.Unmarshal([]byte(d), &dd)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
ddd, err := json.Marshal(dd)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var dddd description
err = json.Unmarshal([]byte(ddd), &dddd)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(dd, dddd) {
t.Errorf("Got %v, expected %v", dddd, dd)
}
}

View file

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

69
sources_2/static/404.css Executable file
View file

@ -0,0 +1,69 @@
body {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
padding: 0px 30px;
background: #ddd;
}
.wrapper {
max-width: 960px;
width: 100%;
margin: 30px auto;
transform: scale(0.8);
}
.landing-page {
max-width: 960px;
height: 475px;
margin: 0;
box-shadow: 0px 0px 8px 1px #ccc;
background: #fafafa;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.logo {
color: #7e7e7e;
font-size: 8em;
text-align: center;
line-height: 1.1;
}
.logo .fa {
color: #c39999;
}
h1 {
font-size: 48px;
margin: 0;
color: #7e7e7e;
}
p {
font-size: 18px;
width: 35%;
margin: 16px auto 24px;
text-align: center;
}
.home-link {
text-decoration: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 18px;
cursor: pointer;
background: #610a86;
color: #fff;
border: none;
box-shadow: 0 4px 4px 0 #ccc;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

31
sources_2/static/404.html Executable file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Page not Found</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/common.css">
<link rel="stylesheet" type="text/css" href="/404.css"/>
<link rel="author" href="https://www.irif.fr/~jch/"/>
<!-- Font Awesome File -->
<link rel="stylesheet" type="text/css" href="/css/fontawesome.min.css">
<link href="/css/solid.css" rel="stylesheet" type="text/css">
<link href="/css/regular.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="wrapper">
<div class="landing-page">
<div class="logo">
<i class="fas fa-frown" aria-hidden="true"></i>
</div>
<h1> Page not found!</h1>
<p> We can't find the page you're looking for.</p>
<a href="/" class="home-link">Back to home</a>
</div>
</div>
</body>
</html>

44
sources_2/static/common.css Executable file
View file

@ -0,0 +1,44 @@
h1 {
font-size: 160%;
}
.inline {
display: inline;
}
.signature {
border-top: solid;
padding-top: 0;
border-width: thin;
clear: both;
height: 3.125rem;
text-align: center;
}
body {
overflow-x: hidden;
}
body, html {
height: 100%;
}
body {
margin: 0;
font-family: Metropolis,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #687281;
text-align: left;
background-color: #eff3f9;
}
*, :after, :before {
box-sizing: border-box;
}
textarea {
font-family: Metropolis,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
}

5
sources_2/static/css/fontawesome.min.css vendored Executable file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
/*!
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.eot");
src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
.far {
font-family: 'Font Awesome 5 Free';
font-weight: 400; }

16
sources_2/static/css/solid.css Executable file
View file

@ -0,0 +1,16 @@
/*!
* Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.eot");
src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
.fa,
.fas {
font-family: 'Font Awesome 5 Free';
font-weight: 900; }

15
sources_2/static/css/toastify.min.css vendored Executable file
View file

@ -0,0 +1,15 @@
/**
* Minified by jsDelivr using clean-css v4.2.3.
* Original file: /npm/toastify-js@1.9.1/src/toastify.css
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Toastify js 1.9.1
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*/
.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215,.61,.355,1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px);z-index:2147483647}.toastify.on{opacity:1}.toast-close{opacity:.4;padding:0 5px}.toastify-right{right:15px}.toastify-left{left:15px}.toastify-top{top:-150px}.toastify-bottom{bottom:-150px}.toastify-rounded{border-radius:25px}.toastify-avatar{width:1.5em;height:1.5em;margin:-7px 5px;border-radius:2px}.toastify-center{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}@media only screen and (max-width:360px){.toastify-left,.toastify-right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
/*# sourceMappingURL=/sm/9c0bbf2acc17f6468f9dd75307f4d772b55e466d0ddceef6dc95ee31ca309918.map */

1307
sources_2/static/galene.css Executable file

File diff suppressed because it is too large Load diff

255
sources_2/static/galene.html Executable file
View file

@ -0,0 +1,255 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Galène</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="ScreenOrientation" content="autoRotate:disabled">
<link rel="stylesheet" type="text/css" href="/common.css"/>
<link rel="stylesheet" type="text/css" href="/galene.css"/>
<link rel="author" href="https://www.irif.fr/~jch/"/>
<!-- Font Awesome File -->
<link href="/css/fontawesome.min.css" rel="stylesheet" type="text/css">
<link href="/css/solid.css" rel="stylesheet" type="text/css">
<link href="/css/regular.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" type="text/css" href="/css/toastify.min.css">
</head>
<body>
<div id="main" class="app">
<div class="row full-height">
<nav id="left-sidebar">
<div class="users-header">
<div class="galene-header">Galène</div>
</div>
<div class="header-sep"></div>
<div id="users"></div>
</nav>
<div class="container">
<header>
<nav class="topnav navbar navbar-expand navbar-light fixed-top">
<div id="header">
<div class="collapse" title="Collapse left panel" id="sidebarCollapse">
<svg class="svg-inline--fa" aria-hidden="true" data-icon="align-left" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" d="M288 44v40c0 8.837-7.163 16-16 16H16c-8.837 0-16-7.163-16-16V44c0-8.837 7.163-16 16-16h256c8.837 0 16 7.163 16 16zM0 172v40c0 8.837 7.163 16 16 16h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16zm16 312h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm256-200H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16h256c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16z"></path>
</svg>
</div>
<h1 id="title" class="header-title"></h1>
</div>
<ul class="nav-menu">
<li>
<button id="presentbutton" class="invisible btn btn-success">
<i class="fas fa-play" aria-hidden="true"></i><span class="nav-text"> Ready</span>
</button>
</li>
<li>
<button id="unpresentbutton" class="invisible btn btn-cancel">
<i class="fas fa-stop" aria-hidden="true"></i><span class="nav-text"> Panic</span>
</button>
</li>
<li>
<div id="mutebutton" class="nav-link nav-button">
<span><i class="fas fa-microphone-slash" aria-hidden="true"></i></span>
<label>Mute</label>
</div>
</li>
<li>
<div id="sharebutton" class="invisible nav-link nav-button">
<span><i class="fas fa-share-square" aria-hidden="true"></i></span>
<label>Share Screen</label>
</div>
</li>
<li>
<div id="unsharebutton" class="invisible nav-link nav-button nav-cancel">
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
<label>Unshare Screen</label>
</div>
</li>
<li>
<div id="stopvideobutton" class="invisible nav-link nav-button nav-cancel">
<span><i class="fas fa-window-close" aria-hidden="true"></i></span>
<label>Stop Video</label>
</div>
</li>
<li>
<div class="nav-button nav-link nav-more" id="openside">
<span><i class="fas fa-ellipsis-v" aria-hidden="true"></i></span>
</div>
</li>
</ul>
</nav>
</header>
<div class="row full-width" id="mainrow">
<div class="coln-left" id="left">
<div id="chat">
<div id="chatbox">
<div class="close-chat" id="close-chat" title="Hide chat">
<span class="close-icon"></span>
</div>
<div id="box"></div>
<div class="reply">
<form id="inputform">
<textarea id="input" class="form-reply"></textarea>
<input id="inputbutton" type="submit" value="&#10148;" class="btn btn-default"/>
</form>
</div>
</div>
</div>
</div>
<div id="resizer"></div>
<div class="coln-right" id="right">
<span class="show-video blink" id="switch-video"><i class="fas fa-exchange" aria-hidden="true"></i></span>
<div class="collapse-video" id="collapse-video">
<i class="far fa-comment-alt open-chat" title="Open chat"></i>
</div>
<div class="video-container no-video" id="video-container">
<div id="expand-video" class="expand-video">
<div id="peers"></div>
</div>
</div>
<div class="login-container invisible" id="login-container">
<div class="login-box">
<form id="userform" class="userform">
<label for="username">Username</label>
<input id="username" type="text" name="username"
autocomplete="username" class="form-control"/>
<label for="password">Password</label>
<input id="password" type="password" name="password"
autocomplete="current-password" class="form-control"/>
<label>Auto ready</label>
<div class="present-switch">
<p class="switch-radio">
<input id="presentoff" type="radio" name="presentradio" value="" checked/>
<label for="presentoff">Disabled</label>
</p>
<p class="switch-radio">
<input id="presentmike" type="radio" name="presentradio" value="mike"/>
<label for="presentmike">Enable microphone</label>
</p>
<p class="switch-radio">
<input id="presentboth" type="radio" name="presentradio" value="both"/>
<label for="presentboth">Enable camera and microphone</label>
</p>
</div>
<div class="clear"></div>
<div class="connect">
<input id="connectbutton" type="submit" class="btn btn-blue" value="Connect"/>
</div>
</form>
<div class="clear"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="sidebarnav" class="sidenav">
<div class="sidenav-header">
<h2>Settings</h2>
<a class="closebtn" id="clodeside"><i class="fas fa-times" aria-hidden="true"></i></a>
</div>
<div class="sidenav-content" id="optionsdiv">
<div id="profile" class="profile invisible">
<div class="profile-user">
<div class="profile-logo">
<span><i class="fas fa-user" aria-hidden="true"></i></span>
</div>
<div class="profile-info">
<span id="userspan"></span>
<span id="permspan"></span>
</div>
<div class="user-logout">
<a id="disconnectbutton">
<span class="logout-icon"><i class="fas fa-sign-out-alt"></i></span>
<span class="logout-text">Logout</span>
</a>
</div>
</div>
</div>
<div id="mediaoptions" class="invisible">
<fieldset>
<legend>Media Options</legend>
<label for="videoselect" class="sidenav-label-first">Camera:</label>
<select id="videoselect" class="select select-inline">
<option value="">off</option>
</select>
<label for="audioselect" class="sidenav-label">Microphone:</label>
<select id="audioselect" class="select select-inline">
<option value="">off</option>
</select>
<form>
<input id="blackboardbox" type="checkbox"/>
<label for="blackboardbox">Blackboard mode</label>
</form>
</fieldset>
</div>
<fieldset>
<legend>Other Settings</legend>
<form id="sendform">
<label for="sendselect" class="sidenav-label-first">Send:</label>
<select id="sendselect" class="select select-inline">
<option value="lowest">lowest</option>
<option value="low">low</option>
<option value="normal" selected>normal</option>
<option value="unlimited">unlimited</option>
</select>
</form>
<form id="requestform">
<label for="requestselect" class="sidenav-label">Receive:</label>
<select id="requestselect" class="select select-inline">
<option value="">nothing</option>
<option value="audio">audio only</option>
<option value="screenshare">screen share</option>
<option value="everything" selected>everything</option>
</select>
</form>
<form>
<input id="activitybox" type="checkbox"/>
<label for="activitybox">Activity detection</label>
</form>
</fieldset>
<form id="fileform">
<label for="fileinput" class=".sidenav-label-first">Play local file:</label>
<input type="file" id="fileinput" accept="audio/*,video/*" multiple/>
</form>
</div>
</div>
<div id="videocontrols-template" class="invisible">
<div class="video-controls vc-overlay">
<div class="controls-button controls-left">
<span class="video-play" title="Play video">
<i class="fas fa-play"></i>
</span>
<span class="volume" title="Volume">
<i class="fas fa-volume-up volume-mute" aria-hidden="true"></i>
<input class="volume-slider" type="range" max="100" value="100" min="0" step="5" >
</span>
</div>
<div class="controls-button controls-right">
<span class="pip" title="Picture In Picture">
<i class="far fa-clone" aria-hidden="true"></i>
</span>
<span class="fullscreen" title="Fullscreen">
<i class="fas fa-expand" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
<script src="/protocol.js" defer></script>
<script src="/scripts/toastify.js" defer></script>
<script src="/galene.js" defer></script>
</body>
</html>

2353
sources_2/static/galene.js Executable file

File diff suppressed because it is too large Load diff

42
sources_2/static/index.html Executable file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Galène</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/common.css">
<link rel="stylesheet" href="/mainpage.css">
<link rel="stylesheet" type="text/css" href="/galene.css"/>
<link rel="author" href="https://www.irif.fr/~jch/"/>
<!-- Font Awesome File -->
<link href="/css/fontawesome.min.css" rel="stylesheet" type="text/css">
<link href="/css/solid.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="home">
<h1 id="title" class="navbar-brand">Galène</h1>
<form id="groupform">
<label for="group">Group:</label>
<input id="group" type="text" name="group" class="form-control form-control-inline"/>
<input type="submit" value="Join" class="btn btn-default btn-large"/><br/>
</form>
<div id="public-groups" class="groups">
<h2>Public groups</h2>
<table id="public-groups-table"></table>
</div>
</div>
<footer class="signature">
<p><a href="https://galene.org/">Galène</a> by <a href="https://www.irif.fr/~jch/" rel="author">Juliusz Chroboczek</a>
</footer>
<script src="/mainpage.js" defer></script>
</body>
</html>

40
sources_2/static/mainpage.css Executable file
View file

@ -0,0 +1,40 @@
.groups {
}
.nogroups {
display: none;
}
.navbar-brand {
margin-bottom: 5rem;
}
.home {
height: calc(100vh - 50px);
padding: 1.875rem;
}
#public-groups-table tr a{
margin-left: 0.9375rem;
font-weight: 700;
}
a {
text-decoration: none;
color: #0058e4;
}
a:hover {
color: #0a429c;
}
label {
display: block;
}
@media only screen and (max-device-width: 768px) {
.home {
padding: 0.625rem;
}
}

73
sources_2/static/mainpage.js Executable file
View file

@ -0,0 +1,73 @@
// Copyright (c) 2020 by Juliusz Chroboczek.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
document.getElementById('groupform').onsubmit = function(e) {
e.preventDefault();
let group = document.getElementById('group').value.trim();
if(group !== '')
location.href = '/group/' + group;
};
async function listPublicGroups() {
let div = document.getElementById('public-groups');
let table = document.getElementById('public-groups-table');
let l;
try {
l = await (await fetch('/public-groups.json')).json();
} catch(e) {
console.error(e);
l = [];
}
if (l.length === 0) {
table.textContent = '(No groups found.)';
div.classList.remove('groups');
div.classList.add('nogroups');
return;
}
div.classList.remove('nogroups');
div.classList.add('groups');
for(let i = 0; i < l.length; i++) {
let group = l[i];
let tr = document.createElement('tr');
let td = document.createElement('td');
let a = document.createElement('a');
a.textContent = group.name;
a.href = '/group/' + encodeURIComponent(group.name);
td.appendChild(a);
tr.appendChild(td);
let td2 = document.createElement('td');
if(group.description)
td2.textContent = group.description;
tr.appendChild(td2);
let td3 = document.createElement('td');
td3.textContent = `(${group.clientCount} clients)`;
tr.appendChild(td3);
table.appendChild(tr);
}
}
listPublicGroups();

1156
sources_2/static/protocol.js Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v3.14.1.
* Original file: /npm/toastify-js@1.9.1/src/toastify.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(t,o){"object"==typeof module&&module.exports?module.exports=o():t.Toastify=o()}(this,function(t){var o=function(t){return new o.lib.init(t)};function i(t,o){return o.offset[t]?isNaN(o.offset[t])?o.offset[t]:o.offset[t]+"px":"0px"}function s(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&t.className.trim().split(/\s+/gi).indexOf(o)>-1)}return o.lib=o.prototype={toastify:"1.9.1",constructor:o,init:function(t){t||(t={}),this.options={},this.toastElement=null,this.options.text=t.text||"Hi there!",this.options.node=t.node,this.options.duration=0===t.duration?0:t.duration||3e3,this.options.selector=t.selector,this.options.callback=t.callback||function(){},this.options.destination=t.destination,this.options.newWindow=t.newWindow||!1,this.options.close=t.close||!1,this.options.gravity="bottom"===t.gravity?"toastify-bottom":"toastify-top",this.options.positionLeft=t.positionLeft||!1,this.options.position=t.position||"",this.options.backgroundColor=t.backgroundColor,this.options.avatar=t.avatar||"",this.options.className=t.className||"",this.options.stopOnFocus=void 0===t.stopOnFocus||t.stopOnFocus,this.options.onClick=t.onClick;return this.options.offset=t.offset||{x:0,y:0},this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");if(t.className="toastify on "+this.options.className,this.options.position?t.className+=" toastify-"+this.options.position:!0===this.options.positionLeft?(t.className+=" toastify-left",console.warn("Property `positionLeft` will be depreciated in further versions. Please use `position` instead.")):t.className+=" toastify-right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&(t.style.background=this.options.backgroundColor),this.options.node&&this.options.node.nodeType===Node.ELEMENT_NODE)t.appendChild(this.options.node);else if(t.innerHTML=this.options.text,""!==this.options.avatar){var o=document.createElement("img");o.src=this.options.avatar,o.className="toastify-avatar","left"==this.options.position||!0===this.options.positionLeft?t.appendChild(o):t.insertAdjacentElement("afterbegin",o)}if(!0===this.options.close){var s=document.createElement("span");s.innerHTML="&#10006;",s.className="toast-close",s.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(this.toastElement),window.clearTimeout(this.toastElement.timeOutValue)}.bind(this));var n=window.innerWidth>0?window.innerWidth:screen.width;("left"==this.options.position||!0===this.options.positionLeft)&&n>360?t.insertAdjacentElement("afterbegin",s):t.appendChild(s)}if(this.options.stopOnFocus&&this.options.duration>0){const o=this;t.addEventListener("mouseover",function(o){window.clearTimeout(t.timeOutValue)}),t.addEventListener("mouseleave",function(){t.timeOutValue=window.setTimeout(function(){o.removeElement(t)},o.options.duration)})}if(void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),"function"==typeof this.options.onClick&&void 0===this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),this.options.onClick()}.bind(this)),"object"==typeof this.options.offset){var e=i("x",this.options),a=i("y",this.options);const o="left"==this.options.position?e:`-${e}`,s="toastify-top"==this.options.gravity?a:`-${a}`;t.style.transform=`translate(${o}, ${s})`}return t},showToast:function(){var t;if(this.toastElement=this.buildToast(),!(t=void 0===this.options.selector?document.body:document.getElementById(this.options.selector)))throw"Root element is not defined";return t.insertBefore(this.toastElement,t.firstChild),o.reposition(),this.options.duration>0&&(this.toastElement.timeOutValue=window.setTimeout(function(){this.removeElement(this.toastElement)}.bind(this),this.options.duration)),this},hideToast:function(){this.toastElement.timeOutValue&&clearTimeout(this.toastElement.timeOutValue),this.removeElement(this.toastElement)},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){this.options.node&&this.options.node.parentNode&&this.options.node.parentNode.removeChild(this.options.node),t.parentNode&&t.parentNode.removeChild(t),this.options.callback.call(t),o.reposition()}.bind(this),400)}},o.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},n={top:15,bottom:15},e=document.getElementsByClassName("toastify"),a=0;a<e.length;a++){t=!0===s(e[a],"toastify-top")?"toastify-top":"toastify-bottom";var p=e[a].offsetHeight;t=t.substr(9,t.length-1);(window.innerWidth>0?window.innerWidth:screen.width)<=360?(e[a].style[t]=n[t]+"px",n[t]+=p+15):!0===s(e[a],"toastify-left")?(e[a].style[t]=o[t]+"px",o[t]+=p+15):(e[a].style[t]=i[t]+"px",i[t]+=p+15)}return this},o.lib.init.prototype=o.lib,o});
//# sourceMappingURL=/sm/1df7b098cd6209fd67b5cc8f6f6518b79e5214ec3802d91f56f825883253df69.map

19
sources_2/static/tsconfig.json Executable file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES6",
"allowJs": true,
"checkJs": true,
"declaration": true,
"noImplicitThis": true,
"emitDeclarationOnly": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true
},
"files": [
"protocol.js",
"galene.js"
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.