move server to server directory.

This commit is contained in:
Miroslav Šedivý
2024-06-23 17:48:14 +02:00
parent da45f62ca8
commit 5b98344205
211 changed files with 18 additions and 10 deletions

6
server/pkg/types/api.go Normal file
View File

@ -0,0 +1,6 @@
package types
type ApiManager interface {
Route(r Router)
AddRouter(path string, router func(Router))
}

245
server/pkg/types/capture.go Normal file
View File

@ -0,0 +1,245 @@
package types
import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/PaesslerAG/gval"
"github.com/demodesk/neko/pkg/types/codec"
)
var (
ErrCapturePipelineAlreadyExists = errors.New("capture pipeline already exists")
)
type Sample struct {
// buffer with encoded media
Data []byte
Length int
// timing information
Timestamp time.Time
Duration time.Duration
// metadata
DeltaUnit bool // this unit cannot be decoded independently.
}
type SampleListener interface {
WriteSample(Sample)
}
type BroadcastManager interface {
Start(url string) error
Stop()
Started() bool
Url() string
}
type ScreencastManager interface {
Enabled() bool
Started() bool
Image() ([]byte, error)
}
type StreamSelectorType int
const (
// select exact stream
StreamSelectorTypeExact StreamSelectorType = iota
// select nearest stream (in either direction) if exact stream is not available
StreamSelectorTypeNearest
// if exact stream is found select the next lower stream, otherwise select the nearest lower stream
StreamSelectorTypeLower
// if exact stream is found select the next higher stream, otherwise select the nearest higher stream
StreamSelectorTypeHigher
)
func (s StreamSelectorType) String() string {
switch s {
case StreamSelectorTypeExact:
return "exact"
case StreamSelectorTypeNearest:
return "nearest"
case StreamSelectorTypeLower:
return "lower"
case StreamSelectorTypeHigher:
return "higher"
default:
return fmt.Sprintf("%d", int(s))
}
}
func (s *StreamSelectorType) UnmarshalText(text []byte) error {
switch strings.ToLower(string(text)) {
case "exact", "":
*s = StreamSelectorTypeExact
case "nearest":
*s = StreamSelectorTypeNearest
case "lower":
*s = StreamSelectorTypeLower
case "higher":
*s = StreamSelectorTypeHigher
default:
return fmt.Errorf("invalid stream selector type: %s", string(text))
}
return nil
}
func (s StreamSelectorType) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
type StreamSelector struct {
// type of stream selector
Type StreamSelectorType `json:"type"`
// select stream by its ID
ID string `json:"id"`
// select stream by its bitrate
Bitrate uint64 `json:"bitrate"`
}
type StreamSelectorManager interface {
IDs() []string
Codec() codec.RTPCodec
GetStream(selector StreamSelector) (StreamSinkManager, bool)
}
type StreamSinkManager interface {
ID() string
Codec() codec.RTPCodec
Bitrate() uint64
AddListener(listener SampleListener) error
RemoveListener(listener SampleListener) error
MoveListenerTo(listener SampleListener, targetStream StreamSinkManager) error
ListenersCount() int
Started() bool
CreatePipeline() error
DestroyPipeline()
}
type StreamSrcManager interface {
Codec() codec.RTPCodec
Start(codec codec.RTPCodec) error
Stop()
Push(bytes []byte)
Started() bool
}
type CaptureManager interface {
Start()
Shutdown() error
Broadcast() BroadcastManager
Screencast() ScreencastManager
Audio() StreamSinkManager
Video() StreamSelectorManager
Webcam() StreamSrcManager
Microphone() StreamSrcManager
}
type VideoConfig struct {
Width string `mapstructure:"width"` // expression
Height string `mapstructure:"height"` // expression
Fps string `mapstructure:"fps"` // expression
Bitrate int `mapstructure:"bitrate"` // pipeline bitrate
GstPrefix string `mapstructure:"gst_prefix"` // pipeline prefix, starts with !
GstEncoder string `mapstructure:"gst_encoder"` // gst encoder name
GstParams map[string]string `mapstructure:"gst_params"` // map of expressions
GstSuffix string `mapstructure:"gst_suffix"` // pipeline suffix, starts with !
GstPipeline string `mapstructure:"gst_pipeline"` // whole pipeline as a string
}
func (config *VideoConfig) GetPipeline(screen ScreenSize) (string, error) {
values := map[string]any{
"width": screen.Width,
"height": screen.Height,
"fps": screen.Rate,
}
language := []gval.Language{
gval.Function("round", func(args ...any) (any, error) {
return (int)(math.Round(args[0].(float64))), nil
}),
}
// get fps pipeline
fpsPipeline := "! video/x-raw ! videoconvert ! queue"
if config.Fps != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Fps)
if err != nil {
return "", err
}
val, err := eval.EvalFloat64(context.Background(), values)
if err != nil {
return "", err
}
fpsPipeline = fmt.Sprintf("! capsfilter caps=video/x-raw,framerate=%d/100 name=framerate ! videoconvert ! queue", int(val*100))
}
// get scale pipeline
scalePipeline := ""
if config.Width != "" && config.Height != "" {
eval, err := gval.Full(language...).NewEvaluable(config.Width)
if err != nil {
return "", err
}
w, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
}
eval, err = gval.Full(language...).NewEvaluable(config.Height)
if err != nil {
return "", err
}
h, err := eval.EvalInt(context.Background(), values)
if err != nil {
return "", err
}
// element videoscale parameter method to 0 meaning nearest neighbor
scalePipeline = fmt.Sprintf("! videoscale method=0 ! capsfilter caps=video/x-raw,width=%d,height=%d name=resolution ! queue", w, h)
}
// get encoder pipeline
encPipeline := fmt.Sprintf("! %s name=encoder", config.GstEncoder)
for key, expr := range config.GstParams {
if expr == "" {
continue
}
val, err := gval.Evaluate(expr, values, language...)
if err != nil {
return "", err
}
if val != nil {
encPipeline += fmt.Sprintf(" %s=%v", key, val)
} else {
encPipeline += fmt.Sprintf(" %s=%s", key, expr)
}
}
// join strings with space
return strings.Join([]string{
fpsPipeline,
scalePipeline,
config.GstPrefix,
encPipeline,
config.GstSuffix,
}[:], " "), nil
}

