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:
parent
f3c8e800c4
commit
67965ba464
31 changed files with 6557 additions and 0 deletions
1
sources_2/data/ice-servers.json
Executable file
1
sources_2/data/ice-servers.json
Executable file
|
@ -0,0 +1 @@
|
||||||
|
[]
|
BIN
sources_2/galene
Executable file
BIN
sources_2/galene
Executable file
Binary file not shown.
107
sources_2/group/client.go
Executable file
107
sources_2/group/client.go
Executable 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
87
sources_2/group/client_test.go
Executable 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
852
sources_2/group/group.go
Executable 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
58
sources_2/group/group_test.go
Executable 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)
|
||||||
|
}
|
||||||
|
}
|
4
sources_2/groups/groupname.json
Normal file
4
sources_2/groups/groupname.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"op": [{"username": "__ADMIN__", "password": "__PASSWORD__"}],
|
||||||
|
"presenter": [{}]
|
||||||
|
}
|
69
sources_2/static/404.css
Executable file
69
sources_2/static/404.css
Executable 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
31
sources_2/static/404.html
Executable 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
44
sources_2/static/common.css
Executable 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
5
sources_2/static/css/fontawesome.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
15
sources_2/static/css/regular.css
Executable file
15
sources_2/static/css/regular.css
Executable 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
16
sources_2/static/css/solid.css
Executable 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
15
sources_2/static/css/toastify.min.css
vendored
Executable 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
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
255
sources_2/static/galene.html
Executable 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="➤" 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
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
42
sources_2/static/index.html
Executable 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
40
sources_2/static/mainpage.css
Executable 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
73
sources_2/static/mainpage.js
Executable 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
1156
sources_2/static/protocol.js
Executable file
File diff suppressed because it is too large
Load diff
8
sources_2/static/scripts/toastify.js
Executable file
8
sources_2/static/scripts/toastify.js
Executable 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="✖",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
19
sources_2/static/tsconfig.json
Executable 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"
|
||||||
|
]
|
||||||
|
}
|
BIN
sources_2/static/webfonts/fa-regular-400.eot
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.eot
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-regular-400.ttf
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.ttf
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-regular-400.woff
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.woff
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-regular-400.woff2
Executable file
BIN
sources_2/static/webfonts/fa-regular-400.woff2
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.eot
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.eot
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.ttf
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.ttf
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.woff
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.woff
Executable file
Binary file not shown.
BIN
sources_2/static/webfonts/fa-solid-900.woff2
Executable file
BIN
sources_2/static/webfonts/fa-solid-900.woff2
Executable file
Binary file not shown.
Loading…
Add table
Reference in a new issue