first commit

This commit is contained in:
Craig 2020-01-13 08:05:38 +00:00
commit 0c8af21fab
95 changed files with 5312 additions and 0 deletions

129
.devcontainer/Dockerfile Normal file
View 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

View 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"
]
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View File

38
.gitignore vendored Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
> 1%
last 2 versions

9
client/.editorconfig Normal file
View 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
View 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
View 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
View 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
View File

34
client/package.json Normal file
View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

23
client/public/index.html Normal file
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View 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

View 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
View 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>

View 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

View 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;
}

View 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;

View 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;
}

View 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
View 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
View File

@ -0,0 +1 @@
declare module '*.scss' {}

13
client/src/types/shims-tsx.d.ts vendored Normal file
View 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
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

37
client/tsconfig.json Normal file
View 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
View File

@ -0,0 +1,11 @@
module.exports = {
css: {
loaderOptions: {
sass: {
prependData: `
@import "@/assets/styles/_variables.scss";
`,
},
},
},
}

47
neko.code-workspace Normal file
View 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
View 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
View 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
View 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
View 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
View File

18
server/cmd/neko/main.go Normal file
View 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
View 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
View 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
View 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
View 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=

View File

@ -0,0 +1,8 @@
package config
import "github.com/spf13/cobra"
type Config interface {
Init(cmd *cobra.Command) error
Set()
}

View 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")
}

View 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
View 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), &copy, &copy_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
View 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
View 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

View 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),
}
}

View 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)
}
}
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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)
}

View 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
}

View 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"
}

View 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"
}

View 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)
}

View 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")
}
}

View 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))
}
}

View 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)
}

View 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...)
}

View File

@ -0,0 +1,10 @@
package utils
const Header = `&34
_ __ __
/ | / /__ / /______ \ /\
/ |/ / _ \/ //_/ __ \ ) ( ')
/ /| / __/ ,< / /_/ / ( / )
/_/ |_/\___/_/|_|\____/ \(__)|
&1&37 nurdism/neko &33%s v%s&0
`

View 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)
}

View 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
}

View 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
}

View 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"`
}

View 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
}

View 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
}

View 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
View 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
View File

@ -0,0 +1,3 @@
{
"extends": "./client/tsconfig.json"
}