View File

@ -0,0 +1,207 @@
package codec
import (
"strings"
"github.com/pion/webrtc/v3"
)
var RTCPFeedback = []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBTransportCC, Parameter: ""},
{Type: webrtc.TypeRTCPFBGoogREMB, Parameter: ""}, // TODO: Deprecated.
// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-19
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-15
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
{Type: webrtc.TypeRTCPFBNACK, Parameter: ""},
}
func ParseRTC(codec webrtc.RTPCodecParameters) (RTPCodec, bool) {
codecName := strings.Split(codec.RTPCodecCapability.MimeType, "/")[1]
return ParseStr(codecName)
}
func ParseStr(codecName string) (codec RTPCodec, ok bool) {
ok = true
switch strings.ToLower(codecName) {
case VP8().Name:
codec = VP8()
case VP9().Name:
codec = VP9()
case H264().Name:
codec = H264()
case Opus().Name:
codec = Opus()
case G722().Name:
codec = G722()
case PCMU().Name:
codec = PCMU()
case PCMA().Name:
codec = PCMA()
default:
ok = false
}
return
}
type RTPCodec struct {
Name string
PayloadType webrtc.PayloadType
Type webrtc.RTPCodecType
Capability webrtc.RTPCodecCapability
Pipeline string
}
func (codec *RTPCodec) Register(engine *webrtc.MediaEngine) error {
return engine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: codec.Capability,
PayloadType: codec.PayloadType,
}, codec.Type)
}
func (codec *RTPCodec) IsVideo() bool {
return codec.Type == webrtc.RTPCodecTypeVideo
}
func (codec *RTPCodec) IsAudio() bool {
return codec.Type == webrtc.RTPCodecTypeAudio
}
func (codec *RTPCodec) String() string {
return codec.Type.String() + "/" + codec.Name
}
func VP8() RTPCodec {
return RTPCodec{
Name: "vp8",
PayloadType: 96,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: RTCPFeedback,
},
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html
// gstreamer1.0-plugins-good
Pipeline: "vp8enc cpu-used=16 threads=4 deadline=1 error-resilient=partitions keyframe-max-dist=15 static-threshold=20",
}
}
// TODO: Profile ID.
func VP9() RTPCodec {
return RTPCodec{
Name: "vp9",
PayloadType: 98,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "profile-id=0",
RTCPFeedback: RTCPFeedback,
},
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html
// gstreamer1.0-plugins-good
Pipeline: "vp9enc cpu-used=16 threads=4 deadline=1 keyframe-max-dist=15 static-threshold=20",
}
}
// TODO: Profile ID.
func H264() RTPCodec {
return RTPCodec{
Name: "h264",
PayloadType: 102,
Type: webrtc.RTPCodecTypeVideo,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: RTCPFeedback,
},
// https://gstreamer.freedesktop.org/documentation/x264/index.html
// gstreamer1.0-plugins-ugly
Pipeline: "video/x-raw,format=I420 ! x264enc threads=4 bitrate=4096 key-int-max=15 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream",
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html
// gstreamer1.0-plugins-bad
//Pipeline: "openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream",
}
}
func Opus() RTPCodec {
return RTPCodec{
Name: "opus",
PayloadType: 111,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "useinbandfec=1;stereo=1",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
// gstreamer1.0-plugins-base
Pipeline: "opusenc inband-fec=true bitrate=128000",
}
}
func G722() RTPCodec {
return RTPCodec{
Name: "g722",
PayloadType: 9,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeG722,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html
// gstreamer1.0-libav
Pipeline: "avenc_g722",
}
}
func PCMU() RTPCodec {
return RTPCodec{
Name: "pcmu",
PayloadType: 0,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html
// gstreamer1.0-plugins-good
Pipeline: "audio/x-raw, rate=8000 ! mulawenc",
}
}
func PCMA() RTPCodec {
return RTPCodec{
Name: "pcma",
PayloadType: 8,
Type: webrtc.RTPCodecTypeAudio,
Capability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
ClockRate: 8000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: []webrtc.RTCPFeedback{},
},
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html
// gstreamer1.0-plugins-good
Pipeline: "audio/x-raw, rate=8000 ! alawenc",
}
}

