├── README.md ├── .gitignore ├── frontend ├── .browserslistrc ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ └── logo.png │ ├── views │ │ ├── AboutView.vue │ │ └── MainView.vue │ ├── shims-vue.d.ts │ ├── App.vue │ ├── router │ │ └── index.ts │ ├── main.ts │ └── components │ │ └── MainComponent.vue ├── vue.config.js ├── .gitignore ├── README.md ├── .eslintrc.js ├── tsconfig.json └── package.json ├── staticAssets └── handler.go ├── go.mod ├── LICENSE ├── backend ├── handlers_test.go └── handlers.go ├── mainWindow └── run.go ├── main.go └── go.sum /README.md: -------------------------------------------------------------------------------- 1 | # desktop-web-app -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /desktop-web-app 3 | /desktop-web-app.exe -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis/desktop-web-app/master/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis/desktop-web-app/master/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("@vue/cli-service"); 2 | module.exports = defineConfig({ 3 | transpileDependencies: true, 4 | publicPath: process.env.NODE_ENV === "production" ? "/ui" : "/", 5 | }); 6 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/src/views/MainView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "plugin:prettier/recommended", 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | }, 15 | rules: { 16 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 17 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "prettier/prettier": [ 19 | "error", 20 | { 21 | "endOfLine": "auto" 22 | }, 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; 2 | import MainView from "../views/MainView.vue"; 3 | 4 | const routes: Array = [ 5 | { 6 | path: "/", 7 | name: "home", 8 | component: MainView, 9 | }, 10 | { 11 | path: "/about", 12 | name: "about", 13 | // route level code-splitting 14 | // this generates a separate chunk (about.[hash].js) for this route 15 | // which is lazy-loaded when the route is visited. 16 | component: () => 17 | import(/* webpackChunkName: "about" */ "../views/AboutView.vue"), 18 | }, 19 | ]; 20 | 21 | const router = createRouter({ 22 | history: createWebHashHistory(), 23 | routes, 24 | }); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /staticAssets/handler.go: -------------------------------------------------------------------------------- 1 | package staticAssets 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | "os" 8 | "path" 9 | ) 10 | 11 | type pseudoFs func(name string) (fs.File, error) 12 | 13 | func (f pseudoFs) Open(name string) (fs.File, error) { 14 | return f(name) 15 | } 16 | 17 | func Handler(embedFs embed.FS, embedFsRoot string, urlPrefix string, defaultUrl string) http.Handler { 18 | handler := pseudoFs(func(name string) (fs.File, error) { 19 | assetPath := path.Join(embedFsRoot, name) 20 | 21 | file, err := embedFs.Open(assetPath) 22 | if os.IsNotExist(err) { 23 | return embedFs.Open(path.Join(embedFsRoot, defaultUrl)) 24 | } 25 | 26 | return file, err 27 | }) 28 | 29 | return http.StripPrefix(urlPrefix, http.FileServer(http.FS(handler))) 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module desktop-web-app 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/chromedp/chromedp v0.9.1 7 | github.com/go-chi/chi/v5 v5.0.8 8 | github.com/sirupsen/logrus v1.9.0 9 | github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 10 | nhooyr.io/websocket v1.8.7 11 | ) 12 | 13 | require ( 14 | github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 // indirect 15 | github.com/chromedp/sysutil v1.0.0 // indirect 16 | github.com/gobwas/httphead v0.1.0 // indirect 17 | github.com/gobwas/pool v0.2.1 // indirect 18 | github.com/gobwas/ws v1.1.0 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/klauspost/compress v1.10.3 // indirect 21 | github.com/mailru/easyjson v0.7.7 // indirect 22 | github.com/stretchr/testify v1.8.1 // indirect 23 | golang.org/x/sys v0.6.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | 5 | const app = createApp(App); 6 | 7 | const url = new URL("/api/time-event", window.location.href); 8 | url.protocol = url.protocol.replace("http", "ws"); 9 | const webSocket = new WebSocket(url.href, ["time"]); 10 | 11 | // // Disable common Chrome shortcuts like Ctrl+N, Ctrl+T, Ctrl+F etc. 12 | // window.addEventListener("keydown", (event: KeyboardEvent) => { 13 | // if (event.ctrlKey) { 14 | // event.stopPropagation(); 15 | // event.preventDefault(); 16 | // } 17 | // // return true; 18 | // }); 19 | // 20 | // // Disable Chrome context menu 21 | // window.addEventListener("contextmenu", (event: MouseEvent) => { 22 | // event.stopPropagation(); 23 | // event.preventDefault(); 24 | // }); 25 | 26 | app.provide("webSocket", webSocket); 27 | app.use(router).mount("#app"); 28 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "useDefineForClassFields": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.30.0", 12 | "vue": "^3.2.47", 13 | "vue-router": "^4.1.6" 14 | }, 15 | "devDependencies": { 16 | "@typescript-eslint/eslint-plugin": "^5.57.1", 17 | "@typescript-eslint/parser": "^5.57.1", 18 | "@vue/cli-plugin-babel": "~5.0.8", 19 | "@vue/cli-plugin-eslint": "~5.0.8", 20 | "@vue/cli-plugin-router": "~5.0.8", 21 | "@vue/cli-plugin-typescript": "~5.0.8", 22 | "@vue/cli-service": "~5.0.8", 23 | "@vue/eslint-config-typescript": "^11.0.2", 24 | "eslint": "^8.37.0", 25 | "eslint-config-prettier": "^8.8.0", 26 | "eslint-plugin-prettier": "^4.2.1", 27 | "eslint-plugin-vue": "^9.10.0", 28 | "prettier": "^2.8.7", 29 | "typescript": "~5.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexey Pisanko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/handlers_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/sirupsen/logrus" 6 | easy "github.com/t-tomalak/logrus-easy-formatter" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func init() { 15 | log = &logrus.Logger{ 16 | Out: os.Stderr, 17 | Level: logrus.DebugLevel, 18 | Formatter: &easy.Formatter{ 19 | TimestampFormat: "2006-01-02 15:04:05", 20 | LogFormat: "[%lvl%] %time% - %msg%", 21 | }, 22 | } 23 | } 24 | 25 | func TestHandleGetTime(t *testing.T) { 26 | 27 | // Create a ResponseRecorder to record the response 28 | responseRecorder := httptest.NewRecorder() 29 | 30 | // Create a request to pass to the handler 31 | request, err := http.NewRequest("GET", "/get-time", nil) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | initialTime := time.Now().Format(time.RFC3339) 37 | 38 | // Call the handler 39 | handleGetTime()(responseRecorder, request) 40 | 41 | // Check the status code is what we expect 42 | if status := responseRecorder.Code; status != http.StatusOK { 43 | t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) 44 | } 45 | 46 | var returnedTime string 47 | err = json.Unmarshal(responseRecorder.Body.Bytes(), &returnedTime) 48 | if err != nil { 49 | t.Errorf("unmarshal failed, for %v", responseRecorder.Body.String()) 50 | } 51 | 52 | if initialTime != returnedTime { 53 | t.Errorf("time is not matching, for %v and %v", initialTime, returnedTime) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/MainComponent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 71 | 72 | 99 | -------------------------------------------------------------------------------- /mainWindow/run.go: -------------------------------------------------------------------------------- 1 | package mainWindow 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/chromedp/chromedp" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const windowLeft = 0 11 | const windowTop = 0 12 | const windowWidth = 800 13 | const windowHeight = 600 14 | 15 | const jsCode = ` 16 | // Disable common Chrome shortcuts like Ctrl+N, Ctrl+T, Ctrl+F etc. 17 | window.addEventListener("keydown", (event) => { 18 | if (event.ctrlKey) { 19 | event.stopPropagation(); 20 | event.preventDefault(); 21 | } 22 | }); 23 | 24 | // Disable Chrome context menu 25 | window.addEventListener("contextmenu", (event) => { 26 | event.stopPropagation(); 27 | event.preventDefault(); 28 | }); 29 | ` 30 | 31 | func Run(host string, port int, url string, log *logrus.Logger) { 32 | log.Infof("Starting main window\n") 33 | 34 | opts := []chromedp.ExecAllocatorOption{ 35 | chromedp.NoFirstRun, 36 | chromedp.NoDefaultBrowserCheck, 37 | //chromedp.Flag("app", true), 38 | //chromedp.Flag("app", "data:text/html,"), 39 | //chromedp.Flag("app", "data:text/html,TEST"), 40 | //chromedp.Flag("app", "data:text/html,App"), 41 | chromedp.Flag("app", "data:text/html,App"), 42 | //chromedp.Flag("app", "data:text/html,"), 43 | // chromedp.Flag("window-size", fmt.Sprintf("%d,%d", windowWidth, windowHeight)), 44 | // chromedp.Flag("window-position", fmt.Sprintf("%d,%d", windowLeft, windowTop)), 45 | chromedp.Flag("start-maximized", true), 46 | } 47 | 48 | allocatorContext, _ := chromedp.NewExecAllocator(context.Background(), opts...) 49 | 50 | ctx, cancel := chromedp.NewContext( 51 | allocatorContext, 52 | //chromedp.WithDebugf(log.Printf), 53 | ) 54 | defer cancel() 55 | 56 | if err := chromedp.Run(ctx, chromedp.Tasks{ 57 | chromedp.Navigate(fmt.Sprintf("http://%s:%d%s", host, port, url)), 58 | chromedp.Evaluate(jsCode, nil), 59 | }); err != nil { 60 | log.Fatalf("chromedp.Run() failed: %s\n", 61 | fmt.Errorf("%w", err)) 62 | } 63 | 64 | <-chromedp.FromContext(ctx).Browser.LostConnection 65 | 66 | log.Infof("Stopping main window\n") 67 | } 68 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "desktop-web-app/backend" 6 | "desktop-web-app/mainWindow" 7 | "desktop-web-app/staticAssets" 8 | "embed" 9 | "fmt" 10 | "github.com/go-chi/chi/v5" 11 | "github.com/go-chi/chi/v5/middleware" 12 | "github.com/sirupsen/logrus" 13 | easy "github.com/t-tomalak/logrus-easy-formatter" 14 | "net" 15 | "net/http" 16 | "os" 17 | "time" 18 | ) 19 | 20 | const localHost = "127.0.0.1" 21 | const anyPort = 0 22 | const uiUrlPrefix = "/ui" 23 | const apiUrlPrefix = "/api" 24 | const embedFsRoot = "frontend/dist" 25 | const defaultUiUrl = "index.html" 26 | 27 | //go:embed frontend/dist 28 | var embedFs embed.FS 29 | 30 | var log *logrus.Logger 31 | 32 | func main() { 33 | log = &logrus.Logger{ 34 | Out: os.Stderr, 35 | Level: logrus.DebugLevel, 36 | Formatter: &easy.Formatter{ 37 | TimestampFormat: "2006-01-02 15:04:05", 38 | LogFormat: "[%lvl%] %time% - %msg%", 39 | }, 40 | } 41 | 42 | log.Infof("Application starting\n") 43 | 44 | listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", localHost, anyPort)) 45 | if err != nil { 46 | log.Fatalf("Server net.Listen() failed: %s\n", 47 | fmt.Errorf("%w", err)) 48 | } 49 | 50 | ephemeralPort := listener.Addr().(*net.TCPAddr).Port 51 | 52 | log.Infof("Ephemeral port: %d\n", ephemeralPort) 53 | 54 | router := chi.NewRouter() 55 | router.Use(middleware.Logger) 56 | router.Mount(apiUrlPrefix, backend.Router(log)) 57 | router.Handle(uiUrlPrefix+"*", staticAssets.Handler(embedFs, embedFsRoot, uiUrlPrefix, defaultUiUrl)) 58 | 59 | server := &http.Server{ 60 | Handler: router, 61 | } 62 | 63 | go func() { 64 | log.Infof("Server starting\n") 65 | 66 | err := server.Serve(listener) 67 | if err != nil { 68 | if err != nil && err != http.ErrServerClosed { 69 | log.Fatalf("Server ListenAndServe() failed: %s\n", 70 | fmt.Errorf("%w", err)) 71 | } 72 | } 73 | 74 | log.Infof("Server stopped\n") 75 | }() 76 | 77 | mainWindow.Run(localHost, ephemeralPort, uiUrlPrefix, log) 78 | 79 | log.Infof("Server stopping\n") 80 | 81 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 82 | defer func() { 83 | cancel() 84 | }() 85 | 86 | if err := server.Shutdown(ctx); err != nil { 87 | log.Fatalf("Server Shutdown() failed: %s\n", 88 | fmt.Errorf("%w", err)) 89 | } 90 | 91 | log.Infof("Application stopped\n") 92 | } 93 | -------------------------------------------------------------------------------- /backend/handlers.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/go-chi/chi/v5" 7 | "github.com/sirupsen/logrus" 8 | "net/http" 9 | "nhooyr.io/websocket" 10 | "nhooyr.io/websocket/wsjson" 11 | "time" 12 | ) 13 | 14 | var log *logrus.Logger 15 | 16 | func Router(externalLog *logrus.Logger) chi.Router { 17 | log = externalLog 18 | router := chi.NewRouter() 19 | router.Get("/get-time", handleGetTime()) 20 | router.Get("/time-event", handleTimeEvent()) 21 | return router 22 | } 23 | 24 | func handleGetTime() http.HandlerFunc { 25 | return func(writer http.ResponseWriter, request *http.Request) { 26 | log.Infof("GetTime request from client\n") 27 | 28 | writer.Header().Set("Content-Type", "application/json") 29 | 30 | message, err := json.Marshal(getCurrentTime()) 31 | if err != nil { 32 | log.Errorf("json.Marshal() failed: %s\n", 33 | fmt.Errorf("%w", err)) 34 | writer.WriteHeader(http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | _, err = writer.Write(message) 39 | if err != nil { 40 | log.Errorf("writer.Write() failed: %s\n", 41 | fmt.Errorf("%w", err)) 42 | writer.WriteHeader(http.StatusInternalServerError) 43 | return 44 | } 45 | } 46 | } 47 | 48 | func getCurrentTime() string { 49 | return time.Now().Format(time.RFC3339) 50 | } 51 | 52 | func handleTimeEvent() http.HandlerFunc { 53 | return func(writer http.ResponseWriter, request *http.Request) { 54 | log.Infof("TimeEvent request from client\n") 55 | 56 | conn, err := websocket.Accept(writer, request, &websocket.AcceptOptions{ 57 | Subprotocols: []string{"time"}, 58 | }) 59 | if err != nil { 60 | log.Errorf("websocket.Accept() failed: %s\n", 61 | fmt.Errorf("%w", err)) 62 | return 63 | } 64 | 65 | defer func() { 66 | err := conn.Close(websocket.StatusInternalError, "unexpected error") 67 | if err != nil { 68 | log.Errorf("conn.Close() failed: %s\n", 69 | fmt.Errorf("%w", err)) 70 | return 71 | } 72 | 73 | log.Infof("websocket connection closed\n") 74 | }() 75 | 76 | for { 77 | //if websocket.CloseStatus(err) != -1 { 78 | // log.Infof("websocket closed\n") 79 | // return 80 | //} 81 | 82 | currentTime := getCurrentTime() 83 | 84 | log.Infof("about to send event message: %s\n", currentTime) 85 | 86 | err = wsjson.Write(request.Context(), conn, currentTime) 87 | if err != nil { 88 | log.Errorf("wsjson.Write() failed: %s\n", 89 | fmt.Errorf("%w", err)) 90 | return 91 | } 92 | 93 | log.Infof("sent event message: %s\n", currentTime) 94 | 95 | time.Sleep(3 * time.Second) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY= 2 | github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= 3 | github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA= 4 | github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ= 5 | github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= 6 | github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 11 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 12 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 13 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 14 | github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= 15 | github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 16 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 17 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 18 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 19 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 20 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 21 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 22 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 23 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 24 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 25 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 26 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 27 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 28 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 29 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 30 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= 31 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 32 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 33 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 34 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 35 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 36 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 39 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 40 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 41 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 42 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 43 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 44 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 45 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 46 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 47 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= 48 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 49 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 50 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 51 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 52 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 53 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 54 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 56 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 58 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 59 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= 60 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 64 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 65 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 66 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 69 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 70 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 72 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 73 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 76 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 77 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 78 | github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= 79 | github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= 80 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 81 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 82 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 83 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 84 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 89 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 91 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 92 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 93 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 94 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 98 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 101 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 103 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 104 | --------------------------------------------------------------------------------