first commit
129
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
FROM golang:stretch
|
||||||
|
|
||||||
|
# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser"
|
||||||
|
# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs
|
||||||
|
# will be updated to match your local UID/GID (when using the dockerFile property).
|
||||||
|
# See https://aka.ms/vscode-remote/containers/non-root-user for details.
|
||||||
|
ARG USERNAME=vscode
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
|
||||||
|
# Avoid warnings by switching to noninteractive
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Runtime for testing and compileing
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get -y install supervisor openbox dbus-x11 ttf-freefont xvfb pulseaudio consolekit firefox-esr x11vnc \
|
||||||
|
&& apt-get -y install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good gstreamer1.0-pulseaudio \
|
||||||
|
&& apt-get -y install gcc libc6-dev \
|
||||||
|
&& apt-get -y install libx11-dev xorg-dev libxtst-dev libpng++-dev \
|
||||||
|
&& apt-get -y install xcb libxcb-xkb-dev x11-xkb-utils libx11-xcb-dev libxkbcommon-x11-dev \
|
||||||
|
&& apt-get -y install libxkbcommon-dev \
|
||||||
|
&& apt-get -y install xsel xclip
|
||||||
|
|
||||||
|
# Configure apt, install packages and tools
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get -y install --no-install-recommends apt-utils apt-transport-https dialog 2>&1 \
|
||||||
|
#
|
||||||
|
# Verify git, process tools, lsb-release (common in install instructions for CLIs) installed
|
||||||
|
&& apt-get -y install git iproute2 procps lsb-release xz-utils \
|
||||||
|
#
|
||||||
|
# Install Docker CE CLI
|
||||||
|
&& apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common lsb-release \
|
||||||
|
&& curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | (OUT=$(apt-key add - 2>&1) || echo $OUT) \
|
||||||
|
&& add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y docker-ce-cli \
|
||||||
|
#
|
||||||
|
# Install Docker Compose
|
||||||
|
&& curl -sSL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose \
|
||||||
|
&& chmod +x /usr/local/bin/docker-compose \
|
||||||
|
#
|
||||||
|
# Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user.
|
||||||
|
&& groupadd --gid $USER_GID $USERNAME \
|
||||||
|
&& useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME \
|
||||||
|
&& adduser $USERNAME audio \
|
||||||
|
&& adduser $USERNAME video \
|
||||||
|
&& adduser $USERNAME pulse \
|
||||||
|
# Add sudo support for the non-root user
|
||||||
|
&& apt-get install -y sudo \
|
||||||
|
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/$USERNAME
|
||||||
|
|
||||||
|
ENV NODE_VERSION 12.14.1
|
||||||
|
|
||||||
|
RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
|
||||||
|
&& case "${dpkgArch##*-}" in \
|
||||||
|
amd64) ARCH='x64';; \
|
||||||
|
ppc64el) ARCH='ppc64le';; \
|
||||||
|
s390x) ARCH='s390x';; \
|
||||||
|
arm64) ARCH='arm64';; \
|
||||||
|
armhf) ARCH='armv7l';; \
|
||||||
|
i386) ARCH='x86';; \
|
||||||
|
*) echo "unsupported architecture"; exit 1 ;; \
|
||||||
|
esac \
|
||||||
|
# gpg keys listed at https://github.com/nodejs/node#release-keys
|
||||||
|
&& set -ex \
|
||||||
|
&& for key in \
|
||||||
|
94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
|
||||||
|
FD3A5288F042B6850C66B31F09FE44734EB7990E \
|
||||||
|
71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
|
||||||
|
DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
|
||||||
|
C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
|
||||||
|
B9AE9905FFD7803F25714661B63B535A4C206CA9 \
|
||||||
|
77984A986EBC2AA786BC0F66B01FBB92821C587A \
|
||||||
|
8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \
|
||||||
|
4ED778F539E3634C779C87C6D7062848A1AB005C \
|
||||||
|
A48C2BEE680E841632CD4E44F07496B3EB3C1762 \
|
||||||
|
B9E2F5981AA6E0CD28160D9FF13993A75599653C \
|
||||||
|
; do \
|
||||||
|
gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys "$key" || \
|
||||||
|
gpg --batch --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys "$key" || \
|
||||||
|
gpg --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys "$key" ; \
|
||||||
|
done \
|
||||||
|
&& curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \
|
||||||
|
&& curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
|
||||||
|
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
|
||||||
|
&& grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
|
||||||
|
&& tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
|
||||||
|
&& rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
|
||||||
|
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
|
||||||
|
|
||||||
|
USER $USERNAME
|
||||||
|
|
||||||
|
# Install packages and tools
|
||||||
|
RUN go get -x -d github.com/stamblerre/gocode 2>&1 \
|
||||||
|
&& go build -o gocode-gomod github.com/stamblerre/gocode \
|
||||||
|
&& mv gocode-gomod $GOPATH/bin/ \
|
||||||
|
#
|
||||||
|
# Install Go tools
|
||||||
|
&& go get -u -v \
|
||||||
|
github.com/mdempsky/gocode \
|
||||||
|
github.com/uudashr/gopkgs/cmd/gopkgs \
|
||||||
|
github.com/ramya-rao-a/go-outline \
|
||||||
|
github.com/acroca/go-symbols \
|
||||||
|
github.com/godoctor/godoctor \
|
||||||
|
golang.org/x/tools/cmd/guru \
|
||||||
|
golang.org/x/tools/cmd/gorename \
|
||||||
|
github.com/rogpeppe/godef \
|
||||||
|
github.com/zmb3/gogetdoc \
|
||||||
|
github.com/haya14busa/goplay/cmd/goplay \
|
||||||
|
github.com/sqs/goreturns \
|
||||||
|
github.com/josharian/impl \
|
||||||
|
github.com/davidrjenni/reftools/cmd/fillstruct \
|
||||||
|
github.com/fatih/gomodifytags \
|
||||||
|
github.com/cweill/gotests/... \
|
||||||
|
golang.org/x/tools/cmd/goimports \
|
||||||
|
golang.org/x/lint/golint \
|
||||||
|
golang.org/x/tools/gopls \
|
||||||
|
github.com/alecthomas/gometalinter \
|
||||||
|
honnef.co/go/tools/... \
|
||||||
|
github.com/golangci/golangci-lint/cmd/golangci-lint \
|
||||||
|
github.com/mgechev/revive \
|
||||||
|
github.com/derekparker/delve/cmd/dlv 2>&1
|
||||||
|
|
||||||
|
ENV GO111MODULE=on
|
||||||
|
|
||||||
|
# Switch back to dialog for any ad-hoc use of apt-get
|
||||||
|
ENV DEBIAN_FRONTEND=dialog
|
||||||
|
|
25
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "neko",
|
||||||
|
"service": "neko",
|
||||||
|
"dockerComposeFile": "docker-compose.yaml",
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
"remoteUser": "vscode",
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
|
"go.gopath": "/go"
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"ms-vscode.go",
|
||||||
|
"octref.vetur",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-vscode-remote.vscode-remote-extensionpack",
|
||||||
|
"ms-vscode-remote.remote-containers",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"psioniq.psi-header",
|
||||||
|
"gruntfuggly.todo-tree",
|
||||||
|
"swyphcosmo.spellchecker",
|
||||||
|
"eamodio.gitlens"
|
||||||
|
]
|
||||||
|
}
|
16
.devcontainer/docker-compose.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
version: '3.6'
|
||||||
|
services:
|
||||||
|
neko:
|
||||||
|
network_mode: host
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
shm_size: '2gb'
|
||||||
|
cap_add:
|
||||||
|
- SYS_PTRACE
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
volumes:
|
||||||
|
- /home/nurd/neko:/workspace
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
command: "/bin/sh -c \"while sleep 1000; do :; done\""
|
10
.docker/build.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd ../server
|
||||||
|
go get && make build
|
||||||
|
|
||||||
|
cd ../client
|
||||||
|
npm install && npm run build
|
||||||
|
|
||||||
|
cd ../
|
||||||
|
docker build -f Dockerfile -t neko .
|
7
.docker/entrypoint.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Starting dbus"
|
||||||
|
/etc/init.d/dbus start
|
||||||
|
|
||||||
|
echo "Starting supervisord"
|
||||||
|
su -p -l $NEKO_USER -c '/usr/bin/supervisord -c /etc/neko/supervisord.conf' -s /bin/bash
|
760
.docker/openbox.xml
Normal file
@ -0,0 +1,760 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!-- Default openbox config but all window decorations are moved
|
||||||
|
thereby making it harder to accidentally close the virtual browser -->
|
||||||
|
|
||||||
|
<openbox_config xmlns="http://openbox.org/3.4/rc"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
|
||||||
|
<resistance>
|
||||||
|
<strength>10</strength>
|
||||||
|
<screen_edge_strength>20</screen_edge_strength>
|
||||||
|
</resistance>
|
||||||
|
|
||||||
|
<applications>
|
||||||
|
<!-- Match all windows and remove their decorations -->
|
||||||
|
<application class="*"> <decor>no</decor> </application>
|
||||||
|
<!-- Make the window fullscreen -->
|
||||||
|
<fullscreen>yes</fullscreen>
|
||||||
|
</applications>
|
||||||
|
|
||||||
|
<focus>
|
||||||
|
<focusNew>yes</focusNew>
|
||||||
|
<!-- always try to focus new windows when they appear. other rules do
|
||||||
|
apply -->
|
||||||
|
<followMouse>no</followMouse>
|
||||||
|
<!-- move focus to a window when you move the mouse into it -->
|
||||||
|
<focusLast>yes</focusLast>
|
||||||
|
<!-- focus the last used window when changing desktops, instead of the one
|
||||||
|
under the mouse pointer. when followMouse is enabled -->
|
||||||
|
<underMouse>no</underMouse>
|
||||||
|
<!-- move focus under the mouse, even when the mouse is not moving -->
|
||||||
|
<focusDelay>200</focusDelay>
|
||||||
|
<!-- when followMouse is enabled, the mouse must be inside the window for
|
||||||
|
this many milliseconds (1000 = 1 sec) before moving focus to it -->
|
||||||
|
<raiseOnFocus>no</raiseOnFocus>
|
||||||
|
<!-- when followMouse is enabled, and a window is given focus by moving the
|
||||||
|
mouse into it, also raise the window -->
|
||||||
|
</focus>
|
||||||
|
|
||||||
|
<placement>
|
||||||
|
<policy>Smart</policy>
|
||||||
|
<!-- 'Smart' or 'UnderMouse' -->
|
||||||
|
<center>yes</center>
|
||||||
|
<!-- whether to place windows in the center of the free area found or
|
||||||
|
the top left corner -->
|
||||||
|
<monitor>Primary</monitor>
|
||||||
|
<!-- with Smart placement on a multi-monitor system, try to place new windows
|
||||||
|
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
|
||||||
|
the active window is, 'Primary' - only on the primary monitor -->
|
||||||
|
<primaryMonitor>1</primaryMonitor>
|
||||||
|
<!-- The monitor where Openbox should place popup dialogs such as the
|
||||||
|
focus cycling popup, or the desktop switch popup. It can be an index
|
||||||
|
from 1, specifying a particular monitor. Or it can be one of the
|
||||||
|
following: 'Mouse' - where the mouse is, or
|
||||||
|
'Active' - where the active window is -->
|
||||||
|
</placement>
|
||||||
|
|
||||||
|
<theme>
|
||||||
|
<name>Clearlooks</name>
|
||||||
|
<titleLayout>NLIMC</titleLayout>
|
||||||
|
<!--
|
||||||
|
available characters are NDSLIMC, each can occur at most once.
|
||||||
|
N: window icon
|
||||||
|
L: window label (AKA title).
|
||||||
|
I: iconify
|
||||||
|
M: maximize
|
||||||
|
C: close
|
||||||
|
S: shade (roll up/down)
|
||||||
|
D: omnipresent (on all desktops).
|
||||||
|
-->
|
||||||
|
<keepBorder>yes</keepBorder>
|
||||||
|
<animateIconify>yes</animateIconify>
|
||||||
|
<font place="ActiveWindow">
|
||||||
|
<name>sans</name>
|
||||||
|
<size>8</size>
|
||||||
|
<!-- font size in points -->
|
||||||
|
<weight>bold</weight>
|
||||||
|
<!-- 'bold' or 'normal' -->
|
||||||
|
<slant>normal</slant>
|
||||||
|
<!-- 'italic' or 'normal' -->
|
||||||
|
</font>
|
||||||
|
<font place="InactiveWindow">
|
||||||
|
<name>sans</name>
|
||||||
|
<size>8</size>
|
||||||
|
<!-- font size in points -->
|
||||||
|
<weight>bold</weight>
|
||||||
|
<!-- 'bold' or 'normal' -->
|
||||||
|
<slant>normal</slant>
|
||||||
|
<!-- 'italic' or 'normal' -->
|
||||||
|
</font>
|
||||||
|
<font place="MenuHeader">
|
||||||
|
<name>sans</name>
|
||||||
|
<size>9</size>
|
||||||
|
<!-- font size in points -->
|
||||||
|
<weight>normal</weight>
|
||||||
|
<!-- 'bold' or 'normal' -->
|
||||||
|
<slant>normal</slant>
|
||||||
|
<!-- 'italic' or 'normal' -->
|
||||||
|
</font>
|
||||||
|
<font place="MenuItem">
|
||||||
|
<name>sans</name>
|
||||||
|
<size>9</size>
|
||||||
|
<!-- font size in points -->
|
||||||
|
<weight>normal</weight>
|
||||||
|
<!-- 'bold' or 'normal' -->
|
||||||
|
<slant>normal</slant>
|
||||||
|
<!-- 'italic' or 'normal' -->
|
||||||
|
</font>
|
||||||
|
<font place="ActiveOnScreenDisplay">
|
||||||
|
<name>sans</name>
|
||||||
|
<size>9</size>
|
||||||
|
<!-- font size in points -->
|
||||||
|
<weight>bold</weight>
|
||||||
|
<!-- 'bold' or 'normal' -->
|
||||||
|
<slant>normal</slant>
|
||||||
|
<!-- 'italic' or 'normal' -->
|
||||||
|
</font>
|
||||||
|
<font place="InactiveOnScreenDisplay">
|
||||||
|
<name>sans</name>
|
||||||
|
<size>9</size>
|
||||||
|
<!-- font size in points -->
|
||||||
|
<weight>bold</weight>
|
||||||
|
<!-- 'bold' or 'normal' -->
|
||||||
|
<slant>normal</slant>
|
||||||
|
<!-- 'italic' or 'normal' -->
|
||||||
|
</font>
|
||||||
|
</theme>
|
||||||
|
|
||||||
|
<desktops>
|
||||||
|
<!-- this stuff is only used at startup, pagers allow you to change them
|
||||||
|
during a session
|
||||||
|
|
||||||
|
these are default values to use when other ones are not already set
|
||||||
|
by other applications, or saved in your session
|
||||||
|
|
||||||
|
use obconf if you want to change these without having to log out
|
||||||
|
and back in -->
|
||||||
|
<number>4</number>
|
||||||
|
<firstdesk>1</firstdesk>
|
||||||
|
<names>
|
||||||
|
<!-- set names up here if you want to, like this:
|
||||||
|
<name>desktop 1</name>
|
||||||
|
<name>desktop 2</name>
|
||||||
|
-->
|
||||||
|
</names>
|
||||||
|
<popupTime>875</popupTime>
|
||||||
|
<!-- The number of milliseconds to show the popup for when switching
|
||||||
|
desktops. Set this to 0 to disable the popup. -->
|
||||||
|
</desktops>
|
||||||
|
|
||||||
|
<resize>
|
||||||
|
<drawContents>yes</drawContents>
|
||||||
|
<popupShow>Nonpixel</popupShow>
|
||||||
|
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
|
||||||
|
<popupPosition>Center</popupPosition>
|
||||||
|
<!-- 'Center', 'Top', or 'Fixed' -->
|
||||||
|
<popupFixedPosition>
|
||||||
|
<!-- these are used if popupPosition is set to 'Fixed' -->
|
||||||
|
|
||||||
|
<x>10</x>
|
||||||
|
<!-- positive number for distance from left edge, negative number for
|
||||||
|
distance from right edge, or 'Center' -->
|
||||||
|
<y>10</y>
|
||||||
|
<!-- positive number for distance from top edge, negative number for
|
||||||
|
distance from bottom edge, or 'Center' -->
|
||||||
|
</popupFixedPosition>
|
||||||
|
</resize>
|
||||||
|
|
||||||
|
<!-- You can reserve a portion of your screen where windows will not cover when
|
||||||
|
they are maximized, or when they are initially placed.
|
||||||
|
Many programs reserve space automatically, but you can use this in other
|
||||||
|
cases. -->
|
||||||
|
<margins>
|
||||||
|
<top>0</top>
|
||||||
|
<bottom>0</bottom>
|
||||||
|
<left>0</left>
|
||||||
|
<right>0</right>
|
||||||
|
</margins>
|
||||||
|
|
||||||
|
<dock>
|
||||||
|
<position>TopLeft</position>
|
||||||
|
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
|
||||||
|
<floatingX>0</floatingX>
|
||||||
|
<floatingY>0</floatingY>
|
||||||
|
<noStrut>no</noStrut>
|
||||||
|
<stacking>Above</stacking>
|
||||||
|
<!-- 'Above', 'Normal', or 'Below' -->
|
||||||
|
<direction>Vertical</direction>
|
||||||
|
<!-- 'Vertical' or 'Horizontal' -->
|
||||||
|
<autoHide>no</autoHide>
|
||||||
|
<hideDelay>300</hideDelay>
|
||||||
|
<!-- in milliseconds (1000 = 1 second) -->
|
||||||
|
<showDelay>300</showDelay>
|
||||||
|
<!-- in milliseconds (1000 = 1 second) -->
|
||||||
|
<moveButton>Middle</moveButton>
|
||||||
|
<!-- 'Left', 'Middle', 'Right' -->
|
||||||
|
</dock>
|
||||||
|
|
||||||
|
<keyboard>
|
||||||
|
<chainQuitKey>C-g</chainQuitKey>
|
||||||
|
|
||||||
|
<!-- Keybindings for desktop switching -->
|
||||||
|
<keybind key="C-A-Left">
|
||||||
|
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="C-A-Right">
|
||||||
|
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="C-A-Up">
|
||||||
|
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="C-A-Down">
|
||||||
|
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="S-A-Left">
|
||||||
|
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="S-A-Right">
|
||||||
|
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="S-A-Up">
|
||||||
|
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="S-A-Down">
|
||||||
|
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-F1">
|
||||||
|
<action name="GoToDesktop"><to>1</to></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-F2">
|
||||||
|
<action name="GoToDesktop"><to>2</to></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-F3">
|
||||||
|
<action name="GoToDesktop"><to>3</to></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-F4">
|
||||||
|
<action name="GoToDesktop"><to>4</to></action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-d">
|
||||||
|
<action name="ToggleShowDesktop"/>
|
||||||
|
</keybind>
|
||||||
|
|
||||||
|
<!-- Keybindings for windows -->
|
||||||
|
<keybind key="A-F4">
|
||||||
|
<action name="Close"/>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="A-Escape">
|
||||||
|
<action name="Lower"/>
|
||||||
|
<action name="FocusToBottom"/>
|
||||||
|
<action name="Unfocus"/>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="A-space">
|
||||||
|
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
|
||||||
|
</keybind>
|
||||||
|
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
|
||||||
|
<keybind key="A-Print">
|
||||||
|
<action name="Execute"><command>scrot -s</command></action>
|
||||||
|
</keybind>
|
||||||
|
|
||||||
|
<!-- Keybindings for window switching -->
|
||||||
|
<keybind key="A-Tab">
|
||||||
|
<action name="NextWindow">
|
||||||
|
<finalactions>
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</finalactions>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="A-S-Tab">
|
||||||
|
<action name="PreviousWindow">
|
||||||
|
<finalactions>
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</finalactions>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="C-A-Tab">
|
||||||
|
<action name="NextWindow">
|
||||||
|
<panels>yes</panels><desktop>yes</desktop>
|
||||||
|
<finalactions>
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</finalactions>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
|
||||||
|
<!-- Keybindings for window switching with the arrow keys -->
|
||||||
|
<keybind key="W-S-Right">
|
||||||
|
<action name="DirectionalCycleWindows">
|
||||||
|
<direction>right</direction>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-S-Left">
|
||||||
|
<action name="DirectionalCycleWindows">
|
||||||
|
<direction>left</direction>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-S-Up">
|
||||||
|
<action name="DirectionalCycleWindows">
|
||||||
|
<direction>up</direction>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
<keybind key="W-S-Down">
|
||||||
|
<action name="DirectionalCycleWindows">
|
||||||
|
<direction>down</direction>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
|
||||||
|
<!-- Keybindings for running applications -->
|
||||||
|
<keybind key="W-e">
|
||||||
|
<action name="Execute">
|
||||||
|
<startupnotify>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
<name>Konqueror</name>
|
||||||
|
</startupnotify>
|
||||||
|
<command>kfmclient openProfile filemanagement</command>
|
||||||
|
</action>
|
||||||
|
</keybind>
|
||||||
|
<!-- Launch scrot when Print is pressed -->
|
||||||
|
<keybind key="Print">
|
||||||
|
<action name="Execute"><command>scrot</command></action>
|
||||||
|
</keybind>
|
||||||
|
</keyboard>
|
||||||
|
|
||||||
|
<mouse>
|
||||||
|
<dragThreshold>1</dragThreshold>
|
||||||
|
<!-- number of pixels the mouse must move before a drag begins -->
|
||||||
|
<doubleClickTime>500</doubleClickTime>
|
||||||
|
<!-- in milliseconds (1000 = 1 second) -->
|
||||||
|
<screenEdgeWarpTime>400</screenEdgeWarpTime>
|
||||||
|
<!-- Time before changing desktops when the pointer touches the edge of the
|
||||||
|
screen while moving a window, in milliseconds (1000 = 1 second).
|
||||||
|
Set this to 0 to disable warping -->
|
||||||
|
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
|
||||||
|
<!-- Set this to TRUE to move the mouse pointer across the desktop when
|
||||||
|
switching due to hitting the edge of the screen -->
|
||||||
|
|
||||||
|
<context name="Frame">
|
||||||
|
<mousebind button="A-Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Left" action="Click">
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Left" action="Drag">
|
||||||
|
<action name="Move"/>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="A-Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Right" action="Drag">
|
||||||
|
<action name="Resize"/>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="A-Middle" action="Press">
|
||||||
|
<action name="Lower"/>
|
||||||
|
<action name="FocusToBottom"/>
|
||||||
|
<action name="Unfocus"/>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="A-Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="C-A-Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="C-A-Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-S-Up" action="Click">
|
||||||
|
<action name="SendToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-S-Down" action="Click">
|
||||||
|
<action name="SendToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Titlebar">
|
||||||
|
<mousebind button="Left" action="Drag">
|
||||||
|
<action name="Move"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="DoubleClick">
|
||||||
|
<action name="ToggleMaximize"/>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="Up" action="Click">
|
||||||
|
<action name="if">
|
||||||
|
<shaded>no</shaded>
|
||||||
|
<then>
|
||||||
|
<action name="Shade"/>
|
||||||
|
<action name="FocusToBottom"/>
|
||||||
|
<action name="Unfocus"/>
|
||||||
|
<action name="Lower"/>
|
||||||
|
</then>
|
||||||
|
</action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Down" action="Click">
|
||||||
|
<action name="if">
|
||||||
|
<shaded>yes</shaded>
|
||||||
|
<then>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</then>
|
||||||
|
</action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="Middle" action="Press">
|
||||||
|
<action name="Lower"/>
|
||||||
|
<action name="FocusToBottom"/>
|
||||||
|
<action name="Unfocus"/>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<!--mousebind button="Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||||
|
</mousebind-->
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Top">
|
||||||
|
<mousebind button="Left" action="Drag">
|
||||||
|
<action name="Resize"><edge>top</edge></action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Left">
|
||||||
|
<mousebind button="Left" action="Drag">
|
||||||
|
<action name="Resize"><edge>left</edge></action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Right">
|
||||||
|
<mousebind button="Left" action="Drag">
|
||||||
|
<action name="Resize"><edge>right</edge></action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Bottom">
|
||||||
|
<mousebind button="Left" action="Drag">
|
||||||
|
<action name="Resize"><edge>bottom</edge></action>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<!--mousebind button="Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||||
|
</mousebind-->
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="TRCorner BRCorner TLCorner BLCorner">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="Drag">
|
||||||
|
<action name="Resize"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Client">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Middle" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Icon">
|
||||||
|
<!--mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="ShowMenu"><menu>client-menu</menu></action>
|
||||||
|
</mousebind-->
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="AllDesktops">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="Click">
|
||||||
|
<action name="ToggleOmnipresent"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Shade">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="Click">
|
||||||
|
<action name="ToggleShade"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Iconify">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="Click">
|
||||||
|
<action name="Iconify"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Maximize">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Middle" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="Click">
|
||||||
|
<action name="ToggleMaximize"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Middle" action="Click">
|
||||||
|
<action name="ToggleMaximize"><direction>vertical</direction></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Right" action="Click">
|
||||||
|
<action name="ToggleMaximize"><direction>horizontal</direction></action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Close">
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
<action name="Unshade"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Left" action="Click">
|
||||||
|
<action name="Close"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Desktop">
|
||||||
|
<mousebind button="Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="A-Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="C-A-Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="C-A-Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
|
||||||
|
<mousebind button="Left" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Right" action="Press">
|
||||||
|
<action name="Focus"/>
|
||||||
|
<action name="Raise"/>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="Root">
|
||||||
|
<!-- Menus -->
|
||||||
|
<!--mousebind button="Middle" action="Press">
|
||||||
|
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Right" action="Press">
|
||||||
|
<action name="ShowMenu"><menu>root-menu</menu></action>
|
||||||
|
</mousebind-->
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<context name="MoveResize">
|
||||||
|
<mousebind button="Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Up" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>previous</to></action>
|
||||||
|
</mousebind>
|
||||||
|
<mousebind button="A-Down" action="Click">
|
||||||
|
<action name="GoToDesktop"><to>next</to></action>
|
||||||
|
</mousebind>
|
||||||
|
</context>
|
||||||
|
</mouse>
|
||||||
|
|
||||||
|
<menu>
|
||||||
|
<!-- You can specify more than one menu file in here and they are all loaded,
|
||||||
|
just don't make menu ids clash or, well, it'll be kind of pointless -->
|
||||||
|
|
||||||
|
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
|
||||||
|
<!-- system menu files on Debian systems -->
|
||||||
|
<file>/var/lib/openbox/debian-menu.xml</file>
|
||||||
|
<file>menu.xml</file>
|
||||||
|
<hideDelay>200</hideDelay>
|
||||||
|
<!-- if a press-release lasts longer than this setting (in milliseconds), the
|
||||||
|
menu is hidden again -->
|
||||||
|
<middle>no</middle>
|
||||||
|
<!-- center submenus vertically about the parent entry -->
|
||||||
|
<submenuShowDelay>100</submenuShowDelay>
|
||||||
|
<!-- time to delay before showing a submenu after hovering over the parent
|
||||||
|
entry.
|
||||||
|
if this is a negative value, then the delay is infinite and the
|
||||||
|
submenu will not be shown until it is clicked on -->
|
||||||
|
<submenuHideDelay>400</submenuHideDelay>
|
||||||
|
<!-- time to delay before hiding a submenu when selecting another
|
||||||
|
entry in parent menu
|
||||||
|
if this is a negative value, then the delay is infinite and the
|
||||||
|
submenu will not be hidden until a different submenu is opened -->
|
||||||
|
<showIcons>yes</showIcons>
|
||||||
|
<!-- controls if icons appear in the client-list-(combined-)menu -->
|
||||||
|
<manageDesktops>yes</manageDesktops>
|
||||||
|
<!-- show the manage desktops section in the client-list-(combined-)menu -->
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<applications>
|
||||||
|
<!--
|
||||||
|
# this is an example with comments through out. use these to make your
|
||||||
|
# own rules, but without the comments of course.
|
||||||
|
# you may use one or more of the name/class/role/title/type rules to specify
|
||||||
|
# windows to match
|
||||||
|
|
||||||
|
<application name="the window's _OB_APP_NAME property (see obxprop)"
|
||||||
|
class="the window's _OB_APP_CLASS property (see obxprop)"
|
||||||
|
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
|
||||||
|
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
|
||||||
|
role="the window's _OB_APP_ROLE property (see obxprop)"
|
||||||
|
title="the window's _OB_APP_TITLE property (see obxprop)"
|
||||||
|
type="the window's _OB_APP_TYPE property (see obxprob)..
|
||||||
|
(if unspecified, then it is 'dialog' for child windows)">
|
||||||
|
# you may set only one of name/class/role/title/type, or you may use more
|
||||||
|
# than one together to restrict your matches.
|
||||||
|
|
||||||
|
# the name, class, role, and title use simple wildcard matching such as those
|
||||||
|
# used by a shell. you can use * to match any characters and ? to match
|
||||||
|
# any single character.
|
||||||
|
|
||||||
|
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
|
||||||
|
# or desktop
|
||||||
|
|
||||||
|
# when multiple rules match a window, they will all be applied, in the
|
||||||
|
# order that they appear in this list
|
||||||
|
|
||||||
|
|
||||||
|
# each rule element can be left out or set to 'default' to specify to not
|
||||||
|
# change that attribute of the window
|
||||||
|
|
||||||
|
<decor>yes</decor>
|
||||||
|
# enable or disable window decorations
|
||||||
|
|
||||||
|
<shade>no</shade>
|
||||||
|
# make the window shaded when it appears, or not
|
||||||
|
|
||||||
|
<position force="no">
|
||||||
|
# the position is only used if both an x and y coordinate are provided
|
||||||
|
# (and not set to 'default')
|
||||||
|
# when force is "yes", then the window will be placed here even if it
|
||||||
|
# says you want it placed elsewhere. this is to override buggy
|
||||||
|
# applications who refuse to behave
|
||||||
|
<x>center</x>
|
||||||
|
# a number like 50, or 'center' to center on screen. use a negative number
|
||||||
|
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
|
||||||
|
# the right edge (or bottom). use 'default' to specify using value
|
||||||
|
# provided by the application, or chosen by openbox, instead.
|
||||||
|
<y>200</y>
|
||||||
|
<monitor>1</monitor>
|
||||||
|
# specifies the monitor in a xinerama setup.
|
||||||
|
# 1 is the first head, or 'mouse' for wherever the mouse is
|
||||||
|
</position>
|
||||||
|
|
||||||
|
<size>
|
||||||
|
# the size to make the window.
|
||||||
|
<width>20</width>
|
||||||
|
# a number like 20, or 'default' to use the size given by the application.
|
||||||
|
# you can use fractions such as 1/2 or percentages such as 75% in which
|
||||||
|
# case the value is relative to the size of the monitor that the window
|
||||||
|
# appears on.
|
||||||
|
<height>30%</height>
|
||||||
|
</size>
|
||||||
|
|
||||||
|
<focus>yes</focus>
|
||||||
|
# if the window should try be given focus when it appears. if this is set
|
||||||
|
# to yes it doesn't guarantee the window will be given focus. some
|
||||||
|
# restrictions may apply, but Openbox will try to
|
||||||
|
|
||||||
|
<desktop>1</desktop>
|
||||||
|
# 1 is the first desktop, 'all' for all desktops
|
||||||
|
|
||||||
|
<layer>normal</layer>
|
||||||
|
# 'above', 'normal', or 'below'
|
||||||
|
|
||||||
|
<iconic>no</iconic>
|
||||||
|
# make the window iconified when it appears, or not
|
||||||
|
|
||||||
|
<skip_pager>no</skip_pager>
|
||||||
|
# asks to not be shown in pagers
|
||||||
|
|
||||||
|
<skip_taskbar>no</skip_taskbar>
|
||||||
|
# asks to not be shown in taskbars. window cycling actions will also
|
||||||
|
# skip past such windows
|
||||||
|
|
||||||
|
<fullscreen>yes</fullscreen>
|
||||||
|
# make the window in fullscreen mode when it appears
|
||||||
|
|
||||||
|
<maximized>true</maximized>
|
||||||
|
# 'Horizontal', 'Vertical' or boolean (yes/no)
|
||||||
|
</application>
|
||||||
|
|
||||||
|
# end of the example
|
||||||
|
-->
|
||||||
|
</applications>
|
||||||
|
|
||||||
|
</openbox_config>
|
25
.docker/policies.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"policies": {
|
||||||
|
"DisableAppUpdate": true,
|
||||||
|
"DisableTelemetry": true,
|
||||||
|
"DontCheckDefaultBrowser": true,
|
||||||
|
"BlockAboutConfig": true,
|
||||||
|
"OverrideFirstRunPage": "",
|
||||||
|
"OfferToSaveLogins": false,
|
||||||
|
"PromptForDownloadLocation":false,
|
||||||
|
"ExtensionSettings": {
|
||||||
|
"uBlock0@raymondhill.net": {
|
||||||
|
"installation_mode": "force_installed",
|
||||||
|
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WebsiteFilter": {
|
||||||
|
"Block": [],
|
||||||
|
"Exceptions": []
|
||||||
|
},
|
||||||
|
"Homepage": {
|
||||||
|
"Additional": [],
|
||||||
|
"StartPage": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
.docker/pulseaudio.pa
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
unload-module module-suspend-on-idle
|
||||||
|
|
||||||
|
# Allow pulse audio to be accessed via TCP (from localhost only), to allow other users to access the virtual devices
|
||||||
|
load-module module-native-protocol-unix socket=/tmp/pulseaudio.socket auth-anonymous=1
|
32
.docker/supervisord.conf
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[supervisord]
|
||||||
|
environment=PULSE_SERVER="unix:/tmp/pulseaudio.socket",DISPLAY=":%(ENV_NEKO_DISPLAY)s"
|
||||||
|
nodaemon=true
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:xvfb]
|
||||||
|
command=/usr/bin/Xvfb :%(ENV_NEKO_DISPLAY)s -screen 0 %(ENV_NEKO_WIDTH)sx%(ENV_NEKO_HEIGHT)sx24+32
|
||||||
|
redirect_stderr=true
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
|
||||||
|
[program:pulseaudio]
|
||||||
|
command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1 --file=/etc/neko/pulseaudio.pa
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
|
||||||
|
[program:openbox]
|
||||||
|
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
|
||||||
|
[program:firefox-esr]
|
||||||
|
command=/usr/lib/firefox-esr/firefox-esr --display=:%(ENV_NEKO_DISPLAY)s --setDefaultBrowser -width %(ENV_NEKO_WIDTH)s -height %(ENV_NEKO_HEIGHT)s %(ENV_NEKO_URL)s
|
||||||
|
autorestart=true
|
||||||
|
priority=400
|
||||||
|
|
||||||
|
[program:neko]
|
||||||
|
command=/usr/bin/neko serve -d --static "/var/www"
|
||||||
|
autorestart=true
|
||||||
|
priority=500
|
28
.docker/supervisord.dev.conf
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[supervisord]
|
||||||
|
environment=PULSE_SERVER="unix:/tmp/pulseaudio.socket",DISPLAY=":%(ENV_NEKO_DISPLAY)s"
|
||||||
|
nodaemon=true
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
#logfile=/dev/null
|
||||||
|
#logfile_maxbytes=0
|
||||||
|
loglevel=debug
|
||||||
|
|
||||||
|
[program:xvfb]
|
||||||
|
command=/usr/bin/Xvfb :%(ENV_NEKO_DISPLAY)s -screen 0 %(ENV_NEKO_WIDTH)sx%(ENV_NEKO_HEIGHT)sx24+32
|
||||||
|
redirect_stderr=true
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
|
||||||
|
[program:pulseaudio]
|
||||||
|
command=/usr/bin/pulseaudio --disallow-module-loading -vvvv --disallow-exit --exit-idle-time=-1 --file=/etc/neko/pulseaudio.pa
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
|
||||||
|
[program:openbox]
|
||||||
|
command=/usr/bin/openbox --config-file /etc/neko/openbox.xml
|
||||||
|
autorestart=true
|
||||||
|
priority=300
|
||||||
|
|
||||||
|
[program:firefox-esr]
|
||||||
|
command=/usr/lib/firefox-esr/firefox-esr --display=:%(ENV_NEKO_DISPLAY)s --setDefaultBrowser -width %(ENV_NEKO_WIDTH)s -height %(ENV_NEKO_HEIGHT)s %(ENV_NEKO_URL)s
|
||||||
|
autorestart=true
|
||||||
|
priority=400
|
46
.docker/test.sh
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# usefull debugging tools pavucontrol htop x11vnc
|
||||||
|
|
||||||
|
sudo mkdir -p /var/run/dbus /etc/neko
|
||||||
|
sudo /etc/init.d/dbus start
|
||||||
|
|
||||||
|
sudo cp supervisord.conf /etc/neko/supervisord.conf
|
||||||
|
sudo cp pulseaudio.pa /etc/neko/pulseaudio.pa
|
||||||
|
sudo cp openbox.xml /etc/neko/openbox.xml
|
||||||
|
sudo cp policies.json /usr/lib/firefox-esr/distribution/policies.json
|
||||||
|
|
||||||
|
if [ ! -f /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi ]; then
|
||||||
|
sudo mkdir -p /usr/lib/firefox-esr/distribution/extensions
|
||||||
|
sudo curl -o /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/addon-607454-latest.xpi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f ../server/bin/neko ]; then
|
||||||
|
echo "build server before testing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d ../client/dist/ ]; then
|
||||||
|
echo "build client before testing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo cp ../server/bin/neko /usr/bin/neko
|
||||||
|
sudo cp -R ../client/dist /var/www/
|
||||||
|
|
||||||
|
sudo rm -rf $HOME/.mozilla
|
||||||
|
sudo rm -rf /var/run/supervisord.pid
|
||||||
|
|
||||||
|
mkdir -p $HOME/.config/pulse
|
||||||
|
echo "default-server=unix:/tmp/pulseaudio.socket" > $HOME/.config/pulse/client.conf
|
||||||
|
|
||||||
|
export NEKO_DISPLAY=0
|
||||||
|
export NEKO_WIDTH=1280
|
||||||
|
export NEKO_HEIGHT=720
|
||||||
|
export NEKO_URL=https://www.youtube.com/embed/QH2-TGUlwu4
|
||||||
|
export NEKO_PASSWORD=neko
|
||||||
|
export NEKO_BIND=0.0.0.0:80
|
||||||
|
export NEKO_KEY=
|
||||||
|
export NEKO_CERT=
|
||||||
|
|
||||||
|
supervisord --configuration ./supervisord.dev.conf
|
7
.docker/x11vnc.sh
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -f "${HOME}/.vnc/passwd" ]; then
|
||||||
|
x11vnc -storepasswd
|
||||||
|
fi
|
||||||
|
|
||||||
|
/usr/bin/x11vnc -display :0 -6 -xkb -rfbport 5901 -rfbauth $HOME/.vnc/passwd -wait 20 -nap -noxrecord -nopw -noxfixes -noxdamage -repeat
|
22
.gitattributes
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
* text=auto
|
||||||
|
*.css text
|
||||||
|
*.js text
|
||||||
|
*.ts text
|
||||||
|
*.json text
|
||||||
|
*.htm text
|
||||||
|
*.html text
|
||||||
|
*.env text
|
||||||
|
*.xml text
|
||||||
|
*.svg text
|
||||||
|
*.txt text
|
||||||
|
*.ini text
|
||||||
|
*.sql text
|
||||||
|
*.sh text
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.mov binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
0
.github/.gitkeep
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Log/Temp files
|
||||||
|
.tmp/
|
||||||
|
tmp/
|
||||||
|
*.tmp
|
||||||
|
.logs/
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
core
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# TypeScript incremental compilation cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Node modules
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
bin
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
*.env
|
69
Dockerfile
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
FROM buildpack-deps:stretch
|
||||||
|
|
||||||
|
ARG USERNAME=neko
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
|
||||||
|
# Avoid warnings by switching to noninteractive
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get -y install curl supervisor openbox dbus-x11 ttf-freefont xvfb pulseaudio consolekit firefox-esr \
|
||||||
|
&& apt-get -y install gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-pulseaudio libxcb-xkb-dev libxkbcommon-x11-dev \
|
||||||
|
#
|
||||||
|
# Create a non-root user
|
||||||
|
&& groupadd --gid $USER_GID $USERNAME \
|
||||||
|
&& useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME \
|
||||||
|
&& adduser $USERNAME audio \
|
||||||
|
&& adduser $USERNAME video \
|
||||||
|
&& adduser $USERNAME pulse \
|
||||||
|
#
|
||||||
|
# Install uBlock
|
||||||
|
&& mkdir -p /usr/lib/firefox-esr/distribution/extensions \
|
||||||
|
&& curl -o /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/addon-607454-latest.xpi \
|
||||||
|
#
|
||||||
|
# Make directories for neko
|
||||||
|
&& mkdir -p /etc/neko /var/www \
|
||||||
|
#
|
||||||
|
# Setup Pulse Audio
|
||||||
|
mkdir -p /home/$USERNAME/.config/pulse/ \
|
||||||
|
&& echo "default-server=unix:/tmp/pulseaudio.socket" > /home/$USERNAME/.config/pulse/client.conf \
|
||||||
|
&& chown -R $USERNAME:$USERNAME /home/$USERNAME \
|
||||||
|
#
|
||||||
|
# Clean up
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean -y \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copy configuation files
|
||||||
|
COPY .docker/pulseaudio.pa /etc/neko/pulseaudio.pa
|
||||||
|
COPY .docker/openbox.xml /etc/neko/openbox.xml
|
||||||
|
COPY .docker/supervisord.conf /etc/neko/supervisord.conf
|
||||||
|
COPY .docker/policies.json /usr/lib/firefox-esr/distribution/policies.json
|
||||||
|
|
||||||
|
#
|
||||||
|
# Neko files
|
||||||
|
COPY client/dist/ /var/www
|
||||||
|
COPY server/bin/neko /usr/bin/neko
|
||||||
|
|
||||||
|
#
|
||||||
|
# Neko Env
|
||||||
|
ENV NEKO_USER=$USERNAME
|
||||||
|
ENV NEKO_DISPLAY=0
|
||||||
|
ENV NEKO_WIDTH=1280
|
||||||
|
ENV NEKO_HEIGHT=720
|
||||||
|
ENV NEKO_URL=https://www.youtube.com/embed/QH2-TGUlwu4
|
||||||
|
ENV NEKO_PASSWORD=neko
|
||||||
|
ENV NEKO_BIND=0.0.0.0:80
|
||||||
|
ENV NEKO_KEY=
|
||||||
|
ENV NEKO_CERT=
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copy entrypoint
|
||||||
|
COPY .docker/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Run entrypoint
|
||||||
|
CMD ["/bin/bash", "/entrypoint.sh"]
|
17
README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# n.eko
|
||||||
|
This is a proof of concept project I threw together over the last few days, its ugly its not perfect but it looks nice. This uses web rtc to stream a desktop inside of a docker container, I made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and my internet can't handle streaming and discord keeps crashing. I just want to watch anime with my friends ლ(ಠ益ಠლ) so I started digging throughout the net and found a few *kinda* clones, but non of them had the virtual browser, then I found [Turtus](https://github.com/Khauri/Turtus) and I was able to figure out the rest.
|
||||||
|
|
||||||
|
This is by no means a fully featured clone of rabbit. It has no concept of other peers. It has bugs, but for the most part it works. I'm not sure what the future holds for this. If I continue to use it and like it, I'll probably keep pushing updates to it. I'd be happy to accept PRs for any improvements.
|
||||||
|
|
||||||
|
### Why n.eko?
|
||||||
|
I like cats, I'm a weeb and a nerd, I own the domain [n.eko.moe](https://n.eko.moe/) and I love that logo I came across, had to use it for something /shrug
|
||||||
|
|
||||||
|
### I need help setting this up!
|
||||||
|
Its a docker container, you need to have docker installed and then run
|
||||||
|
|
||||||
|
```
|
||||||
|
TODO:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
*Highly* recommend you use a dev container for vscode, I've included the .devcontainer I've used to develop this app
|
2
client/.browserslistrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
9
client/.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
16
client/.eslintrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"no-dupe-class-members": "off",
|
||||||
|
"no-console": "off",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
8
client/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"vueIndentScriptAndStyle": true
|
||||||
|
}
|
25
client/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"files.encoding": "utf8",
|
||||||
|
"files.eol": "\n",
|
||||||
|
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||||
|
"todo-tree.filtering.excludeGlobs": ["**/node_modules/**"],
|
||||||
|
"eslint.validate": [
|
||||||
|
"vue",
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
],
|
||||||
|
"vetur.validation.template": true,
|
||||||
|
"vetur.useWorkspaceDependencies": true,
|
||||||
|
"remote.extensionKind": {
|
||||||
|
"ms-azuretools.vscode-docker": "ui"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
0
client/README.md
Normal file
34
client/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "neko-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Client for neko streaming server",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vue-class-component": "^7.0.2",
|
||||||
|
"vue-notification": "^1.3.20",
|
||||||
|
"vue-property-decorator": "^8.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-eslint": "^4.1.0",
|
||||||
|
"@vue/cli-plugin-typescript": "^4.1.0",
|
||||||
|
"@vue/cli-plugin-vuex": "^4.1.0",
|
||||||
|
"@vue/cli-service": "^4.1.0",
|
||||||
|
"@vue/eslint-config-prettier": "^5.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^4.0.0",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
|
"node-sass": "^4.12.0",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"sass-loader": "^8.0.0",
|
||||||
|
"typescript": "~3.5.3",
|
||||||
|
"vue-template-compiler": "^2.6.10"
|
||||||
|
}
|
||||||
|
}
|
BIN
client/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
client/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
client/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
9
client/public/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#19bd9c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
client/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 662 B |
BIN
client/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1003 B |
23
client/public/index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>n.eko</title>
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#19bd9c">
|
||||||
|
<meta name="msapplication-TileColor" content="#19bd9c">
|
||||||
|
<meta name="theme-color" content="#19bd9c">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but test doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="neko"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
client/public/mstile-144x144.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
client/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
client/public/mstile-310x150.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
client/public/mstile-310x310.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
client/public/mstile-70x70.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
73
client/public/safari-pinned-tab.svg
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M947 6714 c-329 -50 -672 -278 -782 -519 -15 -33 -31 -67 -35 -75 -6
|
||||||
|
-14 -38 -133 -45 -170 -7 -39 -19 -180 -18 -222 5 -248 89 -392 243 -415 72
|
||||||
|
-11 93 -8 122 22 42 41 53 96 63 300 10 218 29 297 91 383 103 141 273 226
|
||||||
|
454 225 235 -1 423 -138 507 -369 28 -79 44 -179 59 -378 17 -229 60 -380 138
|
||||||
|
-490 97 -136 236 -231 413 -281 237 -68 463 -71 656 -11 52 17 79 11 91 -20
|
||||||
|
26 -68 -174 -126 -409 -119 -133 4 -169 9 -334 51 -9 2 -38 9 -64 16 l-48 12
|
||||||
|
-122 -125 c-201 -204 -418 -542 -540 -839 -55 -135 -119 -349 -141 -473 -3
|
||||||
|
-15 -7 -35 -10 -45 -2 -9 -7 -39 -11 -67 -3 -27 -8 -61 -10 -75 -34 -182 -41
|
||||||
|
-701 -11 -890 2 -14 7 -50 11 -80 10 -86 15 -123 21 -150 3 -14 8 -36 10 -50
|
||||||
|
2 -14 7 -36 10 -50 3 -14 8 -36 10 -50 26 -143 133 -475 200 -617 19 -39 34
|
||||||
|
-74 34 -77 0 -14 127 -236 183 -319 82 -123 233 -274 312 -313 112 -54 216
|
||||||
|
-39 282 41 40 48 53 95 48 179 -5 106 -14 139 -105 421 -38 116 -74 230 -81
|
||||||
|
255 -7 25 -13 47 -14 50 -1 3 -5 25 -9 50 -4 25 -9 54 -11 65 -2 11 -4 30 -4
|
||||||
|
42 -1 12 -9 24 -21 28 -33 11 -193 120 -238 162 -47 45 -56 85 -22 103 24 13
|
||||||
|
29 11 100 -51 50 -43 211 -149 225 -149 4 0 21 -10 38 -23 18 -13 57 -31 87
|
||||||
|
-40 67 -19 292 -63 375 -73 33 -3 69 -8 80 -10 138 -27 594 -23 695 6 16 5 33
|
||||||
|
9 37 9 7 0 8 3 17 87 4 32 9 75 11 94 2 19 7 69 10 110 7 93 16 163 22 172 2
|
||||||
|
5 17 8 33 8 35 0 50 -23 45 -71 -2 -19 -6 -65 -10 -104 -3 -38 -8 -83 -10
|
||||||
|
-100 -2 -16 -7 -57 -10 -90 -8 -86 -15 -142 -25 -205 -5 -30 -12 -71 -15 -90
|
||||||
|
-7 -43 -14 -69 -24 -95 -5 -11 -25 -67 -44 -125 -19 -58 -60 -175 -89 -260
|
||||||
|
-70 -200 -86 -263 -83 -341 2 -83 23 -121 77 -147 108 -50 177 -50 290 -1 144
|
||||||
|
63 229 141 346 317 23 35 42 66 42 69 0 3 8 17 19 31 10 15 56 99 101 187 46
|
||||||
|
88 102 191 124 229 73 122 84 144 129 259 45 113 94 256 101 294 3 12 14 56
|
||||||
|
25 99 71 274 84 536 35 745 -23 97 -18 118 28 119 28 0 45 -38 66 -140 31
|
||||||
|
-154 26 -368 -13 -605 -21 -125 -100 -397 -160 -550 -19 -50 -35 -93 -35 -97
|
||||||
|
0 -18 260 118 290 151 3 3 26 23 51 43 190 153 335 354 397 552 22 69 47 86
|
||||||
|
84 56 l23 -18 -18 -68 c-29 -113 -108 -258 -201 -373 -48 -59 -196 -201 -261
|
||||||
|
-251 -78 -59 -72 -39 -62 -210 21 -327 126 -523 297 -555 68 -12 88 -12 142 5
|
||||||
|
99 30 168 105 168 180 0 45 -35 106 -91 160 -53 51 -62 79 -48 154 4 22 10 55
|
||||||
|
12 71 17 97 60 268 152 600 103 370 109 392 120 485 3 25 8 56 12 70 3 14 7
|
||||||
|
150 8 303 1 231 4 280 16 287 15 10 47 4 64 -12 6 -5 11 -113 12 -276 l1 -267
|
||||||
|
59 3 c226 10 529 142 698 303 l40 38 50 -46 c64 -58 192 -156 236 -181 37 -21
|
||||||
|
72 -16 82 12 11 28 -3 44 -86 100 -45 30 -113 83 -150 118 l-68 63 28 34 29
|
||||||
|
34 40 -21 c21 -11 105 -48 186 -82 122 -52 150 -60 167 -50 26 13 27 52 3 71
|
||||||
|
-19 17 -32 23 -157 75 -76 31 -183 81 -193 89 -1 1 11 42 27 91 39 118 55 242
|
||||||
|
48 378 -3 61 -8 122 -11 136 -3 14 -7 36 -10 50 -29 159 -115 376 -197 497
|
||||||
|
-70 104 -211 256 -349 374 -158 137 -227 183 -370 249 -24 11 -36 132 -39 384
|
||||||
|
-2 212 -6 290 -16 341 -2 11 -4 26 -4 34 -1 11 -15 13 -68 8 -165 -15 -344
|
||||||
|
-127 -561 -349 -137 -141 -299 -346 -368 -465 l-18 -33 35 -32 c146 -136 384
|
||||||
|
-495 539 -813 126 -259 154 -364 100 -378 -32 -8 -55 21 -85 107 -76 221 -303
|
||||||
|
621 -493 869 -178 233 -477 448 -792 572 -87 34 -200 72 -239 80 -9 2 -54 12
|
||||||
|
-101 23 -263 62 -456 62 -815 1 -146 -24 -283 -42 -330 -42 -73 0 -88 3 -158
|
||||||
|
37 -187 90 -286 253 -351 579 -29 146 -34 179 -50 383 -14 174 -169 410 -352
|
||||||
|
536 -74 51 -248 136 -293 143 -19 3 -38 8 -42 11 -20 12 -201 11 -282 -2z
|
||||||
|
m1833 -2648 c0 -12 -18 -47 -40 -80 -22 -32 -40 -62 -40 -66 0 -4 15 -6 33 -3
|
||||||
|
106 12 173 11 190 -4 9 -9 17 -23 17 -32 -1 -36 -18 -44 -124 -56 -59 -7 -109
|
||||||
|
-14 -112 -17 -6 -7 134 -148 146 -148 15 0 22 -38 10 -60 -22 -41 -61 -26
|
||||||
|
-151 59 l-84 80 -18 -52 c-19 -54 -25 -81 -26 -108 -1 -23 -35 -30 -60 -12
|
||||||
|
-24 16 -26 41 -7 103 7 25 16 57 20 72 l6 27 -72 -30 c-59 -24 -77 -28 -96
|
||||||
|
-19 -19 8 -23 17 -20 42 3 31 7 34 85 63 45 16 84 33 88 36 3 4 -13 41 -36 83
|
||||||
|
-36 67 -40 81 -30 102 13 29 51 32 73 7 8 -10 27 -39 42 -65 15 -27 29 -48 31
|
||||||
|
-48 2 0 18 24 35 54 17 30 44 68 59 85 26 28 32 30 55 20 16 -8 26 -20 26 -33z
|
||||||
|
m2668 -96 c33 -20 62 -71 62 -109 0 -12 -7 -38 -16 -60 -54 -135 -243 -72
|
||||||
|
-220 74 16 96 99 141 174 95z m957 -333 c11 -4 30 -22 43 -41 38 -56 30 -135
|
||||||
|
-19 -182 -88 -84 -208 21 -168 148 22 67 78 96 144 75z m-420 -161 c131 -61
|
||||||
|
175 -184 83 -232 -35 -18 -109 -18 -153 0 -49 21 -122 92 -135 132 -34 102 81
|
||||||
|
158 205 100z"/>
|
||||||
|
<path d="M6425 5089 c-95 -14 -204 -58 -454 -184 -171 -86 -188 -96 -184 -117
|
||||||
|
4 -16 23 -32 63 -51 93 -47 226 -152 373 -298 121 -121 137 -134 137 -110 1
|
||||||
|
54 13 113 72 346 64 255 83 391 57 411 -9 6 -33 7 -64 3z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.8 KiB |
19
client/public/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "n.eko",
|
||||||
|
"short_name": "n.eko",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#19bd9c",
|
||||||
|
"background_color": "#19bd9c",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
847
client/src/App.vue
Normal file
@ -0,0 +1,847 @@
|
|||||||
|
<template>
|
||||||
|
<div class="video-player">
|
||||||
|
<div ref="video" class="video">
|
||||||
|
<div ref="container" class="video-container">
|
||||||
|
<video
|
||||||
|
ref="player"
|
||||||
|
tabindex="0"
|
||||||
|
@click.stop.prevent
|
||||||
|
@contextmenu.stop.prevent
|
||||||
|
@wheel.stop.prevent="onWheel"
|
||||||
|
@mousemove.stop.prevent="onMouseMove"
|
||||||
|
@mousedown.stop.prevent="onMouseDown"
|
||||||
|
@mouseup.stop.prevent="onMouseUp"
|
||||||
|
@mouseenter.stop.prevent="onMouseEnter"
|
||||||
|
@mouseleave.stop.prevent="onMouseLeave"
|
||||||
|
@keydown.stop.prevent="onKeyDown"
|
||||||
|
@keyup.stop.prevent="onKeyUp"
|
||||||
|
/>
|
||||||
|
<div v-if="!playing" class="video-overlay">
|
||||||
|
<i @click.stop.prevent="toggleMedia" class="fas fa-play-circle" />
|
||||||
|
</div>
|
||||||
|
<div ref="aspect" class="aspect" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="neko">
|
||||||
|
<img src="@/assets/logo.svg" alt="n.eko" />
|
||||||
|
<span><b>n</b>.eko</span>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<i
|
||||||
|
alt="Request Control"
|
||||||
|
:class="[{ enabled: controlling }, 'request', 'fas', 'fa-keyboard']"
|
||||||
|
@click.stop.prevent="toggleControl"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i
|
||||||
|
alt="Play/Pause"
|
||||||
|
:class="[playing ? 'fa-pause-circle' : 'fa-play-circle', 'play', 'fas']"
|
||||||
|
@click.stop.prevent="toggleMedia"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="volume">
|
||||||
|
<input
|
||||||
|
@input="setVolume"
|
||||||
|
:class="[volume === 0 ? 'fa-volume-mute' : 'fa-volume-up', 'fas']"
|
||||||
|
ref="volume"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i @click.stop.prevent="fullscreen" alt="Full Screen" class="fullscreen fas fa-expand-alt" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="right"></div>
|
||||||
|
</div>
|
||||||
|
<div class="connect" v-if="!connected">
|
||||||
|
<div class="window">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="@/assets/logo.svg" alt="n.eko" />
|
||||||
|
<span><b>n</b>.eko</span>
|
||||||
|
</div>
|
||||||
|
<div class="message" v-if="!connecting">
|
||||||
|
<span>Please enter the password:</span>
|
||||||
|
<input type="password" v-model="password" />
|
||||||
|
<span class="button" @click.stop.prevent="connect">
|
||||||
|
Connect
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="spinner" v-if="connecting">
|
||||||
|
<div class="double-bounce1"></div>
|
||||||
|
<div class="double-bounce2"></div>
|
||||||
|
</div>
|
||||||
|
<div class="loader" v-if="connecting" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<notifications group="neko" position="bottom left" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.video-player {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
.video {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 16 / 9 * 100vh;
|
||||||
|
|
||||||
|
video {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
background: #000;
|
||||||
|
|
||||||
|
&::-webkit-media-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba($color: $style-darker, $alpha: 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
cursor: pointer;
|
||||||
|
&::before {
|
||||||
|
font-size: 120px;
|
||||||
|
color: rgba($color: $style-light, $alpha: 0.4);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aspect {
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 56.25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 60px;
|
||||||
|
background: $style-darker;
|
||||||
|
padding: 0 50px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.neko {
|
||||||
|
flex: 1; /* shorthand for: flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
height: 54px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $style-light;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 56px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 0 10px;
|
||||||
|
color: $style-light;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.request {
|
||||||
|
color: rgba($color: $style-light, $alpha: 0.5);
|
||||||
|
|
||||||
|
&.enabled {
|
||||||
|
color: $style-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume {
|
||||||
|
display: block;
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
input[type='range'] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
width: 200px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $style-light;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: $style-primary;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
color: $style-light;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba($color: $style-darker, $alpha: 0.8);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.window {
|
||||||
|
width: 300px;
|
||||||
|
background: $style-light;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
color: $style-darker;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: invert(100%);
|
||||||
|
height: 90px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 56px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: solid 1px rgba($color: $style-darker, $alpha: 0.4);
|
||||||
|
padding: 3px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
background: $style-primary;
|
||||||
|
color: $style-light;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 30px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.double-bounce1,
|
||||||
|
.double-bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $style-primary;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||||
|
animation: sk-bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.double-bounce2 {
|
||||||
|
-webkit-animation-delay: -1s;
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sk-bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||||
|
|
||||||
|
const MOUSE_MOVE = 0x01
|
||||||
|
const MOUSE_UP = 0x02
|
||||||
|
const MOUSE_DOWN = 0x03
|
||||||
|
const MOUSE_CLK = 0x04
|
||||||
|
const KEY_DOWN = 0x05
|
||||||
|
const KEY_UP = 0x06
|
||||||
|
const KEY_CLK = 0x07
|
||||||
|
|
||||||
|
@Component({ name: 'stream-video' })
|
||||||
|
export default class extends Vue {
|
||||||
|
@Ref('player') readonly _player!: HTMLVideoElement
|
||||||
|
@Ref('container') readonly _container!: HTMLElement
|
||||||
|
@Ref('aspect') readonly _aspect!: HTMLElement
|
||||||
|
@Ref('video') readonly _video!: HTMLElement
|
||||||
|
@Ref('volume') readonly _volume!: HTMLInputElement
|
||||||
|
|
||||||
|
private focused = false
|
||||||
|
private connected = false
|
||||||
|
private connecting = false
|
||||||
|
private controlling = false
|
||||||
|
private playing = false
|
||||||
|
private volume = 0
|
||||||
|
private width = 1280
|
||||||
|
private height = 720
|
||||||
|
private state: RTCIceConnectionState = 'disconnected'
|
||||||
|
private password = ''
|
||||||
|
|
||||||
|
private ws?: WebSocket
|
||||||
|
private peer?: RTCPeerConnection
|
||||||
|
private channel?: RTCDataChannel
|
||||||
|
private id?: string
|
||||||
|
private stream?: MediaStream
|
||||||
|
private timeout?: number
|
||||||
|
|
||||||
|
@Watch('volume')
|
||||||
|
onVolumeChanged(volume: number) {
|
||||||
|
if (this._player) {
|
||||||
|
this._player.volume = this.volume / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('resize', this.onResise)
|
||||||
|
this.onResise()
|
||||||
|
this.volume = this._player.volume * 100
|
||||||
|
this._volume.value = `${this.volume}`
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.onResise)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleControl() {
|
||||||
|
if (!this.ws) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.controlling) {
|
||||||
|
this.ws.send(JSON.stringify({ event: 'control/release' }))
|
||||||
|
} else {
|
||||||
|
this.ws.send(JSON.stringify({ event: 'control/request' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMedia() {
|
||||||
|
if (!this.playing) {
|
||||||
|
this._player
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
this.playing = true
|
||||||
|
this.width = this._player.videoWidth
|
||||||
|
this.height = this._player.videoHeight
|
||||||
|
this.onResise()
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this._player.pause()
|
||||||
|
this.playing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume() {
|
||||||
|
this.volume = parseInt(this._volume.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullscreen() {
|
||||||
|
this._video.requestFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
/*
|
||||||
|
this.ws = new WebSocket(
|
||||||
|
`${/https/gi.test(location.protocol) ? 'wss' : 'ws'}://${location.host}/ws?password=${this.password}`,
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
this.ws = new WebSocket(`ws://localhost:3000/ws?password=${this.password}`)
|
||||||
|
this.ws.onmessage = this.onMessage.bind(this)
|
||||||
|
this.ws.onerror = event => console.error((event as ErrorEvent).error)
|
||||||
|
this.ws.onclose = event => this.onClose.bind(this)
|
||||||
|
this.onConnecting()
|
||||||
|
this.timeout = setTimeout(this.onTimeout.bind(this), 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
createPeer() {
|
||||||
|
if (!this.ws) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] })
|
||||||
|
this.peer.onicecandidate = event => {
|
||||||
|
if (event.candidate === null && this.peer!.localDescription) {
|
||||||
|
this.ws!.send(
|
||||||
|
JSON.stringify({
|
||||||
|
event: 'sdp/provide',
|
||||||
|
sdp: this.peer!.localDescription.sdp,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.peer.oniceconnectionstatechange = event => {
|
||||||
|
this.state = this.peer!.iceConnectionState
|
||||||
|
|
||||||
|
switch (this.state) {
|
||||||
|
case 'connected':
|
||||||
|
this.onConnected()
|
||||||
|
break
|
||||||
|
case 'disconnected':
|
||||||
|
this.onClose()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.peer.ontrack = this.onTrack.bind(this)
|
||||||
|
this.peer.addTransceiver('audio', { direction: 'recvonly' })
|
||||||
|
this.peer.addTransceiver('video', { direction: 'recvonly' })
|
||||||
|
|
||||||
|
this.channel = this.peer.createDataChannel('data')
|
||||||
|
|
||||||
|
this.peer
|
||||||
|
.createOffer()
|
||||||
|
.then(d => this.peer!.setLocalDescription(d))
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateControles(event: 'wheel', data: { x: number; y: number }): void
|
||||||
|
updateControles(event: 'mousemove', data: { x: number; y: number; rect: DOMRect }): void
|
||||||
|
updateControles(event: 'mousedown' | 'mouseup' | 'keydown' | 'keyup', data: { key: number }): void
|
||||||
|
updateControles(event: string, data: any) {
|
||||||
|
if (!this.controlling) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer: ArrayBuffer
|
||||||
|
let payload: DataView
|
||||||
|
switch (event) {
|
||||||
|
case 'mousemove':
|
||||||
|
buffer = new ArrayBuffer(7)
|
||||||
|
payload = new DataView(buffer)
|
||||||
|
payload.setUint8(0, MOUSE_MOVE)
|
||||||
|
payload.setUint16(1, 4, true)
|
||||||
|
payload.setUint16(3, Math.round((this.width / data.rect.width) * (data.x - data.rect.left)), true)
|
||||||
|
payload.setUint16(5, Math.round((this.height / data.rect.height) * (data.y - data.rect.top)), true)
|
||||||
|
break
|
||||||
|
case 'wheel':
|
||||||
|
buffer = new ArrayBuffer(4)
|
||||||
|
payload = new DataView(buffer)
|
||||||
|
payload.setUint8(0, MOUSE_CLK)
|
||||||
|
payload.setUint16(1, 1, true)
|
||||||
|
|
||||||
|
const ydir = Math.sign(data.y)
|
||||||
|
const xdir = Math.sign(data.x)
|
||||||
|
|
||||||
|
if ((!xdir && !ydir) || (xdir && ydir)) return
|
||||||
|
if (ydir && ydir < 0) payload.setUint8(3, 4)
|
||||||
|
if (ydir && ydir > 0) payload.setUint8(3, 5)
|
||||||
|
if (xdir && xdir < 0) payload.setUint8(3, 6)
|
||||||
|
if (xdir && xdir > 0) payload.setUint8(3, 7)
|
||||||
|
break
|
||||||
|
case 'mousedown':
|
||||||
|
buffer = new ArrayBuffer(4)
|
||||||
|
payload = new DataView(buffer)
|
||||||
|
payload.setUint8(0, MOUSE_DOWN)
|
||||||
|
payload.setUint16(1, 1, true)
|
||||||
|
payload.setUint8(3, data.key)
|
||||||
|
break
|
||||||
|
case 'mouseup':
|
||||||
|
buffer = new ArrayBuffer(4)
|
||||||
|
payload = new DataView(buffer)
|
||||||
|
payload.setUint8(0, MOUSE_UP)
|
||||||
|
payload.setUint16(1, 1, true)
|
||||||
|
payload.setUint8(3, data.key)
|
||||||
|
break
|
||||||
|
case 'keydown':
|
||||||
|
buffer = new ArrayBuffer(5)
|
||||||
|
payload = new DataView(buffer)
|
||||||
|
payload.setUint8(0, KEY_DOWN)
|
||||||
|
payload.setUint16(1, 2, true)
|
||||||
|
payload.setUint16(3, data.key, true)
|
||||||
|
break
|
||||||
|
case 'keyup':
|
||||||
|
buffer = new ArrayBuffer(5)
|
||||||
|
payload = new DataView(buffer)
|
||||||
|
payload.setUint8(0, KEY_UP)
|
||||||
|
payload.setUint16(1, 2, true)
|
||||||
|
payload.setUint16(3, data.key, true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (this.channel && typeof buffer !== 'undefined') {
|
||||||
|
this.channel.send(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAspect() {
|
||||||
|
const { width, height } = this
|
||||||
|
|
||||||
|
if ((height == 0 && width == 0) || (height == 0 && width != 0) || (height != 0 && width == 0)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height == width) {
|
||||||
|
return {
|
||||||
|
horizontal: 1,
|
||||||
|
vertical: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dividend = width
|
||||||
|
let divisor = height
|
||||||
|
let gcd = -1
|
||||||
|
|
||||||
|
if (height > width) {
|
||||||
|
dividend = height
|
||||||
|
divisor = width
|
||||||
|
}
|
||||||
|
|
||||||
|
while (gcd == -1) {
|
||||||
|
const remainder = dividend % divisor
|
||||||
|
if (remainder == 0) {
|
||||||
|
gcd = divisor
|
||||||
|
} else {
|
||||||
|
dividend = divisor
|
||||||
|
divisor = remainder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
horizontal: width / gcd,
|
||||||
|
vertical: height / gcd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMousePos(e: MouseEvent) {
|
||||||
|
const rect = this._player.getBoundingClientRect() as DOMRect
|
||||||
|
this.updateControles('mousemove', {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
rect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel(e: WheelEvent) {
|
||||||
|
this.onMousePos(e)
|
||||||
|
this.updateControles('wheel', { x: e.deltaX, y: e.deltaY })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(e: MouseEvent) {
|
||||||
|
this.onMousePos(e)
|
||||||
|
this.updateControles('mousedown', { key: e.button })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(e: MouseEvent) {
|
||||||
|
this.onMousePos(e)
|
||||||
|
this.updateControles('mouseup', { key: e.button })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(e: MouseEvent) {
|
||||||
|
this.onMousePos(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter(e: MouseEvent) {
|
||||||
|
this._player.focus()
|
||||||
|
this.focused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave(e: MouseEvent) {
|
||||||
|
this.focused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!this.focused) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateControles('keydown', { key: e.keyCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyUp(e: KeyboardEvent) {
|
||||||
|
if (!this.focused) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateControles('keyup', { key: e.keyCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
onResise() {
|
||||||
|
const aspect = this.getAspect()
|
||||||
|
if (!aspect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { horizontal, vertical } = aspect
|
||||||
|
this._container.style.maxWidth = `${(horizontal / vertical) * this._video.offsetHeight}px`
|
||||||
|
this._aspect.style.paddingBottom = `${(vertical / horizontal) * 100}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(e: MessageEvent) {
|
||||||
|
const { event, ...payload } = JSON.parse(e.data)
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case 'sdp/reply':
|
||||||
|
if (!this.peer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.peer.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: payload.sdp }))
|
||||||
|
break
|
||||||
|
case 'identity/provide':
|
||||||
|
this.id = payload.id
|
||||||
|
this.createPeer()
|
||||||
|
break
|
||||||
|
case 'control/requesting':
|
||||||
|
this.controlling = true
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Another user is requesting the controls',
|
||||||
|
duration: 3000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'control/give':
|
||||||
|
this.controlling = true
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'info',
|
||||||
|
title: 'You have the controls',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'control/locked':
|
||||||
|
this.controlling = false
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Another user has the controls',
|
||||||
|
duration: 3000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'control/given':
|
||||||
|
this.controlling = false
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Someone has taken the controls',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'control/release':
|
||||||
|
this.controlling = false
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'info',
|
||||||
|
title: 'You released the controls',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'control/released':
|
||||||
|
this.controlling = false
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'info',
|
||||||
|
title: 'The controls have been released',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn(`[NEKO] Unknown message event ${event}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTrack(event: RTCTrackEvent) {
|
||||||
|
if (event.track.kind === 'audio') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stream = event.streams[0]
|
||||||
|
if (!this.stream) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('srcObject' in this._player) {
|
||||||
|
this._player.srcObject = this.stream
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
this._player.src = window.URL.createObjectURL(this.stream) // for older browsers
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._player.paused) {
|
||||||
|
this.toggleMedia()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTimeout() {
|
||||||
|
this.connected = false
|
||||||
|
this.connecting = false
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'error',
|
||||||
|
title: 'Unable to connect to server!',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnecting() {
|
||||||
|
this.connecting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnected() {
|
||||||
|
this.connected = true
|
||||||
|
this.connecting = false
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'success',
|
||||||
|
title: 'Successfully connected!',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this.controlling = false
|
||||||
|
this.connected = false
|
||||||
|
this.connecting = false
|
||||||
|
this.ws = undefined
|
||||||
|
this.peer = undefined
|
||||||
|
if (this.playing) {
|
||||||
|
this.toggleMedia()
|
||||||
|
}
|
||||||
|
this.$notify({
|
||||||
|
group: 'neko',
|
||||||
|
type: 'error',
|
||||||
|
title: 'Disconnected from server!',
|
||||||
|
duration: 5000,
|
||||||
|
speed: 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
40
client/src/assets/logo.svg
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<path marker-start="none" marker-end="none" class="st0" d="M47.8,95c-1.9-1-0.5-4.7,0.8-8.3c0.5-1.3,1-2.6,1.3-3.9
|
||||||
|
c0.1-0.1,0.2-0.2,0.2-0.4c0.7-2.6,0.9-6.2,1.2-8.8l0.1-1.1c0-0.4-0.2-0.7-0.6-0.7c-0.4,0-0.7,0.2-0.7,0.6L50,73.5
|
||||||
|
c-0.1,1.5-0.3,3.3-0.5,5.1c-6.3,1.4-15.7-0.4-18-1.3c0,0-0.1-0.1-0.1-0.1c-0.1,0-0.2-0.1-0.3-0.2c0-0.1-0.1-0.1-0.2-0.2c0,0,0,0,0,0
|
||||||
|
c-0.1,0-0.2-0.1-0.3-0.1c0,0,0,0,0,0c-1.3-0.7-3-1.8-3.8-2.7c-0.2-0.3-0.7-0.3-0.9-0.1c-0.3,0.2-0.3,0.7-0.1,0.9
|
||||||
|
c0.9,1.1,2.8,2.3,4.2,3c0.1,2.1,0.9,4.5,1.7,6.8c1.2,3.5,2.4,7.1,0.8,8.7c-0.8,0.8-1.6,1.1-2.5,1c-2.9-0.3-5.9-4.7-6.7-6.1
|
||||||
|
c-7.9-12.8-8.6-32.9-1.6-44.8c1.3-2.3,3.3-5.5,5-7.3c0.4-0.4,0.9-0.9,1.5-1.5c0.6-0.5,0.9-0.9,1.2-1.1c1.9,0.6,4.2,1.2,6.7,1.2
|
||||||
|
c1.6,0,3.4-0.2,5-0.9c0.3-0.1,0.5-0.5,0.4-0.9c-0.1-0.3-0.5-0.5-0.9-0.4c-3.9,1.5-8.2,0.6-11.1-0.4c0,0,0,0,0,0
|
||||||
|
c-2.4-1.2-3.5-1.9-5-4.2c-1.1-1.8-1.4-4.4-1.6-6.9c-0.1-1.3-0.2-2.5-0.4-3.6c-1-4.6-4.4-6.5-7.2-6.6c-3.5-0.2-6.7,1.8-7.8,4.9
|
||||||
|
c-0.3,1-0.4,2.3-0.4,3.6c-0.1,1.8-0.1,3.8-1,4.6c-0.3,0.3-0.7,0.4-1.2,0.3c-1.4-0.1-2.4-0.7-3.1-2c-1.7-3.2-0.6-9,0.9-11.4
|
||||||
|
c2.9-4.9,11.6-8.7,17.1-6c1.4,0.7,4.3,2.1,6,5.1l0.3,0.5c0.4,0.7,0.9,1.6,1,2.1c0.2,0.5,0.3,1.3,0.3,1.8c0.1,2.3,0.8,7,2,9.6
|
||||||
|
c1.5,3,4.2,4.7,6.9,4.3l6.6-1.1c7.4-1,16.6,2,22.2,7.2c0,0,0.1,0,0.1,0.1c0.1,0.1,0.3,0.3,0.5,0.4c3.1,2.6,8.6,12.1,9.6,15.9
|
||||||
|
c0.1,0.3,0.3,0.5,0.6,0.5c0.1,0,0.1,0,0.2,0c0.4-0.1,0.6-0.5,0.5-0.8c-1-4.1-6.7-13.9-10.1-16.7c0,0,0,0,0,0
|
||||||
|
c2.9-4.9,9.7-12.7,14.4-12.1c0.4,0.6,0.4,3.6,0.4,5.6c0,1.1,0,2.3,0.1,3.3c0,0.6,0.1,1.2,0.2,1.7c0,0.3,0.2,0.5,0.5,0.5
|
||||||
|
c1.6,0.4,6.2,4,9,7.4c0.3,0.3,0.6,0.7,0.9,1.2c1.9,2.7,3,6.5,3.2,8.9c0.3,2.9-0.1,5.5-1.1,7.7c1.1,0.6,2.3,1.1,3.5,1.6
|
||||||
|
c0.5,0.2,1,0.4,1.4,0.6c0.3,0.1,0.5,0.5,0.4,0.9c-0.1,0.3-0.4,0.4-0.6,0.4c-0.1,0-0.2,0-0.3-0.1c-0.5-0.2-0.9-0.4-1.4-0.6
|
||||||
|
c-1.2-0.5-2.5-1-3.7-1.6c-0.2,0.4-0.5,0.7-0.7,1c1.3,1.3,2.7,2.4,4.1,3.2c0.3,0.2,0.4,0.6,0.2,0.9c-0.1,0.2-0.3,0.3-0.6,0.3
|
||||||
|
c-0.1,0-0.2,0-0.3-0.1c-1.5-0.9-3-2.1-4.3-3.4c-0.5,0.5-1,1-1.6,1.4c-3.9,3.1-8.8,3.7-9.9,3.5c0.2-2.3,0.2-4.3,0-7.4
|
||||||
|
c0-0.4-0.3-0.6-0.7-0.6c-0.4,0-0.6,0.3-0.6,0.7c0.3,4.4,0.2,6.6-0.5,10.6c0,0,0,0,0,0c-0.4,1.4-0.8,3-1.3,4.7
|
||||||
|
c-1.8,6.4-2.8,10.3-2.5,11.5c0,0.1,0.1,0.3,0.2,0.3c1.5,1.1,2.1,2.3,1.7,3.4c-0.4,1.2-1.8,2-3.3,2.1c-1,0-3.3-0.3-4.4-3.5
|
||||||
|
c-0.7-1.9-0.8-3.5-0.9-5.4c0-0.5-0.1-0.9-0.1-1.4c3.3-2.3,6.9-6,7.8-10.3c0.1-0.4-0.1-0.7-0.5-0.8c-0.4-0.1-0.7,0.1-0.8,0.5
|
||||||
|
c-1,4.2-4.7,7.9-8,10c-0.2,0.1-0.4,0.2-0.6,0.3c-0.5,0.2-1.4,0.7-2.2,1c1.8-4.1,4.8-13.2,2.7-19.6c-0.1-0.3-0.5-0.5-0.8-0.4
|
||||||
|
c-0.4,0.1-0.5,0.5-0.4,0.8c2.2,6.6-1.5,16.5-3.2,19.7c0,0,0,0,0,0c-0.7,1-1.4,2.3-2.1,3.7c-1.9,3.7-4.3,8.3-7.6,9.3
|
||||||
|
C50.7,96,49.3,95.8,47.8,95z M86.8,53.6c2.1-1.3-1.4-4.3-3.4-3.5c-0.5,0.2-0.8,0.5-0.9,0.8C82,52.3,84.9,54.8,86.8,53.6z M92.5,49.8
|
||||||
|
c0-1-0.7-1.9-1.6-1.9c-0.9,0-1.6,0.8-1.6,1.9c0,1,0.7,1.9,1.6,1.9C91.7,51.7,92.5,50.8,92.5,49.8z M36.9,48.7
|
||||||
|
c0.1-0.8,0.4-1.5,0.6-2.2c0.6,0.7,1.3,1.3,1.9,1.9l0.4,0.3c0.1,0.1,0.3,0.2,0.5,0.2c0.2,0,0.4-0.1,0.5-0.2c0.3-0.3,0.2-0.7,0-0.9
|
||||||
|
l-0.4-0.3c-0.7-0.6-1.3-1.3-1.9-1.9c0.9-0.2,1.8-0.3,2.8-0.3c0.4,0,0.7-0.3,0.7-0.7c0-0.4-0.3-0.7-0.7-0.7c-1,0-1.9,0.1-2.8,0.2
|
||||||
|
c0.3-0.6,0.7-1.2,1.1-1.7c0.2-0.3,0.2-0.7-0.1-0.9c-0.3-0.2-0.7-0.2-0.9,0.1c-0.5,0.7-1,1.4-1.4,2.2c-0.3-0.5-0.6-1-0.9-1.6
|
||||||
|
c-0.2-0.3-0.5-0.5-0.9-0.3c-0.3,0.1-0.5,0.5-0.3,0.9c0.3,0.7,0.7,1.3,1.1,2c-0.7,0.3-1.4,0.6-2.3,0.9c-0.3,0.1-0.5,0.5-0.3,0.9
|
||||||
|
c0.1,0.2,0.4,0.4,0.6,0.4c0.1,0,0.2,0,0.3-0.1c0.7-0.3,1.3-0.5,1.8-0.8c-0.2,0.7-0.4,1.5-0.6,2.3c-0.1,0.4,0.2,0.7,0.5,0.8
|
||||||
|
c0,0,0.1,0,0.1,0C36.6,49.3,36.9,49.1,36.9,48.7z M78.7,44.9c0-1.1-0.8-1.9-1.7-1.9c-0.9,0-1.7,0.9-1.7,1.9c0,1.1,0.7,1.9,1.7,1.9
|
||||||
|
C78,46.8,78.7,45.9,78.7,44.9z M82.7,31.9c0-0.2,0-0.4-0.1-0.6c0.2-0.1,0.5-0.3,1.2-0.6c1.9-1,7.5-4,8.9-3.4c0.1,0,0.1,0.1,0.1,0.2
|
||||||
|
c0.3,1.2-0.3,3.3-0.9,5.6c-0.5,1.9-1,3.8-1.1,5.5C88.3,35.6,84.7,32.6,82.7,31.9z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
358
client/src/assets/styles/_reset.scss
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make sure to set some focus styles for accessibility */
|
||||||
|
:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=search]::-webkit-search-cancel-button,
|
||||||
|
input[type=search]::-webkit-search-decoration,
|
||||||
|
input[type=search]::-webkit-search-results-button,
|
||||||
|
input[type=search]::-webkit-search-results-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=search] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
vertical-align: top;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
video {
|
||||||
|
display: inline-block;
|
||||||
|
*display: inline;
|
||||||
|
*zoom: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
|
||||||
|
* Known issue: no IE 6 support.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
|
||||||
|
* `em` units.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `outline` inconsistency between Chrome and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
|
||||||
|
* 2. Improve image quality when scaled in IE 7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
-ms-interpolation-mode: bicubic; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct margin displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited in IE 6/7/8/9.
|
||||||
|
* 2. Correct text not wrapping in Firefox 3.
|
||||||
|
* 3. Correct alignment displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
padding: 0;
|
||||||
|
white-space: normal; /* 2 */
|
||||||
|
*margin-left: -7px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct font size not being inherited in all browsers.
|
||||||
|
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
|
||||||
|
* and Chrome.
|
||||||
|
* 3. Improve appearance and consistency in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
vertical-align: baseline; /* 3 */
|
||||||
|
*vertical-align: middle; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
|
||||||
|
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
|
||||||
|
* Known issue: inner spacing remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
*overflow: visible; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address box sizing set to content-box in IE 8/9.
|
||||||
|
* 2. Remove excess padding in IE 8/9.
|
||||||
|
* 3. Remove excess padding in IE 7.
|
||||||
|
* Known issue: excess padding remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
*height: 13px; /* 3 */
|
||||||
|
*width: 13px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
-webkit-box-sizing: content-box; /* 2 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||||
|
* on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 3+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
|
||||||
|
* 2. Improve readability and alignment in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto; /* 1 */
|
||||||
|
vertical-align: top; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chromeframe {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
10
client/src/assets/styles/_variables.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
$style-dark: #2c2c2c;
|
||||||
|
$style-darker: #1a1a1a;
|
||||||
|
$style-light: #fafafa;
|
||||||
|
$style-primary: #19bd9c;
|
||||||
|
|
||||||
|
$style-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
$style-font-color: $style-dark;
|
||||||
|
$style-font-size: 14px;
|
23
client/src/assets/styles/main.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@charset "utf-8";
|
||||||
|
|
||||||
|
// Import variables
|
||||||
|
@import "variables";
|
||||||
|
|
||||||
|
// Reset CSS
|
||||||
|
@import "reset";
|
||||||
|
|
||||||
|
// Import Vendor
|
||||||
|
@import "vendor/font-awesome";
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
-webkit-font-smoothing: subpixel-antialiased;
|
||||||
|
background-color: $style-dark;
|
||||||
|
font-family: $style-font-family;
|
||||||
|
font-size: $style-font-size;
|
||||||
|
color: $style-font-color;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
||||||
|
|
20
client/src/assets/styles/vendor/_font-awesome.scss
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Variables
|
||||||
|
|
||||||
|
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||||
|
$fa-font-size-base: 16px;
|
||||||
|
$fa-font-display: auto;
|
||||||
|
$fa-css-prefix: fa;
|
||||||
|
$fa-border-color: #eee;
|
||||||
|
$fa-inverse: #fff;
|
||||||
|
$fa-li-width: 2em;
|
||||||
|
$fa-fw-width: (20em / 16);
|
||||||
|
$fa-primary-opacity: 1;
|
||||||
|
$fa-secondary-opacity: .4;
|
||||||
|
|
||||||
|
$fa-family-default: 'Font Awesome 5 Free';
|
||||||
|
|
||||||
|
// Import FA source files
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/brands";
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/solid";
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/regular";
|
||||||
|
@import "~@fortawesome/fontawesome-free/scss/fontawesome";
|
12
client/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import './assets/styles/main.scss'
|
||||||
|
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Notifications from 'vue-notification'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.use(Notifications)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#neko')
|
1
client/src/types/shims-scss.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module '*.scss' {}
|
13
client/src/types/shims-tsx.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Vue, { VNode } from 'vue'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
// tslint:disable no-empty-interface
|
||||||
|
interface Element extends VNode {}
|
||||||
|
// tslint:disable no-empty-interface
|
||||||
|
interface ElementClass extends Vue {}
|
||||||
|
interface IntrinsicElements {
|
||||||
|
[elem: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
client/src/types/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import Vue from 'vue'
|
||||||
|
export default Vue
|
||||||
|
}
|
37
client/tsconfig.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"importHelpers": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": [
|
||||||
|
"webpack-env"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
11
client/vue.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
sass: {
|
||||||
|
prependData: `
|
||||||
|
@import "@/assets/styles/_variables.scss";
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
47
neko.code-workspace
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "client"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"files.encoding": "utf8",
|
||||||
|
"files.eol": "\n",
|
||||||
|
"typescript.tsdk": "./client/node_modules/typescript/lib",
|
||||||
|
"todo-tree.filtering.excludeGlobs": ["**/node_modules/**"],
|
||||||
|
"eslint.validate": [
|
||||||
|
"vue",
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
],
|
||||||
|
"vetur.validation.template": true,
|
||||||
|
"vetur.useWorkspaceDependencies": true,
|
||||||
|
"remote.extensionKind": {
|
||||||
|
"ms-azuretools.vscode-docker": "ui"
|
||||||
|
},
|
||||||
|
"remote.containers.defaultExtensions": [
|
||||||
|
"ms-vscode.go",
|
||||||
|
"octref.vetur",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-vscode-remote.vscode-remote-extensionpack",
|
||||||
|
"ms-vscode-remote.remote-containers",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"gruntfuggly.todo-tree",
|
||||||
|
"swyphcosmo.spellchecker",
|
||||||
|
"eamodio.gitlens"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
9
server/.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
16
server/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "launch",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "debug",
|
||||||
|
"program": "${workspaceFolder}/cmd/neko",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"output": "${workspaceFolder}/bin/debug/neko",
|
||||||
|
"cwd": "${workspaceFolder}/",
|
||||||
|
"args": ["serve", "-d", "--bind", "0.0.0.0:3000", "--static", "../client/dist", "--password", "test"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
server/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"go.formatTool": "goformat",
|
||||||
|
"go.inferGopath": false,
|
||||||
|
"go.autocompleteUnimportedPackages": true,
|
||||||
|
"go.delveConfig": {
|
||||||
|
"useApiV1": false,
|
||||||
|
"dlvLoadConfig": {
|
||||||
|
"followPointers": true,
|
||||||
|
"maxVariableRecurse": 3,
|
||||||
|
"maxStringLen": 400,
|
||||||
|
"maxArrayValues": 400,
|
||||||
|
"maxStructFields": -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"[go]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
server/Makefile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'`
|
||||||
|
GIT_COMMIT=`git rev-parse --short HEAD`
|
||||||
|
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
|
||||||
|
GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
|
||||||
|
|
||||||
|
LDFLAGS=-ldflags "-s -X version.buildTime=${BUILD_TIME} -X version.gitRevision=${GIT_DIRTY}${GIT_REVISION} -X version.gitBranch=${GIT_BRANCH}"
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o bin/neko ${LDFLAGS} -i cmd/neko/main.go
|
0
server/README.md
Normal file
18
server/cmd/neko/main.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"n.eko.moe/neko"
|
||||||
|
"n.eko.moe/neko/cmd"
|
||||||
|
"n.eko.moe/neko/internal/utils"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Print(utils.Colorf(utils.Header, "server", neko.Service.Version))
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
log.Panic().Err(err).Msg("Failed to execute command")
|
||||||
|
}
|
||||||
|
}
|
35
server/cmd/root.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"n.eko.moe/neko"
|
||||||
|
"n.eko.moe/neko/internal/preflight"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Execute() error {
|
||||||
|
return root.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = &cobra.Command{
|
||||||
|
Use: "neko",
|
||||||
|
Short: "",
|
||||||
|
Long: ``,
|
||||||
|
Version: neko.Service.Version.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(func() {
|
||||||
|
preflight.Logs("neko")
|
||||||
|
preflight.Config("neko")
|
||||||
|
neko.Service.Root.Set()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := neko.Service.Root.Init(root); err != nil {
|
||||||
|
neko.Service.Logger.Panic().Err(err).Msg("Unable to run command")
|
||||||
|
}
|
||||||
|
|
||||||
|
root.SetVersionTemplate(fmt.Sprintf("Version: %s\n", neko.Service.Version))
|
||||||
|
}
|
37
server/cmd/serve.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"n.eko.moe/neko"
|
||||||
|
"n.eko.moe/neko/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
command := &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "",
|
||||||
|
Long: ``,
|
||||||
|
Run: neko.Service.ServeCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
configs := []config.Config{
|
||||||
|
neko.Service.Serve,
|
||||||
|
}
|
||||||
|
|
||||||
|
cobra.OnInitialize(func() {
|
||||||
|
for _, cfg := range configs {
|
||||||
|
cfg.Set()
|
||||||
|
}
|
||||||
|
neko.Service.Preflight()
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
if err := cfg.Init(command); err != nil {
|
||||||
|
log.Panic().Err(err).Msg("Unable to run command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.AddCommand(command)
|
||||||
|
}
|
15
server/go.mod
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module n.eko.moe/neko
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi v4.0.3+incompatible
|
||||||
|
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57
|
||||||
|
github.com/gorilla/websocket v1.4.1
|
||||||
|
github.com/matoous/go-nanoid v1.1.0
|
||||||
|
github.com/pion/webrtc/v2 v2.1.18
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/rs/zerolog v1.17.2
|
||||||
|
github.com/spf13/cobra v0.0.5
|
||||||
|
github.com/spf13/viper v1.6.1
|
||||||
|
)
|
300
server/go.sum
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg=
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
||||||
|
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||||
|
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
|
||||||
|
github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||||
|
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||||
|
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57 h1:0BtenaNSwWghFTsHDQGTdurEdCMm7lsNmWfMoBS2JiY=
|
||||||
|
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57/go.mod h1:P6/F9lmSF2Z/74P/m80qEm6ApjE5HmB+rSzfBCNqIPo=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
|
||||||
|
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
||||||
|
github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1 h1:h0wbuSK8xUNmMwDdCxZx2OLdkVck6Bb31zj4CxCN5I4=
|
||||||
|
github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
|
||||||
|
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
||||||
|
github.com/matoous/go-nanoid v1.1.0 h1:B4BSMxTVgYrCHqtovL/adb8GFkE4mPCNntOOrdZLeCk=
|
||||||
|
github.com/matoous/go-nanoid v1.1.0/go.mod h1:L+uFUqrYwDgNWu5R02GrSxxcqX7ghiFuKPlKEOZ90GE=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||||
|
github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4=
|
||||||
|
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
|
||||||
|
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
|
||||||
|
github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pion/datachannel v1.4.13 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3Ss=
|
||||||
|
github.com/pion/datachannel v1.4.13/go.mod h1:+rBUwEDonA63KXx994DP/ofyyGVAm6AIMvOqQZxjWRU=
|
||||||
|
github.com/pion/dtls/v2 v2.0.0-rc.3 h1:u9utI+EDJOjOWfrkGQsD8WNssPcTwfYIanFB6oI8K+4=
|
||||||
|
github.com/pion/dtls/v2 v2.0.0-rc.3/go.mod h1:x0XH+cN5z+l/+/4nYL8r4sB8g6+0d1Zp2Pfkcoz8BKY=
|
||||||
|
github.com/pion/ice v0.7.6 h1:EARj1MBq5NYaMtXVhYkK03i0RS/meejNHvZS++K5tSY=
|
||||||
|
github.com/pion/ice v0.7.6/go.mod h1:4xCajahEEvc5w0AM+Ujx/Rr2EczON/fKndi3jLyDdh4=
|
||||||
|
github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
|
||||||
|
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
||||||
|
github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
|
||||||
|
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
||||||
|
github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak=
|
||||||
|
github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
|
||||||
|
github.com/pion/rtp v1.1.3/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
|
||||||
|
github.com/pion/rtp v1.1.4 h1:P6xh8Y8JfzR7+JAbI79X2M8kfYETaqbuM5Otm+Z+k6U=
|
||||||
|
github.com/pion/rtp v1.1.4/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
|
||||||
|
github.com/pion/sctp v1.7.3 h1:Pok18oncuAq/WjNxbyltfBSLvbv/6QSCyVJKYyDWP5M=
|
||||||
|
github.com/pion/sctp v1.7.3/go.mod h1:c6C9jaDGX7f5xeSRVju/140XatpO9sOVe81EwpfzAc8=
|
||||||
|
github.com/pion/sdp/v2 v2.3.1 h1:45dub4NRdwyDmQCD3GIY7DZuqC49GBUwBdjuetvdOr0=
|
||||||
|
github.com/pion/sdp/v2 v2.3.1/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA=
|
||||||
|
github.com/pion/srtp v1.2.6 h1:mHQuAMh0P67R7/j1F260u3O+fbRWLyjKLRPZYYvODFM=
|
||||||
|
github.com/pion/srtp v1.2.6/go.mod h1:rd8imc5htjfs99XiEoOjLMEOcVjME63UHx9Ek9IGst0=
|
||||||
|
github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw=
|
||||||
|
github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
|
||||||
|
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
||||||
|
github.com/pion/transport v0.8.9/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8=
|
||||||
|
github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk=
|
||||||
|
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
||||||
|
github.com/pion/turn v1.4.0 h1:7NUMRehQz4fIo53Qv9ui1kJ0Kr1CA82I81RHKHCeM80=
|
||||||
|
github.com/pion/turn v1.4.0/go.mod h1:aDSi6hWX/hd1+gKia9cExZOR0MU95O7zX9p3Gw/P2aU=
|
||||||
|
github.com/pion/webrtc/v2 v2.1.18 h1:g0VN0xfEUSlVNfQmlCD6yOeXy/tMaktESBmHMnBS3bk=
|
||||||
|
github.com/pion/webrtc/v2 v2.1.18/go.mod h1:m0rKlYgLRZWyhmcMWegpF6xtK1ASxmOg8DAR74ttzQY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9 h1:7vKLsPiS3XTrejZHaoKDS3/26K3t10rAtuf1+fRfIVA=
|
||||||
|
github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9/go.mod h1:n1o8s7fg6QGcgIsN9AmWQnBi6KrcbEUX0kFVUwTP53g=
|
||||||
|
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY=
|
||||||
|
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
||||||
|
github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk=
|
||||||
|
github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||||
|
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/shirou/gopsutil v2.19.6+incompatible h1:49/Gru26Lne9Cl3IoAVDZVM09hvkSrUodgIIsCVRwbs=
|
||||||
|
github.com/shirou/gopsutil v2.19.6+incompatible/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
|
||||||
|
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
|
||||||
|
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||||
|
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53 h1:tYb/9KQi8dTCSDia2NwbuhUbKlaurqC/S7MFQo96nLk=
|
||||||
|
github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53/go.mod h1:5txYrXKrQG6ZJYdGIiMVVxiOhbdACnwBcHzIwGQ7Nkw=
|
||||||
|
github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8 h1:9Y+hoKBYa+UtzGqkODfs8c0Q6gp2UfniVNsHQWghPi0=
|
||||||
|
github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8/go.mod h1:52+3yYrTNjWKh+CkQozNRCLWCE/X666yAWPGbYC3DZI=
|
||||||
|
github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b h1:psGhQitWSo4KBpLghvJPlhHxTJ8LQl1y0ekjSreqvu4=
|
||||||
|
github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||||
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
|
||||||
|
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
8
server/internal/config/config.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
Init(cmd *cobra.Command) error
|
||||||
|
Set()
|
||||||
|
}
|
37
server/internal/config/root.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Root struct {
|
||||||
|
Debug bool
|
||||||
|
Logs bool
|
||||||
|
CfgFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Root) Init(cmd *cobra.Command) error {
|
||||||
|
cmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug mode")
|
||||||
|
if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().BoolP("logs", "l", false, "Save logs to file")
|
||||||
|
if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().String("config", "", "Configuration file path")
|
||||||
|
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Root) Set() {
|
||||||
|
s.Logs = viper.GetBool("logs")
|
||||||
|
s.Debug = viper.GetBool("debug")
|
||||||
|
s.CfgFile = viper.GetString("config")
|
||||||
|
}
|
52
server/internal/config/serve.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Serve struct {
|
||||||
|
Cert string
|
||||||
|
Key string
|
||||||
|
Bind string
|
||||||
|
Password string
|
||||||
|
Static string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Serve) Init(cmd *cobra.Command) error {
|
||||||
|
|
||||||
|
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "Address/port/socket to serve neko")
|
||||||
|
if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().String("cert", "", "Path to the SSL cert used to secure the neko server")
|
||||||
|
if err := viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().String("key", "", "Path to the SSL key used to secure the neko server")
|
||||||
|
if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream")
|
||||||
|
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().String("static", "./www", "Static files to serve")
|
||||||
|
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Serve) Set() {
|
||||||
|
s.Cert = viper.GetString("cert")
|
||||||
|
s.Key = viper.GetString("key")
|
||||||
|
s.Bind = viper.GetString("bind")
|
||||||
|
s.Password = viper.GetString("password")
|
||||||
|
s.Static = viper.GetString("static")
|
||||||
|
}
|
88
server/internal/gst/gst.c
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
#include "gst.h"
|
||||||
|
|
||||||
|
#include <gst/app/gstappsrc.h>
|
||||||
|
|
||||||
|
typedef struct SampleHandlerUserData {
|
||||||
|
int pipelineId;
|
||||||
|
} SampleHandlerUserData;
|
||||||
|
|
||||||
|
GMainLoop *gstreamer_send_main_loop = NULL;
|
||||||
|
void gstreamer_send_start_mainloop(void) {
|
||||||
|
gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE);
|
||||||
|
|
||||||
|
g_main_loop_run(gstreamer_send_main_loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean gstreamer_send_bus_call(GstBus *bus, GstMessage *msg, gpointer data) {
|
||||||
|
switch (GST_MESSAGE_TYPE(msg)) {
|
||||||
|
|
||||||
|
case GST_MESSAGE_EOS:
|
||||||
|
g_print("End of stream\n");
|
||||||
|
exit(1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GST_MESSAGE_ERROR: {
|
||||||
|
gchar *debug;
|
||||||
|
GError *error;
|
||||||
|
|
||||||
|
gst_message_parse_error(msg, &error, &debug);
|
||||||
|
g_free(debug);
|
||||||
|
|
||||||
|
g_printerr("Error: %s\n", error->message);
|
||||||
|
g_error_free(error);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
|
||||||
|
GstSample *sample = NULL;
|
||||||
|
GstBuffer *buffer = NULL;
|
||||||
|
gpointer copy = NULL;
|
||||||
|
gsize copy_size = 0;
|
||||||
|
SampleHandlerUserData *s = (SampleHandlerUserData *)user_data;
|
||||||
|
|
||||||
|
g_signal_emit_by_name (object, "pull-sample", &sample);
|
||||||
|
if (sample) {
|
||||||
|
buffer = gst_sample_get_buffer(sample);
|
||||||
|
if (buffer) {
|
||||||
|
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), ©, ©_size);
|
||||||
|
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), s->pipelineId);
|
||||||
|
}
|
||||||
|
gst_sample_unref (sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GST_FLOW_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
GstElement *gstreamer_send_create_pipeline(char *pipeline) {
|
||||||
|
gst_init(NULL, NULL);
|
||||||
|
GError *error = NULL;
|
||||||
|
return gst_parse_launch(pipeline, &error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId) {
|
||||||
|
SampleHandlerUserData *s = calloc(1, sizeof(SampleHandlerUserData));
|
||||||
|
s->pipelineId = pipelineId;
|
||||||
|
|
||||||
|
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
|
||||||
|
gst_bus_add_watch(bus, gstreamer_send_bus_call, NULL);
|
||||||
|
gst_object_unref(bus);
|
||||||
|
|
||||||
|
GstElement *appsink = gst_bin_get_by_name(GST_BIN(pipeline), "appsink");
|
||||||
|
g_object_set(appsink, "emit-signals", TRUE, NULL);
|
||||||
|
g_signal_connect(appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), s);
|
||||||
|
gst_object_unref(appsink);
|
||||||
|
|
||||||
|
gst_element_set_state(pipeline, GST_STATE_PLAYING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gstreamer_send_stop_pipeline(GstElement *pipeline) {
|
||||||
|
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
125
server/internal/gst/gst.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package gst
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
|
||||||
|
|
||||||
|
#include "gst.h"
|
||||||
|
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/pion/webrtc/v2"
|
||||||
|
"github.com/pion/webrtc/v2/pkg/media"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go C.gstreamer_send_start_mainloop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipeline is a wrapper for a GStreamer Pipeline
|
||||||
|
type Pipeline struct {
|
||||||
|
Pipeline *C.GstElement
|
||||||
|
tracks []*webrtc.Track
|
||||||
|
id int
|
||||||
|
codecName string
|
||||||
|
clockRate float32
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipelines = make(map[int]*Pipeline)
|
||||||
|
var pipelinesLock sync.Mutex
|
||||||
|
|
||||||
|
const (
|
||||||
|
videoClockRate = 90000
|
||||||
|
audioClockRate = 48000
|
||||||
|
pcmClockRate = 8000
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreatePipeline creates a GStreamer Pipeline
|
||||||
|
func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline {
|
||||||
|
pipelineStr := "appsink name=appsink"
|
||||||
|
var clockRate float32
|
||||||
|
|
||||||
|
switch codecName {
|
||||||
|
case webrtc.VP8:
|
||||||
|
pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr
|
||||||
|
clockRate = videoClockRate
|
||||||
|
|
||||||
|
case webrtc.VP9:
|
||||||
|
pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr
|
||||||
|
clockRate = videoClockRate
|
||||||
|
|
||||||
|
case webrtc.H264:
|
||||||
|
pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 speed-preset=veryfast key-int-max=60 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
|
||||||
|
clockRate = videoClockRate
|
||||||
|
|
||||||
|
case webrtc.Opus:
|
||||||
|
pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr
|
||||||
|
clockRate = audioClockRate
|
||||||
|
|
||||||
|
case webrtc.G722:
|
||||||
|
pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr
|
||||||
|
clockRate = audioClockRate
|
||||||
|
|
||||||
|
case webrtc.PCMU:
|
||||||
|
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr
|
||||||
|
clockRate = pcmClockRate
|
||||||
|
|
||||||
|
case webrtc.PCMA:
|
||||||
|
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr
|
||||||
|
clockRate = pcmClockRate
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("Unhandled codec " + codecName)
|
||||||
|
}
|
||||||
|
|
||||||
|
pipelineStrUnsafe := C.CString(pipelineStr)
|
||||||
|
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
|
||||||
|
|
||||||
|
pipelinesLock.Lock()
|
||||||
|
defer pipelinesLock.Unlock()
|
||||||
|
|
||||||
|
pipeline := &Pipeline{
|
||||||
|
Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe),
|
||||||
|
tracks: tracks,
|
||||||
|
id: len(pipelines),
|
||||||
|
codecName: codecName,
|
||||||
|
clockRate: clockRate,
|
||||||
|
}
|
||||||
|
|
||||||
|
pipelines[pipeline.id] = pipeline
|
||||||
|
return pipeline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the GStreamer Pipeline
|
||||||
|
func (p *Pipeline) Start() {
|
||||||
|
C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the GStreamer Pipeline
|
||||||
|
func (p *Pipeline) Stop() {
|
||||||
|
C.gstreamer_send_stop_pipeline(p.Pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export goHandlePipelineBuffer
|
||||||
|
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
|
||||||
|
pipelinesLock.Lock()
|
||||||
|
pipeline, ok := pipelines[int(pipelineID)]
|
||||||
|
pipelinesLock.Unlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
samples := uint32(pipeline.clockRate * (float32(duration) / 1000000000))
|
||||||
|
for _, t := range pipeline.tracks {
|
||||||
|
if err := t.WriteSample(media.Sample{Data: C.GoBytes(buffer, bufferLen), Samples: samples}); err != nil && err != io.ErrClosedPipe {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("discarding buffer, no pipeline with id %d", int(pipelineID))
|
||||||
|
}
|
||||||
|
C.free(buffer)
|
||||||
|
}
|
16
server/internal/gst/gst.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#ifndef GST_H
|
||||||
|
#define GST_H
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
#include <gst/gst.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
|
||||||
|
|
||||||
|
GstElement *gstreamer_send_create_pipeline(char *pipeline);
|
||||||
|
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
|
||||||
|
void gstreamer_send_stop_pipeline(GstElement *pipeline);
|
||||||
|
void gstreamer_send_start_mainloop(void);
|
||||||
|
|
||||||
|
#endif
|
14
server/internal/http/api.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/http/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(bind, password, static string) *http.Server {
|
||||||
|
return &http.Server{
|
||||||
|
Addr: bind,
|
||||||
|
Handler: handler.New(password, static),
|
||||||
|
}
|
||||||
|
}
|
102
server/internal/http/endpoint/endpoint.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Endpoint func(http.ResponseWriter, *http.Request) error
|
||||||
|
|
||||||
|
ErrResponse struct {
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
Err string `json:"error,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
RequestID string `json:"request,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handle(handler Endpoint) http.HandlerFunc {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := handler(w, r); err != nil {
|
||||||
|
WriteError(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonErrorsCodes = map[int]bool{
|
||||||
|
404: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func errResponse(input interface{}) *ErrResponse {
|
||||||
|
var res *ErrResponse
|
||||||
|
var err interface{}
|
||||||
|
|
||||||
|
switch input.(type) {
|
||||||
|
case *HandlerError:
|
||||||
|
e := input.(*HandlerError)
|
||||||
|
res = &ErrResponse{
|
||||||
|
Status: e.Status,
|
||||||
|
Err: http.StatusText(e.Status),
|
||||||
|
Message: e.Message,
|
||||||
|
}
|
||||||
|
err = e.Err
|
||||||
|
default:
|
||||||
|
res = &ErrResponse{
|
||||||
|
Status: http.StatusInternalServerError,
|
||||||
|
Err: http.StatusText(http.StatusInternalServerError),
|
||||||
|
}
|
||||||
|
err = input
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case *error:
|
||||||
|
e := err.(error)
|
||||||
|
res.Details = e.Error()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
res.Details = fmt.Sprintf("%+v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) {
|
||||||
|
hlog := log.With().
|
||||||
|
Str("module", "http").
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
res := errResponse(err)
|
||||||
|
|
||||||
|
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||||
|
res.RequestID = reqID
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(res.Status)
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
|
hlog.Warn().Err(err).Msg("Failed writing json error response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nonErrorsCodes[res.Status] {
|
||||||
|
logEntry := middleware.GetLogEntry(r)
|
||||||
|
if logEntry != nil {
|
||||||
|
logEntry.Panic(err, debug.Stack())
|
||||||
|
} else {
|
||||||
|
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
server/internal/http/endpoint/error.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type HandlerError struct {
|
||||||
|
Status int
|
||||||
|
Message string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HandlerError) Error() string {
|
||||||
|
if e.Err != nil {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Message
|
||||||
|
}
|
55
server/internal/http/handler/handler.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/http/middleware"
|
||||||
|
"n.eko.moe/neko/internal/http/endpoint"
|
||||||
|
"n.eko.moe/neko/internal/webrtc"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
router *chi.Mux
|
||||||
|
manager *webrtc.WebRTCManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(password, static string) *chi.Mux {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
manager, err := webrtc.NewManager(password)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := &Handler{
|
||||||
|
router: router,
|
||||||
|
manager: manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||||
|
// router.Use(middleware.Logger) // Log API request calls
|
||||||
|
|
||||||
|
router.Get("/ping", endpoint.Handle(handler.Ping))
|
||||||
|
router.Get("/ws", endpoint.Handle(handler.WebSocket))
|
||||||
|
|
||||||
|
fs := http.FileServer(http.Dir(static))
|
||||||
|
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if _, err := os.Stat(static + r.RequestURI); os.IsNotExist(err) {
|
||||||
|
http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
fs.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.NotFound(endpoint.Handle(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return &endpoint.HandlerError{
|
||||||
|
Status: http.StatusNotFound,
|
||||||
|
Message: fmt.Sprintf("Endpoint '%s' is not avalible", r.RequestURI),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
10
server/internal/http/handler/ping.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("."))
|
||||||
|
return nil
|
||||||
|
}
|
9
server/internal/http/handler/websocket.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return h.manager.Upgrade(w, r)
|
||||||
|
}
|
80
server/internal/http/middleware/logger.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Logger(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := map[string]interface{}{}
|
||||||
|
|
||||||
|
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||||
|
req["id"] = reqID
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
req["scheme"] = scheme
|
||||||
|
req["proto"] = r.Proto
|
||||||
|
req["method"] = r.Method
|
||||||
|
req["remote"] = r.RemoteAddr
|
||||||
|
req["agent"] = r.UserAgent()
|
||||||
|
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||||
|
|
||||||
|
fields := map[string]interface{}{}
|
||||||
|
fields["req"] = req
|
||||||
|
|
||||||
|
entry := &entry{
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||||
|
t1 := time.Now()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1))
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
fields map[string]interface{}
|
||||||
|
errors []map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *entry) Write(status, bytes int, elapsed time.Duration) {
|
||||||
|
res := map[string]interface{}{}
|
||||||
|
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||||
|
res["status"] = status
|
||||||
|
res["bytes"] = bytes
|
||||||
|
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||||
|
|
||||||
|
e.fields["res"] = res
|
||||||
|
e.fields["module"] = "api"
|
||||||
|
|
||||||
|
if len(e.errors) > 0 {
|
||||||
|
e.fields["errors"] = e.errors
|
||||||
|
log.Error().Fields(e.fields).Msgf("Request failed (%d)", status)
|
||||||
|
} else {
|
||||||
|
log.Debug().Fields(e.fields).Msgf("Request complete (%d)", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *entry) Panic(v interface{}, stack []byte) {
|
||||||
|
err := map[string]interface{}{}
|
||||||
|
err["message"] = fmt.Sprintf("%+v", v)
|
||||||
|
err["stack"] = string(stack)
|
||||||
|
|
||||||
|
e.errors = append(e.errors, err)
|
||||||
|
}
|
12
server/internal/http/middleware/middleware.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
// contextKey is a value for use with context.WithValue. It's used as
|
||||||
|
// a pointer so it fits in an interface{} without allocation. This technique
|
||||||
|
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||||
|
type ctxKey struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *ctxKey) String() string {
|
||||||
|
return "neko/ctx/" + k.name
|
||||||
|
}
|
24
server/internal/http/middleware/recover.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
// The original work was derived from Goji's middleware, source:
|
||||||
|
// https://github.com/zenazn/goji/tree/master/web/middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/http/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Recoverer(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if rvr := recover(); rvr != nil {
|
||||||
|
endpoint.WriteError(w, r, rvr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
89
server/internal/http/middleware/request.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key to use when setting the request ID.
|
||||||
|
type ctxKeyRequestID int
|
||||||
|
|
||||||
|
// RequestIDKey is the key that holds the unique request ID in a request context.
|
||||||
|
const RequestIDKey ctxKeyRequestID = 0
|
||||||
|
|
||||||
|
var prefix string
|
||||||
|
var reqid uint64
|
||||||
|
|
||||||
|
// A quick note on the statistics here: we're trying to calculate the chance that
|
||||||
|
// two randomly generated base62 prefixes will collide. We use the formula from
|
||||||
|
// http://en.wikipedia.org/wiki/Birthday_problem
|
||||||
|
//
|
||||||
|
// P[m, n] \approx 1 - e^{-m^2/2n}
|
||||||
|
//
|
||||||
|
// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server
|
||||||
|
// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$
|
||||||
|
//
|
||||||
|
// For a $k$ character base-62 identifier, we have $n(k) = 62^k$
|
||||||
|
//
|
||||||
|
// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for
|
||||||
|
// our purposes, and is surely more than anyone would ever need in practice -- a
|
||||||
|
// process that is rebooted a handful of times a day for a hundred years has less
|
||||||
|
// than a millionth of a percent chance of generating two colliding IDs.
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if hostname == "" || err != nil {
|
||||||
|
hostname = "localhost"
|
||||||
|
}
|
||||||
|
var buf [12]byte
|
||||||
|
var b64 string
|
||||||
|
for len(b64) < 10 {
|
||||||
|
rand.Read(buf[:])
|
||||||
|
b64 = base64.StdEncoding.EncodeToString(buf[:])
|
||||||
|
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestID is a middleware that injects a request ID into the context of each
|
||||||
|
// request. A request ID is a string of the form "host.example.com/random-0001",
|
||||||
|
// where "random" is a base62 random string that uniquely identifies this go
|
||||||
|
// process, and where the last number is an atomically incremented request
|
||||||
|
// counter.
|
||||||
|
func RequestID(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
requestID := r.Header.Get("X-Request-Id")
|
||||||
|
if requestID == "" {
|
||||||
|
myid := atomic.AddUint64(&reqid, 1)
|
||||||
|
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, RequestIDKey, requestID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReqID returns a request ID from the given context if one is present.
|
||||||
|
// Returns the empty string if a request ID cannot be found.
|
||||||
|
func GetReqID(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||||
|
return reqID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextRequestID generates the next request ID in the sequence.
|
||||||
|
func NextRequestID() uint64 {
|
||||||
|
return atomic.AddUint64(&reqid, 1)
|
||||||
|
}
|
32
server/internal/http/response/response.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/http/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON encodes data to rw in JSON format. Returns a pointer to a
|
||||||
|
// HandlerError if encoding fails.
|
||||||
|
func JSON(w http.ResponseWriter, data interface{}, status int) error {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return &endpoint.HandlerError{
|
||||||
|
Status: http.StatusInternalServerError,
|
||||||
|
Message: "Unable to write JSON response",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty merely sets the response code to NoContent (204).
|
||||||
|
func Empty(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return nil
|
||||||
|
}
|
203
server/internal/keys/keyboard.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
const KEY_0 = 48
|
||||||
|
const KEY_1 = 49
|
||||||
|
const KEY_2 = 50
|
||||||
|
const KEY_3 = 51
|
||||||
|
const KEY_4 = 52
|
||||||
|
const KEY_5 = 53
|
||||||
|
const KEY_6 = 54
|
||||||
|
const KEY_7 = 55
|
||||||
|
const KEY_8 = 56
|
||||||
|
const KEY_9 = 57
|
||||||
|
|
||||||
|
const KEY_A = 65
|
||||||
|
const KEY_B = 66
|
||||||
|
const KEY_C = 67
|
||||||
|
const KEY_D = 68
|
||||||
|
const KEY_E = 69
|
||||||
|
const KEY_F = 70
|
||||||
|
const KEY_G = 71
|
||||||
|
const KEY_H = 72
|
||||||
|
const KEY_I = 73
|
||||||
|
const KEY_J = 74
|
||||||
|
const KEY_K = 75
|
||||||
|
const KEY_L = 76
|
||||||
|
const KEY_M = 77
|
||||||
|
const KEY_N = 78
|
||||||
|
const KEY_O = 79
|
||||||
|
const KEY_P = 80
|
||||||
|
const KEY_Q = 81
|
||||||
|
const KEY_R = 82
|
||||||
|
const KEY_S = 83
|
||||||
|
const KEY_T = 84
|
||||||
|
const KEY_U = 85
|
||||||
|
const KEY_V = 86
|
||||||
|
const KEY_W = 87
|
||||||
|
const KEY_X = 88
|
||||||
|
const KEY_Y = 89
|
||||||
|
const KEY_Z = 90
|
||||||
|
|
||||||
|
const KEY_NUMPAD0 = 96
|
||||||
|
const KEY_NUMPAD1 = 97
|
||||||
|
const KEY_NUMPAD2 = 98
|
||||||
|
const KEY_NUMPAD3 = 99
|
||||||
|
const KEY_NUMPAD4 = 100
|
||||||
|
const KEY_NUMPAD5 = 101
|
||||||
|
const KEY_NUMPAD6 = 102
|
||||||
|
const KEY_NUMPAD7 = 103
|
||||||
|
const KEY_NUMPAD8 = 104
|
||||||
|
const KEY_NUMPAD9 = 105
|
||||||
|
|
||||||
|
const KEY_F1 = 112
|
||||||
|
const KEY_F2 = 113
|
||||||
|
const KEY_F3 = 114
|
||||||
|
const KEY_F4 = 115
|
||||||
|
const KEY_F5 = 116
|
||||||
|
const KEY_F6 = 117
|
||||||
|
const KEY_F7 = 118
|
||||||
|
const KEY_F8 = 119
|
||||||
|
const KEY_F9 = 120
|
||||||
|
const KEY_F10 = 121
|
||||||
|
const KEY_F11 = 122
|
||||||
|
const KEY_F12 = 123
|
||||||
|
|
||||||
|
const KEY_BACK_SPACE = 8
|
||||||
|
const KEY_TAB = 9
|
||||||
|
const KEY_ENTER = 13
|
||||||
|
const KEY_ENTER_ALT = 14
|
||||||
|
const KEY_SHIFT = 16
|
||||||
|
const KEY_CONTROL = 17
|
||||||
|
const KEY_ALT = 18
|
||||||
|
const KEY_ESCAPE = 27
|
||||||
|
const KEY_SPACE = 32
|
||||||
|
const KEY_PAGE_UP = 33
|
||||||
|
const KEY_PAGE_DOWN = 34
|
||||||
|
const KEY_END = 35
|
||||||
|
const KEY_LEFT = 37
|
||||||
|
const KEY_UP = 38
|
||||||
|
const KEY_RIGHT = 39
|
||||||
|
const KEY_DOWN = 40
|
||||||
|
const KEY_DELETE = 46
|
||||||
|
const KEY_SEMICOLON = 59
|
||||||
|
const KEY_SEMICOLON_ALT = 186
|
||||||
|
const KEY_EQUALS = 61
|
||||||
|
const KEY_EQUALS_ALT = 187
|
||||||
|
const KEY_MULTIPLY = 106
|
||||||
|
const KEY_ADD = 107
|
||||||
|
const KEY_SEPARATOR = 108
|
||||||
|
const KEY_SUBTRACT = 109
|
||||||
|
const KEY_SUBTRACT_ALT = 189
|
||||||
|
const KEY_DECIMAL = 110
|
||||||
|
const KEY_DIVIDE = 111
|
||||||
|
const KEY_COMMA = 188
|
||||||
|
const KEY_PERIOD = 190
|
||||||
|
const KEY_SLASH = 191
|
||||||
|
const KEY_BACK_QUOTE = 192
|
||||||
|
const KEY_BACK_SLASH = 220
|
||||||
|
const KEY_OPEN_BRACKET = 219
|
||||||
|
const KEY_CLOSE_BRACKET = 221
|
||||||
|
const KEY_QUOTE = 222
|
||||||
|
|
||||||
|
var Keyboard = map[int]string{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Keyboard[KEY_A] = "a"
|
||||||
|
Keyboard[KEY_B] = "b"
|
||||||
|
Keyboard[KEY_C] = "c"
|
||||||
|
Keyboard[KEY_D] = "d"
|
||||||
|
Keyboard[KEY_E] = "e"
|
||||||
|
Keyboard[KEY_F] = "f"
|
||||||
|
Keyboard[KEY_G] = "g"
|
||||||
|
Keyboard[KEY_H] = "h"
|
||||||
|
Keyboard[KEY_I] = "i"
|
||||||
|
Keyboard[KEY_J] = "j"
|
||||||
|
Keyboard[KEY_K] = "k"
|
||||||
|
Keyboard[KEY_L] = "l"
|
||||||
|
Keyboard[KEY_M] = "m"
|
||||||
|
Keyboard[KEY_N] = "n"
|
||||||
|
Keyboard[KEY_O] = "o"
|
||||||
|
Keyboard[KEY_P] = "p"
|
||||||
|
Keyboard[KEY_Q] = "q"
|
||||||
|
Keyboard[KEY_R] = "r"
|
||||||
|
Keyboard[KEY_S] = "s"
|
||||||
|
Keyboard[KEY_T] = "r"
|
||||||
|
Keyboard[KEY_U] = "u"
|
||||||
|
Keyboard[KEY_V] = "v"
|
||||||
|
Keyboard[KEY_W] = "w"
|
||||||
|
Keyboard[KEY_X] = "x"
|
||||||
|
Keyboard[KEY_Y] = "y"
|
||||||
|
Keyboard[KEY_Z] = "z"
|
||||||
|
|
||||||
|
Keyboard[KEY_0] = "0"
|
||||||
|
Keyboard[KEY_1] = "1"
|
||||||
|
Keyboard[KEY_2] = "2"
|
||||||
|
Keyboard[KEY_3] = "3"
|
||||||
|
Keyboard[KEY_4] = "4"
|
||||||
|
Keyboard[KEY_5] = "5"
|
||||||
|
Keyboard[KEY_6] = "6"
|
||||||
|
Keyboard[KEY_7] = "7"
|
||||||
|
Keyboard[KEY_8] = "8"
|
||||||
|
Keyboard[KEY_9] = "9"
|
||||||
|
|
||||||
|
Keyboard[KEY_NUMPAD0] = "0"
|
||||||
|
Keyboard[KEY_NUMPAD1] = "1"
|
||||||
|
Keyboard[KEY_NUMPAD2] = "2"
|
||||||
|
Keyboard[KEY_NUMPAD3] = "3"
|
||||||
|
Keyboard[KEY_NUMPAD4] = "4"
|
||||||
|
Keyboard[KEY_NUMPAD5] = "5"
|
||||||
|
Keyboard[KEY_NUMPAD6] = "6"
|
||||||
|
Keyboard[KEY_NUMPAD7] = "7"
|
||||||
|
Keyboard[KEY_NUMPAD8] = "8"
|
||||||
|
Keyboard[KEY_NUMPAD9] = "9"
|
||||||
|
|
||||||
|
Keyboard[KEY_F1] = "f1"
|
||||||
|
Keyboard[KEY_F2] = "f2"
|
||||||
|
Keyboard[KEY_F3] = "f3"
|
||||||
|
Keyboard[KEY_F4] = "f4"
|
||||||
|
Keyboard[KEY_F5] = "f5"
|
||||||
|
Keyboard[KEY_F6] = "f6"
|
||||||
|
Keyboard[KEY_F7] = "f7"
|
||||||
|
Keyboard[KEY_F8] = "f8"
|
||||||
|
Keyboard[KEY_F9] = "f9"
|
||||||
|
Keyboard[KEY_F10] = "f10"
|
||||||
|
Keyboard[KEY_F11] = "f11"
|
||||||
|
Keyboard[KEY_F12] = "f12"
|
||||||
|
|
||||||
|
Keyboard[KEY_QUOTE] = "'"
|
||||||
|
Keyboard[KEY_COMMA] = ","
|
||||||
|
Keyboard[KEY_PERIOD] = "."
|
||||||
|
Keyboard[KEY_SEMICOLON] = ";"
|
||||||
|
Keyboard[KEY_SEMICOLON_ALT] = ";"
|
||||||
|
Keyboard[KEY_SLASH] = "/"
|
||||||
|
Keyboard[KEY_BACK_SLASH] = "\\"
|
||||||
|
Keyboard[KEY_BACK_QUOTE] = "`"
|
||||||
|
Keyboard[KEY_OPEN_BRACKET] = "["
|
||||||
|
Keyboard[KEY_CLOSE_BRACKET] = "]"
|
||||||
|
Keyboard[KEY_EQUALS] = "="
|
||||||
|
Keyboard[KEY_EQUALS_ALT] = "="
|
||||||
|
Keyboard[KEY_MULTIPLY] = "*"
|
||||||
|
Keyboard[KEY_ADD] = "+"
|
||||||
|
Keyboard[KEY_SEPARATOR] = "."
|
||||||
|
Keyboard[KEY_SUBTRACT] = "-"
|
||||||
|
Keyboard[KEY_SUBTRACT_ALT] = "-"
|
||||||
|
Keyboard[KEY_DECIMAL] = "."
|
||||||
|
Keyboard[KEY_DIVIDE] = "/"
|
||||||
|
Keyboard[KEY_BACK_SPACE] = "backspace"
|
||||||
|
Keyboard[KEY_DELETE] = "delete"
|
||||||
|
Keyboard[KEY_ENTER] = "enter"
|
||||||
|
Keyboard[KEY_ENTER_ALT] = "enter"
|
||||||
|
Keyboard[KEY_TAB] = "tab"
|
||||||
|
Keyboard[KEY_ESCAPE] = "escape"
|
||||||
|
Keyboard[KEY_UP] = "up"
|
||||||
|
Keyboard[KEY_DOWN] = "down"
|
||||||
|
Keyboard[KEY_RIGHT] = "right"
|
||||||
|
Keyboard[KEY_LEFT] = "left"
|
||||||
|
Keyboard[KEY_END] = "end"
|
||||||
|
Keyboard[KEY_PAGE_UP] = "pageup"
|
||||||
|
Keyboard[KEY_PAGE_DOWN] = "pagedown"
|
||||||
|
Keyboard[KEY_ALT] = "alt"
|
||||||
|
Keyboard[KEY_CONTROL] = "control"
|
||||||
|
Keyboard[KEY_SHIFT] = "shift"
|
||||||
|
Keyboard[KEY_SPACE] = "space"
|
||||||
|
}
|
21
server/internal/keys/mouse.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
const MOUSE_LEFT = 0
|
||||||
|
const MOUSE_MIDDLE = 1
|
||||||
|
const MOUSE_RIGHT = 2
|
||||||
|
const MOUSE_WHEEL_UP = 4
|
||||||
|
const MOUSE_WHEEL_DOWN = 5
|
||||||
|
const MOUSE_WHEEL_RIGH = 6
|
||||||
|
const MOUSE_WHEEL_LEFT = 7
|
||||||
|
|
||||||
|
var Mouse = map[int]string{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Mouse[MOUSE_LEFT] = "left"
|
||||||
|
Mouse[MOUSE_MIDDLE] = "center"
|
||||||
|
Mouse[MOUSE_RIGHT] = "right"
|
||||||
|
Mouse[MOUSE_WHEEL_UP] = "wheelUp"
|
||||||
|
Mouse[MOUSE_WHEEL_DOWN] = "wheelDown"
|
||||||
|
Mouse[MOUSE_WHEEL_RIGH] = "wheelRight"
|
||||||
|
Mouse[MOUSE_WHEEL_LEFT] = "wheelLeft"
|
||||||
|
}
|
71
server/internal/nanoid/nanoid.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package nanoid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gonanoid "github.com/matoous/go-nanoid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nano *NanoID
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
nano = &NanoID{
|
||||||
|
alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
size: 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(alphabet string, size int) *NanoID {
|
||||||
|
return &NanoID{
|
||||||
|
alphabet: alphabet,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NanoID struct {
|
||||||
|
alphabet string
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NanoID) NewID() (string, error) {
|
||||||
|
return gonanoid.Generate(n.alphabet, n.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NanoID) NewIDSize(size int) (string, error) {
|
||||||
|
return gonanoid.Generate(n.alphabet, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NanoID) NewIDRang(max int, min int) (string, error) {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
return gonanoid.Generate(n.alphabet, rand.Intn(max-min)+min)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NanoID) GenerateID(alphabet string, size int) (string, error) {
|
||||||
|
return gonanoid.Generate(alphabet, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NanoID) GenerateIDRange(alphabet string, max int, min int) (string, error) {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
return gonanoid.Generate(alphabet, rand.Intn(max-min)+min)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewID() (string, error) {
|
||||||
|
return nano.NewID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDSize(size int) (string, error) {
|
||||||
|
return nano.NewIDSize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDRang(max int, min int) (string, error) {
|
||||||
|
return nano.NewIDRang(max, min)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateID(alphabet string, size int) (string, error) {
|
||||||
|
return nano.GenerateID(alphabet, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateIDRange(alphabet string, max int, min int) (string, error) {
|
||||||
|
return nano.GenerateIDRange(alphabet, max, min)
|
||||||
|
}
|
48
server/internal/preflight/config.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package preflight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Config(name string) {
|
||||||
|
config := viper.GetString("neko.config")
|
||||||
|
|
||||||
|
if config != "" {
|
||||||
|
viper.SetConfigFile(config) // Use config file from the flag.
|
||||||
|
} else {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
viper.AddConfigPath("/etc/neko/")
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.SetConfigName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetEnvPrefix("NEKO")
|
||||||
|
viper.AutomaticEnv() // read in environment variables that match
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
|
log.Error().Err(err)
|
||||||
|
}
|
||||||
|
if config != "" {
|
||||||
|
log.Error().Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file := viper.ConfigFileUsed()
|
||||||
|
logger := log.With().
|
||||||
|
Bool("debug", viper.GetBool("neko.debug")).
|
||||||
|
Str("logging", viper.GetString("neko.logs")).
|
||||||
|
Str("config", file).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
if file == "" {
|
||||||
|
logger.Warn().Msg("Preflight complete without config file")
|
||||||
|
} else {
|
||||||
|
logger.Info().Msg("Preflight complete")
|
||||||
|
}
|
||||||
|
}
|
60
server/internal/preflight/logs.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package preflight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/diode"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Logs(name string) {
|
||||||
|
zerolog.TimeFieldFormat = ""
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
|
||||||
|
if viper.GetBool("neko.debug") {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
console := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||||
|
|
||||||
|
if !viper.GetBool("neko.logs") {
|
||||||
|
log.Logger = log.Output(console)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
logs := filepath.Join(".", "logs")
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
logs = "/var/log/neko"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(logs); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(logs, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
latest := filepath.Join(logs, name+"-latest.log")
|
||||||
|
_, err := os.Stat(latest)
|
||||||
|
if err == nil {
|
||||||
|
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||||
|
if err != nil {
|
||||||
|
log.Panic().Err(err).Msg("Failed to rotate log file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic().Err(err).Msg("Failed to create log file")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||||
|
fmt.Printf("Logger Dropped %d messages", missed)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Logger = log.Output(io.MultiWriter(console, logger))
|
||||||
|
}
|
||||||
|
}
|
21
server/internal/structs/version.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Major string
|
||||||
|
Minor string
|
||||||
|
Patch string
|
||||||
|
Version string
|
||||||
|
GitVersion string
|
||||||
|
GitCommit string
|
||||||
|
GitTreeState string
|
||||||
|
BuildDate string
|
||||||
|
GoVersion string
|
||||||
|
Compiler string
|
||||||
|
Platform string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Version) String() string {
|
||||||
|
return fmt.Sprintf("%s.%s.%s", i.Major, i.Minor, i.Patch)
|
||||||
|
}
|
34
server/internal/utils/color.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
char = "&"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Colors: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html
|
||||||
|
var re = regexp.MustCompile(char + `(?m)([0-9]{1,2};[0-9]{1,2}|[0-9]{1,2})`)
|
||||||
|
|
||||||
|
func Color(str string) string {
|
||||||
|
result := ""
|
||||||
|
lastIndex := 0
|
||||||
|
|
||||||
|
for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
|
||||||
|
groups := []string{}
|
||||||
|
for i := 0; i < len(v); i += 2 {
|
||||||
|
groups = append(groups, str[v[i]:v[i+1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
result += str[lastIndex:v[0]] + "\033[" + groups[1] + "m"
|
||||||
|
lastIndex = v[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + str[lastIndex:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Colorf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(Color(format), a...)
|
||||||
|
}
|
10
server/internal/utils/header.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
const Header = `&34
|
||||||
|
_ __ __
|
||||||
|
/ | / /__ / /______ \ /\
|
||||||
|
/ |/ / _ \/ //_/ __ \ ) ( ')
|
||||||
|
/ /| / __/ ,< / /_/ / ( / )
|
||||||
|
/_/ |_/\___/_/|_|\____/ \(__)|
|
||||||
|
&1&37 nurdism/neko &33%s v%s&0
|
||||||
|
`
|
25
server/internal/utils/map.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CountedSyncMap struct {
|
||||||
|
sync.Map
|
||||||
|
len uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CountedSyncMap) CountedDelete(key interface{}) {
|
||||||
|
m.Delete(key)
|
||||||
|
atomic.AddUint64(&m.len, ^uint64(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CountedSyncMap) CountedStore(key, value interface{}) {
|
||||||
|
m.Store(key, value)
|
||||||
|
atomic.AddUint64(&m.len, uint64(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CountedSyncMap) CountedLen() uint64 {
|
||||||
|
return atomic.LoadUint64(&m.len)
|
||||||
|
}
|
22
server/internal/webrtc/data.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
type dataHeader struct {
|
||||||
|
Event uint8
|
||||||
|
Length uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataMouseMove struct {
|
||||||
|
dataHeader
|
||||||
|
X int16
|
||||||
|
Y int16
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataMouseKey struct {
|
||||||
|
dataHeader
|
||||||
|
Key uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataKeyboardKey struct {
|
||||||
|
dataHeader
|
||||||
|
Key uint16
|
||||||
|
}
|
73
server/internal/webrtc/manager.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pion/webrtc/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/gst"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewManager(password string) (*WebRTCManager, error) {
|
||||||
|
engine := webrtc.MediaEngine{}
|
||||||
|
|
||||||
|
videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
|
||||||
|
video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{video}, "ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert").Start()
|
||||||
|
engine.RegisterCodec(videoCodec)
|
||||||
|
// ximagesrc xid=0 show-pointer=true ! videoconvert ! queue | videotestsrc
|
||||||
|
|
||||||
|
audioCodec := webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
|
||||||
|
audio, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "stream", "stream", audioCodec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audio}, "pulsesrc device=auto_null.monitor ! audioconvert").Start()
|
||||||
|
engine.RegisterCodec(audioCodec)
|
||||||
|
// pulsesrc device=auto_null.monitor ! audioconvert | audiotestsrc
|
||||||
|
// gst-launch-1.0 -v pulsesrc device=auto_null.monitor ! audioconvert ! vorbisenc ! oggmux ! filesink location=alsasrc.ogg
|
||||||
|
|
||||||
|
return &WebRTCManager{
|
||||||
|
logger: log.With().Str("service", "webrtc").Logger(),
|
||||||
|
engine: engine,
|
||||||
|
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)),
|
||||||
|
video: video,
|
||||||
|
audio: audio,
|
||||||
|
controller: "",
|
||||||
|
password: password,
|
||||||
|
sessions: make(map[string]*session),
|
||||||
|
upgrader: websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: webrtc.Configuration{
|
||||||
|
ICEServers: []webrtc.ICEServer{
|
||||||
|
{
|
||||||
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebRTCManager struct {
|
||||||
|
logger zerolog.Logger
|
||||||
|
upgrader websocket.Upgrader
|
||||||
|
engine webrtc.MediaEngine
|
||||||
|
api *webrtc.API
|
||||||
|
config webrtc.Configuration
|
||||||
|
password string
|
||||||
|
controller string
|
||||||
|
sessions map[string]*session
|
||||||
|
video *webrtc.Track
|
||||||
|
audio *webrtc.Track
|
||||||
|
}
|
15
server/internal/webrtc/messages.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageIdentityProvide struct {
|
||||||
|
message
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageSDP struct {
|
||||||
|
message
|
||||||
|
SDP string `json:"sdp"`
|
||||||
|
}
|
220
server/internal/webrtc/peer.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/go-vgo/robotgo"
|
||||||
|
"github.com/pion/webrtc/v2"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) createPeer(session *session, raw []byte) error {
|
||||||
|
payload := messageSDP{}
|
||||||
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := manager.api.NewPeerConnection(manager.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = peer.AddTrack(manager.video)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = peer.AddTrack(manager.audio)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.SetRemoteDescription(webrtc.SessionDescription{
|
||||||
|
SDP: payload.SDP,
|
||||||
|
Type: webrtc.SDPTypeOffer,
|
||||||
|
})
|
||||||
|
|
||||||
|
answer, err := peer.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = peer.SetLocalDescription(answer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session.send(messageSDP{
|
||||||
|
message{Event: "sdp/reply"},
|
||||||
|
answer.SDP,
|
||||||
|
})
|
||||||
|
|
||||||
|
session.peer = peer
|
||||||
|
|
||||||
|
peer.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||||
|
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
|
if err = manager.onData(session, msg); err != nil {
|
||||||
|
manager.logger.Warn().Err(err).Msg("onData failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
|
||||||
|
switch connectionState {
|
||||||
|
case webrtc.PeerConnectionStateDisconnected:
|
||||||
|
case webrtc.PeerConnectionStateFailed:
|
||||||
|
manager.destroy(session)
|
||||||
|
break
|
||||||
|
case webrtc.PeerConnectionStateConnected:
|
||||||
|
manager.logger.Info().Str("ID", session.id).Msg("Peer connected")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var debounce = map[int]bool{}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) onData(session *session, msg webrtc.DataChannelMessage) error {
|
||||||
|
if manager.controller != session.id {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
header := &dataHeader{}
|
||||||
|
buffer := bytes.NewBuffer(msg.Data)
|
||||||
|
byt := make([]byte, 3)
|
||||||
|
_, err := buffer.Read(byt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = binary.Read(bytes.NewBuffer(byt), binary.LittleEndian, header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = bytes.NewBuffer(msg.Data)
|
||||||
|
|
||||||
|
switch header.Event {
|
||||||
|
case 0x01: // MOUSE_MOVE
|
||||||
|
payload := &dataMouseMove{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
robotgo.Move(int(payload.X), int(payload.Y))
|
||||||
|
break
|
||||||
|
case 0x02: // MOUSE_UP
|
||||||
|
payload := &dataMouseKey{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key, ok := keys.Mouse[int(payload.Key)]; ok {
|
||||||
|
if !debounce[int(payload.Key)] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
debounce[int(payload.Key)] = false
|
||||||
|
robotgo.MouseToggle("up", key)
|
||||||
|
} else {
|
||||||
|
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 0x03: // MOUSE_DOWN
|
||||||
|
payload := &dataMouseKey{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key, ok := keys.Mouse[int(payload.Key)]; ok {
|
||||||
|
if debounce[int(payload.Key)] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
debounce[int(payload.Key)] = true
|
||||||
|
|
||||||
|
robotgo.MouseToggle("down", key)
|
||||||
|
} else {
|
||||||
|
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 0x04: // MOUSE_CLK
|
||||||
|
payload := &dataMouseKey{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key, ok := keys.Mouse[int(payload.Key)]; ok {
|
||||||
|
switch int(payload.Key) {
|
||||||
|
case keys.MOUSE_WHEEL_DOWN:
|
||||||
|
robotgo.Scroll(0, -10)
|
||||||
|
break
|
||||||
|
case keys.MOUSE_WHEEL_UP:
|
||||||
|
robotgo.Scroll(0, 10)
|
||||||
|
break
|
||||||
|
case keys.MOUSE_WHEEL_LEFT:
|
||||||
|
robotgo.Scroll(-10, 0)
|
||||||
|
break
|
||||||
|
case keys.MOUSE_WHEEL_RIGH:
|
||||||
|
robotgo.Scroll(10, 0)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
robotgo.Click(key, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
manager.logger.Warn().Msgf("Unknown MOUSE_CLK key: %v", payload.Key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 0x05: // KEY_DOWN
|
||||||
|
payload := &dataKeyboardKey{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
|
||||||
|
if debounce[int(payload.Key)] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
debounce[int(payload.Key)] = true
|
||||||
|
robotgo.KeyToggle(key, "down")
|
||||||
|
} else {
|
||||||
|
manager.logger.Warn().Msgf("Unknown KEY_DOWN key: %v", payload.Key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 0x06: // KEY_UP
|
||||||
|
payload := &dataKeyboardKey{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
|
||||||
|
if !debounce[int(payload.Key)] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
debounce[int(payload.Key)] = false
|
||||||
|
robotgo.KeyToggle(key, "up")
|
||||||
|
} else {
|
||||||
|
manager.logger.Warn().Msgf("Unknown KEY_UP key: %v", payload.Key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 0x07: // KEY_CLK
|
||||||
|
payload := &dataKeyboardKey{}
|
||||||
|
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
|
||||||
|
robotgo.KeyTap(key)
|
||||||
|
} else {
|
||||||
|
manager.logger.Warn().Msgf("Unknown KEY_CLK key: %v", payload.Key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
41
server/internal/webrtc/session.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pion/webrtc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
id string
|
||||||
|
socket *websocket.Conn
|
||||||
|
peer *webrtc.PeerConnection
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *session) send(v interface{}) error {
|
||||||
|
session.mu.Lock()
|
||||||
|
defer session.mu.Unlock()
|
||||||
|
|
||||||
|
if session.socket != nil {
|
||||||
|
return session.socket.WriteJSON(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *session) destroy() error {
|
||||||
|
if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected {
|
||||||
|
if err := session.peer.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.socket != nil {
|
||||||
|
if err := session.socket.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
222
server/internal/webrtc/websocket.go
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/nanoid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Send pings to peer with this period. Must be less than pongWait.
|
||||||
|
pingPeriod = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) Upgrade(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
socket, err := manager.upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
manager.logger.Error().Err(err).Msg("Failed to upgrade websocket!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, ok := manager.authenticate(r)
|
||||||
|
if ok != true {
|
||||||
|
manager.logger.Warn().Msg("Authenticatetion failed")
|
||||||
|
if err = socket.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &session{
|
||||||
|
id: sessionID,
|
||||||
|
socket: socket,
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.logger.
|
||||||
|
Info().
|
||||||
|
Str("ID", sessionID).
|
||||||
|
Str("RemoteAddr", socket.RemoteAddr().String()).
|
||||||
|
Msg("Created Session")
|
||||||
|
|
||||||
|
manager.sessions[sessionID] = session
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
manager.destroy(session)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = manager.onConnected(session); err != nil {
|
||||||
|
manager.logger.Error().Err(err).Msg("onConnected failed!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.handleWS(session)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) authenticate(r *http.Request) (sessionID string, ok bool) {
|
||||||
|
|
||||||
|
passwords, ok := r.URL.Query()["password"]
|
||||||
|
if !ok || len(passwords[0]) < 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if passwords[0] != manager.password {
|
||||||
|
manager.logger.Warn().Str("Password", passwords[0]).Msg("Wrong password: ")
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := nanoid.NewIDSize(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) onConnected(session *session) error {
|
||||||
|
if err := session.send(messageIdentityProvide{
|
||||||
|
message: message{Event: "identity/provide"},
|
||||||
|
ID: session.id,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) onMessage(session *session, raw []byte) error {
|
||||||
|
message := message{}
|
||||||
|
if err := json.Unmarshal(raw, &message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch message.Event {
|
||||||
|
case "sdp/provide":
|
||||||
|
return errors.Wrap(manager.createPeer(session, raw), "sdp/provide failed")
|
||||||
|
case "control/release":
|
||||||
|
return errors.Wrap(manager.controlRelease(session), "control/release failed")
|
||||||
|
case "control/request":
|
||||||
|
return errors.Wrap(manager.controlRequest(session), "control/request failed")
|
||||||
|
default:
|
||||||
|
manager.logger.Warn().Msgf("Unknown client method %s", message.Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) handleWS(session *session) {
|
||||||
|
bytes := make(chan []byte)
|
||||||
|
cancel := make(chan struct{})
|
||||||
|
ticker := time.NewTicker(pingPeriod)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
manager.logger.Info().Str("RemoteAddr", session.socket.RemoteAddr().String()).Msg("Handle WS ending")
|
||||||
|
manager.destroy(session)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, raw, err := session.socket.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||||
|
manager.logger.Warn().Err(err).Msg("ReadMessage error")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bytes <- raw
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case raw := <-bytes:
|
||||||
|
manager.logger.Info().
|
||||||
|
Str("ID", session.id).
|
||||||
|
Str("Message", string(raw)).
|
||||||
|
Msg("Reading from Websocket")
|
||||||
|
if err := manager.onMessage(session, raw); err != nil {
|
||||||
|
manager.logger.Error().Err(err).Msg("onClientMessage has failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-cancel:
|
||||||
|
return
|
||||||
|
case _ = <-ticker.C:
|
||||||
|
if err := session.socket.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) destroy(session *session) {
|
||||||
|
if manager.controller == session.id {
|
||||||
|
manager.controller = ""
|
||||||
|
for id, sess := range manager.sessions {
|
||||||
|
if id != session.id {
|
||||||
|
if err := sess.send(message{Event: "control/released"}); err != nil {
|
||||||
|
manager.logger.Error().Err(err).Msg("session.send has failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.destroy(); err != nil {
|
||||||
|
manager.logger.Error().Err(err).Msg("session.destroy has failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(manager.sessions, session.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) controlRelease(session *session) error {
|
||||||
|
if manager.controller == session.id {
|
||||||
|
manager.controller = ""
|
||||||
|
|
||||||
|
if err := session.send(message{Event: "control/release"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, sess := range manager.sessions {
|
||||||
|
if id != session.id {
|
||||||
|
if err := sess.send(message{Event: "control/released"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *WebRTCManager) controlRequest(session *session) error {
|
||||||
|
if manager.controller == "" {
|
||||||
|
manager.controller = session.id
|
||||||
|
|
||||||
|
if err := session.send(message{Event: "control/give"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, sess := range manager.sessions {
|
||||||
|
if id != session.id {
|
||||||
|
if err := sess.send(message{Event: "control/given"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := session.send(message{Event: "control/locked"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
controller, ok := manager.sessions[manager.controller]
|
||||||
|
if ok {
|
||||||
|
controller.send(message{Event: "control/requesting"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
116
server/neko.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package neko
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"n.eko.moe/neko/internal/config"
|
||||||
|
"n.eko.moe/neko/internal/structs"
|
||||||
|
api "n.eko.moe/neko/internal/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//
|
||||||
|
buildDate = ""
|
||||||
|
//
|
||||||
|
gitCommit = ""
|
||||||
|
//
|
||||||
|
gitVersion = ""
|
||||||
|
//
|
||||||
|
gitState = ""
|
||||||
|
// Major version when you make incompatible API changes,
|
||||||
|
major = "0"
|
||||||
|
// Minor version when you add functionality in a backwards-compatible manner, and
|
||||||
|
minor = "0"
|
||||||
|
// Patch version when you make backwards-compatible bug fixeneko.
|
||||||
|
patch = "0"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Service *Neko
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Service = &Neko{
|
||||||
|
Version: &structs.Version{
|
||||||
|
Major: major,
|
||||||
|
Minor: minor,
|
||||||
|
Patch: patch,
|
||||||
|
GitVersion: gitVersion,
|
||||||
|
GitCommit: gitCommit,
|
||||||
|
GitTreeState: gitState,
|
||||||
|
BuildDate: buildDate,
|
||||||
|
GoVersion: runtime.Version(),
|
||||||
|
Compiler: runtime.Compiler,
|
||||||
|
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||||
|
},
|
||||||
|
Root: &config.Root{},
|
||||||
|
Serve: &config.Serve{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Neko struct {
|
||||||
|
Version *structs.Version
|
||||||
|
Root *config.Root
|
||||||
|
Serve *config.Serve
|
||||||
|
Logger zerolog.Logger
|
||||||
|
http *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (neko *Neko) Preflight() {
|
||||||
|
neko.Logger = log.With().Str("service", "neko").Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (neko *Neko) Start() {
|
||||||
|
server := api.New(neko.Serve.Bind, neko.Serve.Password, neko.Serve.Static)
|
||||||
|
|
||||||
|
if neko.Serve.Cert != "" && neko.Serve.Key != "" {
|
||||||
|
go func() {
|
||||||
|
if err := server.ListenAndServeTLS(neko.Serve.Cert, neko.Serve.Key); err != http.ErrServerClosed {
|
||||||
|
neko.Logger.Panic().Err(err).Msg("Unable to start https server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
neko.Logger.Info().Msgf("HTTPS listening on %s", server.Addr)
|
||||||
|
} else {
|
||||||
|
go func() {
|
||||||
|
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
|
neko.Logger.Panic().Err(err).Msg("Unable to start http server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
neko.Logger.Warn().Msgf("HTTP listening on %s", server.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
neko.http = server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (neko *Neko) Shutdown() {
|
||||||
|
if neko.http != nil {
|
||||||
|
if err := neko.http.Shutdown(context.Background()); err != nil {
|
||||||
|
neko.Logger.Err(err).Msg("HTTP server shutdown with an error")
|
||||||
|
} else {
|
||||||
|
neko.Logger.Debug().Msg("HTTP server shutdown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) {
|
||||||
|
neko.Logger.Info().Msg("Starting HTTP/S server")
|
||||||
|
neko.Start()
|
||||||
|
|
||||||
|
neko.Logger.Info().Msg("Service ready")
|
||||||
|
|
||||||
|
quit := make(chan os.Signal)
|
||||||
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
sig := <-quit
|
||||||
|
|
||||||
|
neko.Logger.Warn().Msgf("Received %s, attempting graceful shutdown: \n", sig)
|
||||||
|
|
||||||
|
neko.Shutdown()
|
||||||
|
neko.Logger.Info().Msg("Shutting down complete")
|
||||||
|
}
|
3
tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./client/tsconfig.json"
|
||||||
|
}
|