104
server/pkg/types/desktop.go Normal file
View File

@ -0,0 +1,104 @@
package types
import (
"fmt"
"image"
)
type CursorImage struct {
Width uint16
Height uint16
Xhot uint16
Yhot uint16
Serial uint64
Image *image.RGBA
}
type ScreenSize struct {
Width int `json:"width"`
Height int `json:"height"`
Rate int16 `json:"rate"`
}
func (s ScreenSize) String() string {
return fmt.Sprintf("%dx%d@%d", s.Width, s.Height, s.Rate)
}
type KeyboardModifiers struct {
Shift *bool `json:"shift"`
CapsLock *bool `json:"capslock"`
Control *bool `json:"control"`
Alt *bool `json:"alt"`
NumLock *bool `json:"numlock"`
Meta *bool `json:"meta"`
Super *bool `json:"super"`
AltGr *bool `json:"altgr"`
}
type KeyboardMap struct {
Layout string `json:"layout"`
Variant string `json:"variant"`
}
type ClipboardText struct {
Text string
HTML string
}
type DesktopManager interface {
Start()
Shutdown() error
OnBeforeScreenSizeChange(listener func())
OnAfterScreenSizeChange(listener func())
// xorg
Move(x, y int)
GetCursorPosition() (int, int)
Scroll(deltaX, deltaY int, controlKey bool)
ButtonDown(code uint32) error
KeyDown(code uint32) error
ButtonUp(code uint32) error
KeyUp(code uint32) error
ButtonPress(code uint32) error
KeyPress(codes ...uint32) error
ResetKeys()
ScreenConfigurations() []ScreenSize
SetScreenSize(ScreenSize) (ScreenSize, error)
GetScreenSize() ScreenSize
SetKeyboardMap(KeyboardMap) error
GetKeyboardMap() (*KeyboardMap, error)
SetKeyboardModifiers(mod KeyboardModifiers)
GetKeyboardModifiers() KeyboardModifiers
GetCursorImage() *CursorImage
GetScreenshotImage() *image.RGBA
// xevent
OnCursorChanged(listener func(serial uint64))
OnClipboardUpdated(listener func())
OnFileChooserDialogOpened(listener func())
OnFileChooserDialogClosed(listener func())
OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8))
// input driver
HasTouchSupport() bool
TouchBegin(touchId uint32, x, y int, pressure uint8) error
TouchUpdate(touchId uint32, x, y int, pressure uint8) error
TouchEnd(touchId uint32, x, y int, pressure uint8) error
// clipboard
ClipboardGetText() (*ClipboardText, error)
ClipboardSetText(data ClipboardText) error
ClipboardGetBinary(mime string) ([]byte, error)
ClipboardSetBinary(mime string, data []byte) error
ClipboardGetTargets() ([]string, error)
// drop
DropFiles(x int, y int, files []string) bool
IsUploadDropEnabled() bool
// filechooser
HandleFileChooserDialog(uri string) error
CloseFileChooserDialog()
IsFileChooserDialogEnabled() bool
IsFileChooserDialogOpened() bool
}

View File

@ -0,0 +1,84 @@
package event
const (
SYSTEM_INIT = "system/init"
SYSTEM_ADMIN = "system/admin"
SYSTEM_SETTINGS = "system/settings"
SYSTEM_LOGS = "system/logs"
SYSTEM_DISCONNECT = "system/disconnect"
SYSTEM_HEARTBEAT = "system/heartbeat"
)
const (
SIGNAL_REQUEST = "signal/request"
SIGNAL_RESTART = "signal/restart"
SIGNAL_OFFER = "signal/offer"
SIGNAL_ANSWER = "signal/answer"
SIGNAL_PROVIDE = "signal/provide"
SIGNAL_CANDIDATE = "signal/candidate"
SIGNAL_VIDEO = "signal/video"
SIGNAL_AUDIO = "signal/audio"
SIGNAL_CLOSE = "signal/close"
)
const (
SESSION_CREATED = "session/created"
SESSION_DELETED = "session/deleted"
SESSION_PROFILE = "session/profile"
SESSION_STATE = "session/state"
SESSION_CURSORS = "session/cursors"
)
const (
CONTROL_HOST = "control/host"
CONTROL_RELEASE = "control/release"
CONTROL_REQUEST = "control/request"
// mouse
CONTROL_MOVE = "control/move"
CONTROL_SCROLL = "control/scroll"
CONTROL_BUTTONPRESS = "control/buttonpress"
CONTROL_BUTTONDOWN = "control/buttondown"
CONTROL_BUTTONUP = "control/buttonup"
// keyboard
CONTROL_KEYPRESS = "control/keypress"
CONTROL_KEYDOWN = "control/keydown"
CONTROL_KEYUP = "control/keyup"
// touch
CONTROL_TOUCHBEGIN = "control/touchbegin"
CONTROL_TOUCHUPDATE = "control/touchupdate"
CONTROL_TOUCHEND = "control/touchend"
// actions
CONTROL_CUT = "control/cut"
CONTROL_COPY = "control/copy"
CONTROL_PASTE = "control/paste"
CONTROL_SELECT_ALL = "control/select_all"
)
const (
SCREEN_UPDATED = "screen/updated"
SCREEN_SET = "screen/set"
)
const (
CLIPBOARD_UPDATED = "clipboard/updated"
CLIPBOARD_SET = "clipboard/set"
)
const (
KEYBOARD_MODIFIERS = "keyboard/modifiers"
KEYBOARD_MAP = "keyboard/map"
)
const (
BORADCAST_STATUS = "broadcast/status"
)
const (
SEND_UNICAST = "send/unicast"
SEND_BROADCAST = "send/broadcast"
)
const (
FILE_CHOOSER_DIALOG_OPENED = "file_chooser_dialog/opened"
FILE_CHOOSER_DIALOG_CLOSED = "file_chooser_dialog/closed"
)

27
server/pkg/types/http.go Normal file
View File

@ -0,0 +1,27 @@
package types
import (
"context"
"net/http"
)
type RouterHandler func(w http.ResponseWriter, r *http.Request) error
type MiddlewareHandler func(w http.ResponseWriter, r *http.Request) (context.Context, error)
type Router interface {
Group(fn func(Router))
Route(pattern string, fn func(Router))
Get(pattern string, fn RouterHandler)
Post(pattern string, fn RouterHandler)
Put(pattern string, fn RouterHandler)
Patch(pattern string, fn RouterHandler)
Delete(pattern string, fn RouterHandler)
With(fn MiddlewareHandler) Router
Use(fn MiddlewareHandler)
ServeHTTP(w http.ResponseWriter, req *http.Request)
}
type HttpManager interface {
Start()
Shutdown() error
}

View File

@ -0,0 +1,48 @@
package types
import "errors"
var (
ErrMemberAlreadyExists = errors.New("member already exists")
ErrMemberDoesNotExist = errors.New("member does not exist")
ErrMemberInvalidPassword = errors.New("invalid password")
)
type MemberProfile struct {
Name string `json:"name"`
// permissions
IsAdmin bool `json:"is_admin" mapstructure:"is_admin"`
CanLogin bool `json:"can_login" mapstructure:"can_login"`
CanConnect bool `json:"can_connect" mapstructure:"can_connect"`
CanWatch bool `json:"can_watch" mapstructure:"can_watch"`
CanHost bool `json:"can_host" mapstructure:"can_host"`
CanShareMedia bool `json:"can_share_media" mapstructure:"can_share_media"`
CanAccessClipboard bool `json:"can_access_clipboard" mapstructure:"can_access_clipboard"`
SendsInactiveCursor bool `json:"sends_inactive_cursor" mapstructure:"sends_inactive_cursor"`
CanSeeInactiveCursors bool `json:"can_see_inactive_cursors" mapstructure:"can_see_inactive_cursors"`
// plugin scope
Plugins PluginSettings `json:"plugins"`
}
type MemberProvider interface {
Connect() error
Disconnect() error
Authenticate(username string, password string) (id string, profile MemberProfile, err error)
Insert(username string, password string, profile MemberProfile) (id string, err error)
Select(id string) (profile MemberProfile, err error)
SelectAll(limit int, offset int) (profiles map[string]MemberProfile, err error)
UpdateProfile(id string, profile MemberProfile) error
UpdatePassword(id string, password string) error
Delete(id string) error
}
type MemberManager interface {
MemberProvider
Login(username string, password string) (Session, string, error)
Logout(id string) error
}

View File

@ -0,0 +1,212 @@
package message
import (
"github.com/pion/webrtc/v3"
"github.com/demodesk/neko/pkg/types"
)
/////////////////////////////
// System
/////////////////////////////
type SystemWebRTC struct {
Videos []string `json:"videos"`
}
type SystemInit struct {
SessionId string `json:"session_id"`
ControlHost ControlHost `json:"control_host"`
ScreenSize types.ScreenSize `json:"screen_size"`
Sessions map[string]SessionData `json:"sessions"`
Settings types.Settings `json:"settings"`
TouchEvents bool `json:"touch_events"`
ScreencastEnabled bool `json:"screencast_enabled"`
WebRTC SystemWebRTC `json:"webrtc"`
}
type SystemAdmin struct {
ScreenSizesList []types.ScreenSize `json:"screen_sizes_list"`
BroadcastStatus BroadcastStatus `json:"broadcast_status"`
}
type SystemLogs = []SystemLog
type SystemLog struct {
Level string `json:"level"`
Fields map[string]any `json:"fields"`
Message string `json:"message"`
}
type SystemDisconnect struct {
Message string `json:"message"`
}
type SystemSettingsUpdate struct {
ID string `json:"id"`
types.Settings
}
/////////////////////////////
// Signal
/////////////////////////////
type SignalRequest struct {
Video types.PeerVideoRequest `json:"video"`
Audio types.PeerAudioRequest `json:"audio"`
Auto bool `json:"auto"` // TODO: Remove this
}
type SignalProvide struct {
SDP string `json:"sdp"`
ICEServers []types.ICEServer `json:"iceservers"`
Video types.PeerVideo `json:"video"`
Audio types.PeerAudio `json:"audio"`
}
type SignalCandidate struct {
webrtc.ICECandidateInit
}
type SignalDescription struct {
SDP string `json:"sdp"`
}
type SignalVideo struct {
types.PeerVideoRequest
}
type SignalAudio struct {
types.PeerAudioRequest
}
/////////////////////////////
// Session
/////////////////////////////
type SessionID struct {
ID string `json:"id"`
}
type MemberProfile struct {
ID string `json:"id"`
types.MemberProfile
}
type SessionState struct {
ID string `json:"id"`
types.SessionState
}
type SessionData struct {
ID string `json:"id"`
Profile types.MemberProfile `json:"profile"`
State types.SessionState `json:"state"`
}
type SessionCursors struct {
ID string `json:"id"`
Cursors []types.Cursor `json:"cursors"`
}
/////////////////////////////
// Control
/////////////////////////////
type ControlHost struct {
ID string `json:"id"`
HasHost bool `json:"has_host"`
HostID string `json:"host_id,omitempty"`
}
type ControlScroll struct {
// TOOD: remove this once the client is fixed
X int `json:"x"`
Y int `json:"y"`
DeltaX int `json:"delta_x"`
DeltaY int `json:"delta_y"`
ControlKey bool `json:"control_key"`
}
type ControlPos struct {
X int `json:"x"`
Y int `json:"y"`
}
type ControlButton struct {
*ControlPos
Code uint32 `json:"code"`
}
type ControlKey struct {
*ControlPos
Keysym uint32 `json:"keysym"`
}
type ControlTouch struct {
TouchId uint32 `json:"touch_id"`
*ControlPos
Pressure uint8 `json:"pressure"`
}
/////////////////////////////
// Screen
/////////////////////////////
type ScreenSize struct {
types.ScreenSize
}
type ScreenSizeUpdate struct {
ID string `json:"id"`
types.ScreenSize
}
/////////////////////////////
// Clipboard
/////////////////////////////
type ClipboardData struct {
Text string `json:"text"`
}
/////////////////////////////
// Keyboard
/////////////////////////////
type KeyboardMap struct {
types.KeyboardMap
}
type KeyboardModifiers struct {
types.KeyboardModifiers
}
/////////////////////////////
// Broadcast
/////////////////////////////
type BroadcastStatus struct {
IsActive bool `json:"is_active"`
URL string `json:"url,omitempty"`
}
/////////////////////////////
// Send (opaque comunication channel)
/////////////////////////////
type SendUnicast struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Subject string `json:"subject"`
Body any `json:"body"`
}
type SendBroadcast struct {
Sender string `json:"sender"`
Subject string `json:"subject"`
Body any `json:"body"`
}

View File

@ -0,0 +1,91 @@
package types
import (
"errors"
"fmt"
"strings"
"github.com/demodesk/neko/pkg/utils"
"github.com/spf13/cobra"
)
var (
ErrPluginSettingsNotFound = errors.New("plugin settings not found")
)
type Plugin interface {
Name() string
Config() PluginConfig
Start(PluginManagers) error
Shutdown() error
}
type DependablePlugin interface {
Plugin
DependsOn() []string
}
type ExposablePlugin interface {
Plugin
ExposeService() any
}
type PluginConfig interface {
Init(cmd *cobra.Command) error
Set()
}
type PluginMetadata struct {
Name string
IsDependable bool
IsExposable bool
DependsOn []string `json:",omitempty"`
}
type PluginManagers struct {
SessionManager SessionManager
WebSocketManager WebSocketManager
ApiManager ApiManager
LoadServiceFromPlugin func(string) (any, error)
}
func (p *PluginManagers) Validate() error {
if p.SessionManager == nil {
return errors.New("SessionManager is nil")
}
if p.WebSocketManager == nil {
return errors.New("WebSocketManager is nil")
}
if p.ApiManager == nil {
return errors.New("ApiManager is nil")
}
if p.LoadServiceFromPlugin == nil {
return errors.New("LoadServiceFromPlugin is nil")
}
return nil
}
type PluginSettings map[string]any
func (p PluginSettings) Unmarshal(name string, def any) error {
if p == nil {
return fmt.Errorf("%w: %s", ErrPluginSettingsNotFound, name)
}
// loop through the plugin settings and take only the one that starts with the name
// because the settings are stored in a map["plugin_name.setting_name"] = value
newMap := make(map[string]any)
for k, v := range p {
if strings.HasPrefix(k, name+".") {
newMap[strings.TrimPrefix(k, name+".")] = v
}
}
fmt.Printf("newMap: %+v\n", newMap)
if len(newMap) == 0 {
return fmt.Errorf("%w: %s", ErrPluginSettingsNotFound, name)
}
return utils.Decode(newMap, def)
}

115
server/pkg/types/session.go Normal file
View File

@ -0,0 +1,115 @@
package types
import (
"errors"
"net/http"
"time"
)
var (
ErrSessionNotFound = errors.New("session not found")
ErrSessionAlreadyExists = errors.New("session already exists")
ErrSessionAlreadyConnected = errors.New("session is already connected")
ErrSessionLoginDisabled = errors.New("session login disabled")
ErrSessionLoginsLocked = errors.New("session logins locked")
)
type Cursor struct {
X int `json:"x"`
Y int `json:"y"`
}
type SessionProfile struct {
Id string
Token string
Profile MemberProfile
}
type SessionState struct {
IsConnected bool `json:"is_connected"`
// when the session was last connected
ConnectedSince *time.Time `json:"connected_since,omitempty"`
// when the session was last not connected
NotConnectedSince *time.Time `json:"not_connected_since,omitempty"`
IsWatching bool `json:"is_watching"`
// when the session was last watching
WatchingSince *time.Time `json:"watching_since,omitempty"`
// when the session was last not watching
NotWatchingSince *time.Time `json:"not_watching_since,omitempty"`
}
type Settings struct {
PrivateMode bool `json:"private_mode"`
LockedLogins bool `json:"locked_logins"`
LockedControls bool `json:"locked_controls"`
ControlProtection bool `json:"control_protection"`
ImplicitHosting bool `json:"implicit_hosting"`
InactiveCursors bool `json:"inactive_cursors"`
MercifulReconnect bool `json:"merciful_reconnect"`
// plugin scope
Plugins PluginSettings `json:"plugins"`
}
type Session interface {
ID() string
Profile() MemberProfile
State() SessionState
IsHost() bool
SetAsHost()
SetAsHostBy(session Session)
ClearHost()
PrivateModeEnabled() bool
// cursor
SetCursor(cursor Cursor)
// websocket
ConnectWebSocketPeer(websocketPeer WebSocketPeer)
DisconnectWebSocketPeer(websocketPeer WebSocketPeer, delayed bool)
DestroyWebSocketPeer(reason string)
Send(event string, payload any)
// webrtc
SetWebRTCPeer(webrtcPeer WebRTCPeer)
SetWebRTCConnected(webrtcPeer WebRTCPeer, connected bool)
GetWebRTCPeer() WebRTCPeer
}
type SessionManager interface {
Create(id string, profile MemberProfile) (Session, string, error)
Update(id string, profile MemberProfile) error
Delete(id string) error
Disconnect(id string) error
Get(id string) (Session, bool)
GetByToken(token string) (Session, bool)
List() []Session
Range(func(Session) bool)
GetHost() (Session, bool)
SetCursor(cursor Cursor, session Session)
PopCursors() map[Session][]Cursor
Broadcast(event string, payload any, exclude ...string)
AdminBroadcast(event string, payload any, exclude ...string)
InactiveCursorsBroadcast(event string, payload any, exclude ...string)
OnCreated(listener func(session Session))
OnDeleted(listener func(session Session))
OnConnected(listener func(session Session))
OnDisconnected(listener func(session Session))
OnProfileChanged(listener func(session Session, new, old MemberProfile))
OnStateChanged(listener func(session Session))
OnHostChanged(listener func(session, host Session))
OnSettingsChanged(listener func(session Session, new, old Settings))
UpdateSettingsFunc(session Session, f func(settings *Settings) bool)
Settings() Settings
CookieEnabled() bool
CookieSetToken(w http.ResponseWriter, token string)
CookieClearToken(w http.ResponseWriter, r *http.Request)
Authenticate(r *http.Request) (Session, error)
}

View File

@ -0,0 +1,70 @@
package types
import (
"errors"
"github.com/pion/webrtc/v3"
)
var (
ErrWebRTCDataChannelNotFound = errors.New("webrtc data channel not found")
ErrWebRTCConnectionNotFound = errors.New("webrtc connection not found")
ErrWebRTCStreamNotFound = errors.New("webrtc stream not found")
)
type ICEServer struct {
URLs []string `mapstructure:"urls" json:"urls"`
Username string `mapstructure:"username" json:"username,omitempty"`
Credential string `mapstructure:"credential" json:"credential,omitempty"`
}
type PeerVideo struct {
Disabled bool `json:"disabled"`
ID string `json:"id"`
Video string `json:"video"` // TODO: Remove this, used for compatibility with old clients.
Auto bool `json:"auto"`
}
type PeerVideoRequest struct {
Disabled *bool `json:"disabled,omitempty"`
Selector *StreamSelector `json:"selector,omitempty"`
Auto *bool `json:"auto,omitempty"`
}
type PeerAudio struct {
Disabled bool `json:"disabled"`
}
type PeerAudioRequest struct {
Disabled *bool `json:"disabled,omitempty"`
}
type WebRTCPeer interface {
CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error)
CreateAnswer() (*webrtc.SessionDescription, error)
SetRemoteDescription(webrtc.SessionDescription) error
SetCandidate(webrtc.ICECandidateInit) error
SetPaused(isPaused bool) error
Paused() bool
SetVideo(PeerVideoRequest) error
Video() PeerVideo
SetAudio(PeerAudioRequest) error
Audio() PeerAudio
SendCursorPosition(x, y int) error
SendCursorImage(cur *CursorImage, img []byte) error
Destroy()
}
type WebRTCManager interface {
Start()
Shutdown() error
ICEServers() []ICEServer
CreatePeer(session Session) (*webrtc.SessionDescription, WebRTCPeer, error)
SetCursorPosition(x, y int)
}

View File

@ -0,0 +1,28 @@
package types
import (
"encoding/json"
"net/http"
)
type WebSocketMessage struct {
Event string `json:"event"`
Payload json.RawMessage `json:"payload,omitempty"`
}
type WebSocketHandler func(Session, WebSocketMessage) bool
type CheckOrigin func(r *http.Request) bool
type WebSocketPeer interface {
Send(event string, payload any)
Ping() error
Destroy(reason string)
}
type WebSocketManager interface {
Start()
Shutdown() error
AddHandler(handler WebSocketHandler)
Upgrade(checkOrigin CheckOrigin) RouterHandler
}